diff --git a/diffs/AaveV3Arbitrum_GHOCCIP151Upgrade_20241209_before_AaveV3Arbitrum_GHOCCIP151Upgrade_20241209_after.md b/diffs/AaveV3Arbitrum_GHOCCIP151Upgrade_20241209_before_AaveV3Arbitrum_GHOCCIP151Upgrade_20241209_after.md new file mode 100644 index 000000000..c15d3e2bc --- /dev/null +++ b/diffs/AaveV3Arbitrum_GHOCCIP151Upgrade_20241209_before_AaveV3Arbitrum_GHOCCIP151Upgrade_20241209_after.md @@ -0,0 +1,5 @@ +## Raw diff + +```json +{} +``` \ No newline at end of file diff --git a/diffs/AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229_before_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229_after.md b/diffs/AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229_before_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229_after.md new file mode 100644 index 000000000..e7c42860b --- /dev/null +++ b/diffs/AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229_before_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229_after.md @@ -0,0 +1,30 @@ +## Reserve changes + +### Reserves altered + +#### GHO ([0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f](https://etherscan.io/address/0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f)) + +| description | value before | value after | +| --- | --- | --- | +| aTokenUnderlyingBalance | 6,108,315.1602 GHO [6108315160265916496775469] | 16,108,315.1602 GHO [16108315160265916496775469] | +| virtualBalance | 6,108,315.1602 GHO [6108315160265916496775469] | 16,108,315.1602 GHO [16108315160265916496775469] | + + +## Raw diff + +```json +{ + "reserves": { + "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f": { + "aTokenUnderlyingBalance": { + "from": "6108315160265916496775469", + "to": "16108315160265916496775469" + }, + "virtualBalance": { + "from": "6108315160265916496775469", + "to": "16108315160265916496775469" + } + } + } +} +``` \ No newline at end of file diff --git a/diffs/AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224_before_AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224_after.md b/diffs/AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224_before_AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224_after.md new file mode 100644 index 000000000..400bb480a --- /dev/null +++ b/diffs/AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224_before_AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224_after.md @@ -0,0 +1,48 @@ +## Emodes changed + +### EMode: ETH correlated(id: 1) + + + +### EMode: LRT Stablecoins main(id: 2) + + + +### EMode: LRT wstETH main(id: 3) + + + +### EMode: sUSDe Stablecoins(id: 4) + +| description | value before | value after | +| --- | --- | --- | +| eMode.label (unchanged) | sUSDe Stablecoins | sUSDe Stablecoins | +| eMode.ltv (unchanged) | 90 % | 90 % | +| eMode.liquidationThreshold (unchanged) | 92 % | 92 % | +| eMode.liquidationBonus | 3 % | 4 % | +| eMode.borrowableBitmap | USDS, USDC, GHO | USDC, GHO | +| eMode.collateralBitmap (unchanged) | sUSDe | sUSDe | + + +### EMode: rsETH LST main(id: 5) + + + +## Raw diff + +```json +{ + "eModes": { + "4": { + "borrowableBitmap": { + "from": "76", + "to": "72" + }, + "liquidationBonus": { + "from": 10300, + "to": 10400 + } + } + } +} +``` \ No newline at end of file diff --git a/diffs/AaveV3Ethereum_GHOCCIP151Upgrade_20241209_before_AaveV3Ethereum_GHOCCIP151Upgrade_20241209_after.md b/diffs/AaveV3Ethereum_GHOCCIP151Upgrade_20241209_before_AaveV3Ethereum_GHOCCIP151Upgrade_20241209_after.md new file mode 100644 index 000000000..c15d3e2bc --- /dev/null +++ b/diffs/AaveV3Ethereum_GHOCCIP151Upgrade_20241209_before_AaveV3Ethereum_GHOCCIP151Upgrade_20241209_after.md @@ -0,0 +1,5 @@ +## Raw diff + +```json +{} +``` \ No newline at end of file diff --git a/diffs/AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224_before_AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224_after.md b/diffs/AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224_before_AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224_after.md new file mode 100644 index 000000000..499afdcb0 --- /dev/null +++ b/diffs/AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224_before_AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224_after.md @@ -0,0 +1,36 @@ +## Emodes changed + +### EMode: ETH correlated(id: 1) + + + +### EMode: sUSDe Stablecoins(id: 2) + +| description | value before | value after | +| --- | --- | --- | +| eMode.label (unchanged) | sUSDe Stablecoins | sUSDe Stablecoins | +| eMode.ltv (unchanged) | 90 % | 90 % | +| eMode.liquidationThreshold (unchanged) | 92 % | 92 % | +| eMode.liquidationBonus | 3 % | 4 % | +| eMode.borrowableBitmap (unchanged) | USDC, USDT, USDS | USDC, USDT, USDS | +| eMode.collateralBitmap (unchanged) | sUSDe | sUSDe | + + +### EMode: rsETH LST main(id: 3) + + + +## Raw diff + +```json +{ + "eModes": { + "2": { + "liquidationBonus": { + "from": 10300, + "to": 10400 + } + } + } +} +``` \ No newline at end of file diff --git a/diffs/AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224_before_AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224_after.md b/diffs/AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224_before_AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224_after.md new file mode 100644 index 000000000..13ec6044f --- /dev/null +++ b/diffs/AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224_before_AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224_after.md @@ -0,0 +1,70 @@ +## Reserve changes + +### Reserve altered + +#### GNO ([0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb](https://gnosisscan.io/address/0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb)) + +| description | value before | value after | +| --- | --- | --- | +| debtCeiling | 2,000,000 $ [200000000] | 0 $ [0] | + + +#### EURe ([0xcB444e90D8198415266c6a2724b7900fb12FC56E](https://gnosisscan.io/address/0xcB444e90D8198415266c6a2724b7900fb12FC56E)) + +| description | value before | value after | +| --- | --- | --- | +| reserveFactor | 20 % [2000] | 10 % [1000] | + + +## Emodes changed + +### EMode: ETH correlated(id: 1) + + + +### EMode: sDAI / EURe(id: 2) + +| description | value before | value after | +| --- | --- | --- | +| eMode.label | - | sDAI / EURe | +| eMode.ltv | - | 85 % | +| eMode.liquidationThreshold | - | 87.5 % | +| eMode.liquidationBonus | - | 5 % | +| eMode.borrowableBitmap | - | EURe | +| eMode.collateralBitmap | - | sDAI | + + +## Raw diff + +```json +{ + "eModes": { + "2": { + "from": null, + "to": { + "borrowableBitmap": "32", + "collateralBitmap": "64", + "eModeCategory": 2, + "label": "sDAI / EURe", + "liquidationBonus": 10500, + "liquidationThreshold": 8750, + "ltv": 8500 + } + } + }, + "reserves": { + "0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb": { + "debtCeiling": { + "from": 200000000, + "to": 0 + } + }, + "0xcB444e90D8198415266c6a2724b7900fb12FC56E": { + "reserveFactor": { + "from": 2000, + "to": 1000 + } + } + } +} +``` \ No newline at end of file diff --git a/lib/aave-helpers b/lib/aave-helpers index 1821d62cc..7cdcda651 160000 --- a/lib/aave-helpers +++ b/lib/aave-helpers @@ -1 +1 @@ -Subproject commit 1821d62ccddb4d9d66ad35405cbaaeec795bd1b2 +Subproject commit 7cdcda6516b6c08d3f3cee9e27b541c179e66fe3 diff --git a/src/20241209_Multi_GHOCCIP151Upgrade/AaveV3Arbitrum_GHOCCIP151Upgrade_20241209.sol b/src/20241209_Multi_GHOCCIP151Upgrade/AaveV3Arbitrum_GHOCCIP151Upgrade_20241209.sol new file mode 100644 index 000000000..a1b86e1b9 --- /dev/null +++ b/src/20241209_Multi_GHOCCIP151Upgrade/AaveV3Arbitrum_GHOCCIP151Upgrade_20241209.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {ITransparentUpgradeableProxy} from 'solidity-utils/contracts/transparent-proxy/TransparentUpgradeableProxy.sol'; +import {IUpgradeableBurnMintTokenPool_1_4, IUpgradeableBurnMintTokenPool_1_5_1} from 'src/interfaces/ccip/tokenPool/IUpgradeableBurnMintTokenPool.sol'; +import {IProposalGenericExecutor} from 'aave-helpers/src/interfaces/IProposalGenericExecutor.sol'; +import {ITokenAdminRegistry} from 'src/interfaces/ccip/ITokenAdminRegistry.sol'; +import {IRateLimiter} from 'src/interfaces/ccip/IRateLimiter.sol'; +import {IProxyPool} from 'src/interfaces/ccip/IProxyPool.sol'; +import {ILegacyProxyAdmin} from 'src/interfaces/ILegacyProxyAdmin.sol'; +import {IGhoBucketSteward} from 'src/interfaces/IGhoBucketSteward.sol'; +import {IGhoToken} from 'src/interfaces/IGhoToken.sol'; +import {AaveV3Arbitrum} from 'aave-address-book/AaveV3Arbitrum.sol'; +import {MiscArbitrum} from 'aave-address-book/MiscArbitrum.sol'; +import {GhoArbitrum} from 'aave-address-book/GhoArbitrum.sol'; +import {AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; +import {AaveV3ArbitrumAssets} from 'aave-address-book/AaveV3Arbitrum.sol'; + +/** + * @title GHO CCIP 1.5.1 Upgrade + * @author Aave Labs + * - Discussion: TODO + */ +contract AaveV3Arbitrum_GHOCCIP151Upgrade_20241209 is IProposalGenericExecutor { + uint64 public constant ETH_CHAIN_SELECTOR = 5009297550715157269; + + // https://arbiscan.io/address/0x39AE1032cF4B334a1Ed41cdD0833bdD7c7E7751E + ITokenAdminRegistry public constant TOKEN_ADMIN_REGISTRY = + ITokenAdminRegistry(0x39AE1032cF4B334a1Ed41cdD0833bdD7c7E7751E); + + // https://arbiscan.io/address/0x26329558f08cbb40d6a4CCA0E0C67b29D64A8c50 + IProxyPool public constant EXISTING_PROXY_POOL = + IProxyPool(0x26329558f08cbb40d6a4CCA0E0C67b29D64A8c50); + // https://arbiscan.io/address/0xF168B83598516A532a85995b52504a2Fa058C068 + IUpgradeableBurnMintTokenPool_1_4 public constant EXISTING_TOKEN_POOL = + IUpgradeableBurnMintTokenPool_1_4(GhoArbitrum.GHO_CCIP_TOKEN_POOL); // will be updated in address-book after AIP + // https://arbiscan.io/address/0xB94Ab28c6869466a46a42abA834ca2B3cECCA5eB + IUpgradeableBurnMintTokenPool_1_5_1 public constant NEW_TOKEN_POOL = + IUpgradeableBurnMintTokenPool_1_5_1(0xB94Ab28c6869466a46a42abA834ca2B3cECCA5eB); + + // https://arbiscan.io/address/0xcd04d93bea13921dad05240d577090b5ac36dfca + address public constant EXISTING_GHO_AAVE_STEWARD = 0xCd04D93bEA13921DaD05240D577090b5AC36DfCA; + // https://arbiscan.io/address/0xd2D586f849620ef042FE3aF52eAa10e9b78bf7De + address public constant NEW_GHO_AAVE_STEWARD = 0xd2D586f849620ef042FE3aF52eAa10e9b78bf7De; + // https://arbiscan.io/address/0xa9afaE6A53E90f9E4CE0717162DF5Bc3d9aBe7B2 + IGhoBucketSteward public constant GHO_BUCKET_STEWARD = + IGhoBucketSteward(0xa9afaE6A53E90f9E4CE0717162DF5Bc3d9aBe7B2); + // https://arbiscan.io/address/0xCd5ab470AaC5c13e1063ee700503f3346b7C90Db + address public constant NEW_GHO_CCIP_STEWARD = 0xCd5ab470AaC5c13e1063ee700503f3346b7C90Db; + + // https://etherscan.io/address/0x9Ec9F9804733df96D1641666818eFb5198eC50f0 + address public constant EXISTING_REMOTE_POOL_ETH = 0x9Ec9F9804733df96D1641666818eFb5198eC50f0; // ProxyPool on ETH + // https://etherscan.io/address/0x06179f7C1be40863405f374E7f5F8806c728660A + address public constant NEW_REMOTE_POOL_ETH = 0x06179f7C1be40863405f374E7f5F8806c728660A; + + // https://arbiscan.io/address/0xA5Ba213867E175A182a5dd6A9193C6158738105A + // https://github.com/aave/ccip/commit/ca73ec8c4f7dc0f6a99ae1ea0acde43776c7b9bb + address public constant EXISTING_TOKEN_POOL_UPGRADE_IMPL = + 0xA5Ba213867E175A182a5dd6A9193C6158738105A; + + // https://arbiscan.io/address/0x7dfF72693f6A4149b17e7C6314655f6A9F7c8B33 + IGhoToken public constant GHO = IGhoToken(AaveV3ArbitrumAssets.GHO_UNDERLYING); + + // Token Rate Limit Capacity: 300_000 GHO + uint128 public constant CCIP_RATE_LIMIT_CAPACITY = 300_000e18; + // Token Rate Limit Refill Rate: 60 GHO per second (=> 216_000 GHO per hour) + uint128 public constant CCIP_RATE_LIMIT_REFILL_RATE = 60e18; + + function execute() external { + _acceptOwnership(); + _migrateLiquidity(); + _setupAndRegisterNewPool(); + _updateStewards(); + } + + // pre-req - chainlink transfers gho token pool ownership on token admin registry + function _acceptOwnership() internal { + EXISTING_PROXY_POOL.acceptOwnership(); + NEW_TOKEN_POOL.acceptOwnership(); + TOKEN_ADMIN_REGISTRY.acceptAdminRole(AaveV3ArbitrumAssets.GHO_UNDERLYING); + } + + function _migrateLiquidity() internal { + // bucketLevel === bridgedAmount + (uint256 bucketCapacity, uint256 bucketLevel) = GHO.getFacilitatorBucket( + address(EXISTING_TOKEN_POOL) + ); + + GHO.addFacilitator(address(NEW_TOKEN_POOL), 'CCIP TokenPool v1.5.1', uint128(bucketCapacity)); + NEW_TOKEN_POOL.directMint(address(EXISTING_TOKEN_POOL), bucketLevel); // increase facilitator level + + _upgradeExistingTokenPool(); // introduce `directBurn` method + EXISTING_TOKEN_POOL.directBurn(bucketLevel); // decrease facilitator level + + GHO.removeFacilitator(address(EXISTING_TOKEN_POOL)); + } + + function _setupAndRegisterNewPool() internal { + IRateLimiter.Config memory rateLimiterConfig = IRateLimiter.Config({ + isEnabled: true, + capacity: CCIP_RATE_LIMIT_CAPACITY, + rate: CCIP_RATE_LIMIT_REFILL_RATE + }); + + IUpgradeableBurnMintTokenPool_1_5_1.ChainUpdate[] + memory chains = new IUpgradeableBurnMintTokenPool_1_5_1.ChainUpdate[](1); + + bytes[] memory remotePoolAddresses = new bytes[](2); + remotePoolAddresses[0] = abi.encode(EXISTING_REMOTE_POOL_ETH); + remotePoolAddresses[1] = abi.encode(NEW_REMOTE_POOL_ETH); + + chains[0] = IUpgradeableBurnMintTokenPool_1_5_1.ChainUpdate({ + remoteChainSelector: ETH_CHAIN_SELECTOR, + remotePoolAddresses: remotePoolAddresses, + remoteTokenAddress: abi.encode(AaveV3EthereumAssets.GHO_UNDERLYING), + outboundRateLimiterConfig: rateLimiterConfig, + inboundRateLimiterConfig: rateLimiterConfig + }); + + // setup new pool + NEW_TOKEN_POOL.applyChainUpdates({ + remoteChainSelectorsToRemove: new uint64[](0), + chainsToAdd: chains + }); + + // register new pool + TOKEN_ADMIN_REGISTRY.setPool(address(GHO), address(NEW_TOKEN_POOL)); + } + + function _updateStewards() internal { + // Gho Aave Steward + AaveV3Arbitrum.ACL_MANAGER.revokeRole( + AaveV3Arbitrum.ACL_MANAGER.RISK_ADMIN_ROLE(), + EXISTING_GHO_AAVE_STEWARD + ); + AaveV3Arbitrum.ACL_MANAGER.grantRole( + AaveV3Arbitrum.ACL_MANAGER.RISK_ADMIN_ROLE(), + NEW_GHO_AAVE_STEWARD + ); + + // Gho Bucket Steward + address[] memory facilitatorList = new address[](1); + facilitatorList[0] = address(EXISTING_TOKEN_POOL); + GHO_BUCKET_STEWARD.setControlledFacilitator({facilitatorList: facilitatorList, approve: false}); + facilitatorList[0] = address(NEW_TOKEN_POOL); + GHO_BUCKET_STEWARD.setControlledFacilitator({facilitatorList: facilitatorList, approve: true}); + + // Gho Ccip Steward + NEW_TOKEN_POOL.setRateLimitAdmin(NEW_GHO_CCIP_STEWARD); + } + + function _upgradeExistingTokenPool() internal { + ILegacyProxyAdmin(MiscArbitrum.PROXY_ADMIN).upgrade( + ITransparentUpgradeableProxy(payable(address(EXISTING_TOKEN_POOL))), + EXISTING_TOKEN_POOL_UPGRADE_IMPL + ); + } +} diff --git a/src/20241209_Multi_GHOCCIP151Upgrade/AaveV3Arbitrum_GHOCCIP151Upgrade_20241209.t.sol b/src/20241209_Multi_GHOCCIP151Upgrade/AaveV3Arbitrum_GHOCCIP151Upgrade_20241209.t.sol new file mode 100644 index 000000000..212ae0857 --- /dev/null +++ b/src/20241209_Multi_GHOCCIP151Upgrade/AaveV3Arbitrum_GHOCCIP151Upgrade_20241209.t.sol @@ -0,0 +1,743 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; + +import {IUpgradeableBurnMintTokenPool_1_4, IUpgradeableBurnMintTokenPool_1_5_1} from 'src/interfaces/ccip/tokenPool/IUpgradeableBurnMintTokenPool.sol'; +import {IPool as IPool_CCIP} from 'src/interfaces/ccip/tokenPool/IPool.sol'; +import {IClient} from 'src/interfaces/ccip/IClient.sol'; +import {IInternal} from 'src/interfaces/ccip/IInternal.sol'; +import {IRouter} from 'src/interfaces/ccip/IRouter.sol'; +import {IRateLimiter} from 'src/interfaces/ccip/IRateLimiter.sol'; +import {IProxyPool} from 'src/interfaces/ccip/IProxyPool.sol'; +import {IEVM2EVMOnRamp} from 'src/interfaces/ccip/IEVM2EVMOnRamp.sol'; +import {ITypeAndVersion} from 'src/interfaces/ccip/ITypeAndVersion.sol'; +import {IEVM2EVMOffRamp_1_5} from 'src/interfaces/ccip/IEVM2EVMOffRamp.sol'; +import {ITokenAdminRegistry} from 'src/interfaces/ccip/ITokenAdminRegistry.sol'; +import {IGhoToken} from 'src/interfaces/IGhoToken.sol'; +import {IGhoAaveSteward} from 'src/interfaces/IGhoAaveSteward.sol'; +import {IGhoBucketSteward} from 'src/interfaces/IGhoBucketSteward.sol'; +import {IGhoCcipSteward} from 'src/interfaces/IGhoCcipSteward.sol'; +import {IOwnable} from 'aave-address-book/common/IOwnable.sol'; +import {DataTypes, IDefaultInterestRateStrategyV2} from 'aave-address-book/AaveV3.sol'; + +import {ReserveConfiguration} from 'aave-v3-origin/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; +import {ProtocolV3TestBase} from 'aave-helpers/src/ProtocolV3TestBase.sol'; +import {AaveV3Arbitrum} from 'aave-address-book/AaveV3Arbitrum.sol'; +import {AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; +import {AaveV3ArbitrumAssets} from 'aave-address-book/AaveV3Arbitrum.sol'; +import {GhoArbitrum} from 'aave-address-book/GhoArbitrum.sol'; +import {GovernanceV3Arbitrum} from 'aave-address-book/GovernanceV3Arbitrum.sol'; + +import {ProxyAdmin} from 'solidity-utils/contracts/transparent-proxy/ProxyAdmin.sol'; +import {CCIPUtils} from './utils/CCIPUtils.sol'; +import {AaveV3Arbitrum_GHOCCIP151Upgrade_20241209} from './AaveV3Arbitrum_GHOCCIP151Upgrade_20241209.sol'; + +/** + * @dev Test for AaveV3Arbitrum_GHOCCIP151Upgrade_20241209 + * command: FOUNDRY_PROFILE=arbitrum forge test --match-path=src/20241209_Multi_GHOCCIP151Upgrade/AaveV3Arbitrum_GHOCCIP151Upgrade_20241209.t.sol -vv + */ +contract AaveV3Arbitrum_GHOCCIP151Upgrade_20241209_Base is ProtocolV3TestBase { + struct CCIPSendParams { + address sender; + uint256 amount; + CCIPUtils.PoolVersion poolVersion; + } + + uint64 internal constant ETH_CHAIN_SELECTOR = CCIPUtils.ETH_CHAIN_SELECTOR; + uint64 internal constant ARB_CHAIN_SELECTOR = CCIPUtils.ARB_CHAIN_SELECTOR; + uint256 internal constant CCIP_RATE_LIMIT_CAPACITY = 300_000e18; + uint256 internal constant CCIP_RATE_LIMIT_REFILL_RATE = 60e18; + + IGhoToken internal constant GHO = IGhoToken(AaveV3ArbitrumAssets.GHO_UNDERLYING); + ITokenAdminRegistry internal constant TOKEN_ADMIN_REGISTRY = + ITokenAdminRegistry(0x39AE1032cF4B334a1Ed41cdD0833bdD7c7E7751E); + address internal constant ETH_PROXY_POOL = 0x9Ec9F9804733df96D1641666818eFb5198eC50f0; + IEVM2EVMOnRamp internal constant ON_RAMP = + IEVM2EVMOnRamp(0x67761742ac8A21Ec4D76CA18cbd701e5A6F3Bef3); + IEVM2EVMOffRamp_1_5 internal constant OFF_RAMP = + IEVM2EVMOffRamp_1_5(0x91e46cc5590A4B9182e47f40006140A7077Dec31); + IRouter internal constant ROUTER = IRouter(0x141fa059441E0ca23ce184B6A78bafD2A517DdE8); + + IGhoAaveSteward public constant EXISTING_GHO_AAVE_STEWARD = + IGhoAaveSteward(0xCd04D93bEA13921DaD05240D577090b5AC36DfCA); + IGhoAaveSteward public constant NEW_GHO_AAVE_STEWARD = + IGhoAaveSteward(0xd2D586f849620ef042FE3aF52eAa10e9b78bf7De); + IGhoBucketSteward internal constant GHO_BUCKET_STEWARD = + IGhoBucketSteward(0xa9afaE6A53E90f9E4CE0717162DF5Bc3d9aBe7B2); + IGhoCcipSteward internal constant EXISTING_GHO_CCIP_STEWARD = + IGhoCcipSteward(0xb329CEFF2c362F315900d245eC88afd24C4949D5); + IGhoCcipSteward internal constant NEW_GHO_CCIP_STEWARD = + IGhoCcipSteward(0xCd5ab470AaC5c13e1063ee700503f3346b7C90Db); + + IProxyPool internal constant EXISTING_PROXY_POOL = + IProxyPool(0x26329558f08cbb40d6a4CCA0E0C67b29D64A8c50); + IUpgradeableBurnMintTokenPool_1_4 internal constant EXISTING_TOKEN_POOL = + IUpgradeableBurnMintTokenPool_1_4(0xF168B83598516A532a85995b52504a2Fa058C068); // GhoArbitrum.GHO_CCIP_TOKEN_POOL; will be updated in address-book after AIP + IUpgradeableBurnMintTokenPool_1_5_1 internal constant NEW_TOKEN_POOL = + IUpgradeableBurnMintTokenPool_1_5_1(0xB94Ab28c6869466a46a42abA834ca2B3cECCA5eB); + address internal constant NEW_REMOTE_POOL_ETH = 0x06179f7C1be40863405f374E7f5F8806c728660A; + + AaveV3Arbitrum_GHOCCIP151Upgrade_20241209 internal proposal; + + address internal alice = makeAddr('alice'); + address internal bob = makeAddr('bob'); + address internal carol = makeAddr('carol'); + + event Burned(address indexed sender, uint256 amount); + event Minted(address indexed sender, address indexed recipient, uint256 amount); + event CCIPSendRequested(IInternal.EVM2EVMMessage message); + + function setUp() public virtual { + vm.createSelectFork(vm.rpcUrl('arbitrum'), 293345614); + proposal = new AaveV3Arbitrum_GHOCCIP151Upgrade_20241209(); + + // pre-req - chainlink transfers gho token pool ownership on token admin registry + address CLL_OWNER = TOKEN_ADMIN_REGISTRY.owner(); + vm.startPrank(CLL_OWNER); + TOKEN_ADMIN_REGISTRY.transferAdminRole(address(GHO), GovernanceV3Arbitrum.EXECUTOR_LVL_1); + EXISTING_PROXY_POOL.transferOwnership(GovernanceV3Arbitrum.EXECUTOR_LVL_1); + vm.stopPrank(); + + _validateConstants(); + } + + function _validateConstants() private view { + assertEq(address(proposal.TOKEN_ADMIN_REGISTRY()), address(TOKEN_ADMIN_REGISTRY)); + assertEq(proposal.ETH_CHAIN_SELECTOR(), ETH_CHAIN_SELECTOR); + assertEq(address(proposal.EXISTING_PROXY_POOL()), address(EXISTING_PROXY_POOL)); + assertEq(address(proposal.EXISTING_TOKEN_POOL()), address(EXISTING_TOKEN_POOL)); + assertEq(address(proposal.EXISTING_REMOTE_POOL_ETH()), ETH_PROXY_POOL); + assertEq(address(proposal.NEW_TOKEN_POOL()), address(NEW_TOKEN_POOL)); + assertEq(address(proposal.EXISTING_GHO_AAVE_STEWARD()), address(EXISTING_GHO_AAVE_STEWARD)); + assertEq(address(proposal.NEW_GHO_AAVE_STEWARD()), address(NEW_GHO_AAVE_STEWARD)); + assertEq(address(proposal.GHO_BUCKET_STEWARD()), address(GHO_BUCKET_STEWARD)); + assertEq(address(proposal.NEW_GHO_CCIP_STEWARD()), address(NEW_GHO_CCIP_STEWARD)); + assertEq(address(proposal.NEW_REMOTE_POOL_ETH()), NEW_REMOTE_POOL_ETH); + assertEq(proposal.CCIP_RATE_LIMIT_CAPACITY(), CCIP_RATE_LIMIT_CAPACITY); + assertEq(proposal.CCIP_RATE_LIMIT_REFILL_RATE(), CCIP_RATE_LIMIT_REFILL_RATE); + + assertEq(address(proposal.EXISTING_PROXY_POOL()), EXISTING_TOKEN_POOL.getProxyPool()); + + assertEq(TOKEN_ADMIN_REGISTRY.typeAndVersion(), 'TokenAdminRegistry 1.5.0'); + assertEq(NEW_TOKEN_POOL.typeAndVersion(), 'BurnMintTokenPool 1.5.1'); + assertEq(EXISTING_TOKEN_POOL.typeAndVersion(), 'BurnMintTokenPool 1.4.0'); + assertEq( + ITypeAndVersion(EXISTING_TOKEN_POOL.getProxyPool()).typeAndVersion(), + 'BurnMintTokenPoolAndProxy 1.5.0' + ); + assertEq(ON_RAMP.typeAndVersion(), 'EVM2EVMOnRamp 1.5.0'); + assertEq(OFF_RAMP.typeAndVersion(), 'EVM2EVMOffRamp 1.5.0'); + + assertEq(ROUTER.typeAndVersion(), 'Router 1.2.0'); + assertEq(EXISTING_TOKEN_POOL.getRouter(), address(ROUTER)); + + assertEq(ROUTER.getOnRamp(ETH_CHAIN_SELECTOR), address(ON_RAMP)); + assertTrue(ROUTER.isOffRamp(ETH_CHAIN_SELECTOR, address(OFF_RAMP))); + + assertEq(EXISTING_TOKEN_POOL.getRateLimitAdmin(), address(EXISTING_GHO_CCIP_STEWARD)); + + assertTrue( + AaveV3Arbitrum.ACL_MANAGER.hasRole( + AaveV3Arbitrum.ACL_MANAGER.RISK_ADMIN_ROLE(), + address(EXISTING_GHO_AAVE_STEWARD) + ) + ); + + assertTrue(GHO.hasRole(GHO.BUCKET_MANAGER_ROLE(), address(GHO_BUCKET_STEWARD))); + + assertEq(IOwnable(address(NEW_GHO_AAVE_STEWARD)).owner(), GovernanceV3Arbitrum.EXECUTOR_LVL_1); + assertEq( + NEW_GHO_AAVE_STEWARD.POOL_ADDRESSES_PROVIDER(), + address(AaveV3Arbitrum.POOL_ADDRESSES_PROVIDER) + ); + assertEq( + NEW_GHO_AAVE_STEWARD.POOL_DATA_PROVIDER(), + address(AaveV3Arbitrum.AAVE_PROTOCOL_DATA_PROVIDER) + ); + assertEq(NEW_GHO_AAVE_STEWARD.RISK_COUNCIL(), EXISTING_GHO_AAVE_STEWARD.RISK_COUNCIL()); + assertEq( + NEW_GHO_AAVE_STEWARD.getBorrowRateConfig(), + IGhoAaveSteward.BorrowRateConfig({ + optimalUsageRatioMaxChange: 5_00, + baseVariableBorrowRateMaxChange: 5_00, + variableRateSlope1MaxChange: 5_00, + variableRateSlope2MaxChange: 5_00 + }) + ); + + assertEq(EXISTING_TOKEN_POOL.getRateLimitAdmin(), address(EXISTING_GHO_CCIP_STEWARD)); + assertEq(NEW_GHO_CCIP_STEWARD.RISK_COUNCIL(), EXISTING_GHO_CCIP_STEWARD.RISK_COUNCIL()); + assertEq(NEW_GHO_CCIP_STEWARD.GHO_TOKEN(), AaveV3ArbitrumAssets.GHO_UNDERLYING); + assertEq(NEW_GHO_CCIP_STEWARD.GHO_TOKEN_POOL(), address(NEW_TOKEN_POOL)); + assertFalse(NEW_GHO_CCIP_STEWARD.BRIDGE_LIMIT_ENABLED()); // *not present* on remote token pool, only on eth + assertEq( + abi.encode(NEW_GHO_CCIP_STEWARD.getCcipTimelocks()), + abi.encode(IGhoCcipSteward.CcipDebounce(0, 0)) + ); + + assertEq(_getProxyAdmin(address(NEW_TOKEN_POOL)).UPGRADE_INTERFACE_VERSION(), '5.0.0'); + } + + function _getTokenMessage( + CCIPSendParams memory params + ) internal returns (IClient.EVM2AnyMessage memory, IInternal.EVM2EVMMessage memory) { + IClient.EVM2AnyMessage memory message = CCIPUtils.generateMessage(params.sender, 1); + message.tokenAmounts[0] = IClient.EVMTokenAmount({ + token: AaveV3ArbitrumAssets.GHO_UNDERLYING, + amount: params.amount + }); + + uint256 feeAmount = ROUTER.getFee(ETH_CHAIN_SELECTOR, message); + deal(params.sender, feeAmount); + + IInternal.EVM2EVMMessage memory eventArg = CCIPUtils.messageToEvent( + CCIPUtils.MessageToEventParams({ + message: message, + router: ROUTER, + sourceChainSelector: ARB_CHAIN_SELECTOR, + feeTokenAmount: feeAmount, + originalSender: params.sender, + sourceToken: AaveV3ArbitrumAssets.GHO_UNDERLYING, + destinationToken: AaveV3EthereumAssets.GHO_UNDERLYING, + poolVersion: params.poolVersion + }) + ); + + return (message, eventArg); + } + + function _getStaticParams(address tokenPool) internal view returns (bytes memory) { + IUpgradeableBurnMintTokenPool_1_4 ghoTokenPool = IUpgradeableBurnMintTokenPool_1_4(tokenPool); + return + abi.encode( + ghoTokenPool.getToken(), + ghoTokenPool.getAllowListEnabled(), + ghoTokenPool.getAllowList(), + ghoTokenPool.getRouter() + ); + } + + function _getDynamicParams(address tokenPool) internal view returns (bytes memory) { + IUpgradeableBurnMintTokenPool_1_4 ghoTokenPool = IUpgradeableBurnMintTokenPool_1_4(tokenPool); + return abi.encode(ghoTokenPool.owner(), ghoTokenPool.getSupportedChains()); + } + + function _tokenBucketToConfig( + IRateLimiter.TokenBucket memory bucket + ) internal pure returns (IRateLimiter.Config memory) { + return + IRateLimiter.Config({ + isEnabled: bucket.isEnabled, + capacity: bucket.capacity, + rate: bucket.rate + }); + } + + function _getDisabledConfig() internal pure returns (IRateLimiter.Config memory) { + return IRateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}); + } + + function _getImplementation(address proxy) internal view returns (address) { + bytes32 slot = bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1); + return address(uint160(uint256(vm.load(proxy, slot)))); + } + + function _getProxyAdmin(address proxy) internal view returns (ProxyAdmin) { + bytes32 slot = bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1); + return ProxyAdmin(address(uint160(uint256(vm.load(proxy, slot))))); + } + + function _readInitialized(address proxy) internal view returns (uint8) { + return uint8(uint256(vm.load(proxy, bytes32(0)))); + } + + function _getRateLimiterConfig() internal view returns (IRateLimiter.Config memory) { + return + IRateLimiter.Config({ + isEnabled: true, + capacity: proposal.CCIP_RATE_LIMIT_CAPACITY(), + rate: proposal.CCIP_RATE_LIMIT_REFILL_RATE() + }); + } + + function _getOutboundRefillTime(uint256 amount) internal pure returns (uint256) { + return (amount / CCIP_RATE_LIMIT_REFILL_RATE) + 1; // account for rounding + } + + function _getInboundRefillTime(uint256 amount) internal pure returns (uint256) { + return amount / CCIP_RATE_LIMIT_REFILL_RATE + 1; // account for rounding + } + + function _min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + + function _isDifferenceLowerThanMax( + uint256 from, + uint256 to, + uint256 max + ) internal pure returns (bool) { + return from < to ? to - from <= max : from - to <= max; + } + + function assertEq( + IDefaultInterestRateStrategyV2.InterestRateData memory a, + IDefaultInterestRateStrategyV2.InterestRateData memory b + ) internal pure { + assertEq(a.optimalUsageRatio, b.optimalUsageRatio); + assertEq(a.baseVariableBorrowRate, b.baseVariableBorrowRate); + assertEq(a.variableRateSlope1, b.variableRateSlope1); + assertEq(a.variableRateSlope2, b.variableRateSlope2); + assertEq(abi.encode(a), abi.encode(b)); // sanity check + } + + function assertEq( + IGhoAaveSteward.BorrowRateConfig memory a, + IGhoAaveSteward.BorrowRateConfig memory b + ) internal pure { + assertEq(a.optimalUsageRatioMaxChange, b.optimalUsageRatioMaxChange); + assertEq(a.baseVariableBorrowRateMaxChange, b.baseVariableBorrowRateMaxChange); + assertEq(a.variableRateSlope1MaxChange, b.variableRateSlope1MaxChange); + assertEq(a.variableRateSlope2MaxChange, b.variableRateSlope2MaxChange); + assertEq(abi.encode(a), abi.encode(b)); // sanity check + } + + function assertEq( + IRateLimiter.TokenBucket memory bucket, + IRateLimiter.Config memory config + ) internal pure { + assertEq(bucket.isEnabled, config.isEnabled); + assertEq(bucket.capacity, config.capacity); + assertEq(bucket.rate, config.rate); + assertEq(abi.encode(_tokenBucketToConfig(bucket)), abi.encode(config)); // sanity check + } +} + +contract AaveV3Arbitrum_GHOCCIP151Upgrade_20241209_SetupAndProposalActions is + AaveV3Arbitrum_GHOCCIP151Upgrade_20241209_Base +{ + function test_defaultProposalExecution() public { + defaultTest( + 'AaveV3Arbitrum_GHOCCIP151Upgrade_20241209', + AaveV3Arbitrum.POOL, + address(proposal) + ); + } + + function test_tokenPoolOwnershipTransfer() public { + assertFalse( + TOKEN_ADMIN_REGISTRY.isAdministrator(address(GHO), GovernanceV3Arbitrum.EXECUTOR_LVL_1) + ); + ITokenAdminRegistry.TokenConfig memory tokenConfig = TOKEN_ADMIN_REGISTRY.getTokenConfig( + address(GHO) + ); + assertNotEq(tokenConfig.administrator, GovernanceV3Arbitrum.EXECUTOR_LVL_1); + assertEq(tokenConfig.pendingAdministrator, GovernanceV3Arbitrum.EXECUTOR_LVL_1); + assertEq(tokenConfig.tokenPool, address(EXISTING_PROXY_POOL)); + + assertEq(EXISTING_TOKEN_POOL.owner(), GovernanceV3Arbitrum.EXECUTOR_LVL_1); + assertEq(EXISTING_PROXY_POOL.owner(), TOKEN_ADMIN_REGISTRY.owner()); + assertEq(NEW_TOKEN_POOL.owner(), address(0)); + + executePayload(vm, address(proposal)); + + tokenConfig = TOKEN_ADMIN_REGISTRY.getTokenConfig(address(GHO)); + assertEq(tokenConfig.administrator, GovernanceV3Arbitrum.EXECUTOR_LVL_1); + assertEq(tokenConfig.pendingAdministrator, address(0)); + assertEq(tokenConfig.tokenPool, address(NEW_TOKEN_POOL)); + + assertEq(EXISTING_TOKEN_POOL.owner(), GovernanceV3Arbitrum.EXECUTOR_LVL_1); + assertEq(EXISTING_PROXY_POOL.owner(), GovernanceV3Arbitrum.EXECUTOR_LVL_1); + assertEq(NEW_TOKEN_POOL.owner(), GovernanceV3Arbitrum.EXECUTOR_LVL_1); + } + + function test_tokenPoolLiquidityMigration() public { + IGhoToken.Facilitator memory existingFacilitator = GHO.getFacilitator( + address(EXISTING_TOKEN_POOL) + ); + IGhoToken.Facilitator memory newFacilitator = GHO.getFacilitator(address(NEW_TOKEN_POOL)); + + assertEq(bytes(newFacilitator.label).length, 0); + assertEq(newFacilitator.bucketCapacity, 0); + assertEq(newFacilitator.bucketLevel, 0); + + assertEq(existingFacilitator.label, 'CCIP TokenPool'); + assertGt(existingFacilitator.bucketCapacity, 0); + assertGt(existingFacilitator.bucketLevel, 0); + + executePayload(vm, address(proposal)); + + newFacilitator = GHO.getFacilitator(address(NEW_TOKEN_POOL)); + + assertEq(newFacilitator.label, 'CCIP TokenPool v1.5.1'); + assertEq(newFacilitator.bucketCapacity, existingFacilitator.bucketCapacity); + assertEq(newFacilitator.bucketLevel, existingFacilitator.bucketLevel); + + existingFacilitator = GHO.getFacilitator(address(EXISTING_TOKEN_POOL)); + + assertEq(bytes(existingFacilitator.label).length, 0); + assertEq(existingFacilitator.bucketCapacity, 0); + assertEq(existingFacilitator.bucketLevel, 0); + } + + function test_newTokenPoolSetupAndRegistration() public { + bytes memory staticParams = _getStaticParams(address(EXISTING_TOKEN_POOL)); + bytes memory dynamicParams = _getDynamicParams(address(EXISTING_TOKEN_POOL)); + assertEq(EXISTING_TOKEN_POOL.getRateLimitAdmin(), address(EXISTING_GHO_CCIP_STEWARD)); + + assertEq(TOKEN_ADMIN_REGISTRY.getPool(address(GHO)), EXISTING_TOKEN_POOL.getProxyPool()); + + executePayload(vm, address(proposal)); + + assertEq(staticParams, _getStaticParams(address(NEW_TOKEN_POOL))); + assertEq(dynamicParams, _getDynamicParams(address(NEW_TOKEN_POOL))); + assertEq(NEW_TOKEN_POOL.getRateLimitAdmin(), address(NEW_GHO_CCIP_STEWARD)); + + assertEq(TOKEN_ADMIN_REGISTRY.getPool(address(GHO)), address(NEW_TOKEN_POOL)); + + assertEq(NEW_TOKEN_POOL.getRemotePools(ETH_CHAIN_SELECTOR).length, 2); + assertTrue(NEW_TOKEN_POOL.isRemotePool(ETH_CHAIN_SELECTOR, abi.encode(ETH_PROXY_POOL))); + assertTrue(NEW_TOKEN_POOL.isRemotePool(ETH_CHAIN_SELECTOR, abi.encode(NEW_REMOTE_POOL_ETH))); + assertEq( + NEW_TOKEN_POOL.getRemoteToken(ETH_CHAIN_SELECTOR), + abi.encode(AaveV3EthereumAssets.GHO_UNDERLYING) + ); + assertEq(NEW_TOKEN_POOL.getSupportedChains().length, 1); + assertTrue(NEW_TOKEN_POOL.isSupportedChain(ETH_CHAIN_SELECTOR)); + + assertEq( + NEW_TOKEN_POOL.getCurrentInboundRateLimiterState(ETH_CHAIN_SELECTOR), + _getRateLimiterConfig() + ); + assertEq( + NEW_TOKEN_POOL.getCurrentOutboundRateLimiterState(ETH_CHAIN_SELECTOR), + _getRateLimiterConfig() + ); + } + + function test_newTokenPoolInitialization() public { + vm.expectRevert('Initializable: contract is already initialized'); + NEW_TOKEN_POOL.initialize(makeAddr('owner'), new address[](0), makeAddr('router')); + assertEq(_readInitialized(address(NEW_TOKEN_POOL)), 1); + assertEq(_readInitialized(_getImplementation(address(NEW_TOKEN_POOL))), 255); + } + + function test_stewardRolesAndConfig() public { + bytes32 RISK_ADMIN_ROLE = AaveV3Arbitrum.ACL_MANAGER.RISK_ADMIN_ROLE(); + assertTrue( + AaveV3Arbitrum.ACL_MANAGER.hasRole(RISK_ADMIN_ROLE, address(EXISTING_GHO_AAVE_STEWARD)) + ); + assertFalse(AaveV3Arbitrum.ACL_MANAGER.hasRole(RISK_ADMIN_ROLE, address(NEW_GHO_AAVE_STEWARD))); + + assertEq(GHO_BUCKET_STEWARD.getControlledFacilitators().length, 1); + assertTrue(GHO_BUCKET_STEWARD.isControlledFacilitator(address(EXISTING_TOKEN_POOL))); + assertFalse(GHO_BUCKET_STEWARD.isControlledFacilitator(address(NEW_TOKEN_POOL))); + + assertEq(NEW_TOKEN_POOL.getRateLimitAdmin(), address(0)); + + executePayload(vm, address(proposal)); + + assertFalse( + AaveV3Arbitrum.ACL_MANAGER.hasRole(RISK_ADMIN_ROLE, address(EXISTING_GHO_AAVE_STEWARD)) + ); + assertTrue(AaveV3Arbitrum.ACL_MANAGER.hasRole(RISK_ADMIN_ROLE, address(NEW_GHO_AAVE_STEWARD))); + + assertEq(GHO_BUCKET_STEWARD.getControlledFacilitators().length, 1); + assertFalse(GHO_BUCKET_STEWARD.isControlledFacilitator(address(EXISTING_TOKEN_POOL))); + assertTrue(GHO_BUCKET_STEWARD.isControlledFacilitator(address(NEW_TOKEN_POOL))); + + assertEq(NEW_TOKEN_POOL.getRateLimitAdmin(), address(NEW_GHO_CCIP_STEWARD)); + } +} + +contract AaveV3Arbitrum_GHOCCIP151Upgrade_20241209_PostUpgrade is + AaveV3Arbitrum_GHOCCIP151Upgrade_20241209_Base +{ + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + + function setUp() public override { + super.setUp(); + + executePayload(vm, address(proposal)); + } + + function test_sendMessageSucceedsAndRoutesViaNewPool(uint256 amount) public { + uint256 bridgeableAmount = _min( + GHO.getFacilitator(address(NEW_TOKEN_POOL)).bucketLevel, + CCIP_RATE_LIMIT_CAPACITY + ); + amount = bound(amount, 1, bridgeableAmount); + skip(_getOutboundRefillTime(amount)); // wait for the rate limiter to refill + + deal(address(GHO), alice, amount); + vm.prank(alice); + GHO.approve(address(ROUTER), amount); + + uint256 aliceBalance = GHO.balanceOf(alice); + uint256 bucketLevel = GHO.getFacilitator(address(NEW_TOKEN_POOL)).bucketLevel; + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({amount: amount, sender: alice, poolVersion: CCIPUtils.PoolVersion.V1_5_1}) + ); + + vm.expectEmit(address(NEW_TOKEN_POOL)); // new token pool + emit Burned(address(ON_RAMP), amount); + + vm.expectEmit(address(ON_RAMP)); + emit CCIPSendRequested(eventArg); + + vm.prank(alice); + ROUTER.ccipSend{value: eventArg.feeTokenAmount}(ETH_CHAIN_SELECTOR, message); + + assertEq(GHO.balanceOf(alice), aliceBalance - amount); + assertEq(GHO.getFacilitator(address(NEW_TOKEN_POOL)).bucketLevel, bucketLevel - amount); + } + + // existing pool can no longer on ramp + function test_lockOrBurnRevertsOnExistingPool() public { + uint256 amount = 100_000e18; + skip(_getOutboundRefillTime(amount)); + + // router pulls tokens from the user & sends to the token pool during onRamps + deal(address(GHO), address(EXISTING_TOKEN_POOL), amount); + + vm.prank(EXISTING_TOKEN_POOL.getProxyPool()); + // underflow expected at GHO.burn() => bucketLevel - amount + vm.expectRevert(stdError.arithmeticError); + EXISTING_TOKEN_POOL.lockOrBurn( + alice, + abi.encode(alice), + amount, + ETH_CHAIN_SELECTOR, + new bytes(0) + ); + } + + // on-ramp via new pool + function test_lockOrBurnSucceedsOnNewPool(uint256 amount) public { + uint256 bridgeableAmount = _min( + GHO.getFacilitator(address(NEW_TOKEN_POOL)).bucketLevel, + CCIP_RATE_LIMIT_CAPACITY + ); + amount = bound(amount, 1, bridgeableAmount); + skip(_getOutboundRefillTime(amount)); + + // router pulls tokens from the user & sends to the token pool during onRamps + deal(address(GHO), address(NEW_TOKEN_POOL), amount); + + uint256 bucketLevel = GHO.getFacilitator(address(NEW_TOKEN_POOL)).bucketLevel; + + vm.expectEmit(address(NEW_TOKEN_POOL)); + emit Burned(address(ON_RAMP), amount); + + vm.prank(address(ON_RAMP)); + NEW_TOKEN_POOL.lockOrBurn( + IPool_CCIP.LockOrBurnInV1({ + receiver: abi.encode(alice), + remoteChainSelector: ETH_CHAIN_SELECTOR, + originalSender: alice, + amount: amount, + localToken: address(GHO) + }) + ); + + assertEq(GHO.getFacilitator(address(NEW_TOKEN_POOL)).bucketLevel, bucketLevel - amount); + assertEq(GHO.balanceOf(address(NEW_TOKEN_POOL)), 0); // dealt amount is burned + } + + // existing pool can no longer off ramp + function test_releaseOrMintRevertsOnExistingPool() public { + uint256 amount = 100_000e18; + skip(_getInboundRefillTime(amount)); // wait for the rate limiter to refill + + vm.prank(EXISTING_TOKEN_POOL.getProxyPool()); + vm.expectRevert('FACILITATOR_BUCKET_CAPACITY_EXCEEDED'); + EXISTING_TOKEN_POOL.releaseOrMint( + abi.encode(alice), + alice, + amount, + ETH_CHAIN_SELECTOR, + new bytes(0) + ); + } + + // off-ramp messages sent from new eth token pool (v1.5.1) + function test_releaseOrMintSucceedsOnNewPoolOffRampedViaNewTokenPoolEth(uint256 amount) public { + (uint256 bucketCapacity, uint256 bucketLevel) = GHO.getFacilitatorBucket( + address(NEW_TOKEN_POOL) + ); + uint256 mintAbleAmount = _min(bucketCapacity - bucketLevel, CCIP_RATE_LIMIT_CAPACITY); + amount = bound(amount, 1, mintAbleAmount); + skip(_getInboundRefillTime(amount)); // wait for the rate limiter to refill + + uint256 aliceBalance = GHO.balanceOf(alice); + + vm.expectEmit(address(NEW_TOKEN_POOL)); + emit Minted(address(OFF_RAMP), alice, amount); + + vm.prank(address(OFF_RAMP)); + NEW_TOKEN_POOL.releaseOrMint( + IPool_CCIP.ReleaseOrMintInV1({ + originalSender: abi.encode(alice), + remoteChainSelector: ETH_CHAIN_SELECTOR, + receiver: alice, + amount: amount, + localToken: address(GHO), + sourcePoolAddress: abi.encode(address(NEW_REMOTE_POOL_ETH)), + sourcePoolData: new bytes(0), + offchainTokenData: new bytes(0) + }) + ); + + assertEq(GHO.getFacilitator(address(NEW_TOKEN_POOL)).bucketLevel, bucketLevel + amount); + assertEq(GHO.balanceOf(alice), aliceBalance + amount); + } + + // off-ramp messages sent from existing eth token pool (v1.4) ie ProxyPool + function test_releaseOrMintSucceedsOnNewPoolOffRampedViaExistingTokenPoolEth( + uint256 amount + ) public { + (uint256 bucketCapacity, uint256 bucketLevel) = GHO.getFacilitatorBucket( + address(NEW_TOKEN_POOL) + ); + uint256 mintAbleAmount = _min(bucketCapacity - bucketLevel, CCIP_RATE_LIMIT_CAPACITY); + amount = bound(amount, 1, mintAbleAmount); + skip(_getInboundRefillTime(amount)); + + uint256 aliceBalance = GHO.balanceOf(alice); + + vm.expectEmit(address(NEW_TOKEN_POOL)); + emit Minted(address(OFF_RAMP), alice, amount); + + vm.prank(address(OFF_RAMP)); + NEW_TOKEN_POOL.releaseOrMint( + IPool_CCIP.ReleaseOrMintInV1({ + originalSender: abi.encode(alice), + remoteChainSelector: ETH_CHAIN_SELECTOR, + receiver: alice, + amount: amount, + localToken: address(GHO), + sourcePoolAddress: abi.encode(address(ETH_PROXY_POOL)), + sourcePoolData: new bytes(0), + offchainTokenData: new bytes(0) + }) + ); + + assertEq(GHO.getFacilitator(address(NEW_TOKEN_POOL)).bucketLevel, bucketLevel + amount); + assertEq(GHO.balanceOf(alice), aliceBalance + amount); + } + + function test_ccipStewardCanChangeAndDisableRateLimit() public { + assertEq(NEW_TOKEN_POOL.getRateLimitAdmin(), address(NEW_GHO_CCIP_STEWARD)); // sanity + + IRateLimiter.Config memory outboundConfig = IRateLimiter.Config({ + isEnabled: true, + capacity: 500_000e18, + rate: 100e18 + }); + IRateLimiter.Config memory inboundConfig = IRateLimiter.Config({ + isEnabled: true, + capacity: 100_000e18, + rate: 50e18 + }); + + // we assert the steward can change the rate limit + vm.prank(NEW_GHO_CCIP_STEWARD.RISK_COUNCIL()); + NEW_GHO_CCIP_STEWARD.updateRateLimit( + ETH_CHAIN_SELECTOR, + outboundConfig.isEnabled, + outboundConfig.capacity, + outboundConfig.rate, + inboundConfig.isEnabled, + inboundConfig.capacity, + inboundConfig.rate + ); + + assertEq(NEW_TOKEN_POOL.getCurrentOutboundRateLimiterState(ETH_CHAIN_SELECTOR), outboundConfig); + assertEq(NEW_TOKEN_POOL.getCurrentInboundRateLimiterState(ETH_CHAIN_SELECTOR), inboundConfig); + assertEq(NEW_GHO_CCIP_STEWARD.getCcipTimelocks().rateLimitLastUpdate, vm.getBlockTimestamp()); + + skip(NEW_GHO_CCIP_STEWARD.MINIMUM_DELAY() + 1); + + // now we assert the steward can disable the rate limit + vm.prank(NEW_GHO_CCIP_STEWARD.RISK_COUNCIL()); + NEW_GHO_CCIP_STEWARD.updateRateLimit(ETH_CHAIN_SELECTOR, false, 0, 0, false, 0, 0); + + assertEq( + NEW_TOKEN_POOL.getCurrentOutboundRateLimiterState(ETH_CHAIN_SELECTOR), + _getDisabledConfig() + ); + assertEq( + NEW_TOKEN_POOL.getCurrentInboundRateLimiterState(ETH_CHAIN_SELECTOR), + _getDisabledConfig() + ); + assertEq(NEW_GHO_CCIP_STEWARD.getCcipTimelocks().rateLimitLastUpdate, vm.getBlockTimestamp()); + } + + function test_aaveStewardCanUpdateBorrowRate() public { + IDefaultInterestRateStrategyV2 irStrategy = IDefaultInterestRateStrategyV2( + AaveV3Arbitrum.AAVE_PROTOCOL_DATA_PROVIDER.getInterestRateStrategyAddress(address(GHO)) + ); + + IDefaultInterestRateStrategyV2.InterestRateData + memory currentRateData = IDefaultInterestRateStrategyV2.InterestRateData({ + optimalUsageRatio: 90_00, + baseVariableBorrowRate: 0, + variableRateSlope1: 12_50, + variableRateSlope2: 40_00 + }); + + assertEq(irStrategy.getInterestRateDataBps(address(GHO)), currentRateData); + + currentRateData.variableRateSlope1 -= 4_00; + currentRateData.variableRateSlope2 -= 3_00; + + vm.prank(NEW_GHO_AAVE_STEWARD.RISK_COUNCIL()); + NEW_GHO_AAVE_STEWARD.updateGhoBorrowRate( + currentRateData.optimalUsageRatio, + currentRateData.baseVariableBorrowRate, + currentRateData.variableRateSlope1, + currentRateData.variableRateSlope2 + ); + + assertEq(irStrategy.getInterestRateDataBps(address(GHO)), currentRateData); + assertEq( + NEW_GHO_AAVE_STEWARD.getGhoTimelocks().ghoBorrowRateLastUpdate, + vm.getBlockTimestamp() + ); + } + + function test_aaveStewardCanUpdateBorrowCap(uint256 newBorrowCap) public { + uint256 currentBorrowCap = AaveV3Arbitrum.POOL.getConfiguration(address(GHO)).getBorrowCap(); + assertEq(currentBorrowCap, 22_500_000); + vm.assume( + newBorrowCap != currentBorrowCap && + _isDifferenceLowerThanMax(currentBorrowCap, newBorrowCap, currentBorrowCap) + ); + + vm.prank(NEW_GHO_AAVE_STEWARD.RISK_COUNCIL()); + NEW_GHO_AAVE_STEWARD.updateGhoBorrowCap(newBorrowCap); + + assertEq(AaveV3Arbitrum.POOL.getConfiguration(address(GHO)).getBorrowCap(), newBorrowCap); + assertEq(NEW_GHO_AAVE_STEWARD.getGhoTimelocks().ghoBorrowCapLastUpdate, vm.getBlockTimestamp()); + } + + function test_aaveStewardCanUpdateSupplyCap(uint256 newSupplyCap) public { + uint256 currentSupplyCap = AaveV3Arbitrum.POOL.getConfiguration(address(GHO)).getSupplyCap(); + assertEq(currentSupplyCap, 25_000_000); + vm.assume( + currentSupplyCap != newSupplyCap && + _isDifferenceLowerThanMax(currentSupplyCap, newSupplyCap, currentSupplyCap) + ); + + vm.prank(NEW_GHO_AAVE_STEWARD.RISK_COUNCIL()); + NEW_GHO_AAVE_STEWARD.updateGhoSupplyCap(newSupplyCap); + + assertEq(AaveV3Arbitrum.POOL.getConfiguration(address(GHO)).getSupplyCap(), newSupplyCap); + assertEq(NEW_GHO_AAVE_STEWARD.getGhoTimelocks().ghoSupplyCapLastUpdate, vm.getBlockTimestamp()); + } +} diff --git a/src/20241209_Multi_GHOCCIP151Upgrade/AaveV3E2E_GHOCCIP151Upgrade_20241209.t.sol b/src/20241209_Multi_GHOCCIP151Upgrade/AaveV3E2E_GHOCCIP151Upgrade_20241209.t.sol new file mode 100644 index 000000000..533474dab --- /dev/null +++ b/src/20241209_Multi_GHOCCIP151Upgrade/AaveV3E2E_GHOCCIP151Upgrade_20241209.t.sol @@ -0,0 +1,645 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; + +import {IUpgradeableLockReleaseTokenPool_1_4, IUpgradeableLockReleaseTokenPool_1_5_1} from 'src/interfaces/ccip/tokenPool/IUpgradeableLockReleaseTokenPool.sol'; +import {IUpgradeableBurnMintTokenPool_1_4, IUpgradeableBurnMintTokenPool_1_5_1} from 'src/interfaces/ccip/tokenPool/IUpgradeableBurnMintTokenPool.sol'; +import {IClient} from 'src/interfaces/ccip/IClient.sol'; +import {IInternal} from 'src/interfaces/ccip/IInternal.sol'; +import {IRouter} from 'src/interfaces/ccip/IRouter.sol'; +import {IEVM2EVMOnRamp} from 'src/interfaces/ccip/IEVM2EVMOnRamp.sol'; +import {IEVM2EVMOffRamp_1_5} from 'src/interfaces/ccip/IEVM2EVMOffRamp.sol'; +import {ITokenAdminRegistry} from 'src/interfaces/ccip/ITokenAdminRegistry.sol'; +import {IPriceRegistry} from 'src/interfaces/ccip/IPriceRegistry.sol'; +import {IProxyPool} from 'src/interfaces/ccip/IProxyPool.sol'; +import {IGhoToken} from 'src/interfaces/IGhoToken.sol'; +import {IGhoCcipSteward} from 'src/interfaces/IGhoCcipSteward.sol'; + +import {AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; +import {AaveV3ArbitrumAssets} from 'aave-address-book/AaveV3Arbitrum.sol'; +import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; +import {MiscArbitrum} from 'aave-address-book/MiscArbitrum.sol'; +import {GovernanceV3Ethereum} from 'aave-address-book/GovernanceV3Ethereum.sol'; +import {GovernanceV3Arbitrum} from 'aave-address-book/GovernanceV3Arbitrum.sol'; + +import {ProtocolV3TestBase} from 'aave-helpers/src/ProtocolV3TestBase.sol'; + +import {CCIPUtils} from './utils/CCIPUtils.sol'; +import {AaveV3Ethereum_GHOCCIP151Upgrade_20241209} from './AaveV3Ethereum_GHOCCIP151Upgrade_20241209.sol'; +import {AaveV3Arbitrum_GHOCCIP151Upgrade_20241209} from './AaveV3Arbitrum_GHOCCIP151Upgrade_20241209.sol'; + +/** + * @dev Test for AaveV3E2E_GHOCCIP151Upgrade_20241209 + * command: FOUNDRY_PROFILE=mainnet forge test --match-path=src/20241209_Multi_GHOCCIP151Upgrade/AaveV3E2E_GHOCCIP151Upgrade_20241209.t.sol -vv + */ +contract AaveV3E2E_GHOCCIP151Upgrade_20241209_Base is ProtocolV3TestBase { + struct CCIPSendParams { + IRouter router; + IGhoToken token; + uint256 amount; + uint64 sourceChainSelector; + uint64 destinationChainSelector; + address sender; + CCIPUtils.PoolVersion poolVersion; + } + + struct Common { + IRouter router; + IGhoToken token; + IEVM2EVMOnRamp EVM2EVMOnRamp; + IEVM2EVMOffRamp_1_5 EVM2EVMOffRamp; + ITokenAdminRegistry tokenAdminRegistry; + IGhoCcipSteward newGhoCcipSteward; + IPriceRegistry priceRegistry; + address proxyPool; + uint64 chainSelector; + uint256 forkId; + } + + struct L1 { + AaveV3Ethereum_GHOCCIP151Upgrade_20241209 proposal; + IUpgradeableLockReleaseTokenPool_1_5_1 newTokenPool; + IUpgradeableLockReleaseTokenPool_1_4 existingTokenPool; + Common c; + } + + struct L2 { + AaveV3Arbitrum_GHOCCIP151Upgrade_20241209 proposal; + IUpgradeableBurnMintTokenPool_1_5_1 newTokenPool; + IUpgradeableBurnMintTokenPool_1_4 existingTokenPool; + Common c; + } + + L1 internal l1; + L2 internal l2; + + address internal alice = makeAddr('alice'); + address internal bob = makeAddr('bob'); + address internal carol = makeAddr('carol'); + + uint256 internal constant CCIP_RATE_LIMIT_CAPACITY = 300_000e18; + uint256 internal constant CCIP_RATE_LIMIT_REFILL_RATE = 60e18; + + event CCIPSendRequested(IInternal.EVM2EVMMessage message); + event Locked(address indexed sender, uint256 amount); + event Burned(address indexed sender, uint256 amount); + event Released(address indexed sender, address indexed recipient, uint256 amount); + event Minted(address indexed sender, address indexed recipient, uint256 amount); + + function setUp() public virtual { + l1.c.forkId = vm.createFork(vm.rpcUrl('mainnet'), 21581477); + l2.c.forkId = vm.createFork(vm.rpcUrl('arbitrum'), 293345614); + + vm.selectFork(l1.c.forkId); + l1.proposal = new AaveV3Ethereum_GHOCCIP151Upgrade_20241209(); + l1.existingTokenPool = IUpgradeableLockReleaseTokenPool_1_4( + 0x5756880B6a1EAba0175227bf02a7E87c1e02B28C + ); // MiscEthereum.GHO_CCIP_TOKEN_POOL; will be updated in address-book after AIP + l1.newTokenPool = IUpgradeableLockReleaseTokenPool_1_5_1( + 0x06179f7C1be40863405f374E7f5F8806c728660A + ); + l1.c.router = IRouter(l1.existingTokenPool.getRouter()); + l2.c.chainSelector = l1.existingTokenPool.getSupportedChains()[0]; + l1.c.token = IGhoToken(address(l1.existingTokenPool.getToken())); + l1.c.EVM2EVMOnRamp = IEVM2EVMOnRamp(l1.c.router.getOnRamp(l2.c.chainSelector)); + l1.c.EVM2EVMOffRamp = IEVM2EVMOffRamp_1_5(0xdf615eF8D4C64d0ED8Fd7824BBEd2f6a10245aC9); // new offramp + l1.c.tokenAdminRegistry = ITokenAdminRegistry(0xb22764f98dD05c789929716D677382Df22C05Cb6); + l1.c.priceRegistry = IPriceRegistry(l1.c.EVM2EVMOnRamp.getDynamicConfig().priceRegistry); + l1.c.proxyPool = l1.existingTokenPool.getProxyPool(); + + vm.selectFork(l2.c.forkId); + l2.proposal = new AaveV3Arbitrum_GHOCCIP151Upgrade_20241209(); + l2.existingTokenPool = IUpgradeableBurnMintTokenPool_1_4( + 0xF168B83598516A532a85995b52504a2Fa058C068 + ); // MiscArbitrum.GHO_CCIP_TOKEN_POOL; will be updated in address-book after AIP + l2.newTokenPool = IUpgradeableBurnMintTokenPool_1_5_1( + 0xB94Ab28c6869466a46a42abA834ca2B3cECCA5eB + ); + l2.c.router = IRouter(l2.existingTokenPool.getRouter()); + l1.c.chainSelector = l2.existingTokenPool.getSupportedChains()[0]; + l2.c.token = IGhoToken(address(l2.existingTokenPool.getToken())); + l2.c.EVM2EVMOnRamp = IEVM2EVMOnRamp(l2.c.router.getOnRamp(l1.c.chainSelector)); + l2.c.EVM2EVMOffRamp = IEVM2EVMOffRamp_1_5(0x91e46cc5590A4B9182e47f40006140A7077Dec31); // new offramp + l2.c.tokenAdminRegistry = ITokenAdminRegistry(0x39AE1032cF4B334a1Ed41cdD0833bdD7c7E7751E); + l2.c.priceRegistry = IPriceRegistry(l2.c.EVM2EVMOnRamp.getDynamicConfig().priceRegistry); + l2.c.proxyPool = l2.existingTokenPool.getProxyPool(); + + _validateConfig({upgraded: false}); + + _performPoolTransferCLLPreReq(); // rm once CLL performs this action + } + + function _getTokenMessage( + CCIPSendParams memory params + ) internal returns (IClient.EVM2AnyMessage memory, IInternal.EVM2EVMMessage memory) { + IClient.EVM2AnyMessage memory message = CCIPUtils.generateMessage(params.sender, 1); + message.tokenAmounts[0] = IClient.EVMTokenAmount({ + token: address(params.token), + amount: params.amount + }); + + uint256 feeAmount = params.router.getFee(params.destinationChainSelector, message); + deal(params.sender, feeAmount); + + IInternal.EVM2EVMMessage memory eventArg = CCIPUtils.messageToEvent( + CCIPUtils.MessageToEventParams({ + message: message, + router: params.router, + sourceChainSelector: params.sourceChainSelector, + feeTokenAmount: feeAmount, + originalSender: params.sender, + sourceToken: address(params.token), + destinationToken: address(params.token == l1.c.token ? l2.c.token : l1.c.token), + poolVersion: params.poolVersion + }) + ); + + return (message, eventArg); + } + + function _validateConfig(bool upgraded) internal { + vm.selectFork(l1.c.forkId); + assertEq(l1.c.chainSelector, 5009297550715157269); + assertEq(address(l1.c.token), AaveV3EthereumAssets.GHO_UNDERLYING); + assertEq(l1.c.router.typeAndVersion(), 'Router 1.2.0'); + assertEq(l1.c.EVM2EVMOnRamp.typeAndVersion(), 'EVM2EVMOnRamp 1.5.0'); + assertEq(l1.c.EVM2EVMOffRamp.typeAndVersion(), 'EVM2EVMOffRamp 1.5.0'); + assertEq(l1.existingTokenPool.typeAndVersion(), 'LockReleaseTokenPool 1.4.0'); + assertEq(l1.newTokenPool.typeAndVersion(), 'LockReleaseTokenPool 1.5.1'); + assertEq(l1.c.tokenAdminRegistry.typeAndVersion(), 'TokenAdminRegistry 1.5.0'); + assertEq(l1.c.priceRegistry.typeAndVersion(), 'PriceRegistry 1.2.0'); + assertEq( + l1.c.EVM2EVMOnRamp.getDynamicConfig().priceRegistry, + l1.c.EVM2EVMOffRamp.getDynamicConfig().priceRegistry + ); + assertTrue(l1.c.router.isOffRamp(l2.c.chainSelector, address(l1.c.EVM2EVMOffRamp))); + assertEq(l1.c.router.getOnRamp(l2.c.chainSelector), address(l1.c.EVM2EVMOnRamp)); + + // proposal constants + assertEq(address(l1.proposal.TOKEN_ADMIN_REGISTRY()), address(l1.c.tokenAdminRegistry)); + assertEq(l1.proposal.ARB_CHAIN_SELECTOR(), l2.c.chainSelector); + assertEq(address(l1.proposal.EXISTING_PROXY_POOL()), l1.c.proxyPool); + assertEq(address(l1.proposal.EXISTING_TOKEN_POOL()), address(l1.existingTokenPool)); + assertEq(address(l1.proposal.EXISTING_REMOTE_POOL_ARB()), l2.c.proxyPool); + assertEq(address(l1.proposal.NEW_TOKEN_POOL()), address(l1.newTokenPool)); + assertEq(address(l1.proposal.NEW_REMOTE_POOL_ARB()), address(l2.newTokenPool)); + assertEq(l1.proposal.CCIP_RATE_LIMIT_CAPACITY(), CCIP_RATE_LIMIT_CAPACITY); + assertEq(l1.proposal.CCIP_RATE_LIMIT_REFILL_RATE(), CCIP_RATE_LIMIT_REFILL_RATE); + + if (upgraded) { + assertEq(IProxyPool(l1.c.proxyPool).owner(), GovernanceV3Ethereum.EXECUTOR_LVL_1); + assertEq(l1.newTokenPool.owner(), GovernanceV3Ethereum.EXECUTOR_LVL_1); + + assertEq(l1.c.tokenAdminRegistry.getPool(address(l1.c.token)), address(l1.newTokenPool)); + + assertEq(l1.c.token.balanceOf(address(l1.existingTokenPool)), 0); + // we are not resetting currentBridgedAmount on the existing pool, the pool is deprecated by + // resetting the bridge limit + assertNotEq(l1.existingTokenPool.getCurrentBridgedAmount(), 0); + assertEq(l1.existingTokenPool.getBridgeLimit(), 0); + + assertGt(l1.c.token.balanceOf(address(l1.newTokenPool)), 0); + assertGt(l1.newTokenPool.getCurrentBridgedAmount(), 0); + assertGt(l1.newTokenPool.getBridgeLimit(), 0); + } else { + assertEq(l1.c.tokenAdminRegistry.getPool(address(l1.c.token)), l1.c.proxyPool); + + assertGt(l1.c.token.balanceOf(address(l1.existingTokenPool)), 0); + assertGt(l1.existingTokenPool.getCurrentBridgedAmount(), 0); + assertGt(l1.existingTokenPool.getBridgeLimit(), 0); + + assertEq(l1.c.token.balanceOf(address(l1.newTokenPool)), 0); + assertEq(l1.newTokenPool.getCurrentBridgedAmount(), 0); + assertGt(l1.newTokenPool.getBridgeLimit(), 0); + } + + vm.selectFork(l2.c.forkId); + assertEq(l2.c.chainSelector, 4949039107694359620); + assertEq(address(l2.c.token), AaveV3ArbitrumAssets.GHO_UNDERLYING); + assertEq(l2.c.router.typeAndVersion(), 'Router 1.2.0'); + assertEq(l2.c.EVM2EVMOnRamp.typeAndVersion(), 'EVM2EVMOnRamp 1.5.0'); + assertEq(l2.c.EVM2EVMOffRamp.typeAndVersion(), 'EVM2EVMOffRamp 1.5.0'); + assertEq(l2.existingTokenPool.typeAndVersion(), 'BurnMintTokenPool 1.4.0'); + assertEq(l2.newTokenPool.typeAndVersion(), 'BurnMintTokenPool 1.5.1'); + assertEq(l2.c.tokenAdminRegistry.typeAndVersion(), 'TokenAdminRegistry 1.5.0'); + assertEq(l2.c.priceRegistry.typeAndVersion(), 'PriceRegistry 1.2.0'); + assertEq( + l2.c.EVM2EVMOnRamp.getDynamicConfig().priceRegistry, + l2.c.EVM2EVMOffRamp.getDynamicConfig().priceRegistry + ); + assertTrue(l2.c.router.isOffRamp(l1.c.chainSelector, address(l2.c.EVM2EVMOffRamp))); + assertEq(l2.c.router.getOnRamp(l1.c.chainSelector), address(l2.c.EVM2EVMOnRamp)); + + // proposal constants + assertEq(address(l2.proposal.TOKEN_ADMIN_REGISTRY()), address(l2.c.tokenAdminRegistry)); + assertEq(l2.proposal.ETH_CHAIN_SELECTOR(), l1.c.chainSelector); + assertEq(address(l2.proposal.EXISTING_PROXY_POOL()), l2.c.proxyPool); + assertEq(address(l2.proposal.EXISTING_TOKEN_POOL()), address(l2.existingTokenPool)); + assertEq(address(l2.proposal.EXISTING_REMOTE_POOL_ETH()), l1.c.proxyPool); + assertEq(address(l2.proposal.NEW_TOKEN_POOL()), address(l2.newTokenPool)); + assertEq(address(l2.proposal.NEW_REMOTE_POOL_ETH()), address(l1.newTokenPool)); + assertEq(l2.proposal.CCIP_RATE_LIMIT_CAPACITY(), CCIP_RATE_LIMIT_CAPACITY); + assertEq(l2.proposal.CCIP_RATE_LIMIT_REFILL_RATE(), CCIP_RATE_LIMIT_REFILL_RATE); + + if (upgraded) { + assertEq(IProxyPool(l2.c.proxyPool).owner(), GovernanceV3Arbitrum.EXECUTOR_LVL_1); + assertEq(l2.newTokenPool.owner(), GovernanceV3Arbitrum.EXECUTOR_LVL_1); + + assertEq(l2.c.tokenAdminRegistry.getPool(address(l2.c.token)), address(l2.newTokenPool)); + assertEq(bytes(l2.c.token.getFacilitator(address(l2.existingTokenPool)).label).length, 0); + assertEq(l2.c.token.getFacilitator(address(l2.newTokenPool)).label, 'CCIP TokenPool v1.5.1'); + } else { + assertEq(l2.c.tokenAdminRegistry.getPool(address(l2.c.token)), l2.c.proxyPool); + assertEq(l2.c.token.getFacilitator(address(l2.existingTokenPool)).label, 'CCIP TokenPool'); + assertEq(bytes(l2.c.token.getFacilitator(address(l2.newTokenPool)).label).length, 0); + } + } + + function _executeUpgradeAIP() internal { + vm.selectFork(l1.c.forkId); + executePayload(vm, address(l1.proposal)); + vm.selectFork(l2.c.forkId); + executePayload(vm, address(l2.proposal)); + } + + function _performPoolTransferCLLPreReq() private { + vm.selectFork(l1.c.forkId); + vm.startPrank(l1.c.tokenAdminRegistry.owner()); + l1.c.tokenAdminRegistry.transferAdminRole( + AaveV3EthereumAssets.GHO_UNDERLYING, + GovernanceV3Ethereum.EXECUTOR_LVL_1 + ); + IProxyPool(l1.c.proxyPool).transferOwnership(GovernanceV3Ethereum.EXECUTOR_LVL_1); + vm.stopPrank(); + + vm.selectFork(l2.c.forkId); + vm.startPrank(l2.c.tokenAdminRegistry.owner()); + l2.c.tokenAdminRegistry.transferAdminRole( + AaveV3ArbitrumAssets.GHO_UNDERLYING, + GovernanceV3Arbitrum.EXECUTOR_LVL_1 + ); + IProxyPool(l2.c.proxyPool).transferOwnership(GovernanceV3Arbitrum.EXECUTOR_LVL_1); + vm.stopPrank(); + } + + function _getOutboundRefillTime(uint256 amount) internal pure returns (uint256) { + return (amount / CCIP_RATE_LIMIT_REFILL_RATE) + 1; // account for rounding + } + + function _getInboundRefillTime(uint256 amount) internal pure returns (uint256) { + return (amount / CCIP_RATE_LIMIT_REFILL_RATE) + 1; // account for rounding + } + + function _min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + + // @dev refresh token prices to the last stored such that price is not stale + // @dev assumed c.forkId is already active + function _refreshGasAndTokenPrices(Common memory c) internal { + uint64 destChainSelector = c.forkId == l1.c.forkId ? l2.c.chainSelector : l1.c.chainSelector; + address bridgeToken = address(c.token); + address feeToken = c.router.getWrappedNative(); // needed as we do tests with wrapped native as fee token + address linkToken = c.EVM2EVMOnRamp.getStaticConfig().linkToken; // needed as feeTokenAmount is converted to linkTokenAmount + IInternal.TokenPriceUpdate[] memory tokenPriceUpdates = new IInternal.TokenPriceUpdate[](3); + IInternal.GasPriceUpdate[] memory gasPriceUpdates = new IInternal.GasPriceUpdate[](1); + + tokenPriceUpdates[0] = IInternal.TokenPriceUpdate({ + sourceToken: bridgeToken, + usdPerToken: c.priceRegistry.getTokenPrice(bridgeToken).value + }); + tokenPriceUpdates[1] = IInternal.TokenPriceUpdate({ + sourceToken: feeToken, + usdPerToken: c.priceRegistry.getTokenPrice(feeToken).value + }); + tokenPriceUpdates[2] = IInternal.TokenPriceUpdate({ + sourceToken: linkToken, + usdPerToken: c.priceRegistry.getTokenPrice(linkToken).value + }); + + gasPriceUpdates[0] = IInternal.GasPriceUpdate({ + destChainSelector: destChainSelector, + usdPerUnitGas: c.priceRegistry.getDestinationChainGasPrice(destChainSelector).value + }); + + vm.prank(c.priceRegistry.owner()); + c.priceRegistry.updatePrices( + IInternal.PriceUpdates({ + tokenPriceUpdates: tokenPriceUpdates, + gasPriceUpdates: gasPriceUpdates + }) + ); + } + + // post upgrade + function _runEthToArb(address user, uint256 amount) internal { + vm.selectFork(l1.c.forkId); + + skip(_getOutboundRefillTime(amount)); // wait for the rate limiter to refill + _refreshGasAndTokenPrices(l1.c); + + vm.prank(user); + l1.c.token.approve(address(l1.c.router), amount); + + uint256 tokenPoolBalance = l1.c.token.balanceOf(address(l1.newTokenPool)); + uint256 userBalance = l1.c.token.balanceOf(user); + uint256 bridgedAmount = l1.newTokenPool.getCurrentBridgedAmount(); + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({ + router: l1.c.router, + token: l1.c.token, + amount: amount, + sourceChainSelector: l1.c.chainSelector, + destinationChainSelector: l2.c.chainSelector, + sender: user, + poolVersion: CCIPUtils.PoolVersion.V1_5_1 + }) + ); + + vm.expectEmit(address(l1.newTokenPool)); + emit Locked(address(l1.c.EVM2EVMOnRamp), amount); + + vm.expectEmit(address(l1.c.EVM2EVMOnRamp)); + emit CCIPSendRequested(eventArg); + vm.prank(user); + l1.c.router.ccipSend{value: eventArg.feeTokenAmount}(l2.c.chainSelector, message); + + assertEq(l1.c.token.balanceOf(address(l1.newTokenPool)), tokenPoolBalance + amount); + assertEq(l1.c.token.balanceOf(user), userBalance - amount); + assertEq(l1.newTokenPool.getCurrentBridgedAmount(), bridgedAmount + amount); + + // ARB executeMessage + vm.selectFork(l2.c.forkId); + + skip(_getInboundRefillTime(amount)); + _refreshGasAndTokenPrices(l2.c); + + userBalance = l2.c.token.balanceOf(user); + uint256 bucketLevel = l2.c.token.getFacilitator(address(l2.newTokenPool)).bucketLevel; + + vm.expectEmit(address(l2.newTokenPool)); + emit Minted(address(l2.c.EVM2EVMOffRamp), user, amount); + vm.prank(address(l2.c.EVM2EVMOffRamp)); + l2.c.EVM2EVMOffRamp.executeSingleMessage( + eventArg, + new bytes[](message.tokenAmounts.length), + new uint32[](0) + ); + + assertEq(l2.c.token.balanceOf(user), userBalance + amount); + assertEq(l2.c.token.getFacilitator(address(l2.newTokenPool)).bucketLevel, bucketLevel + amount); + } + + // post upgrade + function _runArbToEth(address user, uint256 amount) internal { + vm.selectFork(l2.c.forkId); + + skip(_getOutboundRefillTime(amount)); + _refreshGasAndTokenPrices(l2.c); + + vm.prank(user); + l2.c.token.approve(address(l2.c.router), amount); + + uint256 userBalance = l2.c.token.balanceOf(user); + uint256 bucketLevel = l2.c.token.getFacilitator(address(l2.newTokenPool)).bucketLevel; + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({ + router: l2.c.router, + token: l2.c.token, + amount: amount, + sourceChainSelector: l2.c.chainSelector, + destinationChainSelector: l1.c.chainSelector, + sender: user, + poolVersion: CCIPUtils.PoolVersion.V1_5_1 + }) + ); + + vm.expectEmit(address(l2.newTokenPool)); + emit Burned(address(l2.c.EVM2EVMOnRamp), amount); + + vm.expectEmit(address(l2.c.EVM2EVMOnRamp)); + emit CCIPSendRequested(eventArg); + vm.prank(user); + l2.c.router.ccipSend{value: eventArg.feeTokenAmount}(l1.c.chainSelector, message); + + assertEq(l2.c.token.balanceOf(user), userBalance - amount); + assertEq(l2.c.token.getFacilitator(address(l2.newTokenPool)).bucketLevel, bucketLevel - amount); + + // ETH executeMessage + vm.selectFork(l1.c.forkId); + + skip(_getInboundRefillTime(amount)); + _refreshGasAndTokenPrices(l1.c); + + uint256 tokenPoolBalance = l1.c.token.balanceOf(address(l1.newTokenPool)); + uint256 bridgedAmount = l1.newTokenPool.getCurrentBridgedAmount(); + userBalance = l1.c.token.balanceOf(user); + + vm.expectEmit(address(l1.newTokenPool)); + emit Released(address(l1.c.EVM2EVMOffRamp), user, amount); + vm.prank(address(l1.c.EVM2EVMOffRamp)); + l1.c.EVM2EVMOffRamp.executeSingleMessage( + eventArg, + new bytes[](message.tokenAmounts.length), + new uint32[](1) // tokenGasOverrides + ); + + assertEq(l1.c.token.balanceOf(address(l1.newTokenPool)), tokenPoolBalance - amount); + assertEq(l1.newTokenPool.getCurrentBridgedAmount(), bridgedAmount - amount); + assertEq(l1.c.token.balanceOf(user), userBalance + amount); + } +} + +contract AaveV3E2E_GHOCCIP151Upgrade_20241209_PostUpgrade is + AaveV3E2E_GHOCCIP151Upgrade_20241209_Base +{ + function setUp() public override { + super.setUp(); + + _executeUpgradeAIP(); + + _validateConfig({upgraded: true}); + } + + function test_E2E_FromEth(uint256 amount) public { + vm.selectFork(l1.c.forkId); + uint256 bridgeableAmount = _min( + l1.newTokenPool.getBridgeLimit() - l1.newTokenPool.getCurrentBridgedAmount(), + CCIP_RATE_LIMIT_CAPACITY + ); + amount = bound(amount, 1, bridgeableAmount); + deal(address(l1.c.token), alice, amount); + + _runEthToArb(alice, amount); + _runArbToEth(alice, amount); + } + + function test_E2E_FromArb(uint256 amount) public { + vm.selectFork(l2.c.forkId); + uint256 bridgeableAmount = _min( + l2.c.token.getFacilitator(address(l2.newTokenPool)).bucketLevel, + CCIP_RATE_LIMIT_CAPACITY + ); + + amount = bound(amount, 1, bridgeableAmount); + deal(address(l2.c.token), alice, amount); + + _runArbToEth(alice, amount); + _runEthToArb(alice, amount); + } +} + +// sendMsg => upgrade => executeMsg +contract AaveV3E2E_GHOCCIP151Upgrade_20241209_InFlightUpgrade is + AaveV3E2E_GHOCCIP151Upgrade_20241209_Base +{ + function test_E2E_InFlightMsg_FromEth() public { + vm.selectFork(l1.c.forkId); + + uint256 amount = 100_000e18; + deal(address(l1.c.token), alice, amount); + skip(_getOutboundRefillTime(amount)); + + vm.prank(alice); + l1.c.token.approve(address(l1.c.router), amount); + + uint256 tokenPoolBalance = l1.c.token.balanceOf(address(l1.existingTokenPool)); + uint256 aliceBalance = l1.c.token.balanceOf(alice); + uint256 bridgedAmount = l1.existingTokenPool.getCurrentBridgedAmount(); + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({ + router: l1.c.router, + token: l1.c.token, + amount: amount, + sourceChainSelector: l1.c.chainSelector, + destinationChainSelector: l2.c.chainSelector, + sender: alice, + poolVersion: CCIPUtils.PoolVersion.V1_5_0 // existing token pool + }) + ); + + // message sent from existing token pool, pre-AIP-execution + vm.expectEmit(address(l1.existingTokenPool)); + emit Locked(l1.c.proxyPool, amount); + vm.expectEmit(l1.c.proxyPool); + emit Locked(address(l1.c.EVM2EVMOnRamp), amount); + + vm.expectEmit(address(l1.c.EVM2EVMOnRamp)); + emit CCIPSendRequested(eventArg); + vm.prank(alice); + l1.c.router.ccipSend{value: eventArg.feeTokenAmount}(l2.c.chainSelector, message); + + assertEq(l1.c.token.balanceOf(address(l1.existingTokenPool)), tokenPoolBalance + amount); + assertEq(l1.c.token.balanceOf(alice), aliceBalance - amount); + assertEq(l1.existingTokenPool.getCurrentBridgedAmount(), bridgedAmount + amount); + + _executeUpgradeAIP(); // token pools upgraded + + // ARB executeMessage + vm.selectFork(l2.c.forkId); + + skip(_getInboundRefillTime(amount)); + aliceBalance = l2.c.token.balanceOf(alice); + uint256 bucketLevel = l2.c.token.getFacilitator(address(l2.newTokenPool)).bucketLevel; + + vm.expectEmit(address(l2.newTokenPool)); + emit Minted(address(l2.c.EVM2EVMOffRamp), alice, amount); + vm.prank(address(l2.c.EVM2EVMOffRamp)); + l2.c.EVM2EVMOffRamp.executeSingleMessage( + eventArg, // pre-upgrade message + new bytes[](message.tokenAmounts.length), + new uint32[](0) + ); + + assertEq(l2.c.token.balanceOf(alice), aliceBalance + amount); + assertEq(l2.c.token.getFacilitator(address(l2.newTokenPool)).bucketLevel, bucketLevel + amount); + + // send tokens back to eth + _runArbToEth(alice, amount); + } + + function test_E2E_InFlightMsg_FromArb() public { + vm.selectFork(l2.c.forkId); + + uint256 amount = 100_000e18; + deal(address(l2.c.token), alice, amount); + skip(_getOutboundRefillTime(amount)); + + vm.prank(alice); + l2.c.token.approve(address(l2.c.router), amount); + + uint256 aliceBalance = l2.c.token.balanceOf(alice); + uint256 bucketLevel = l2.c.token.getFacilitator(address(l2.existingTokenPool)).bucketLevel; + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({ + router: l2.c.router, + token: l2.c.token, + amount: amount, + sourceChainSelector: l2.c.chainSelector, + destinationChainSelector: l1.c.chainSelector, + sender: alice, + poolVersion: CCIPUtils.PoolVersion.V1_5_0 // existing token pool + }) + ); + + // message sent from existing token pool, pre-AIP-execution + vm.expectEmit(address(l2.existingTokenPool)); + emit Burned(l2.c.proxyPool, amount); + vm.expectEmit(l2.c.proxyPool); + emit Burned(address(l2.c.EVM2EVMOnRamp), amount); + + vm.expectEmit(address(l2.c.EVM2EVMOnRamp)); + emit CCIPSendRequested(eventArg); + vm.prank(alice); + l2.c.router.ccipSend{value: eventArg.feeTokenAmount}(l1.c.chainSelector, message); + + assertEq(l2.c.token.balanceOf(alice), aliceBalance - amount); + assertEq( + l2.c.token.getFacilitator(address(l2.existingTokenPool)).bucketLevel, + bucketLevel - amount + ); + + _executeUpgradeAIP(); // token pools upgraded + + // ETH executeMessage + vm.selectFork(l1.c.forkId); + + skip(_getInboundRefillTime(amount)); + uint256 tokenPoolBalance = l1.c.token.balanceOf(address(l1.newTokenPool)); + uint256 bridgedAmount = l1.newTokenPool.getCurrentBridgedAmount(); + aliceBalance = l1.c.token.balanceOf(alice); + + vm.expectEmit(address(l1.newTokenPool)); + emit Released(address(l1.c.EVM2EVMOffRamp), alice, amount); + vm.prank(address(l1.c.EVM2EVMOffRamp)); + l1.c.EVM2EVMOffRamp.executeSingleMessage( + eventArg, // pre-upgrade message + new bytes[](message.tokenAmounts.length), + new uint32[](1) // tokenGasOverrides + ); + + assertEq(l1.c.token.balanceOf(address(l1.newTokenPool)), tokenPoolBalance - amount); + assertEq(l1.newTokenPool.getCurrentBridgedAmount(), bridgedAmount - amount); + assertEq(l1.c.token.balanceOf(alice), aliceBalance + amount); + + // send tokens back to arb + _runEthToArb(alice, amount); + } +} diff --git a/src/20241209_Multi_GHOCCIP151Upgrade/AaveV3Ethereum_GHOCCIP151Upgrade_20241209.sol b/src/20241209_Multi_GHOCCIP151Upgrade/AaveV3Ethereum_GHOCCIP151Upgrade_20241209.sol new file mode 100644 index 000000000..9c9dd50c7 --- /dev/null +++ b/src/20241209_Multi_GHOCCIP151Upgrade/AaveV3Ethereum_GHOCCIP151Upgrade_20241209.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IUpgradeableLockReleaseTokenPool_1_4, IUpgradeableLockReleaseTokenPool_1_5_1} from 'src/interfaces/ccip/tokenPool/IUpgradeableLockReleaseTokenPool.sol'; +import {IProposalGenericExecutor} from 'aave-helpers/src/interfaces/IProposalGenericExecutor.sol'; +import {ITokenAdminRegistry} from 'src/interfaces/ccip/ITokenAdminRegistry.sol'; +import {IProxyPool} from 'src/interfaces/ccip/IProxyPool.sol'; +import {IRateLimiter} from 'src/interfaces/ccip/IRateLimiter.sol'; +import {GhoEthereum} from 'aave-address-book/GhoEthereum.sol'; +import {AaveV3EthereumAssets, AaveV3Ethereum} from 'aave-address-book/AaveV3Ethereum.sol'; +import {AaveV3ArbitrumAssets} from 'aave-address-book/AaveV3Arbitrum.sol'; + +/** + * @title GHO CCIP 1.5.1 Upgrade + * @author Aave Labs + * - Discussion: TODO + */ +contract AaveV3Ethereum_GHOCCIP151Upgrade_20241209 is IProposalGenericExecutor { + uint64 public constant ARB_CHAIN_SELECTOR = 4949039107694359620; + + // https://etherscan.io/address/0xb22764f98dD05c789929716D677382Df22C05Cb6 + ITokenAdminRegistry public constant TOKEN_ADMIN_REGISTRY = + ITokenAdminRegistry(0xb22764f98dD05c789929716D677382Df22C05Cb6); + + // https://etherscan.io/address/0x9Ec9F9804733df96D1641666818eFb5198eC50f0 + IProxyPool public constant EXISTING_PROXY_POOL = + IProxyPool(0x9Ec9F9804733df96D1641666818eFb5198eC50f0); + // https://etherscan.io/address/0x5756880B6a1EAba0175227bf02a7E87c1e02B28C + IUpgradeableLockReleaseTokenPool_1_4 public constant EXISTING_TOKEN_POOL = + IUpgradeableLockReleaseTokenPool_1_4(GhoEthereum.GHO_CCIP_TOKEN_POOL); // will be updated in address-book after AIP + // https://etherscan.io/address/0x06179f7C1be40863405f374E7f5F8806c728660A + IUpgradeableLockReleaseTokenPool_1_5_1 public constant NEW_TOKEN_POOL = + IUpgradeableLockReleaseTokenPool_1_5_1(0x06179f7C1be40863405f374E7f5F8806c728660A); + + // https://etherscan.io/address/0xFEb4e54591660F42288312AE8eB59e9f2B746b66 + address public constant EXISTING_GHO_AAVE_STEWARD = 0xFEb4e54591660F42288312AE8eB59e9f2B746b66; + // https://etherscan.io/address/0x98217A06721Ebf727f2C8d9aD7718ec28b7aAe34 + address public constant NEW_GHO_AAVE_STEWARD = 0x98217A06721Ebf727f2C8d9aD7718ec28b7aAe34; + // https://etherscan.io/address/0xC5BcC58BE6172769ca1a78B8A45752E3C5059c39 + address public constant NEW_GHO_CCIP_STEWARD = 0xC5BcC58BE6172769ca1a78B8A45752E3C5059c39; + + // https://arbiscan.io/address/0x26329558f08cbb40d6a4CCA0E0C67b29D64A8c50 + address public constant EXISTING_REMOTE_POOL_ARB = 0x26329558f08cbb40d6a4CCA0E0C67b29D64A8c50; // ProxyPool on Arb + // https://arbiscan.io/address/0xB94Ab28c6869466a46a42abA834ca2B3cECCA5eB + address public constant NEW_REMOTE_POOL_ARB = 0xB94Ab28c6869466a46a42abA834ca2B3cECCA5eB; + + // Token Rate Limit Capacity: 300_000 GHO + uint128 public constant CCIP_RATE_LIMIT_CAPACITY = 300_000e18; + // Token Rate Limit Refill Rate: 60 GHO per second (=> 216_000 GHO per hour) + uint128 public constant CCIP_RATE_LIMIT_REFILL_RATE = 60e18; + + function execute() external { + _acceptOwnership(); + _migrateLiquidity(); + _setupAndRegisterNewPool(); + _updateStewards(); + } + + // pre-req - chainlink transfers gho token pool ownership on token admin registry + function _acceptOwnership() internal { + EXISTING_PROXY_POOL.acceptOwnership(); + NEW_TOKEN_POOL.acceptOwnership(); + TOKEN_ADMIN_REGISTRY.acceptAdminRole(AaveV3EthereumAssets.GHO_UNDERLYING); + } + + function _migrateLiquidity() internal { + EXISTING_TOKEN_POOL.setRebalancer(address(NEW_TOKEN_POOL)); + uint256 bridgeAmount = EXISTING_TOKEN_POOL.getCurrentBridgedAmount(); + NEW_TOKEN_POOL.transferLiquidity(address(EXISTING_TOKEN_POOL), bridgeAmount); + NEW_TOKEN_POOL.setCurrentBridgedAmount(bridgeAmount); + + // disable existing pool + EXISTING_TOKEN_POOL.setBridgeLimit(0); + } + + function _setupAndRegisterNewPool() internal { + IRateLimiter.Config memory rateLimiterConfig = IRateLimiter.Config({ + isEnabled: true, + capacity: CCIP_RATE_LIMIT_CAPACITY, + rate: CCIP_RATE_LIMIT_REFILL_RATE + }); + + IUpgradeableLockReleaseTokenPool_1_5_1.ChainUpdate[] + memory chains = new IUpgradeableLockReleaseTokenPool_1_5_1.ChainUpdate[](1); + + bytes[] memory remotePoolAddresses = new bytes[](2); + remotePoolAddresses[0] = abi.encode(EXISTING_REMOTE_POOL_ARB); + remotePoolAddresses[1] = abi.encode(NEW_REMOTE_POOL_ARB); + + chains[0] = IUpgradeableLockReleaseTokenPool_1_5_1.ChainUpdate({ + remoteChainSelector: ARB_CHAIN_SELECTOR, + remotePoolAddresses: remotePoolAddresses, + remoteTokenAddress: abi.encode(AaveV3ArbitrumAssets.GHO_UNDERLYING), + outboundRateLimiterConfig: rateLimiterConfig, + inboundRateLimiterConfig: rateLimiterConfig + }); + + // setup new pool + NEW_TOKEN_POOL.applyChainUpdates({ + remoteChainSelectorsToRemove: new uint64[](0), + chainsToAdd: chains + }); + + // register new pool + TOKEN_ADMIN_REGISTRY.setPool(AaveV3EthereumAssets.GHO_UNDERLYING, address(NEW_TOKEN_POOL)); + } + + function _updateStewards() internal { + // Gho Aave Steward + AaveV3Ethereum.ACL_MANAGER.revokeRole( + AaveV3Ethereum.ACL_MANAGER.RISK_ADMIN_ROLE(), + EXISTING_GHO_AAVE_STEWARD + ); + AaveV3Ethereum.ACL_MANAGER.grantRole( + AaveV3Ethereum.ACL_MANAGER.RISK_ADMIN_ROLE(), + NEW_GHO_AAVE_STEWARD + ); + + // Gho Ccip Steward + NEW_TOKEN_POOL.setRateLimitAdmin(NEW_GHO_CCIP_STEWARD); + NEW_TOKEN_POOL.setBridgeLimitAdmin(NEW_GHO_CCIP_STEWARD); + } +} diff --git a/src/20241209_Multi_GHOCCIP151Upgrade/AaveV3Ethereum_GHOCCIP151Upgrade_20241209.t.sol b/src/20241209_Multi_GHOCCIP151Upgrade/AaveV3Ethereum_GHOCCIP151Upgrade_20241209.t.sol new file mode 100644 index 000000000..b535de501 --- /dev/null +++ b/src/20241209_Multi_GHOCCIP151Upgrade/AaveV3Ethereum_GHOCCIP151Upgrade_20241209.t.sol @@ -0,0 +1,757 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; + +import {IUpgradeableLockReleaseTokenPool_1_4, IUpgradeableLockReleaseTokenPool_1_5_1} from 'src/interfaces/ccip/tokenPool/IUpgradeableLockReleaseTokenPool.sol'; +import {IPool as IPool_CCIP} from 'src/interfaces/ccip/tokenPool/IPool.sol'; +import {IClient} from 'src/interfaces/ccip/IClient.sol'; +import {IInternal} from 'src/interfaces/ccip/IInternal.sol'; +import {IRouter} from 'src/interfaces/ccip/IRouter.sol'; +import {IRateLimiter} from 'src/interfaces/ccip/IRateLimiter.sol'; +import {IProxyPool} from 'src/interfaces/ccip/IProxyPool.sol'; +import {IEVM2EVMOnRamp} from 'src/interfaces/ccip/IEVM2EVMOnRamp.sol'; +import {ITypeAndVersion} from 'src/interfaces/ccip/ITypeAndVersion.sol'; +import {IEVM2EVMOffRamp_1_5} from 'src/interfaces/ccip/IEVM2EVMOffRamp.sol'; +import {ITokenAdminRegistry} from 'src/interfaces/ccip/ITokenAdminRegistry.sol'; +import {IGhoToken} from 'src/interfaces/IGhoToken.sol'; +import {IGhoAaveSteward} from 'src/interfaces/IGhoAaveSteward.sol'; +import {IGhoCcipSteward} from 'src/interfaces/IGhoCcipSteward.sol'; +import {IOwnable} from 'aave-address-book/common/IOwnable.sol'; +import {DataTypes, IDefaultInterestRateStrategyV2} from 'aave-address-book/AaveV3.sol'; + +import {ReserveConfiguration} from 'aave-v3-origin/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; +import {ProtocolV3TestBase} from 'aave-helpers/src/ProtocolV3TestBase.sol'; +import {AaveV3Ethereum} from 'aave-address-book/AaveV3Ethereum.sol'; +import {GhoEthereum} from 'aave-address-book/GhoEthereum.sol'; +import {GovernanceV3Ethereum} from 'aave-address-book/GovernanceV3Ethereum.sol'; +import {AaveV3Arbitrum} from 'aave-address-book/AaveV3Arbitrum.sol'; +import {AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; +import {AaveV3ArbitrumAssets} from 'aave-address-book/AaveV3Arbitrum.sol'; + +import {ProxyAdmin} from 'solidity-utils/contracts/transparent-proxy/ProxyAdmin.sol'; +import {CCIPUtils} from './utils/CCIPUtils.sol'; +import {AaveV3Ethereum_GHOCCIP151Upgrade_20241209} from './AaveV3Ethereum_GHOCCIP151Upgrade_20241209.sol'; + +/** + * @dev Test for AaveV3Ethereum_GHOCCIP151Upgrade_20241209 + * command: FOUNDRY_PROFILE=mainnet forge test --match-path=src/20241209_Multi_GHOCCIP151Upgrade/AaveV3Ethereum_GHOCCIP151Upgrade_20241209.t.sol -vv + */ +contract AaveV3Ethereum_GHOCCIP151Upgrade_20241209_Base is ProtocolV3TestBase { + struct CCIPSendParams { + address sender; + uint256 amount; + CCIPUtils.PoolVersion poolVersion; + } + + uint64 internal constant ETH_CHAIN_SELECTOR = CCIPUtils.ETH_CHAIN_SELECTOR; + uint64 internal constant ARB_CHAIN_SELECTOR = CCIPUtils.ARB_CHAIN_SELECTOR; + uint256 internal constant CCIP_RATE_LIMIT_CAPACITY = 300_000e18; + uint256 internal constant CCIP_RATE_LIMIT_REFILL_RATE = 60e18; + + IGhoToken internal constant GHO = IGhoToken(AaveV3EthereumAssets.GHO_UNDERLYING); + ITokenAdminRegistry internal constant TOKEN_ADMIN_REGISTRY = + ITokenAdminRegistry(0xb22764f98dD05c789929716D677382Df22C05Cb6); + address internal constant ARB_PROXY_POOL = 0x26329558f08cbb40d6a4CCA0E0C67b29D64A8c50; + IEVM2EVMOnRamp internal constant ON_RAMP = + IEVM2EVMOnRamp(0x69eCC4E2D8ea56E2d0a05bF57f4Fd6aEE7f2c284); + IEVM2EVMOffRamp_1_5 internal constant OFF_RAMP = + IEVM2EVMOffRamp_1_5(0xdf615eF8D4C64d0ED8Fd7824BBEd2f6a10245aC9); + IRouter internal constant ROUTER = IRouter(0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D); + + IGhoAaveSteward public constant EXISTING_GHO_AAVE_STEWARD = + IGhoAaveSteward(0xFEb4e54591660F42288312AE8eB59e9f2B746b66); + IGhoAaveSteward public constant NEW_GHO_AAVE_STEWARD = + IGhoAaveSteward(0x98217A06721Ebf727f2C8d9aD7718ec28b7aAe34); + IGhoCcipSteward internal constant EXISTING_GHO_CCIP_STEWARD = + IGhoCcipSteward(0x101Efb7b9Beb073B1219Cd5473a7C8A2f2EB84f4); + IGhoCcipSteward internal constant NEW_GHO_CCIP_STEWARD = + IGhoCcipSteward(0xC5BcC58BE6172769ca1a78B8A45752E3C5059c39); + + IProxyPool internal constant EXISTING_PROXY_POOL = + IProxyPool(0x9Ec9F9804733df96D1641666818eFb5198eC50f0); + IUpgradeableLockReleaseTokenPool_1_4 internal constant EXISTING_TOKEN_POOL = + IUpgradeableLockReleaseTokenPool_1_4(0x5756880B6a1EAba0175227bf02a7E87c1e02B28C); // GhoEthereum.GHO_CCIP_TOKEN_POOL; will be updated in address-book after AIP + IUpgradeableLockReleaseTokenPool_1_5_1 internal constant NEW_TOKEN_POOL = + IUpgradeableLockReleaseTokenPool_1_5_1(0x06179f7C1be40863405f374E7f5F8806c728660A); + address internal constant NEW_REMOTE_POOL_ARB = 0xB94Ab28c6869466a46a42abA834ca2B3cECCA5eB; + + AaveV3Ethereum_GHOCCIP151Upgrade_20241209 internal proposal; + + address internal alice = makeAddr('alice'); + address internal bob = makeAddr('bob'); + address internal carol = makeAddr('carol'); + + event Locked(address indexed sender, uint256 amount); + event Released(address indexed sender, address indexed recipient, uint256 amount); + event CCIPSendRequested(IInternal.EVM2EVMMessage message); + + error BridgeLimitExceeded(uint256 limit); + + function setUp() public virtual { + vm.createSelectFork(vm.rpcUrl('mainnet'), 21581477); + proposal = new AaveV3Ethereum_GHOCCIP151Upgrade_20241209(); + + // pre-req - chainlink transfers gho token pool ownership on token admin registry + address CLL_OWNER = TOKEN_ADMIN_REGISTRY.owner(); + vm.startPrank(CLL_OWNER); + TOKEN_ADMIN_REGISTRY.transferAdminRole(address(GHO), GovernanceV3Ethereum.EXECUTOR_LVL_1); + EXISTING_PROXY_POOL.transferOwnership(GovernanceV3Ethereum.EXECUTOR_LVL_1); + vm.stopPrank(); + + _validateConstants(); + } + + function _validateConstants() private view { + assertEq(address(proposal.TOKEN_ADMIN_REGISTRY()), address(TOKEN_ADMIN_REGISTRY)); + assertEq(proposal.ARB_CHAIN_SELECTOR(), ARB_CHAIN_SELECTOR); + assertEq(address(proposal.EXISTING_PROXY_POOL()), address(EXISTING_PROXY_POOL)); + assertEq(address(proposal.EXISTING_TOKEN_POOL()), address(EXISTING_TOKEN_POOL)); + assertEq(address(proposal.EXISTING_REMOTE_POOL_ARB()), ARB_PROXY_POOL); + assertEq(address(proposal.NEW_TOKEN_POOL()), address(NEW_TOKEN_POOL)); + assertEq(address(proposal.EXISTING_GHO_AAVE_STEWARD()), address(EXISTING_GHO_AAVE_STEWARD)); + assertEq(address(proposal.NEW_GHO_AAVE_STEWARD()), address(NEW_GHO_AAVE_STEWARD)); + assertEq(address(proposal.NEW_GHO_CCIP_STEWARD()), address(NEW_GHO_CCIP_STEWARD)); + assertEq(address(proposal.NEW_REMOTE_POOL_ARB()), NEW_REMOTE_POOL_ARB); + assertEq(proposal.CCIP_RATE_LIMIT_CAPACITY(), CCIP_RATE_LIMIT_CAPACITY); + assertEq(proposal.CCIP_RATE_LIMIT_REFILL_RATE(), CCIP_RATE_LIMIT_REFILL_RATE); + + assertEq(address(proposal.EXISTING_PROXY_POOL()), EXISTING_TOKEN_POOL.getProxyPool()); + + assertEq(TOKEN_ADMIN_REGISTRY.typeAndVersion(), 'TokenAdminRegistry 1.5.0'); + assertEq(NEW_TOKEN_POOL.typeAndVersion(), 'LockReleaseTokenPool 1.5.1'); + assertEq(EXISTING_TOKEN_POOL.typeAndVersion(), 'LockReleaseTokenPool 1.4.0'); + assertEq( + ITypeAndVersion(EXISTING_TOKEN_POOL.getProxyPool()).typeAndVersion(), + 'LockReleaseTokenPoolAndProxy 1.5.0' + ); + assertEq(ON_RAMP.typeAndVersion(), 'EVM2EVMOnRamp 1.5.0'); + assertEq(OFF_RAMP.typeAndVersion(), 'EVM2EVMOffRamp 1.5.0'); + + assertEq(ROUTER.typeAndVersion(), 'Router 1.2.0'); + assertEq(EXISTING_TOKEN_POOL.getRouter(), address(ROUTER)); + + assertEq(ROUTER.getOnRamp(ARB_CHAIN_SELECTOR), address(ON_RAMP)); + assertTrue(ROUTER.isOffRamp(ARB_CHAIN_SELECTOR, address(OFF_RAMP))); + + assertTrue( + AaveV3Ethereum.ACL_MANAGER.hasRole( + AaveV3Ethereum.ACL_MANAGER.RISK_ADMIN_ROLE(), + address(EXISTING_GHO_AAVE_STEWARD) + ) + ); + assertEq(IOwnable(address(NEW_GHO_AAVE_STEWARD)).owner(), GovernanceV3Ethereum.EXECUTOR_LVL_1); + assertEq( + NEW_GHO_AAVE_STEWARD.POOL_ADDRESSES_PROVIDER(), + address(AaveV3Ethereum.POOL_ADDRESSES_PROVIDER) + ); + assertEq( + NEW_GHO_AAVE_STEWARD.POOL_DATA_PROVIDER(), + address(AaveV3Ethereum.AAVE_PROTOCOL_DATA_PROVIDER) + ); + assertEq(NEW_GHO_AAVE_STEWARD.RISK_COUNCIL(), EXISTING_GHO_AAVE_STEWARD.RISK_COUNCIL()); + assertEq( + NEW_GHO_AAVE_STEWARD.getBorrowRateConfig(), + IGhoAaveSteward.BorrowRateConfig({ + optimalUsageRatioMaxChange: 5_00, + baseVariableBorrowRateMaxChange: 5_00, + variableRateSlope1MaxChange: 5_00, + variableRateSlope2MaxChange: 5_00 + }) + ); + + assertEq(EXISTING_TOKEN_POOL.getRateLimitAdmin(), address(EXISTING_GHO_CCIP_STEWARD)); + assertEq(NEW_GHO_CCIP_STEWARD.RISK_COUNCIL(), EXISTING_GHO_CCIP_STEWARD.RISK_COUNCIL()); + assertEq(NEW_GHO_CCIP_STEWARD.GHO_TOKEN(), AaveV3EthereumAssets.GHO_UNDERLYING); + assertEq(NEW_GHO_CCIP_STEWARD.GHO_TOKEN_POOL(), address(NEW_TOKEN_POOL)); + assertTrue(NEW_GHO_CCIP_STEWARD.BRIDGE_LIMIT_ENABLED()); // *present* on eth token pool + assertEq( + abi.encode(NEW_GHO_CCIP_STEWARD.getCcipTimelocks()), + abi.encode(IGhoCcipSteward.CcipDebounce(0, 0)) + ); + + assertEq(_getProxyAdmin(address(NEW_TOKEN_POOL)).UPGRADE_INTERFACE_VERSION(), '5.0.0'); + } + + function _getTokenMessage( + CCIPSendParams memory params + ) internal returns (IClient.EVM2AnyMessage memory, IInternal.EVM2EVMMessage memory) { + IClient.EVM2AnyMessage memory message = CCIPUtils.generateMessage(params.sender, 1); + message.tokenAmounts[0] = IClient.EVMTokenAmount({ + token: AaveV3EthereumAssets.GHO_UNDERLYING, + amount: params.amount + }); + + uint256 feeAmount = ROUTER.getFee(ARB_CHAIN_SELECTOR, message); + deal(params.sender, feeAmount); + + IInternal.EVM2EVMMessage memory eventArg = CCIPUtils.messageToEvent( + CCIPUtils.MessageToEventParams({ + message: message, + router: ROUTER, + sourceChainSelector: ETH_CHAIN_SELECTOR, + feeTokenAmount: feeAmount, + originalSender: params.sender, + sourceToken: AaveV3EthereumAssets.GHO_UNDERLYING, + destinationToken: AaveV3ArbitrumAssets.GHO_UNDERLYING, + poolVersion: params.poolVersion + }) + ); + + return (message, eventArg); + } + + function _getStaticParams(address tokenPool) internal view returns (bytes memory) { + IUpgradeableLockReleaseTokenPool_1_4 ghoTokenPool = IUpgradeableLockReleaseTokenPool_1_4( + tokenPool + ); + return + abi.encode( + ghoTokenPool.getToken(), + ghoTokenPool.getAllowListEnabled(), + ghoTokenPool.getAllowList(), + ghoTokenPool.canAcceptLiquidity(), + ghoTokenPool.getRouter() + ); + } + + function _getDynamicParams(address tokenPool) internal view returns (bytes memory) { + IUpgradeableLockReleaseTokenPool_1_4 ghoTokenPool = IUpgradeableLockReleaseTokenPool_1_4( + tokenPool + ); + return + abi.encode( + ghoTokenPool.owner(), + ghoTokenPool.getSupportedChains(), + ghoTokenPool.getRebalancer(), + ghoTokenPool.getBridgeLimit() + ); + } + + function _tokenBucketToConfig( + IRateLimiter.TokenBucket memory bucket + ) internal pure returns (IRateLimiter.Config memory) { + return + IRateLimiter.Config({ + isEnabled: bucket.isEnabled, + capacity: bucket.capacity, + rate: bucket.rate + }); + } + + function _getDisabledConfig() internal pure returns (IRateLimiter.Config memory) { + return IRateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}); + } + + function _getImplementation(address proxy) internal view returns (address) { + bytes32 slot = bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1); + return address(uint160(uint256(vm.load(proxy, slot)))); + } + + function _readInitialized(address proxy) internal view returns (uint8) { + return uint8(uint256(vm.load(proxy, bytes32(0)))); + } + function _getProxyAdmin(address proxy) internal view returns (ProxyAdmin) { + bytes32 slot = bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1); + return ProxyAdmin(address(uint160(uint256(vm.load(proxy, slot))))); + } + + function _getRateLimiterConfig() internal view returns (IRateLimiter.Config memory) { + return + IRateLimiter.Config({ + isEnabled: true, + capacity: proposal.CCIP_RATE_LIMIT_CAPACITY(), + rate: proposal.CCIP_RATE_LIMIT_REFILL_RATE() + }); + } + + function _getOutboundRefillTime(uint256 amount) internal pure returns (uint256) { + return amount / CCIP_RATE_LIMIT_REFILL_RATE + 1; // account for rounding + } + + function _getInboundRefillTime(uint256 amount) internal pure returns (uint256) { + return amount / CCIP_RATE_LIMIT_REFILL_RATE + 1; // account for rounding + } + + function _min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + + function _isDifferenceLowerThanMax( + uint256 from, + uint256 to, + uint256 max + ) internal pure returns (bool) { + return from < to ? to - from <= max : from - to <= max; + } + + function assertEq( + IDefaultInterestRateStrategyV2.InterestRateData memory a, + IDefaultInterestRateStrategyV2.InterestRateData memory b + ) internal pure { + assertEq(a.optimalUsageRatio, b.optimalUsageRatio); + assertEq(a.baseVariableBorrowRate, b.baseVariableBorrowRate); + assertEq(a.variableRateSlope1, b.variableRateSlope1); + assertEq(a.variableRateSlope2, b.variableRateSlope2); + assertEq(abi.encode(a), abi.encode(b)); // sanity check + } + + function assertEq( + IGhoAaveSteward.BorrowRateConfig memory a, + IGhoAaveSteward.BorrowRateConfig memory b + ) internal pure { + assertEq(a.optimalUsageRatioMaxChange, b.optimalUsageRatioMaxChange); + assertEq(a.baseVariableBorrowRateMaxChange, b.baseVariableBorrowRateMaxChange); + assertEq(a.variableRateSlope1MaxChange, b.variableRateSlope1MaxChange); + assertEq(a.variableRateSlope2MaxChange, b.variableRateSlope2MaxChange); + assertEq(abi.encode(a), abi.encode(b)); // sanity check + } + + function assertEq( + IRateLimiter.TokenBucket memory bucket, + IRateLimiter.Config memory config + ) internal pure { + assertEq(bucket.isEnabled, config.isEnabled); + assertEq(bucket.capacity, config.capacity); + assertEq(bucket.rate, config.rate); + assertEq(abi.encode(_tokenBucketToConfig(bucket)), abi.encode(config)); // sanity check + } +} + +contract AaveV3Ethereum_GHOCCIP151Upgrade_20241209_SetupAndProposalActions is + AaveV3Ethereum_GHOCCIP151Upgrade_20241209_Base +{ + /** + * @dev executes the generic test suite including e2e and config snapshots + */ + function test_defaultProposalExecution() public { + defaultTest( + 'AaveV3Ethereum_GHOCCIP151Upgrade_20241209', + AaveV3Ethereum.POOL, + address(proposal) + ); + } + + function test_tokenPoolOwnershipTransfer() public { + assertFalse( + TOKEN_ADMIN_REGISTRY.isAdministrator(address(GHO), GovernanceV3Ethereum.EXECUTOR_LVL_1) + ); + ITokenAdminRegistry.TokenConfig memory tokenConfig = TOKEN_ADMIN_REGISTRY.getTokenConfig( + address(GHO) + ); + assertNotEq(tokenConfig.administrator, GovernanceV3Ethereum.EXECUTOR_LVL_1); + assertEq(tokenConfig.pendingAdministrator, GovernanceV3Ethereum.EXECUTOR_LVL_1); + assertEq(tokenConfig.tokenPool, address(EXISTING_PROXY_POOL)); + + assertEq(EXISTING_TOKEN_POOL.owner(), GovernanceV3Ethereum.EXECUTOR_LVL_1); + assertEq(EXISTING_PROXY_POOL.owner(), TOKEN_ADMIN_REGISTRY.owner()); + assertEq(NEW_TOKEN_POOL.owner(), address(0)); + + executePayload(vm, address(proposal)); + + tokenConfig = TOKEN_ADMIN_REGISTRY.getTokenConfig(address(GHO)); + assertEq(tokenConfig.administrator, GovernanceV3Ethereum.EXECUTOR_LVL_1); + assertEq(tokenConfig.pendingAdministrator, address(0)); + assertEq(tokenConfig.tokenPool, address(NEW_TOKEN_POOL)); + + assertEq(EXISTING_TOKEN_POOL.owner(), GovernanceV3Ethereum.EXECUTOR_LVL_1); + assertEq(EXISTING_PROXY_POOL.owner(), GovernanceV3Ethereum.EXECUTOR_LVL_1); + assertEq(NEW_TOKEN_POOL.owner(), GovernanceV3Ethereum.EXECUTOR_LVL_1); + } + + function test_tokenPoolLiquidityMigration() public { + assertEq(EXISTING_TOKEN_POOL.getRebalancer(), address(0)); + uint256 balance = GHO.balanceOf(address(EXISTING_TOKEN_POOL)); + uint256 bridgedAmount = EXISTING_TOKEN_POOL.getCurrentBridgedAmount(); + + assertGt(balance, 0); + assertGt(bridgedAmount, 0); + + assertEq(GHO.balanceOf(address(NEW_TOKEN_POOL)), 0); + assertEq(NEW_TOKEN_POOL.getCurrentBridgedAmount(), 0); + + assertEq(bridgedAmount, balance); // bridgedAmountInvariant + + executePayload(vm, address(proposal)); + + assertEq(EXISTING_TOKEN_POOL.getRebalancer(), address(NEW_TOKEN_POOL)); + + assertEq(GHO.balanceOf(address(EXISTING_TOKEN_POOL)), 0); + // we do not reset bridgedAmount in the existing token pool, since bridge limit is reset + assertNotEq(EXISTING_TOKEN_POOL.getCurrentBridgedAmount(), 0); + assertEq(EXISTING_TOKEN_POOL.getBridgeLimit(), 0); + + assertEq(GHO.balanceOf(address(NEW_TOKEN_POOL)), balance); + assertEq(NEW_TOKEN_POOL.getCurrentBridgedAmount(), bridgedAmount); + } + + function test_newTokenPoolSetupAndRegistration() public { + bytes memory staticParams = _getStaticParams(address(EXISTING_TOKEN_POOL)); + bytes memory dynamicParams = _getDynamicParams(address(EXISTING_TOKEN_POOL)); + assertEq(EXISTING_TOKEN_POOL.getRateLimitAdmin(), address(EXISTING_GHO_CCIP_STEWARD)); + + assertEq(TOKEN_ADMIN_REGISTRY.getPool(address(GHO)), EXISTING_TOKEN_POOL.getProxyPool()); + + executePayload(vm, address(proposal)); + + assertEq(staticParams, _getStaticParams(address(NEW_TOKEN_POOL))); + assertEq(dynamicParams, _getDynamicParams(address(NEW_TOKEN_POOL))); + assertEq(NEW_TOKEN_POOL.getRateLimitAdmin(), address(NEW_GHO_CCIP_STEWARD)); + + assertEq(TOKEN_ADMIN_REGISTRY.getPool(address(GHO)), address(NEW_TOKEN_POOL)); + + assertEq(NEW_TOKEN_POOL.getRemotePools(ARB_CHAIN_SELECTOR).length, 2); + assertTrue(NEW_TOKEN_POOL.isRemotePool(ARB_CHAIN_SELECTOR, abi.encode(ARB_PROXY_POOL))); + assertTrue(NEW_TOKEN_POOL.isRemotePool(ARB_CHAIN_SELECTOR, abi.encode(NEW_REMOTE_POOL_ARB))); + assertEq( + NEW_TOKEN_POOL.getRemoteToken(ARB_CHAIN_SELECTOR), + abi.encode(AaveV3ArbitrumAssets.GHO_UNDERLYING) + ); + assertEq(NEW_TOKEN_POOL.getSupportedChains().length, 1); + assertTrue(NEW_TOKEN_POOL.isSupportedChain(ARB_CHAIN_SELECTOR)); + + assertEq( + NEW_TOKEN_POOL.getCurrentInboundRateLimiterState(ARB_CHAIN_SELECTOR), + _getRateLimiterConfig() + ); + assertEq( + NEW_TOKEN_POOL.getCurrentOutboundRateLimiterState(ARB_CHAIN_SELECTOR), + _getRateLimiterConfig() + ); + } + + function test_newTokenPoolInitialization() public { + vm.expectRevert('Initializable: contract is already initialized'); + NEW_TOKEN_POOL.initialize(makeAddr('owner'), new address[](0), makeAddr('router'), 13e7); + assertEq(_readInitialized(address(NEW_TOKEN_POOL)), 1); + assertEq(_readInitialized(_getImplementation(address(NEW_TOKEN_POOL))), 255); + } + + function test_stewardRolesAndConfig() public { + bytes32 RISK_ADMIN_ROLE = AaveV3Ethereum.ACL_MANAGER.RISK_ADMIN_ROLE(); + assertTrue( + AaveV3Ethereum.ACL_MANAGER.hasRole(RISK_ADMIN_ROLE, address(EXISTING_GHO_AAVE_STEWARD)) + ); + assertFalse(AaveV3Ethereum.ACL_MANAGER.hasRole(RISK_ADMIN_ROLE, address(NEW_GHO_AAVE_STEWARD))); + + assertEq(NEW_TOKEN_POOL.getRateLimitAdmin(), address(0)); + assertEq(NEW_TOKEN_POOL.getBridgeLimitAdmin(), address(0)); + + executePayload(vm, address(proposal)); + + assertFalse( + AaveV3Ethereum.ACL_MANAGER.hasRole(RISK_ADMIN_ROLE, address(EXISTING_GHO_AAVE_STEWARD)) + ); + assertTrue(AaveV3Ethereum.ACL_MANAGER.hasRole(RISK_ADMIN_ROLE, address(NEW_GHO_AAVE_STEWARD))); + + assertEq(NEW_TOKEN_POOL.getRateLimitAdmin(), address(NEW_GHO_CCIP_STEWARD)); + assertEq(NEW_TOKEN_POOL.getBridgeLimitAdmin(), address(NEW_GHO_CCIP_STEWARD)); + } +} + +contract AaveV3Ethereum_GHOCCIP151Upgrade_20241209_PostUpgrade is + AaveV3Ethereum_GHOCCIP151Upgrade_20241209_Base +{ + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + + function setUp() public override { + super.setUp(); + + executePayload(vm, address(proposal)); + } + + function test_sendMessageSucceedsAndRoutesViaNewPool(uint256 amount) public { + uint256 bridgeableAmount = _min( + NEW_TOKEN_POOL.getBridgeLimit() - NEW_TOKEN_POOL.getCurrentBridgedAmount(), + CCIP_RATE_LIMIT_CAPACITY + ); + amount = bound(amount, 1, bridgeableAmount); + skip(_getOutboundRefillTime(amount)); // wait for the rate limiter to refill + + deal(address(GHO), alice, amount); + vm.prank(alice); + GHO.approve(address(ROUTER), amount); + + uint256 aliceBalance = GHO.balanceOf(alice); + uint256 tokenPoolBalance = GHO.balanceOf(address(NEW_TOKEN_POOL)); + uint256 bridgedAmount = NEW_TOKEN_POOL.getCurrentBridgedAmount(); + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({amount: amount, sender: alice, poolVersion: CCIPUtils.PoolVersion.V1_5_1}) + ); + + vm.expectEmit(address(NEW_TOKEN_POOL)); // new token pool + emit Locked(address(ON_RAMP), amount); + + vm.expectEmit(address(ON_RAMP)); + emit CCIPSendRequested(eventArg); + + vm.prank(alice); + ROUTER.ccipSend{value: eventArg.feeTokenAmount}(ARB_CHAIN_SELECTOR, message); + + assertEq(GHO.balanceOf(alice), aliceBalance - amount); + assertEq(GHO.balanceOf(address(NEW_TOKEN_POOL)), tokenPoolBalance + amount); + assertEq(NEW_TOKEN_POOL.getCurrentBridgedAmount(), bridgedAmount + amount); + } + + // existing pool can no longer on ramp + function test_lockOrBurnRevertsOnExistingPool() public { + uint256 amount = 100_000e18; + skip(_getOutboundRefillTime(amount)); + + // router pulls tokens from the user & sends to the token pool during onRamps + deal(address(GHO), address(EXISTING_TOKEN_POOL), amount); + + vm.prank(EXISTING_TOKEN_POOL.getProxyPool()); + // since we disable the bridge by resetting the bridge limit to 0 + vm.expectRevert(abi.encodeWithSelector(BridgeLimitExceeded.selector, 0)); + EXISTING_TOKEN_POOL.lockOrBurn( + alice, + abi.encode(alice), + amount, + ARB_CHAIN_SELECTOR, + new bytes(0) + ); + } + + // on-ramp via new pool + function test_lockOrBurnSucceedsOnNewPool(uint256 amount) public { + uint256 bridgeableAmount = _min( + NEW_TOKEN_POOL.getBridgeLimit() - NEW_TOKEN_POOL.getCurrentBridgedAmount(), + CCIP_RATE_LIMIT_CAPACITY + ); + amount = bound(amount, 1, bridgeableAmount); + skip(_getOutboundRefillTime(amount)); // wait for the rate limiter to refill + + // router pulls tokens from the user & sends to the token pool during onRamps + // we don't override NEW_TOKEN_POOL balance here & instead transfer because we want + // to check the invariant GHO.balanceOf(tokenPool) >= tokenPool.currentBridgedAmount() + deal(address(GHO), address(alice), amount); + vm.prank(alice); + GHO.transfer(address(NEW_TOKEN_POOL), amount); + + uint256 bridgedAmount = NEW_TOKEN_POOL.getCurrentBridgedAmount(); + + vm.expectEmit(address(NEW_TOKEN_POOL)); + emit Locked(address(ON_RAMP), amount); + + vm.prank(address(ON_RAMP)); + NEW_TOKEN_POOL.lockOrBurn( + IPool_CCIP.LockOrBurnInV1({ + receiver: abi.encode(alice), + remoteChainSelector: ARB_CHAIN_SELECTOR, + originalSender: alice, + amount: amount, + localToken: address(GHO) + }) + ); + + assertEq(GHO.balanceOf(address(NEW_TOKEN_POOL)), NEW_TOKEN_POOL.getCurrentBridgedAmount()); + assertEq(NEW_TOKEN_POOL.getCurrentBridgedAmount(), bridgedAmount + amount); + } + + // existing pool can no longer off ramp + function test_releaseOrMintRevertsOnExistingPool() public { + uint256 amount = 100_000e18; + skip(_getInboundRefillTime(amount)); + + assertEq(GHO.balanceOf(address(EXISTING_TOKEN_POOL)), 0); + + vm.prank(EXISTING_TOKEN_POOL.getProxyPool()); + // underflow expected at tokenPool.GHO.transfer() since existing + // token pool does not hold any gho + vm.expectRevert(stdError.arithmeticError); + EXISTING_TOKEN_POOL.releaseOrMint( + abi.encode(alice), + alice, + amount, + ARB_CHAIN_SELECTOR, + new bytes(0) + ); + } + + // off-ramp messages sent from new eth token pool (v1.5.1) + function test_releaseOrMintSucceedsOnNewPoolOffRampedViaNewTokenPoolEth(uint256 amount) public { + uint256 bridgeableAmount = _min( + NEW_TOKEN_POOL.getCurrentBridgedAmount(), + CCIP_RATE_LIMIT_CAPACITY + ); + amount = bound(amount, 1, bridgeableAmount); + skip(_getInboundRefillTime(amount)); + + uint256 aliceBalance = GHO.balanceOf(alice); + uint256 tokenPoolBalance = GHO.balanceOf(address(NEW_TOKEN_POOL)); + uint256 bridgedAmount = NEW_TOKEN_POOL.getCurrentBridgedAmount(); + + vm.expectEmit(address(NEW_TOKEN_POOL)); + emit Released(address(OFF_RAMP), alice, amount); + + vm.prank(address(OFF_RAMP)); + NEW_TOKEN_POOL.releaseOrMint( + IPool_CCIP.ReleaseOrMintInV1({ + originalSender: abi.encode(alice), + remoteChainSelector: ARB_CHAIN_SELECTOR, + receiver: alice, + amount: amount, + localToken: address(GHO), + sourcePoolAddress: abi.encode(address(NEW_REMOTE_POOL_ARB)), + sourcePoolData: new bytes(0), + offchainTokenData: new bytes(0) + }) + ); + + assertEq(GHO.balanceOf(alice), aliceBalance + amount); + assertEq(GHO.balanceOf(address(NEW_TOKEN_POOL)), tokenPoolBalance - amount); + assertEq(NEW_TOKEN_POOL.getCurrentBridgedAmount(), bridgedAmount - amount); + } + + // off-ramp messages sent from existing arb token pool (v1.4) ie ProxyPool + function test_releaseOrMintSucceedsOnNewPoolOffRampedViaExistingTokenPoolArb( + uint256 amount + ) public { + uint256 bridgeableAmount = _min( + NEW_TOKEN_POOL.getCurrentBridgedAmount(), + CCIP_RATE_LIMIT_CAPACITY + ); + amount = bound(amount, 1, bridgeableAmount); + skip(_getInboundRefillTime(amount)); + + uint256 aliceBalance = GHO.balanceOf(alice); + uint256 tokenPoolBalance = GHO.balanceOf(address(NEW_TOKEN_POOL)); + uint256 bridgedAmount = NEW_TOKEN_POOL.getCurrentBridgedAmount(); + + vm.expectEmit(address(NEW_TOKEN_POOL)); + emit Released(address(OFF_RAMP), alice, amount); + + vm.prank(address(OFF_RAMP)); + NEW_TOKEN_POOL.releaseOrMint( + IPool_CCIP.ReleaseOrMintInV1({ + originalSender: abi.encode(alice), + remoteChainSelector: ARB_CHAIN_SELECTOR, + receiver: alice, + amount: amount, + localToken: address(GHO), + sourcePoolAddress: abi.encode(address(ARB_PROXY_POOL)), + sourcePoolData: new bytes(0), + offchainTokenData: new bytes(0) + }) + ); + + assertEq(GHO.balanceOf(alice), aliceBalance + amount); + assertEq(GHO.balanceOf(address(NEW_TOKEN_POOL)), tokenPoolBalance - amount); + assertEq(NEW_TOKEN_POOL.getCurrentBridgedAmount(), bridgedAmount - amount); + } + + function test_ccipStewardCanChangeAndDisableRateLimit() public { + assertEq(NEW_TOKEN_POOL.getRateLimitAdmin(), address(NEW_GHO_CCIP_STEWARD)); // sanity + + IRateLimiter.Config memory outboundConfig = IRateLimiter.Config({ + isEnabled: true, + capacity: 500_000e18, + rate: 100e18 + }); + IRateLimiter.Config memory inboundConfig = IRateLimiter.Config({ + isEnabled: true, + capacity: 100_000e18, + rate: 50e18 + }); + + // we assert the new steward can change the rate limit + vm.prank(NEW_GHO_CCIP_STEWARD.RISK_COUNCIL()); + NEW_GHO_CCIP_STEWARD.updateRateLimit( + ARB_CHAIN_SELECTOR, + outboundConfig.isEnabled, + outboundConfig.capacity, + outboundConfig.rate, + inboundConfig.isEnabled, + inboundConfig.capacity, + inboundConfig.rate + ); + + assertEq(NEW_TOKEN_POOL.getCurrentOutboundRateLimiterState(ARB_CHAIN_SELECTOR), outboundConfig); + assertEq(NEW_TOKEN_POOL.getCurrentInboundRateLimiterState(ARB_CHAIN_SELECTOR), inboundConfig); + assertEq(NEW_GHO_CCIP_STEWARD.getCcipTimelocks().rateLimitLastUpdate, vm.getBlockTimestamp()); + + skip(NEW_GHO_CCIP_STEWARD.MINIMUM_DELAY() + 1); + + // now we assert the new steward can disable the rate limit + vm.prank(NEW_GHO_CCIP_STEWARD.RISK_COUNCIL()); + NEW_GHO_CCIP_STEWARD.updateRateLimit(ARB_CHAIN_SELECTOR, false, 0, 0, false, 0, 0); + + assertEq( + NEW_TOKEN_POOL.getCurrentOutboundRateLimiterState(ARB_CHAIN_SELECTOR), + _getDisabledConfig() + ); + assertEq( + NEW_TOKEN_POOL.getCurrentInboundRateLimiterState(ARB_CHAIN_SELECTOR), + _getDisabledConfig() + ); + assertEq(NEW_GHO_CCIP_STEWARD.getCcipTimelocks().rateLimitLastUpdate, vm.getBlockTimestamp()); + } + + function test_ccipStewardCanSetBridgeLimit(uint256 newBridgeLimit) public { + uint256 currentBridgeLimit = NEW_TOKEN_POOL.getBridgeLimit(); + vm.assume( + newBridgeLimit != currentBridgeLimit && + _isDifferenceLowerThanMax(currentBridgeLimit, newBridgeLimit, currentBridgeLimit) + ); + vm.prank(NEW_GHO_CCIP_STEWARD.RISK_COUNCIL()); + NEW_GHO_CCIP_STEWARD.updateBridgeLimit(newBridgeLimit); + + assertEq(NEW_TOKEN_POOL.getBridgeLimit(), newBridgeLimit); + assertEq(NEW_GHO_CCIP_STEWARD.getCcipTimelocks().bridgeLimitLastUpdate, vm.getBlockTimestamp()); + } + + function test_aaveStewardCanUpdateBorrowRate() public { + IDefaultInterestRateStrategyV2 irStrategy = IDefaultInterestRateStrategyV2( + AaveV3Ethereum.AAVE_PROTOCOL_DATA_PROVIDER.getInterestRateStrategyAddress(address(GHO)) + ); + + IDefaultInterestRateStrategyV2.InterestRateData + memory currentRateData = IDefaultInterestRateStrategyV2.InterestRateData({ + optimalUsageRatio: 99_00, + baseVariableBorrowRate: 11_50, + variableRateSlope1: 0, + variableRateSlope2: 0 + }); + + assertEq(irStrategy.getInterestRateDataBps(address(GHO)), currentRateData); + + currentRateData.baseVariableBorrowRate += 4_00; + + vm.prank(NEW_GHO_AAVE_STEWARD.RISK_COUNCIL()); + NEW_GHO_AAVE_STEWARD.updateGhoBorrowRate( + currentRateData.optimalUsageRatio, + currentRateData.baseVariableBorrowRate, + currentRateData.variableRateSlope1, + currentRateData.variableRateSlope2 + ); + + assertEq(irStrategy.getInterestRateDataBps(address(GHO)), currentRateData); + assertEq( + NEW_GHO_AAVE_STEWARD.getGhoTimelocks().ghoBorrowRateLastUpdate, + vm.getBlockTimestamp() + ); + } + + function test_aaveStewardCanUpdateBorrowCap(uint256 newBorrowCap) public { + uint256 currentBorrowCap = AaveV3Ethereum.POOL.getConfiguration(address(GHO)).getBorrowCap(); + assertEq(currentBorrowCap, 155_000_000); + vm.assume( + newBorrowCap != currentBorrowCap && + _isDifferenceLowerThanMax(currentBorrowCap, newBorrowCap, currentBorrowCap) + ); + + vm.prank(NEW_GHO_AAVE_STEWARD.RISK_COUNCIL()); + NEW_GHO_AAVE_STEWARD.updateGhoBorrowCap(newBorrowCap); + + assertEq(AaveV3Ethereum.POOL.getConfiguration(address(GHO)).getBorrowCap(), newBorrowCap); + assertEq(NEW_GHO_AAVE_STEWARD.getGhoTimelocks().ghoBorrowCapLastUpdate, vm.getBlockTimestamp()); + + // @dev gho cannot be supplied on ethereum + assertEq(AaveV3Ethereum.POOL.getConfiguration(address(GHO)).getSupplyCap(), 0); + } +} diff --git a/src/20241209_Multi_GHOCCIP151Upgrade/GHOCCIP151Upgrade.md b/src/20241209_Multi_GHOCCIP151Upgrade/GHOCCIP151Upgrade.md new file mode 100644 index 000000000..fb8e2c778 --- /dev/null +++ b/src/20241209_Multi_GHOCCIP151Upgrade/GHOCCIP151Upgrade.md @@ -0,0 +1,22 @@ +--- +title: "GHO CCIP 1.5.1 Upgrade" +author: "Aave Labs" +discussions: "" +--- + +## Simple Summary + +## Motivation + +## Specification + +## References + +- Implementation: [AaveV3Ethereum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20241209_Multi_GHOCCIP151Upgrade/AaveV3Ethereum_GHOCCIP151Upgrade_20241209.sol), [AaveV3Arbitrum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20241209_Multi_GHOCCIP151Upgrade/AaveV3Arbitrum_GHOCCIP151Upgrade_20241209.sol) +- Tests: [AaveV3Ethereum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20241209_Multi_GHOCCIP151Upgrade/AaveV3Ethereum_GHOCCIP151Upgrade_20241209.t.sol), [AaveV3Arbitrum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20241209_Multi_GHOCCIP151Upgrade/AaveV3Arbitrum_GHOCCIP151Upgrade_20241209.t.sol) +- [Snapshot](TODO) +- [Discussion](TODO) + +## Copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). diff --git a/src/20241209_Multi_GHOCCIP151Upgrade/GHOCCIP151Upgrade_20241209.s.sol b/src/20241209_Multi_GHOCCIP151Upgrade/GHOCCIP151Upgrade_20241209.s.sol new file mode 100644 index 000000000..c4a58b46b --- /dev/null +++ b/src/20241209_Multi_GHOCCIP151Upgrade/GHOCCIP151Upgrade_20241209.s.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {GovV3Helpers, IPayloadsControllerCore, PayloadsControllerUtils} from 'aave-helpers/src/GovV3Helpers.sol'; +import {GovernanceV3Ethereum} from 'aave-address-book/GovernanceV3Ethereum.sol'; +import {EthereumScript, ArbitrumScript} from 'solidity-utils/contracts/utils/ScriptUtils.sol'; +import {AaveV3Ethereum_GHOCCIP151Upgrade_20241209} from './AaveV3Ethereum_GHOCCIP151Upgrade_20241209.sol'; +import {AaveV3Arbitrum_GHOCCIP151Upgrade_20241209} from './AaveV3Arbitrum_GHOCCIP151Upgrade_20241209.sol'; + +/** + * @dev Deploy Ethereum + * deploy-command: make deploy-ledger contract=src/20241209_Multi_GHOCCIP151Upgrade/GHOCCIP151Upgrade_20241209.s.sol:DeployEthereum chain=mainnet + * verify-command: FOUNDRY_PROFILE=mainnet npx catapulta-verify -b broadcast/GHOCCIP151Upgrade_20241209.s.sol/1/run-latest.json + */ +contract DeployEthereum is EthereumScript { + function run() external broadcast { + // deploy payloads + address payload0 = GovV3Helpers.deployDeterministic( + type(AaveV3Ethereum_GHOCCIP151Upgrade_20241209).creationCode + ); + + // compose action + IPayloadsControllerCore.ExecutionAction[] + memory actions = new IPayloadsControllerCore.ExecutionAction[](1); + actions[0] = GovV3Helpers.buildAction(payload0); + + // register action at payloadsController + GovV3Helpers.createPayload(actions); + } +} + +/** + * @dev Deploy Arbitrum + * deploy-command: make deploy-ledger contract=src/20241209_Multi_GHOCCIP151Upgrade/GHOCCIP151Upgrade_20241209.s.sol:DeployArbitrum chain=arbitrum + * verify-command: FOUNDRY_PROFILE=arbitrum npx catapulta-verify -b broadcast/GHOCCIP151Upgrade_20241209.s.sol/42161/run-latest.json + */ +contract DeployArbitrum is ArbitrumScript { + function run() external broadcast { + // deploy payloads + address payload0 = GovV3Helpers.deployDeterministic( + type(AaveV3Arbitrum_GHOCCIP151Upgrade_20241209).creationCode + ); + + // compose action + IPayloadsControllerCore.ExecutionAction[] + memory actions = new IPayloadsControllerCore.ExecutionAction[](1); + actions[0] = GovV3Helpers.buildAction(payload0); + + // register action at payloadsController + GovV3Helpers.createPayload(actions); + } +} + +/** + * @dev Create Proposal + * command: make deploy-ledger contract=src/20241209_Multi_GHOCCIP151Upgrade/GHOCCIP151Upgrade_20241209.s.sol:CreateProposal chain=mainnet + */ +contract CreateProposal is EthereumScript { + function run() external { + // create payloads + PayloadsControllerUtils.Payload[] memory payloads = new PayloadsControllerUtils.Payload[](2); + + // compose actions for validation + IPayloadsControllerCore.ExecutionAction[] + memory actionsEthereum = new IPayloadsControllerCore.ExecutionAction[](1); + actionsEthereum[0] = GovV3Helpers.buildAction( + type(AaveV3Ethereum_GHOCCIP151Upgrade_20241209).creationCode + ); + payloads[0] = GovV3Helpers.buildMainnetPayload(vm, actionsEthereum); + + IPayloadsControllerCore.ExecutionAction[] + memory actionsArbitrum = new IPayloadsControllerCore.ExecutionAction[](1); + actionsArbitrum[0] = GovV3Helpers.buildAction( + type(AaveV3Arbitrum_GHOCCIP151Upgrade_20241209).creationCode + ); + payloads[1] = GovV3Helpers.buildArbitrumPayload(vm, actionsArbitrum); + + // create proposal + vm.startBroadcast(); + GovV3Helpers.createProposal( + vm, + payloads, + GovernanceV3Ethereum.VOTING_PORTAL_ETH_POL, + GovV3Helpers.ipfsHashFile(vm, 'src/20241209_Multi_GHOCCIP151Upgrade/GHOCCIP151Upgrade.md') + ); + } +} diff --git a/src/20241209_Multi_GHOCCIP151Upgrade/config.ts b/src/20241209_Multi_GHOCCIP151Upgrade/config.ts new file mode 100644 index 000000000..84435de4a --- /dev/null +++ b/src/20241209_Multi_GHOCCIP151Upgrade/config.ts @@ -0,0 +1,17 @@ +import {ConfigFile} from '../../generator/types'; +export const config: ConfigFile = { + rootOptions: { + pools: ['AaveV3Ethereum', 'AaveV3Arbitrum'], + title: 'GHO CCIP 1.5.1 Upgrade', + shortName: 'GHOCCIP151Upgrade', + date: '20241209', + author: 'Aave Labs', + discussion: '', + snapshot: 'Direct-to-AIP', + votingNetwork: 'POLYGON', + }, + poolOptions: { + AaveV3Ethereum: {configs: {OTHERS: {}}, cache: {blockNumber: 21581477}}, + AaveV3Arbitrum: {configs: {OTHERS: {}}, cache: {blockNumber: 293345614}}, + }, +}; diff --git a/src/20241209_Multi_GHOCCIP151Upgrade/utils/CCIPUtils.sol b/src/20241209_Multi_GHOCCIP151Upgrade/utils/CCIPUtils.sol new file mode 100644 index 000000000..e853cd8cc --- /dev/null +++ b/src/20241209_Multi_GHOCCIP151Upgrade/utils/CCIPUtils.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IClient} from 'src/interfaces/ccip/IClient.sol'; +import {IRouter} from 'src/interfaces/ccip/IRouter.sol'; +import {IInternal} from 'src/interfaces/ccip/IInternal.sol'; +import {IEVM2EVMOnRamp} from 'src/interfaces/ccip/IEVM2EVMOnRamp.sol'; + +library CCIPUtils { + uint64 internal constant ETH_CHAIN_SELECTOR = 5009297550715157269; + uint64 internal constant ARB_CHAIN_SELECTOR = 4949039107694359620; + + bytes32 internal constant LEAF_DOMAIN_SEPARATOR = + 0x0000000000000000000000000000000000000000000000000000000000000000; + bytes32 internal constant INTERNAL_DOMAIN_SEPARATOR = + 0x0000000000000000000000000000000000000000000000000000000000000001; + bytes32 internal constant EVM_2_EVM_MESSAGE_HASH = keccak256('EVM2EVMMessageHashV2'); + bytes4 public constant EVM_EXTRA_ARGS_V1_TAG = 0x97a657c9; + + enum PoolVersion { + V1_5_0, + V1_5_1 + } + + struct SourceTokenData { + bytes sourcePoolAddress; + bytes destTokenAddress; + bytes extraData; + uint32 destGasAmount; + } + + struct MessageToEventParams { + IClient.EVM2AnyMessage message; + IRouter router; + uint64 sourceChainSelector; + uint256 feeTokenAmount; + address originalSender; + address sourceToken; + address destinationToken; + PoolVersion poolVersion; + } + + function generateMessage( + address receiver, + uint256 tokenAmountsLength + ) internal pure returns (IClient.EVM2AnyMessage memory) { + return + IClient.EVM2AnyMessage({ + receiver: abi.encode(receiver), + data: '', + tokenAmounts: new IClient.EVMTokenAmount[](tokenAmountsLength), + feeToken: address(0), + extraArgs: argsToBytes(IClient.EVMExtraArgsV1({gasLimit: 0})) + }); + } + + function messageToEvent( + MessageToEventParams memory params + ) public view returns (IInternal.EVM2EVMMessage memory) { + uint64 destChainSelector = params.sourceChainSelector == ETH_CHAIN_SELECTOR + ? ARB_CHAIN_SELECTOR + : ETH_CHAIN_SELECTOR; + IEVM2EVMOnRamp onRamp = IEVM2EVMOnRamp(params.router.getOnRamp(destChainSelector)); + + bytes memory args = new bytes(params.message.extraArgs.length - 4); + for (uint256 i = 4; i < params.message.extraArgs.length; ++i) { + args[i - 4] = params.message.extraArgs[i]; + } + + IInternal.EVM2EVMMessage memory messageEvent = IInternal.EVM2EVMMessage({ + sequenceNumber: onRamp.getExpectedNextSequenceNumber(), + feeTokenAmount: params.feeTokenAmount, + sender: params.originalSender, + nonce: onRamp.getSenderNonce(params.originalSender) + 1, + gasLimit: abi.decode(args, (IClient.EVMExtraArgsV1)).gasLimit, + strict: false, + sourceChainSelector: params.sourceChainSelector, + receiver: abi.decode(params.message.receiver, (address)), + data: params.message.data, + tokenAmounts: params.message.tokenAmounts, + sourceTokenData: new bytes[](params.message.tokenAmounts.length), + feeToken: params.router.getWrappedNative(), + messageId: '' + }); + + for (uint256 i; i < params.message.tokenAmounts.length; ++i) { + messageEvent.sourceTokenData[i] = abi.encode( + SourceTokenData({ + sourcePoolAddress: abi.encode( + onRamp.getPoolBySourceToken(destChainSelector, params.message.tokenAmounts[i].token) + ), + destTokenAddress: abi.encode(params.destinationToken), + extraData: params.poolVersion == PoolVersion.V1_5_1 + ? abi.encode(getTokenDecimals(params.sourceToken)) + : new bytes(0), + destGasAmount: getDestGasAmount(onRamp, params.message.tokenAmounts[i].token) + }) + ); + } + + messageEvent.messageId = hash( + messageEvent, + generateMetadataHash(params.sourceChainSelector, destChainSelector, address(onRamp)) + ); + return messageEvent; + } + + function generateMetadataHash( + uint64 sourceChainSelector, + uint64 destChainSelector, + address onRamp + ) internal pure returns (bytes32) { + return + keccak256(abi.encode(EVM_2_EVM_MESSAGE_HASH, sourceChainSelector, destChainSelector, onRamp)); + } + + function argsToBytes( + IClient.EVMExtraArgsV1 memory extraArgs + ) internal pure returns (bytes memory bts) { + return abi.encodeWithSelector(EVM_EXTRA_ARGS_V1_TAG, extraArgs); + } + + /// @dev Used to hash messages for single-lane ramps. + /// OnRamp hash(EVM2EVMMessage) = OffRamp hash(EVM2EVMMessage) + /// The EVM2EVMMessage's messageId is expected to be the output of this hash function + /// @param original Message to hash + /// @param metadataHash Immutable metadata hash representing a lane with a fixed OnRamp + /// @return hashedMessage hashed message as a keccak256 + function hash( + IInternal.EVM2EVMMessage memory original, + bytes32 metadataHash + ) internal pure returns (bytes32) { + // Fixed-size message fields are included in nested hash to reduce stack pressure. + // This hashing scheme is also used by RMN. If changing it, please notify the RMN maintainers. + return + keccak256( + abi.encode( + LEAF_DOMAIN_SEPARATOR, + metadataHash, + keccak256( + abi.encode( + original.sender, + original.receiver, + original.sequenceNumber, + original.gasLimit, + original.strict, + original.nonce, + original.feeToken, + original.feeTokenAmount + ) + ), + keccak256(original.data), + keccak256(abi.encode(original.tokenAmounts)), + keccak256(abi.encode(original.sourceTokenData)) + ) + ); + } + + function getDestGasAmount(IEVM2EVMOnRamp onRamp, address token) internal view returns (uint32) { + IEVM2EVMOnRamp.TokenTransferFeeConfig memory config = onRamp.getTokenTransferFeeConfig(token); + return + config.isEnabled + ? config.destGasOverhead + : onRamp.getDynamicConfig().defaultTokenDestGasOverhead; + } + + function getTokenDecimals(address token) internal view returns (uint8) { + (bool success, bytes memory data) = token.staticcall(abi.encodeWithSignature('decimals()')); + require(success, 'CCIPUtils: failed to get token decimals'); + return abi.decode(data, (uint8)); + } +} diff --git a/src/20241224_AaveV3Gnosis_AaveV3GnosisInstanceUpdates/AaveV3GnosisInstanceUpdates.md b/src/20241224_AaveV3Gnosis_AaveV3GnosisInstanceUpdates/AaveV3GnosisInstanceUpdates.md new file mode 100644 index 000000000..26d453118 --- /dev/null +++ b/src/20241224_AaveV3Gnosis_AaveV3GnosisInstanceUpdates/AaveV3GnosisInstanceUpdates.md @@ -0,0 +1,49 @@ +--- +title: "Aave v3 Gnosis Instance Updates Part 1" +author: "Aave-chan Initiative" +discussions: "https://governance.aave.com/t/arfc-aave-v3-gnosis-instance-updates/20334" +snapshot: "https://snapshot.box/#/s:aave.eth/proposal/0x2e93ddd01ba5ec415b0962907b7c65def947d1ed94f1e5b402c5578560b1dddb" +--- + +## Simple Summary + +This AIP proposes several updates to the Aave v3 Gnosis instance to improve capital efficiency and add new use cases on the network. The key changes include removing GNO from isolation mode, adjusting the reserve factor for EURe, and creating a new relevant E-mode. + +## Motivation + +GNO has demonstrated strong stability and market presence on Gnosis Chain, making isolation mode unnecessarily restrictive and hindering network growth. Removing GNO from isolation mode will facilitate further expansion of the network. + +The reduction in the EURe reserve factor aligns with the asset's performance and incentivizes increased lending activity. + +introducing a new E-mode for sDAI & EURe will enhance capital efficiency and foster synergies between stable assets. The unique combination of EUR and USD borrowing opportunities is a distinct advantage for Gnosis Chain. + +## Specification + +### Rates and parameters updates + +- Remove GNO from isolation mode +- change EURe Reserve Factor from 20% to 10% + +### E-Modes + +The followings E-mode will be created: + +| **Parameter** | **Value** | **Value** | +| --------------------- | --------- | --------- | +| Asset | sDAI | EURe | +| Collateral | Yes | No | +| Borrowable | No | Yes | +| Max LTV | 85% | 85% | +| Liquidation Threshold | 87.5% | 87.5% | +| Liquidation Bonus | 5% | 5% | + +## References + +- Implementation: [AaveV3Gnosis](https://github.com/bgd-labs/aave-proposals-v3/blob/0ddae1e40d3255d4a2ff2768ae707bcb99a4aa9e/src/20241224_AaveV3Gnosis_AaveV3GnosisInstanceUpdates/AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224.sol) +- Tests: [AaveV3Gnosis](https://github.com/bgd-labs/aave-proposals-v3/blob/0ddae1e40d3255d4a2ff2768ae707bcb99a4aa9e/src/20241224_AaveV3Gnosis_AaveV3GnosisInstanceUpdates/AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224.t.sol) +- [Snapshot](https://snapshot.box/#/s:aave.eth/proposal/0x2e93ddd01ba5ec415b0962907b7c65def947d1ed94f1e5b402c5578560b1dddb) +- [Discussion](https://governance.aave.com/t/arfc-aave-v3-gnosis-instance-updates/20334) + +## Copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). diff --git a/src/20241224_AaveV3Gnosis_AaveV3GnosisInstanceUpdates/AaveV3GnosisInstanceUpdates_20241224.s.sol b/src/20241224_AaveV3Gnosis_AaveV3GnosisInstanceUpdates/AaveV3GnosisInstanceUpdates_20241224.s.sol new file mode 100644 index 000000000..9d4631866 --- /dev/null +++ b/src/20241224_AaveV3Gnosis_AaveV3GnosisInstanceUpdates/AaveV3GnosisInstanceUpdates_20241224.s.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {GovV3Helpers, IPayloadsControllerCore, PayloadsControllerUtils} from 'aave-helpers/src/GovV3Helpers.sol'; +import {GovernanceV3Ethereum} from 'aave-address-book/GovernanceV3Ethereum.sol'; +import {EthereumScript, GnosisScript} from 'solidity-utils/contracts/utils/ScriptUtils.sol'; +import {AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224} from './AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224.sol'; + +/** + * @dev Deploy Gnosis + * deploy-command: make deploy-ledger contract=src/20241224_AaveV3Gnosis_AaveV3GnosisInstanceUpdates/AaveV3GnosisInstanceUpdates_20241224.s.sol:DeployGnosis chain=gnosis + * verify-command: FOUNDRY_PROFILE=gnosis npx catapulta-verify -b broadcast/AaveV3GnosisInstanceUpdates_20241224.s.sol/100/run-latest.json + */ +contract DeployGnosis is GnosisScript { + function run() external broadcast { + // deploy payloads + address payload0 = GovV3Helpers.deployDeterministic( + type(AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224).creationCode + ); + + // compose action + IPayloadsControllerCore.ExecutionAction[] + memory actions = new IPayloadsControllerCore.ExecutionAction[](1); + actions[0] = GovV3Helpers.buildAction(payload0); + + // register action at payloadsController + GovV3Helpers.createPayload(actions); + } +} + +/** + * @dev Create Proposal + * command: make deploy-ledger contract=src/20241224_AaveV3Gnosis_AaveV3GnosisInstanceUpdates/AaveV3GnosisInstanceUpdates_20241224.s.sol:CreateProposal chain=mainnet + */ +contract CreateProposal is EthereumScript { + function run() external { + // create payloads + PayloadsControllerUtils.Payload[] memory payloads = new PayloadsControllerUtils.Payload[](1); + + // compose actions for validation + IPayloadsControllerCore.ExecutionAction[] + memory actionsGnosis = new IPayloadsControllerCore.ExecutionAction[](1); + actionsGnosis[0] = GovV3Helpers.buildAction( + type(AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224).creationCode + ); + payloads[0] = GovV3Helpers.buildGnosisPayload(vm, actionsGnosis); + + // create proposal + vm.startBroadcast(); + GovV3Helpers.createProposal( + vm, + payloads, + GovernanceV3Ethereum.VOTING_PORTAL_ETH_POL, + GovV3Helpers.ipfsHashFile( + vm, + 'src/20241224_AaveV3Gnosis_AaveV3GnosisInstanceUpdates/AaveV3GnosisInstanceUpdates.md' + ) + ); + } +} diff --git a/src/20241224_AaveV3Gnosis_AaveV3GnosisInstanceUpdates/AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224.sol b/src/20241224_AaveV3Gnosis_AaveV3GnosisInstanceUpdates/AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224.sol new file mode 100644 index 000000000..9f22a712a --- /dev/null +++ b/src/20241224_AaveV3Gnosis_AaveV3GnosisInstanceUpdates/AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {AaveV3Gnosis, AaveV3GnosisAssets} from 'aave-address-book/AaveV3Gnosis.sol'; +import {AaveV3PayloadGnosis} from 'aave-helpers/src/v3-config-engine/AaveV3PayloadGnosis.sol'; +import {EngineFlags} from 'aave-v3-origin/contracts/extensions/v3-config-engine/EngineFlags.sol'; +import {IAaveV3ConfigEngine} from 'aave-v3-origin/contracts/extensions/v3-config-engine/IAaveV3ConfigEngine.sol'; +import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; +import {SafeERC20} from 'solidity-utils/contracts/oz-common/SafeERC20.sol'; +import {IEmissionManager} from 'aave-v3-origin/contracts/rewards/interfaces/IEmissionManager.sol'; +/** + * @title Aave v3 Gnosis Instance Updates + * @author Aave-chan Initiative + * - Snapshot: https://snapshot.box/#/s:aave.eth/proposal/0x2e93ddd01ba5ec415b0962907b7c65def947d1ed94f1e5b402c5578560b1dddb + * - Discussion: https://governance.aave.com/t/arfc-aave-v3-gnosis-instance-updates/20334 + */ +contract AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224 is AaveV3PayloadGnosis { + using SafeERC20 for IERC20; + + function collateralsUpdates() + public + pure + override + returns (IAaveV3ConfigEngine.CollateralUpdate[] memory) + { + IAaveV3ConfigEngine.CollateralUpdate[] + memory collateralUpdate = new IAaveV3ConfigEngine.CollateralUpdate[](1); + + collateralUpdate[0] = IAaveV3ConfigEngine.CollateralUpdate({ + asset: AaveV3GnosisAssets.GNO_UNDERLYING, + ltv: EngineFlags.KEEP_CURRENT, + liqThreshold: EngineFlags.KEEP_CURRENT, + liqBonus: EngineFlags.KEEP_CURRENT, + debtCeiling: 0, + liqProtocolFee: EngineFlags.KEEP_CURRENT + }); + + return collateralUpdate; + } + function borrowsUpdates() + public + pure + override + returns (IAaveV3ConfigEngine.BorrowUpdate[] memory) + { + IAaveV3ConfigEngine.BorrowUpdate[] + memory borrowUpdates = new IAaveV3ConfigEngine.BorrowUpdate[](1); + + borrowUpdates[0] = IAaveV3ConfigEngine.BorrowUpdate({ + asset: AaveV3GnosisAssets.EURe_UNDERLYING, + enabledToBorrow: EngineFlags.KEEP_CURRENT, + flashloanable: EngineFlags.KEEP_CURRENT, + borrowableInIsolation: EngineFlags.KEEP_CURRENT, + withSiloedBorrowing: EngineFlags.KEEP_CURRENT, + reserveFactor: 10_00 + }); + + return borrowUpdates; + } + function eModeCategoriesUpdates() + public + pure + override + returns (IAaveV3ConfigEngine.EModeCategoryUpdate[] memory) + { + IAaveV3ConfigEngine.EModeCategoryUpdate[] + memory eModeUpdates = new IAaveV3ConfigEngine.EModeCategoryUpdate[](1); + + eModeUpdates[0] = IAaveV3ConfigEngine.EModeCategoryUpdate({ + eModeCategory: 2, + ltv: 85_00, + liqThreshold: 87_50, + liqBonus: 5_00, + label: 'sDAI / EURe' + }); + + return eModeUpdates; + } + function assetsEModeUpdates() + public + pure + override + returns (IAaveV3ConfigEngine.AssetEModeUpdate[] memory) + { + IAaveV3ConfigEngine.AssetEModeUpdate[] + memory assetEModeUpdates = new IAaveV3ConfigEngine.AssetEModeUpdate[](2); + + assetEModeUpdates[0] = IAaveV3ConfigEngine.AssetEModeUpdate({ + asset: AaveV3GnosisAssets.EURe_UNDERLYING, + eModeCategory: 2, + borrowable: EngineFlags.ENABLED, + collateral: EngineFlags.DISABLED + }); + assetEModeUpdates[1] = IAaveV3ConfigEngine.AssetEModeUpdate({ + asset: AaveV3GnosisAssets.sDAI_UNDERLYING, + eModeCategory: 2, + borrowable: EngineFlags.DISABLED, + collateral: EngineFlags.ENABLED + }); + + return assetEModeUpdates; + } +} diff --git a/src/20241224_AaveV3Gnosis_AaveV3GnosisInstanceUpdates/AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224.t.sol b/src/20241224_AaveV3Gnosis_AaveV3GnosisInstanceUpdates/AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224.t.sol new file mode 100644 index 000000000..37dcb0b19 --- /dev/null +++ b/src/20241224_AaveV3Gnosis_AaveV3GnosisInstanceUpdates/AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {GovV3Helpers} from 'aave-helpers/src/GovV3Helpers.sol'; +import {AaveV3Gnosis} from 'aave-address-book/AaveV3Gnosis.sol'; +import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; +import {IEmissionManager} from 'aave-v3-origin/contracts/rewards/interfaces/IEmissionManager.sol'; + +import 'forge-std/Test.sol'; +import {ProtocolV3TestBase, ReserveConfig} from 'aave-helpers/src/ProtocolV3TestBase.sol'; +import {AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224} from './AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224.sol'; + +/** + * @dev Test for AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224 + * command: FOUNDRY_PROFILE=gnosis forge test --match-path=src/20241224_AaveV3Gnosis_AaveV3GnosisInstanceUpdates/AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224.t.sol -vv + */ +contract AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224_Test is ProtocolV3TestBase { + AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224 internal proposal; + + function setUp() public { + vm.createSelectFork(vm.rpcUrl('gnosis'), 37810182); + proposal = new AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224(); + } + + /** + * @dev executes the generic test suite including e2e and config snapshots + */ + function test_defaultProposalExecution() public { + defaultTest( + 'AaveV3Gnosis_AaveV3GnosisInstanceUpdates_20241224', + AaveV3Gnosis.POOL, + address(proposal) + ); + } +} diff --git a/src/20241224_AaveV3Gnosis_AaveV3GnosisInstanceUpdates/config.ts b/src/20241224_AaveV3Gnosis_AaveV3GnosisInstanceUpdates/config.ts new file mode 100644 index 000000000..0aa8c2e02 --- /dev/null +++ b/src/20241224_AaveV3Gnosis_AaveV3GnosisInstanceUpdates/config.ts @@ -0,0 +1,86 @@ +import {ConfigFile} from '../../generator/types'; +export const config: ConfigFile = { + rootOptions: { + configFile: 'src/20241224_AaveV3Gnosis_AaveV3GnosisInstanceUpdates/config.ts', + force: true, + author: 'Aave-chan Initiative', + pools: ['AaveV3Gnosis'], + title: 'Aave v3 Gnosis Instance Updates', + shortName: 'AaveV3GnosisInstanceUpdates', + date: '20241224', + discussion: 'https://governance.aave.com/t/arfc-aave-v3-gnosis-instance-updates/20334', + snapshot: + 'https://snapshot.box/#/s:aave.eth/proposal/0x2e93ddd01ba5ec415b0962907b7c65def947d1ed94f1e5b402c5578560b1dddb', + votingNetwork: 'POLYGON', + }, + poolOptions: { + AaveV3Gnosis: { + configs: { + COLLATERALS_UPDATE: [ + { + asset: 'GNO', + ltv: '', + liqThreshold: '', + liqBonus: '', + debtCeiling: '0', + liqProtocolFee: '', + }, + ], + BORROWS_UPDATE: [ + { + enabledToBorrow: 'KEEP_CURRENT', + flashloanable: 'KEEP_CURRENT', + borrowableInIsolation: 'KEEP_CURRENT', + withSiloedBorrowing: 'KEEP_CURRENT', + reserveFactor: '10', + asset: 'EURe', + }, + ], + EMODES_UPDATES: [ + { + eModeCategory: 2, + ltv: '90', + liqThreshold: '92.5', + liqBonus: '2.5', + label: 'osGNO / GNO', + }, + {eModeCategory: 3, ltv: '85', liqThreshold: '87.5', liqBonus: '5', label: 'sDAI / EURe'}, + ], + EMODES_ASSETS: [ + {asset: 'osGNO', eModeCategory: '2', collateral: 'ENABLED', borrowable: 'DISABLED'}, + {asset: 'GNO', eModeCategory: '2', collateral: 'DISABLED', borrowable: 'ENABLED'}, + {asset: 'EURe', eModeCategory: '3', collateral: 'DISABLED', borrowable: 'ENABLED'}, + {asset: 'sDAI', eModeCategory: '3', collateral: 'ENABLED', borrowable: 'DISABLED'}, + ], + ASSET_LISTING: [ + { + assetSymbol: 'osGNO', + decimals: 18, + priceFeed: '0xbE26c8b354208E898EBd88B1576C4df2e216ed30', + ltv: '0.05', + liqThreshold: '0.1', + liqBonus: '7.5', + debtCeiling: '0', + liqProtocolFee: '10', + enabledToBorrow: 'DISABLED', + flashloanable: 'DISABLED', + borrowableInIsolation: 'DISABLED', + withSiloedBorrowing: 'DISABLED', + reserveFactor: '10', + supplyCap: '4750', + borrowCap: '1', + rateStrategyParams: { + optimalUtilizationRate: '50', + baseVariableBorrowRate: '0', + variableRateSlope1: '0', + variableRateSlope2: '0', + }, + asset: '0xF490c80aAE5f2616d3e3BDa2483E30C4CB21d1A0', + admin: '0xac140648435d03f784879cd789130F22Ef588Fcd', + }, + ], + }, + cache: {blockNumber: 37692844}, + }, + }, +}; diff --git a/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.sol b/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.sol new file mode 100644 index 000000000..34822d0b0 --- /dev/null +++ b/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {AaveV3EthereumLidoAssets, AaveV3EthereumLidoEModes} from 'aave-address-book/AaveV3EthereumLido.sol'; +import {AaveV3PayloadEthereumLido} from 'aave-helpers/src/v3-config-engine/AaveV3PayloadEthereumLido.sol'; +import {EngineFlags} from 'aave-v3-origin/contracts/extensions/v3-config-engine/EngineFlags.sol'; +import {IAaveV3ConfigEngine} from 'aave-v3-origin/contracts/extensions/v3-config-engine/IAaveV3ConfigEngine.sol'; +/** + * @title Proposal to Remove USDS from sUSDe Liquid E-Mode in Aave Prime Instance + * @author Aave-chan Initiative + * - Snapshot: https://snapshot.box/#/s:aave.eth/proposal/0x2be035a75fb8c5bb4e99e56006e57b7eb7df8bdd5616d903309ef6fc5b7446de + * - Discussion: https://governance.aave.com/t/arfc-proposal-to-remove-usds-from-susde-liquid-e-mode-in-aave-prime-instance/20264 + */ +contract AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224 is + AaveV3PayloadEthereumLido +{ + function eModeCategoriesUpdates() + public + pure + override + returns (IAaveV3ConfigEngine.EModeCategoryUpdate[] memory) + { + IAaveV3ConfigEngine.EModeCategoryUpdate[] + memory eModeUpdates = new IAaveV3ConfigEngine.EModeCategoryUpdate[](1); + + eModeUpdates[0] = IAaveV3ConfigEngine.EModeCategoryUpdate({ + eModeCategory: AaveV3EthereumLidoEModes.SUSDE_STABLECOINS, + ltv: EngineFlags.KEEP_CURRENT, + liqThreshold: EngineFlags.KEEP_CURRENT, + liqBonus: 4_00, + label: EngineFlags.KEEP_CURRENT_STRING + }); + + return eModeUpdates; + } + function assetsEModeUpdates() + public + pure + override + returns (IAaveV3ConfigEngine.AssetEModeUpdate[] memory) + { + IAaveV3ConfigEngine.AssetEModeUpdate[] + memory assetEModeUpdates = new IAaveV3ConfigEngine.AssetEModeUpdate[](1); + + assetEModeUpdates[0] = IAaveV3ConfigEngine.AssetEModeUpdate({ + asset: AaveV3EthereumLidoAssets.USDS_UNDERLYING, + eModeCategory: AaveV3EthereumLidoEModes.SUSDE_STABLECOINS, + borrowable: EngineFlags.DISABLED, + collateral: EngineFlags.KEEP_CURRENT + }); + + return assetEModeUpdates; + } +} diff --git a/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.t.sol b/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.t.sol new file mode 100644 index 000000000..673c18f5b --- /dev/null +++ b/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {AaveV3EthereumLido} from 'aave-address-book/AaveV3EthereumLido.sol'; + +import 'forge-std/Test.sol'; +import {ProtocolV3TestBase, ReserveConfig} from 'aave-helpers/src/ProtocolV3TestBase.sol'; +import {AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224} from './AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.sol'; + +/** + * @dev Test for AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224 + * command: FOUNDRY_PROFILE=mainnet forge test --match-path=src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.t.sol -vv + */ +contract AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224_Test is + ProtocolV3TestBase +{ + AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224 + internal proposal; + + function setUp() public { + vm.createSelectFork(vm.rpcUrl('mainnet'), 21474028); + proposal = new AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224(); + } + + /** + * @dev executes the generic test suite including e2e and config snapshots + */ + function test_defaultProposalExecution() public { + defaultTest( + 'AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224', + AaveV3EthereumLido.POOL, + address(proposal) + ); + } +} diff --git a/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.sol b/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.sol new file mode 100644 index 000000000..61f55e0dc --- /dev/null +++ b/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {AaveV3EthereumEModes} from 'aave-address-book/AaveV3Ethereum.sol'; +import {AaveV3PayloadEthereum} from 'aave-helpers/src/v3-config-engine/AaveV3PayloadEthereum.sol'; +import {EngineFlags} from 'aave-v3-origin/contracts/extensions/v3-config-engine/EngineFlags.sol'; +import {IAaveV3ConfigEngine} from 'aave-v3-origin/contracts/extensions/v3-config-engine/IAaveV3ConfigEngine.sol'; +/** + * @title Proposal to Remove USDS from sUSDe Liquid E-Mode in Aave Prime Instance + * @author Aave-chan Initiative + * - Snapshot: https://snapshot.box/#/s:aave.eth/proposal/0x2be035a75fb8c5bb4e99e56006e57b7eb7df8bdd5616d903309ef6fc5b7446de + * - Discussion: https://governance.aave.com/t/arfc-proposal-to-remove-usds-from-susde-liquid-e-mode-in-aave-prime-instance/20264 + */ +contract AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224 is + AaveV3PayloadEthereum +{ + function eModeCategoriesUpdates() + public + pure + override + returns (IAaveV3ConfigEngine.EModeCategoryUpdate[] memory) + { + IAaveV3ConfigEngine.EModeCategoryUpdate[] + memory eModeUpdates = new IAaveV3ConfigEngine.EModeCategoryUpdate[](1); + + eModeUpdates[0] = IAaveV3ConfigEngine.EModeCategoryUpdate({ + eModeCategory: AaveV3EthereumEModes.SUSDE_STABLECOINS, + ltv: EngineFlags.KEEP_CURRENT, + liqThreshold: EngineFlags.KEEP_CURRENT, + liqBonus: 4_00, + label: EngineFlags.KEEP_CURRENT_STRING + }); + + return eModeUpdates; + } +} diff --git a/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.t.sol b/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.t.sol new file mode 100644 index 000000000..6d5d74244 --- /dev/null +++ b/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {AaveV3Ethereum} from 'aave-address-book/AaveV3Ethereum.sol'; + +import 'forge-std/Test.sol'; +import {ProtocolV3TestBase, ReserveConfig} from 'aave-helpers/src/ProtocolV3TestBase.sol'; +import {AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224} from './AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.sol'; + +/** + * @dev Test for AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224 + * command: FOUNDRY_PROFILE=mainnet forge test --match-path=src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.t.sol -vv + */ +contract AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224_Test is + ProtocolV3TestBase +{ + AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224 + internal proposal; + + function setUp() public { + vm.createSelectFork(vm.rpcUrl('mainnet'), 21474019); + proposal = new AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224(); + } + + /** + * @dev executes the generic test suite including e2e and config snapshots + */ + function test_defaultProposalExecution() public { + defaultTest( + 'AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224', + AaveV3Ethereum.POOL, + address(proposal) + ); + } +} diff --git a/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance.md b/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance.md new file mode 100644 index 000000000..527259063 --- /dev/null +++ b/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance.md @@ -0,0 +1,45 @@ +--- +title: "Proposal to Remove USDS from sUSDe Liquid E-Mode in Aave Prime Instance" +author: "Aave-chan Initiative" +discussions: "https://governance.aave.com/t/arfc-proposal-to-remove-usds-from-susde-liquid-e-mode-in-aave-prime-instance/20264" +snapshot: "https://snapshot.box/#/s:aave.eth/proposal/0x2be035a75fb8c5bb4e99e56006e57b7eb7df8bdd5616d903309ef6fc5b7446de" +--- + +## Simple Summary + +This proposal recommends the removal of USDS from the sUSDe Liquid E-Mode in the Aave Prime instance. + +## Motivation + +The sUSDe Liquid E-Mode was introduced to enhance capital efficiency for users by allowing higher loan-to-value (LTV) ratios when using sUSDe as collateral to borrow stablecoins like USDS. See [this proposal](https://governance.aave.com/t/arfc-onboard-and-enable-susde-liquid-e-mode-on-aave-v3-mainnet-and-lido-instance/19703) for more context. + +However, recent market observations have indicated increased borrow rates and potential liquidity mismatches involving USDS within this E-Mode. To address these concerns, this proposal suggests temporarily removing USDS from the sUSDe Liquid E-Mode in the Aave Prime instance. + +The primary motivations for this proposal are: + +1. **Risk Mitigation:** The inclusion of USDS in the sUSDe Liquid E-Mode has led to elevated borrow rates and potential liquidity mismatches. +2. **Collateral Isolation:** Until a wrapper is available to isolate USDS collateral in the Prime instance, USDS remains the primary exposure. Removing it from the sUSDe Liquid E-Mode will help manage associated risks more effectively. +3. **User Impact:** This change will not negatively impact existing user positions but will prevent the establishment of new ones involving USDS in the sUSDe Liquid E-Mode, thereby safeguarding current users while mitigating potential future risks. + +## Specification + +The proposed changes are as follows: + +- **Asset Removal:** Exclude USDS from the sUSDe Liquid E-Mode in the Aave Prime instance. +- **Parameter Adjustments:** Update the E-Mode configuration to reflect the removal of USDS, ensuring alignment with the protocol’s risk management framework. +- **Liquidation buffer improvement:** + echoing concerns from @LlamaRisk and other service providers, sUSDe emode on both Prime & Core instances are set to increase their buffer for liquidations. + - Increase Liquidation Bonus from 3 to 4% on e-modes on Core and Prime instances. + +Current ARFC will be reviewed by Risk Service Providers and their feedback will be included in the current ARFC. + +## References + +- Implementation: [AaveV3Ethereum](https://github.com/bgd-labs/aave-proposals-v3/blob/f44270b61b402ce70bd5105418a8191ceea06959/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.sol), [AaveV3EthereumLido](https://github.com/bgd-labs/aave-proposals-v3/blob/f44270b61b402ce70bd5105418a8191ceea06959/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.sol) +- Tests: [AaveV3Ethereum](https://github.com/bgd-labs/aave-proposals-v3/blob/f44270b61b402ce70bd5105418a8191ceea06959/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.t.sol), [AaveV3EthereumLido](https://github.com/bgd-labs/aave-proposals-v3/blob/f44270b61b402ce70bd5105418a8191ceea06959/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.t.sol) +- [Snapshot](https://snapshot.box/#/s:aave.eth/proposal/0x2be035a75fb8c5bb4e99e56006e57b7eb7df8bdd5616d903309ef6fc5b7446de) +- [Discussion](https://governance.aave.com/t/arfc-proposal-to-remove-usds-from-susde-liquid-e-mode-in-aave-prime-instance/20264) + +## Copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). diff --git a/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.s.sol b/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.s.sol new file mode 100644 index 000000000..6e2f8e065 --- /dev/null +++ b/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.s.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {GovV3Helpers, IPayloadsControllerCore, PayloadsControllerUtils} from 'aave-helpers/src/GovV3Helpers.sol'; +import {GovernanceV3Ethereum} from 'aave-address-book/GovernanceV3Ethereum.sol'; +import {EthereumScript} from 'solidity-utils/contracts/utils/ScriptUtils.sol'; +import {AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224} from './AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.sol'; +import {AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224} from './AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.sol'; + +/** + * @dev Deploy Ethereum + * deploy-command: make deploy-ledger contract=src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.s.sol:DeployEthereum chain=mainnet + * verify-command: FOUNDRY_PROFILE=mainnet npx catapulta-verify -b broadcast/ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.s.sol/1/run-latest.json + */ +contract DeployEthereum is EthereumScript { + function run() external broadcast { + // deploy payloads + address payload0 = GovV3Helpers.deployDeterministic( + type(AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224) + .creationCode + ); + address payload1 = GovV3Helpers.deployDeterministic( + type(AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224) + .creationCode + ); + + // compose action + IPayloadsControllerCore.ExecutionAction[] + memory actions = new IPayloadsControllerCore.ExecutionAction[](2); + actions[0] = GovV3Helpers.buildAction(payload0); + actions[1] = GovV3Helpers.buildAction(payload1); + + // register action at payloadsController + GovV3Helpers.createPayload(actions); + } +} + +/** + * @dev Create Proposal + * command: make deploy-ledger contract=src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224.s.sol:CreateProposal chain=mainnet + */ +contract CreateProposal is EthereumScript { + function run() external { + // create payloads + PayloadsControllerUtils.Payload[] memory payloads = new PayloadsControllerUtils.Payload[](1); + + // compose actions for validation + IPayloadsControllerCore.ExecutionAction[] + memory actionsEthereum = new IPayloadsControllerCore.ExecutionAction[](2); + actionsEthereum[0] = GovV3Helpers.buildAction( + type(AaveV3Ethereum_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224) + .creationCode + ); + actionsEthereum[1] = GovV3Helpers.buildAction( + type(AaveV3EthereumLido_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance_20241224) + .creationCode + ); + payloads[0] = GovV3Helpers.buildMainnetPayload(vm, actionsEthereum); + + // create proposal + vm.startBroadcast(); + GovV3Helpers.createProposal( + vm, + payloads, + GovernanceV3Ethereum.VOTING_PORTAL_ETH_POL, + GovV3Helpers.ipfsHashFile( + vm, + 'src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance.md' + ) + ); + } +} diff --git a/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/config.ts b/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/config.ts new file mode 100644 index 000000000..55c053781 --- /dev/null +++ b/src/20241224_Multi_ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance/config.ts @@ -0,0 +1,53 @@ +import {ConfigFile} from '../../generator/types'; +export const config: ConfigFile = { + rootOptions: { + author: 'Aave-chan Initiative', + pools: ['AaveV3Ethereum', 'AaveV3EthereumLido'], + title: 'Proposal to Remove USDS from sUSDe Liquid E-Mode in Aave Prime Instance', + shortName: 'ProposalToRemoveUSDSFromSUSDeLiquidEModeInAavePrimeInstance', + date: '20241224', + discussion: + 'https://governance.aave.com/t/arfc-proposal-to-remove-usds-from-susde-liquid-e-mode-in-aave-prime-instance/20264', + snapshot: + 'https://snapshot.box/#/s:aave.eth/proposal/0x2be035a75fb8c5bb4e99e56006e57b7eb7df8bdd5616d903309ef6fc5b7446de', + votingNetwork: 'POLYGON', + }, + poolOptions: { + AaveV3Ethereum: { + configs: { + EMODES_UPDATES: [ + { + eModeCategory: 'AaveV3EthereumEModes.SUSDE_STABLECOINS', + ltv: '', + liqThreshold: '', + liqBonus: '4', + label: '', + }, + ], + }, + cache: {blockNumber: 21474019}, + }, + AaveV3EthereumLido: { + configs: { + EMODES_UPDATES: [ + { + eModeCategory: 'AaveV3EthereumLidoEModes.SUSDE_STABLECOINS', + ltv: '', + liqThreshold: '', + liqBonus: '4', + label: '', + }, + ], + EMODES_ASSETS: [ + { + asset: 'USDS', + eModeCategory: 'AaveV3EthereumLidoEModes.SUSDE_STABLECOINS', + collateral: 'KEEP_CURRENT', + borrowable: 'DISABLED', + }, + ], + }, + cache: {blockNumber: 21474028}, + }, + }, +}; diff --git a/src/20241229_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance/AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229.sol b/src/20241229_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance/AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229.sol new file mode 100644 index 000000000..1809b38ee --- /dev/null +++ b/src/20241229_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance/AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; +import {AaveV3EthereumLido} from 'aave-address-book/AaveV3EthereumLido.sol'; +import {IAccessControl} from 'openzeppelin-contracts/contracts/access/IAccessControl.sol'; +import {IGhoBucketSteward} from '../interfaces/IGhoBucketSteward.sol'; +import {IGhoToken} from '../interfaces/IGhoToken.sol'; +import {IGhoDirectMinter} from './IGhoDirectMinter.sol'; +import {IProposalGenericExecutor} from 'aave-helpers/src/interfaces/IProposalGenericExecutor.sol'; + +/** + * @title Deploy 10M GHO into Aave v3 Lido Instance + * @author karpatkey_TokenLogic & BGD Labs + * - Snapshot: https://snapshot.box/#/s:aave.eth/proposal/0x4af37d6b4a1c9029c7693c4bdfb646931a9815c4a56d9066ac1fbdbef7cd15e5 + * - Discussion: https://governance.aave.com/t/arfc-mint-deploy-10m-gho-into-aave-v3-lido-instance/19700 + */ +contract AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229 is + IProposalGenericExecutor +{ + uint128 public constant GHO_MINT_AMOUNT = 10_000_000e18; + + // https://etherscan.io/address/0x2cE01c87Fec1b71A9041c52CaED46Fc5f4807285 + address public constant FACILITATOR = 0x2cE01c87Fec1b71A9041c52CaED46Fc5f4807285; + + function execute() external { + IAccessControl(address(AaveV3EthereumLido.ACL_MANAGER)).grantRole( + AaveV3EthereumLido.ACL_MANAGER.RISK_ADMIN_ROLE(), + FACILITATOR + ); + IGhoToken(AaveV3EthereumAssets.GHO_UNDERLYING).addFacilitator( + FACILITATOR, + 'LidoGhoDirectMinter', + GHO_MINT_AMOUNT + ); + IGhoDirectMinter(FACILITATOR).mintAndSupply(GHO_MINT_AMOUNT); + + // Allow risk council to control the bucket capacity + address[] memory vaults = new address[](1); + vaults[0] = FACILITATOR; + // https://etherscan.io/address/0x46Aa1063e5265b43663E81329333B47c517A5409 + IGhoBucketSteward(0x46Aa1063e5265b43663E81329333B47c517A5409).setControlledFacilitator( + vaults, + true + ); + } +} diff --git a/src/20241229_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance/AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229.t.sol b/src/20241229_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance/AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229.t.sol new file mode 100644 index 000000000..ef94ac3f9 --- /dev/null +++ b/src/20241229_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance/AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229.t.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; +import {AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; +import {AaveV3EthereumLido, AaveV3EthereumLidoAssets} from 'aave-address-book/AaveV3EthereumLido.sol'; +import {IAccessControl} from 'openzeppelin-contracts/contracts/access/IAccessControl.sol'; +import {IGhoBucketSteward} from '../interfaces/IGhoBucketSteward.sol'; +import {IGhoToken} from '../interfaces/IGhoToken.sol'; +import {IGhoDirectMinter} from './IGhoDirectMinter.sol'; +import {ProtocolV3TestBase, ReserveConfig} from 'aave-helpers/src/ProtocolV3TestBase.sol'; + +import {AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229} from './AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229.sol'; + +/** + * @dev Test for AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229 + * command: FOUNDRY_PROFILE=mainnet forge test --match-path=src/20241229_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance/AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229.t.sol -vv + */ +contract AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229_Test is ProtocolV3TestBase { + AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229 internal proposal; + + function setUp() public { + vm.createSelectFork(vm.rpcUrl('mainnet'), 21516467); + proposal = new AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229(); + } + + /** + * @dev executes the generic test suite including e2e and config snapshots + */ + function test_defaultProposalExecution() public { + defaultTest( + 'AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229', + AaveV3EthereumLido.POOL, + address(proposal) + ); + } + + function test_accessControl() public { + assertEq( + IAccessControl(address(AaveV3EthereumLido.ACL_MANAGER)).hasRole( + AaveV3EthereumLido.ACL_MANAGER.RISK_ADMIN_ROLE(), + proposal.FACILITATOR() + ), + false + ); + + address[] memory facilitators = IGhoToken(AaveV3EthereumAssets.GHO_UNDERLYING) + .getFacilitatorsList(); + uint256 facilitatorsLength = facilitators.length; + + assertNotEq(proposal.FACILITATOR(), facilitators[facilitators.length - 1]); + + executePayload(vm, address(proposal)); + + assertEq( + IAccessControl(address(AaveV3EthereumLido.ACL_MANAGER)).hasRole( + AaveV3EthereumLido.ACL_MANAGER.RISK_ADMIN_ROLE(), + proposal.FACILITATOR() + ), + true + ); + + facilitators = IGhoToken(AaveV3EthereumAssets.GHO_UNDERLYING).getFacilitatorsList(); + + assertEq(proposal.FACILITATOR(), facilitators[facilitators.length - 1]); + assertEq(facilitators.length, facilitatorsLength + 1); + } + + function test_supplyIncreases() public { + uint256 ghoBalanceBefore = IGhoToken(AaveV3EthereumAssets.GHO_UNDERLYING).balanceOf( + AaveV3EthereumLidoAssets.GHO_A_TOKEN + ); + + uint256 aGhoBalanceBefore = IERC20(AaveV3EthereumLidoAssets.GHO_A_TOKEN).balanceOf( + proposal.FACILITATOR() + ); + + executePayload(vm, address(proposal)); + + assertEq( + IGhoToken(AaveV3EthereumAssets.GHO_UNDERLYING).balanceOf( + AaveV3EthereumLidoAssets.GHO_A_TOKEN + ), + ghoBalanceBefore + proposal.GHO_MINT_AMOUNT() + ); + + assertEq( + IERC20(AaveV3EthereumLidoAssets.GHO_A_TOKEN).balanceOf(proposal.FACILITATOR()), + aGhoBalanceBefore + proposal.GHO_MINT_AMOUNT() + ); + } +} diff --git a/src/20241229_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance/Deploy10MGHOIntoAaveV3LidoInstance.md b/src/20241229_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance/Deploy10MGHOIntoAaveV3LidoInstance.md new file mode 100644 index 000000000..228f43375 --- /dev/null +++ b/src/20241229_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance/Deploy10MGHOIntoAaveV3LidoInstance.md @@ -0,0 +1,35 @@ +--- +title: "Deploy 10M GHO into Aave v3 Lido Instance" +author: "karpatkey_TokenLogic & BGD Labs" +discussions: "https://governance.aave.com/t/arfc-mint-deploy-10m-gho-into-aave-v3-lido-instance/19700" +snapshot: "https://snapshot.box/#/s:aave.eth/proposal/0x4af37d6b4a1c9029c7693c4bdfb646931a9815c4a56d9066ac1fbdbef7cd15e5" +--- + +## Simple Summary + +Following the addition of GHO to the Lido instance of Aave v3 on Ethereum, this publication proposes supporting GHO liquidity by minting 10M units of GHO and depositing into Aave Lido. + +## Motivation + +By providing liquidity on the Lido instance, the Aave DAO shall provide the initial bootstrapping liquidity in a very cost efficient manner and, in doing so, enhance the DAO's revenue generated from the Lido instance. GHO holders will be able to deposit GHO to earn the deposit yield whilst benefit from small than otherwise concentration risk within the reserve due to the DAO providing the bootstrapping liquidity. + +With strong demand for GHO using wstETH as collateral resulting in 23.7% of GHO supply sourced from the Main market being backed by wstETH, this demonstrates that users actively seek to benefit from the capital efficiency of using an LST as collateral. Deploying additional GHO into the Lido instance is expected to further encourage this positive behavior, reinforcing the appeal of wstETH as a reliable, yield-bearing collateral for GHO. + +## Specification + +- Prior to proposal, GhoDirectMinter contract was deployed [here](https://etherscan.io/address/0x2cE01c87Fec1b71A9041c52CaED46Fc5f4807285) +- Grant RISK_ADMIN_ROLE to GhoDirectMinter referenced above +- Add GhoDirectMinter as a GHO token facilitator +- Mint 10M GHO and supply into Aave Lido +- Allow GhoBucketSteward to control GhoDirectMinter + +## References + +- Implementation: [AaveV3EthereumLido](https://github.com/bgd-labs/aave-proposals-v3/blob/308146dc34181b49d22282ac967de30ed28a3296/src/20241229_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance/AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229.sol) +- Tests: [AaveV3EthereumLido](https://github.com/bgd-labs/aave-proposals-v3/blob/308146dc34181b49d22282ac967de30ed28a3296/src/20241229_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance/AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229.t.sol) +- [Snapshot](https://snapshot.box/#/s:aave.eth/proposal/0x4af37d6b4a1c9029c7693c4bdfb646931a9815c4a56d9066ac1fbdbef7cd15e5) +- [Discussion](https://governance.aave.com/t/arfc-mint-deploy-10m-gho-into-aave-v3-lido-instance/19700) + +## Copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). diff --git a/src/20241229_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance/Deploy10MGHOIntoAaveV3LidoInstance_20241229.s.sol b/src/20241229_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance/Deploy10MGHOIntoAaveV3LidoInstance_20241229.s.sol new file mode 100644 index 000000000..62b316566 --- /dev/null +++ b/src/20241229_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance/Deploy10MGHOIntoAaveV3LidoInstance_20241229.s.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {GovV3Helpers, IPayloadsControllerCore, PayloadsControllerUtils} from 'aave-helpers/src/GovV3Helpers.sol'; +import {GovernanceV3Ethereum} from 'aave-address-book/GovernanceV3Ethereum.sol'; +import {EthereumScript} from 'solidity-utils/contracts/utils/ScriptUtils.sol'; +import {AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229} from './AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229.sol'; + +/** + * @dev Deploy Ethereum + * deploy-command: make deploy-ledger contract=src/20241229_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance/Deploy10MGHOIntoAaveV3LidoInstance_20241229.s.sol:DeployEthereum chain=mainnet + * verify-command: FOUNDRY_PROFILE=mainnet npx catapulta-verify -b broadcast/Deploy10MGHOIntoAaveV3LidoInstance_20241229.s.sol/1/run-latest.json + */ +contract DeployEthereum is EthereumScript { + function run() external broadcast { + // deploy payloads + address payload0 = GovV3Helpers.deployDeterministic( + type(AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229).creationCode + ); + + // compose action + IPayloadsControllerCore.ExecutionAction[] + memory actions = new IPayloadsControllerCore.ExecutionAction[](1); + actions[0] = GovV3Helpers.buildAction(payload0); + + // register action at payloadsController + GovV3Helpers.createPayload(actions); + } +} + +/** + * @dev Create Proposal + * command: make deploy-ledger contract=src/20241229_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance/Deploy10MGHOIntoAaveV3LidoInstance_20241229.s.sol:CreateProposal chain=mainnet + */ +contract CreateProposal is EthereumScript { + function run() external { + // create payloads + PayloadsControllerUtils.Payload[] memory payloads = new PayloadsControllerUtils.Payload[](1); + + // compose actions for validation + IPayloadsControllerCore.ExecutionAction[] + memory actionsEthereum = new IPayloadsControllerCore.ExecutionAction[](1); + actionsEthereum[0] = GovV3Helpers.buildAction( + type(AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance_20241229).creationCode + ); + payloads[0] = GovV3Helpers.buildMainnetPayload(vm, actionsEthereum); + + // create proposal + vm.startBroadcast(); + GovV3Helpers.createProposal( + vm, + payloads, + GovernanceV3Ethereum.VOTING_PORTAL_ETH_POL, + GovV3Helpers.ipfsHashFile( + vm, + 'src/20241229_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance/Deploy10MGHOIntoAaveV3LidoInstance.md' + ) + ); + } +} diff --git a/src/20241229_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance/IGhoDirectMinter.sol b/src/20241229_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance/IGhoDirectMinter.sol new file mode 100644 index 000000000..b58153dd4 --- /dev/null +++ b/src/20241229_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance/IGhoDirectMinter.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +interface IGhoDirectMinter { + function mintAndSupply(uint256 amount) external; +} diff --git a/src/20241229_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance/config.ts b/src/20241229_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance/config.ts new file mode 100644 index 000000000..834a5dcd2 --- /dev/null +++ b/src/20241229_AaveV3EthereumLido_Deploy10MGHOIntoAaveV3LidoInstance/config.ts @@ -0,0 +1,16 @@ +import {ConfigFile} from '../../generator/types'; +export const config: ConfigFile = { + rootOptions: { + pools: ['AaveV3EthereumLido'], + title: 'Deploy 10M GHO into Aave v3 Lido Instance', + shortName: 'Deploy10MGHOIntoAaveV3LidoInstance', + date: '20241229', + author: 'karpatkey_TokenLogic & BGD Labs', + discussion: + 'https://governance.aave.com/t/arfc-mint-deploy-10m-gho-into-aave-v3-lido-instance/19700', + snapshot: + 'https://snapshot.box/#/s:aave.eth/proposal/0x4af37d6b4a1c9029c7693c4bdfb646931a9815c4a56d9066ac1fbdbef7cd15e5', + votingNetwork: 'POLYGON', + }, + poolOptions: {AaveV3EthereumLido: {configs: {OTHERS: {}}, cache: {blockNumber: 21510730}}}, +}; diff --git a/src/interfaces/IGhoCcipSteward.sol b/src/interfaces/IGhoCcipSteward.sol index 39bec9294..57c1e8017 100644 --- a/src/interfaces/IGhoCcipSteward.sol +++ b/src/interfaces/IGhoCcipSteward.sol @@ -41,6 +41,12 @@ interface IGhoCcipSteward { uint128 inboundRate ) external; + /** + * @notice Returns timestamp of the last update of Ccip parameters. + * @return The CcipDebounce struct describing the last update of Ccip parameters. + */ + function getCcipTimelocks() external view returns (CcipDebounce memory); + /** * @notice Returns the minimum delay that must be respected between parameters update. * @return The minimum delay between parameter updates (in seconds) diff --git a/src/interfaces/IGhoToken.sol b/src/interfaces/IGhoToken.sol index 2c78aeebd..d37fb1643 100644 --- a/src/interfaces/IGhoToken.sol +++ b/src/interfaces/IGhoToken.sol @@ -1,14 +1,57 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -interface IGhoToken { +import {IERC20} from 'forge-std/interfaces/IERC20.sol'; + +interface IGhoToken is IERC20 { struct Facilitator { uint128 bucketCapacity; uint128 bucketLevel; string label; } - function balanceOf(address user) external returns (uint256); + /** + * @notice Mints the requested amount of tokens to the account address. + * @dev Only facilitators with enough bucket capacity available can mint. + * @dev The bucket level is increased upon minting. + * @param account The address receiving the GHO tokens + * @param amount The amount to mint + */ + function mint(address account, uint256 amount) external; + + /** + * @notice Burns the requested amount of tokens from the account address. + * @dev Only active facilitators (bucket level > 0) can burn. + * @dev The bucket level is decreased upon burning. + * @param amount The amount to burn + */ + function burn(uint256 amount) external; + + /** + * @notice Add the facilitator passed with the parameters to the facilitators list. + * @dev Only accounts with `FACILITATOR_MANAGER_ROLE` role can call this function + * @param facilitatorAddress The address of the facilitator to add + * @param facilitatorLabel A human readable identifier for the facilitator + * @param bucketCapacity The upward limit of GHO can be minted by the facilitator + */ + function addFacilitator( + address facilitatorAddress, + string calldata facilitatorLabel, + uint128 bucketCapacity + ) external; + + /** + * @notice Remove the facilitator from the facilitators list. + * @dev Only accounts with `FACILITATOR_MANAGER_ROLE` role can call this function + * @param facilitatorAddress The address of the facilitator to remove + */ + function removeFacilitator(address facilitatorAddress) external; + + /** + * @notice Returns the list of the addresses of the active facilitator + * @return The list of the facilitators addresses + */ + function getFacilitatorsList() external view returns (address[] memory); /** * @notice Set the bucket capacity of the facilitator. diff --git a/src/interfaces/ccip/IEVM2EVMOffRamp.sol b/src/interfaces/ccip/IEVM2EVMOffRamp.sol index 7f8b84385..3c79cb4ef 100644 --- a/src/interfaces/ccip/IEVM2EVMOffRamp.sol +++ b/src/interfaces/ccip/IEVM2EVMOffRamp.sol @@ -2,9 +2,10 @@ pragma solidity ^0.8.0; -import {IInternal} from 'src/interfaces/ccip/IInternal.sol'; +import {IInternal} from './IInternal.sol'; +import {ITypeAndVersion} from './ITypeAndVersion.sol'; -interface IEVM2EVMOffRamp_1_2 { +interface IEVM2EVMOffRamp_1_2 is ITypeAndVersion { /// @notice Execute a single message. /// @param message The message that will be executed. /// @param offchainTokenData Token transfer data to be passed to TokenPool. @@ -18,7 +19,7 @@ interface IEVM2EVMOffRamp_1_2 { ) external; } -interface IEVM2EVMOffRamp_1_5 { +interface IEVM2EVMOffRamp_1_5 is ITypeAndVersion { /// @notice Execute a single message. /// @param message The message that will be executed. /// @param offchainTokenData Token transfer data to be passed to TokenPool. @@ -31,4 +32,18 @@ interface IEVM2EVMOffRamp_1_5 { bytes[] calldata offchainTokenData, uint32[] memory tokenGasOverrides ) external; + + /// @notice Dynamic offRamp config + /// @dev since OffRampConfig is part of OffRampConfigChanged event, if changing it, we should update the ABI on Atlas + struct DynamicConfig { + uint32 permissionLessExecutionThresholdSeconds; // ─╮ Waiting time before manual execution is enabled + uint32 maxDataBytes; // │ Maximum payload data size in bytes + uint16 maxNumberOfTokensPerMsg; // │ Maximum number of ERC20 token transfers that can be included per message + address router; // ─────────────────────────────────╯ Router address + address priceRegistry; // Price registry address + } + + /// @notice Returns the current dynamic config. + /// @return The current config. + function getDynamicConfig() external view returns (DynamicConfig memory); } diff --git a/src/interfaces/ccip/IEVM2EVMOnRamp.sol b/src/interfaces/ccip/IEVM2EVMOnRamp.sol index 20efb0cfd..306f1ebf0 100644 --- a/src/interfaces/ccip/IEVM2EVMOnRamp.sol +++ b/src/interfaces/ccip/IEVM2EVMOnRamp.sol @@ -3,8 +3,9 @@ pragma solidity ^0.8.0; import {IInternal} from './IInternal.sol'; +import {ITypeAndVersion} from './ITypeAndVersion.sol'; -interface IEVM2EVMOnRamp { +interface IEVM2EVMOnRamp is ITypeAndVersion { struct TokenTransferFeeConfig { uint32 minFeeUSDCents; // ──────────╮ Minimum fee to charge per token transfer, multiples of 0.01 USD uint32 maxFeeUSDCents; // │ Maximum fee to charge per token transfer, multiples of 0.01 USD @@ -34,6 +35,17 @@ interface IEVM2EVMOnRamp { bool enforceOutOfOrder; // ──────────────────╯ Whether to enforce the allowOutOfOrderExecution extraArg value to be true. } + struct StaticConfig { + address linkToken; // ────────╮ Link token address + uint64 chainSelector; // ─────╯ Source chainSelector + uint64 destChainSelector; // ─╮ Destination chainSelector + uint64 defaultTxGasLimit; // │ Default gas limit for a tx + uint96 maxNopFeesJuels; // ───╯ Max nop fee balance onramp can have + address prevOnRamp; // Address of previous-version OnRamp + address rmnProxy; // Address of RMN proxy + address tokenAdminRegistry; // Address of the token admin registry + } + /// @notice Gets the next sequence number to be used in the onRamp /// @return the next sequence number to be used function getExpectedNextSequenceNumber() external view returns (uint64); @@ -68,4 +80,8 @@ interface IEVM2EVMOnRamp { /// @notice Returns the dynamic onRamp config. /// @return dynamicConfig the configuration. function getDynamicConfig() external view returns (DynamicConfig memory dynamicConfig); + + /// @notice Returns the static onRamp config. + /// @return the configuration. + function getStaticConfig() external view returns (StaticConfig memory); } diff --git a/src/interfaces/ccip/IInternal.sol b/src/interfaces/ccip/IInternal.sol index 062ee93da..050e557cd 100644 --- a/src/interfaces/ccip/IInternal.sol +++ b/src/interfaces/ccip/IInternal.sol @@ -5,6 +5,33 @@ pragma solidity ^0.8.0; import {IClient} from 'src/interfaces/ccip/IClient.sol'; interface IInternal { + /// @notice A collection of token price and gas price updates. + /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. + struct PriceUpdates { + TokenPriceUpdate[] tokenPriceUpdates; + GasPriceUpdate[] gasPriceUpdates; + } + + /// @notice Token price in USD. + /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. + struct TokenPriceUpdate { + address sourceToken; // Source token + uint224 usdPerToken; // 1e18 USD per 1e18 of the smallest token denomination. + } + + /// @notice Gas price for a given chain in USD, its value may contain tightly packed fields. + /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. + struct GasPriceUpdate { + uint64 destChainSelector; // Destination chain selector + uint224 usdPerUnitGas; // 1e18 USD per smallest unit (e.g. wei) of destination chain gas + } + + /// @notice A timestamped uint224 value that can contain several tightly packed fields. + struct TimestampedPackedUint224 { + uint224 value; // ───────╮ Value in uint224, packed. + uint32 timestamp; // ────╯ Timestamp of the most recent price update. + } + struct PoolUpdate { address token; // The IERC20 token address address pool; // The token pool address diff --git a/src/interfaces/ccip/IPriceRegistry.sol b/src/interfaces/ccip/IPriceRegistry.sol new file mode 100644 index 000000000..664863b2d --- /dev/null +++ b/src/interfaces/ccip/IPriceRegistry.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {ITypeAndVersion} from './ITypeAndVersion.sol'; +import {IInternal} from './IInternal.sol'; + +interface IPriceRegistry is ITypeAndVersion { + /// @notice Gets the fee token price and the gas price, both denominated in dollars. + /// @param token The source token to get the price for. + /// @param destChainSelector The destination chain to get the gas price for. + /// @return tokenPrice The price of the feeToken in 1e18 dollars per base unit. + /// @return gasPrice The price of gas in 1e18 dollars per base unit. + function getTokenAndGasPrices( + address token, + uint64 destChainSelector + ) external view returns (uint224 tokenPrice, uint224 gasPrice); + + /// @notice Update the price for given tokens and gas prices for given chains. + /// @param priceUpdates The price updates to apply. + function updatePrices(IInternal.PriceUpdates memory priceUpdates) external; + + /// @notice Get the `tokenPrice` for a given token. + /// @param token The token to get the price for. + /// @return tokenPrice The tokenPrice for the given token. + function getTokenPrice( + address token + ) external view returns (IInternal.TimestampedPackedUint224 memory); + + /// @notice Get an encoded `gasPrice` for a given destination chain ID. + /// The 224-bit result encodes necessary gas price components. + /// On L1 chains like Ethereum or Avax, the only component is the gas price. + /// On Optimistic Rollups, there are two components - the L2 gas price, and L1 base fee for data availability. + /// On future chains, there could be more or differing price components. + /// PriceRegistry does not contain chain-specific logic to parse destination chain price components. + /// @param destChainSelector The destination chain to get the price for. + /// @return gasPrice The encoded gasPrice for the given destination chain ID. + function getDestinationChainGasPrice( + uint64 destChainSelector + ) external view returns (IInternal.TimestampedPackedUint224 memory); + + /// @notice Gets the owner of the contract. + /// @return The owner of the contract. + function owner() external view returns (address); +} diff --git a/src/interfaces/ccip/IRouter.sol b/src/interfaces/ccip/IRouter.sol index 89c9275b2..7792d09cc 100644 --- a/src/interfaces/ccip/IRouter.sol +++ b/src/interfaces/ccip/IRouter.sol @@ -2,9 +2,10 @@ pragma solidity ^0.8.0; -import {IClient} from 'src/interfaces/ccip/IClient.sol'; +import {IClient} from './IClient.sol'; +import {ITypeAndVersion} from './ITypeAndVersion.sol'; -interface IRouter { +interface IRouter is ITypeAndVersion { error UnsupportedDestinationChain(uint64 destChainSelector); error InsufficientFeeTokenAmount(); error InvalidMsgValue(); diff --git a/src/interfaces/ccip/ITokenAdminRegistry.sol b/src/interfaces/ccip/ITokenAdminRegistry.sol index 05667ccba..649b1be76 100644 --- a/src/interfaces/ccip/ITokenAdminRegistry.sol +++ b/src/interfaces/ccip/ITokenAdminRegistry.sol @@ -1,13 +1,49 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {ITypeAndVersion} from './ITypeAndVersion.sol'; -interface ITokenAdminRegistry is ITypeAndVersion { +interface ITokenAdminRegistry { + struct TokenConfig { + address administrator; + address pendingAdministrator; + address tokenPool; + } + + error AlreadyRegistered(address token); + error InvalidTokenPoolToken(address token); + error OnlyAdministrator(address sender, address token); + error OnlyPendingAdministrator(address sender, address token); + error OnlyRegistryModuleOrOwner(address sender); + error ZeroAddress(); + + event AdministratorTransferRequested( + address indexed token, + address indexed currentAdmin, + address indexed newAdmin + ); + event AdministratorTransferred(address indexed token, address indexed newAdmin); + event OwnershipTransferRequested(address indexed from, address indexed to); + event OwnershipTransferred(address indexed from, address indexed to); + event PoolSet(address indexed token, address indexed previousPool, address indexed newPool); + event RegistryModuleAdded(address module); + event RegistryModuleRemoved(address indexed module); + + function acceptAdminRole(address localToken) external; + function acceptOwnership() external; + function addRegistryModule(address module) external; + function getAllConfiguredTokens( + uint64 startIndex, + uint64 maxCount + ) external view returns (address[] memory tokens); + function getPool(address token) external view returns (address); + function getPools(address[] memory tokens) external view returns (address[] memory); + function getTokenConfig(address token) external view returns (TokenConfig memory); + function isAdministrator(address localToken, address administrator) external view returns (bool); + function isRegistryModule(address module) external view returns (bool); function owner() external view returns (address); - function acceptAdminRole(address from) external; function proposeAdministrator(address localToken, address administrator) external; - function transferAdminRole(address localToken, address newAdministrator) external; - function isAdministrator(address localToken, address administrator) external view returns (bool); - function getPool(address token) external view returns (address); - function setPool(address source, address pool) external; + function removeRegistryModule(address module) external; + function setPool(address localToken, address pool) external; + function transferAdminRole(address localToken, address newAdmin) external; + function transferOwnership(address to) external; + function typeAndVersion() external view returns (string memory); } diff --git a/src/interfaces/ccip/tokenPool/IPool.sol b/src/interfaces/ccip/tokenPool/IPool.sol new file mode 100644 index 000000000..f85af151e --- /dev/null +++ b/src/interfaces/ccip/tokenPool/IPool.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IPool { + struct LockOrBurnInV1 { + bytes receiver; + uint64 remoteChainSelector; + address originalSender; + uint256 amount; + address localToken; + } + + struct LockOrBurnOutV1 { + bytes destTokenAddress; + bytes destPoolData; + } + + struct ReleaseOrMintInV1 { + bytes originalSender; + uint64 remoteChainSelector; + address receiver; + uint256 amount; + address localToken; + bytes sourcePoolAddress; + bytes sourcePoolData; + bytes offchainTokenData; + } + + struct ReleaseOrMintOutV1 { + uint256 destinationAmount; + } +} diff --git a/src/interfaces/ccip/tokenPool/IUpgradeableBurnMintTokenPool.sol b/src/interfaces/ccip/tokenPool/IUpgradeableBurnMintTokenPool.sol new file mode 100644 index 000000000..00bedbab6 --- /dev/null +++ b/src/interfaces/ccip/tokenPool/IUpgradeableBurnMintTokenPool.sol @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IRateLimiter} from '../IRateLimiter.sol'; +import {IPool} from './IPool.sol'; + +interface IUpgradeableBurnMintTokenPool_1_4 { + struct ChainUpdate { + uint64 remoteChainSelector; + bool allowed; + IRateLimiter.Config outboundIRateLimiterConfig; + IRateLimiter.Config inboundIRateLimiterConfig; + } + + error AggregateValueMaxCapacityExceeded(uint256 capacity, uint256 requested); + error AggregateValueRateLimitReached(uint256 minWaitInSeconds, uint256 available); + error AllowListNotEnabled(); + error BadARMSignal(); + error BucketOverfilled(); + error CallerIsNotARampOnRouter(address caller); + error ChainAlreadyExists(uint64 chainSelector); + error ChainNotAllowed(uint64 remoteChainSelector); + error DisabledNonZeroRateLimit(IRateLimiter.Config config); + error InvalidRatelimitRate(IRateLimiter.Config IRateLimiterConfig); + error NonExistentChain(uint64 remoteChainSelector); + error RateLimitMustBeDisabled(); + error SenderNotAllowed(address sender); + error TokenMaxCapacityExceeded(uint256 capacity, uint256 requested, address tokenAddress); + error TokenRateLimitReached(uint256 minWaitInSeconds, uint256 available, address tokenAddress); + error Unauthorized(address caller); + error ZeroAddressNotAllowed(); + + event AllowListAdd(address sender); + event AllowListRemove(address sender); + event Burned(address indexed sender, uint256 amount); + event ChainAdded( + uint64 remoteChainSelector, + IRateLimiter.Config outboundIRateLimiterConfig, + IRateLimiter.Config inboundIRateLimiterConfig + ); + event ChainConfigured( + uint64 remoteChainSelector, + IRateLimiter.Config outboundIRateLimiterConfig, + IRateLimiter.Config inboundIRateLimiterConfig + ); + event ChainRemoved(uint64 remoteChainSelector); + event ConfigChanged(IRateLimiter.Config config); + event Initialized(uint8 version); + event Locked(address indexed sender, uint256 amount); + event Minted(address indexed sender, address indexed recipient, uint256 amount); + event OwnershipTransferRequested(address indexed from, address indexed to); + event OwnershipTransferred(address indexed from, address indexed to); + event Released(address indexed sender, address indexed recipient, uint256 amount); + event RouterUpdated(address oldRouter, address newRouter); + event TokensConsumed(uint256 tokens); + + function acceptOwnership() external; + function applyAllowListUpdates(address[] memory removes, address[] memory adds) external; + function applyChainUpdates(ChainUpdate[] memory chains) external; + function directBurn(uint256 amount) external; + function getAllowList() external view returns (address[] memory); + function getAllowListEnabled() external view returns (bool); + function getArmProxy() external view returns (address armProxy); + function getCurrentInboundIRateLimiterState( + uint64 remoteChainSelector + ) external view returns (IRateLimiter.TokenBucket memory); + function getCurrentOutboundIRateLimiterState( + uint64 remoteChainSelector + ) external view returns (IRateLimiter.TokenBucket memory); + function getProxyPool() external view returns (address proxyPool); + function getRateLimitAdmin() external view returns (address); + function getRouter() external view returns (address router); + function getSupportedChains() external view returns (uint64[] memory); + function getToken() external view returns (address token); + function initialize(address owner, address[] memory allowlist, address router) external; + function isSupportedChain(uint64 remoteChainSelector) external view returns (bool); + function lockOrBurn( + address originalSender, + bytes memory, + uint256 amount, + uint64 remoteChainSelector, + bytes memory + ) external returns (bytes memory); + function owner() external view returns (address); + function releaseOrMint( + bytes memory, + address receiver, + uint256 amount, + uint64 remoteChainSelector, + bytes memory + ) external; + function setChainRateLimiterConfig( + uint64 remoteChainSelector, + IRateLimiter.Config memory outboundConfig, + IRateLimiter.Config memory inboundConfig + ) external; + function setProxyPool(address proxyPool) external; + function setRateLimitAdmin(address rateLimitAdmin) external; + function setRouter(address newRouter) external; + function supportsInterface(bytes4 interfaceId) external pure returns (bool); + function transferOwnership(address to) external; + function typeAndVersion() external view returns (string memory); +} + +interface IUpgradeableBurnMintTokenPool_1_5_1 { + struct ChainUpdate { + uint64 remoteChainSelector; + bytes[] remotePoolAddresses; + bytes remoteTokenAddress; + IRateLimiter.Config outboundRateLimiterConfig; + IRateLimiter.Config inboundRateLimiterConfig; + } + + error AggregateValueMaxCapacityExceeded(uint256 capacity, uint256 requested); + error AggregateValueRateLimitReached(uint256 minWaitInSeconds, uint256 available); + error AllowListNotEnabled(); + error BucketOverfilled(); + error CallerIsNotARampOnRouter(address caller); + error CannotTransferToSelf(); + error ChainAlreadyExists(uint64 chainSelector); + error ChainNotAllowed(uint64 remoteChainSelector); + error CursedByRMN(); + error DisabledNonZeroRateLimit(IRateLimiter.Config config); + error InvalidDecimalArgs(uint8 expected, uint8 actual); + error InvalidRateLimitRate(IRateLimiter.Config IRateLimiterConfig); + error InvalidRemoteChainDecimals(bytes sourcePoolData); + error InvalidRemotePoolForChain(uint64 remoteChainSelector, bytes remotePoolAddress); + error InvalidSourcePoolAddress(bytes sourcePoolAddress); + error InvalidToken(address token); + error MustBeProposedOwner(); + error NonExistentChain(uint64 remoteChainSelector); + error OnlyCallableByOwner(); + error OverflowDetected(uint8 remoteDecimals, uint8 localDecimals, uint256 remoteAmount); + error OwnerCannotBeZero(); + error PoolAlreadyAdded(uint64 remoteChainSelector, bytes remotePoolAddress); + error RateLimitMustBeDisabled(); + error SenderNotAllowed(address sender); + error TokenMaxCapacityExceeded(uint256 capacity, uint256 requested, address tokenAddress); + error TokenRateLimitReached(uint256 minWaitInSeconds, uint256 available, address tokenAddress); + error Unauthorized(address caller); + error ZeroAddressNotAllowed(); + + event AllowListAdd(address sender); + event AllowListRemove(address sender); + event Burned(address indexed sender, uint256 amount); + event ChainAdded( + uint64 remoteChainSelector, + bytes remoteToken, + IRateLimiter.Config outboundIRateLimiterConfig, + IRateLimiter.Config inboundIRateLimiterConfig + ); + event ChainConfigured( + uint64 remoteChainSelector, + IRateLimiter.Config outboundIRateLimiterConfig, + IRateLimiter.Config inboundIRateLimiterConfig + ); + event ChainRemoved(uint64 remoteChainSelector); + event ConfigChanged(IRateLimiter.Config config); + event Initialized(uint8 version); + event Locked(address indexed sender, uint256 amount); + event Minted(address indexed sender, address indexed recipient, uint256 amount); + event OwnershipTransferRequested(address indexed from, address indexed to); + event OwnershipTransferred(address indexed from, address indexed to); + event RateLimitAdminSet(address rateLimitAdmin); + event Released(address indexed sender, address indexed recipient, uint256 amount); + event RemotePoolAdded(uint64 indexed remoteChainSelector, bytes remotePoolAddress); + event RemotePoolRemoved(uint64 indexed remoteChainSelector, bytes remotePoolAddress); + event RouterUpdated(address oldRouter, address newRouter); + event TokensConsumed(uint256 tokens); + + function acceptOwnership() external; + function addRemotePool(uint64 remoteChainSelector, bytes memory remotePoolAddress) external; + function applyAllowListUpdates(address[] memory removes, address[] memory adds) external; + function applyChainUpdates( + uint64[] memory remoteChainSelectorsToRemove, + ChainUpdate[] memory chainsToAdd + ) external; + function directMint(address to, uint256 amount) external; + function getAllowList() external view returns (address[] memory); + function getAllowListEnabled() external view returns (bool); + function getCurrentInboundRateLimiterState( + uint64 remoteChainSelector + ) external view returns (IRateLimiter.TokenBucket memory); + function getCurrentOutboundRateLimiterState( + uint64 remoteChainSelector + ) external view returns (IRateLimiter.TokenBucket memory); + function getRateLimitAdmin() external view returns (address); + function getRemotePools(uint64 remoteChainSelector) external view returns (bytes[] memory); + function getRemoteToken(uint64 remoteChainSelector) external view returns (bytes memory); + function getRmnProxy() external view returns (address rmnProxy); + function getRouter() external view returns (address router); + function getSupportedChains() external view returns (uint64[] memory); + function getToken() external view returns (address token); + function getTokenDecimals() external view returns (uint8 decimals); + function initialize(address owner_, address[] memory allowlist, address router) external; + function isRemotePool( + uint64 remoteChainSelector, + bytes memory remotePoolAddress + ) external view returns (bool); + function isSupportedChain(uint64 remoteChainSelector) external view returns (bool); + function isSupportedToken(address token) external view returns (bool); + function lockOrBurn( + IPool.LockOrBurnInV1 memory lockOrBurnIn + ) external returns (IPool.LockOrBurnOutV1 memory); + function owner() external view returns (address); + function releaseOrMint( + IPool.ReleaseOrMintInV1 memory releaseOrMintIn + ) external returns (IPool.ReleaseOrMintOutV1 memory); + function removeRemotePool(uint64 remoteChainSelector, bytes memory remotePoolAddress) external; + function setChainRateLimiterConfig( + uint64 remoteChainSelector, + IRateLimiter.Config memory outboundConfig, + IRateLimiter.Config memory inboundConfig + ) external; + function setRateLimitAdmin(address rateLimitAdmin) external; + function setRouter(address newRouter) external; + function supportsInterface(bytes4 interfaceId) external pure returns (bool); + function transferOwnership(address to) external; + function typeAndVersion() external view returns (string memory); +} diff --git a/src/interfaces/ccip/tokenPool/IUpgradeableLockReleaseTokenPool.sol b/src/interfaces/ccip/tokenPool/IUpgradeableLockReleaseTokenPool.sol new file mode 100644 index 000000000..96a3dbe0f --- /dev/null +++ b/src/interfaces/ccip/tokenPool/IUpgradeableLockReleaseTokenPool.sol @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IRateLimiter} from '../IRateLimiter.sol'; +import {IPool} from './IPool.sol'; + +interface IUpgradeableLockReleaseTokenPool_1_4 { + struct ChainUpdate { + uint64 remoteChainSelector; + bool allowed; + IRateLimiter.Config outboundIRateLimiterConfig; + IRateLimiter.Config inboundIRateLimiterConfig; + } + + error AggregateValueMaxCapacityExceeded(uint256 capacity, uint256 requested); + error AggregateValueRateLimitReached(uint256 minWaitInSeconds, uint256 available); + error AllowListNotEnabled(); + error BadARMSignal(); + error BridgeLimitExceeded(uint256 bridgeLimit); + error BucketOverfilled(); + error CallerIsNotARampOnRouter(address caller); + error ChainAlreadyExists(uint64 chainSelector); + error ChainNotAllowed(uint64 remoteChainSelector); + error DisabledNonZeroRateLimit(IRateLimiter.Config config); + error InsufficientLiquidity(); + error InvalidRatelimitRate(IRateLimiter.Config IRateLimiterConfig); + error LiquidityNotAccepted(); + error NonExistentChain(uint64 remoteChainSelector); + error NotEnoughBridgedAmount(); + error RateLimitMustBeDisabled(); + error SenderNotAllowed(address sender); + error TokenMaxCapacityExceeded(uint256 capacity, uint256 requested, address tokenAddress); + error TokenRateLimitReached(uint256 minWaitInSeconds, uint256 available, address tokenAddress); + error Unauthorized(address caller); + error ZeroAddressNotAllowed(); + + event AllowListAdd(address sender); + event AllowListRemove(address sender); + event BridgeLimitAdminUpdated(address indexed oldAdmin, address indexed newAdmin); + event BridgeLimitUpdated(uint256 oldBridgeLimit, uint256 newBridgeLimit); + event Burned(address indexed sender, uint256 amount); + event ChainAdded( + uint64 remoteChainSelector, + IRateLimiter.Config outboundRateLimiterConfig, + IRateLimiter.Config inboundRateLimiterConfig + ); + event ChainConfigured( + uint64 remoteChainSelector, + IRateLimiter.Config outboundRateLimiterConfig, + IRateLimiter.Config inboundRateLimiterConfig + ); + event ChainRemoved(uint64 remoteChainSelector); + event ConfigChanged(IRateLimiter.Config config); + event Initialized(uint8 version); + event LiquidityAdded(address indexed provider, uint256 indexed amount); + event LiquidityRemoved(address indexed provider, uint256 indexed amount); + event Locked(address indexed sender, uint256 amount); + event Minted(address indexed sender, address indexed recipient, uint256 amount); + event OwnershipTransferRequested(address indexed from, address indexed to); + event OwnershipTransferred(address indexed from, address indexed to); + event Released(address indexed sender, address indexed recipient, uint256 amount); + event RouterUpdated(address oldRouter, address newRouter); + event TokensConsumed(uint256 tokens); + + function acceptOwnership() external; + function applyAllowListUpdates(address[] memory removes, address[] memory adds) external; + function applyChainUpdates(ChainUpdate[] memory chains) external; + function canAcceptLiquidity() external view returns (bool); + function getAllowList() external view returns (address[] memory); + function getAllowListEnabled() external view returns (bool); + function getArmProxy() external view returns (address armProxy); + function getBridgeLimit() external view returns (uint256); + function getBridgeLimitAdmin() external view returns (address); + function getCurrentBridgedAmount() external view returns (uint256); + function getCurrentInboundRateLimiterState( + uint64 remoteChainSelector + ) external view returns (IRateLimiter.TokenBucket memory); + function getCurrentOutboundRateLimiterState( + uint64 remoteChainSelector + ) external view returns (IRateLimiter.TokenBucket memory); + function getLockReleaseInterfaceId() external pure returns (bytes4); + function getProxyPool() external view returns (address proxyPool); + function getRateLimitAdmin() external view returns (address); + function getRebalancer() external view returns (address); + function getRouter() external view returns (address router); + function getSupportedChains() external view returns (uint64[] memory); + function getToken() external view returns (address token); + function initialize( + address owner, + address[] memory allowlist, + address router, + uint256 bridgeLimit + ) external; + function isSupportedChain(uint64 remoteChainSelector) external view returns (bool); + function lockOrBurn( + address originalSender, + bytes memory, + uint256 amount, + uint64 remoteChainSelector, + bytes memory + ) external returns (bytes memory); + function owner() external view returns (address); + function provideLiquidity(uint256 amount) external; + function releaseOrMint( + bytes memory, + address receiver, + uint256 amount, + uint64 remoteChainSelector, + bytes memory + ) external; + function setBridgeLimit(uint256 newBridgeLimit) external; + function setBridgeLimitAdmin(address bridgeLimitAdmin) external; + function setChainRateLimiterConfig( + uint64 remoteChainSelector, + IRateLimiter.Config memory outboundConfig, + IRateLimiter.Config memory inboundConfig + ) external; + function setProxyPool(address proxyPool) external; + function setRateLimitAdmin(address rateLimitAdmin) external; + function setRebalancer(address rebalancer) external; + function setRouter(address newRouter) external; + function supportsInterface(bytes4 interfaceId) external pure returns (bool); + function transferOwnership(address to) external; + function typeAndVersion() external view returns (string memory); + function withdrawLiquidity(uint256 amount) external; +} + +interface IUpgradeableLockReleaseTokenPool_1_5_1 { + struct ChainUpdate { + uint64 remoteChainSelector; + bytes[] remotePoolAddresses; + bytes remoteTokenAddress; + IRateLimiter.Config outboundRateLimiterConfig; + IRateLimiter.Config inboundRateLimiterConfig; + } + + error AggregateValueMaxCapacityExceeded(uint256 capacity, uint256 requested); + error AggregateValueRateLimitReached(uint256 minWaitInSeconds, uint256 available); + error AllowListNotEnabled(); + error BridgeLimitExceeded(uint256 bridgeLimit); + error BucketOverfilled(); + error CallerIsNotARampOnRouter(address caller); + error CannotTransferToSelf(); + error ChainAlreadyExists(uint64 chainSelector); + error ChainNotAllowed(uint64 remoteChainSelector); + error CursedByRMN(); + error DisabledNonZeroRateLimit(IRateLimiter.Config config); + error InsufficientLiquidity(); + error InvalidDecimalArgs(uint8 expected, uint8 actual); + error InvalidRateLimitRate(IRateLimiter.Config IRateLimiterConfig); + error InvalidRemoteChainDecimals(bytes sourcePoolData); + error InvalidRemotePoolForChain(uint64 remoteChainSelector, bytes remotePoolAddress); + error InvalidSourcePoolAddress(bytes sourcePoolAddress); + error InvalidToken(address token); + error LiquidityNotAccepted(); + error MustBeProposedOwner(); + error NonExistentChain(uint64 remoteChainSelector); + error NotEnoughBridgedAmount(); + error OnlyCallableByOwner(); + error OverflowDetected(uint8 remoteDecimals, uint8 localDecimals, uint256 remoteAmount); + error OwnerCannotBeZero(); + error PoolAlreadyAdded(uint64 remoteChainSelector, bytes remotePoolAddress); + error RateLimitMustBeDisabled(); + error SenderNotAllowed(address sender); + error TokenMaxCapacityExceeded(uint256 capacity, uint256 requested, address tokenAddress); + error TokenRateLimitReached(uint256 minWaitInSeconds, uint256 available, address tokenAddress); + error Unauthorized(address caller); + error ZeroAddressNotAllowed(); + + event AllowListAdd(address sender); + event AllowListRemove(address sender); + event BridgeLimitAdminUpdated(address indexed oldAdmin, address indexed newAdmin); + event BridgeLimitUpdated(uint256 oldBridgeLimit, uint256 newBridgeLimit); + event Burned(address indexed sender, uint256 amount); + event ChainAdded( + uint64 remoteChainSelector, + bytes remoteToken, + IRateLimiter.Config outboundRateLimiterConfig, + IRateLimiter.Config inboundRateLimiterConfig + ); + event ChainConfigured( + uint64 remoteChainSelector, + IRateLimiter.Config outboundRateLimiterConfig, + IRateLimiter.Config inboundRateLimiterConfig + ); + event ChainRemoved(uint64 remoteChainSelector); + event ConfigChanged(IRateLimiter.Config config); + event Initialized(uint8 version); + event LiquidityAdded(address indexed provider, uint256 indexed amount); + event LiquidityRemoved(address indexed provider, uint256 indexed amount); + event LiquidityTransferred(address indexed from, uint256 amount); + event Locked(address indexed sender, uint256 amount); + event Minted(address indexed sender, address indexed recipient, uint256 amount); + event OwnershipTransferRequested(address indexed from, address indexed to); + event OwnershipTransferred(address indexed from, address indexed to); + event RateLimitAdminSet(address rateLimitAdmin); + event Released(address indexed sender, address indexed recipient, uint256 amount); + event RemotePoolAdded(uint64 indexed remoteChainSelector, bytes remotePoolAddress); + event RemotePoolRemoved(uint64 indexed remoteChainSelector, bytes remotePoolAddress); + event RouterUpdated(address oldRouter, address newRouter); + event TokensConsumed(uint256 tokens); + + function acceptOwnership() external; + function addRemotePool(uint64 remoteChainSelector, bytes memory remotePoolAddress) external; + function applyAllowListUpdates(address[] memory removes, address[] memory adds) external; + function applyChainUpdates( + uint64[] memory remoteChainSelectorsToRemove, + ChainUpdate[] memory chainsToAdd + ) external; + function canAcceptLiquidity() external view returns (bool); + function getAllowList() external view returns (address[] memory); + function getAllowListEnabled() external view returns (bool); + function getBridgeLimit() external view returns (uint256); + function getBridgeLimitAdmin() external view returns (address); + function getCurrentBridgedAmount() external view returns (uint256); + function getCurrentInboundRateLimiterState( + uint64 remoteChainSelector + ) external view returns (IRateLimiter.TokenBucket memory); + function getCurrentOutboundRateLimiterState( + uint64 remoteChainSelector + ) external view returns (IRateLimiter.TokenBucket memory); + function getRateLimitAdmin() external view returns (address); + function getRebalancer() external view returns (address); + function getRemotePools(uint64 remoteChainSelector) external view returns (bytes[] memory); + function getRemoteToken(uint64 remoteChainSelector) external view returns (bytes memory); + function getRmnProxy() external view returns (address rmnProxy); + function getRouter() external view returns (address router); + function getSupportedChains() external view returns (uint64[] memory); + function getToken() external view returns (address token); + function getTokenDecimals() external view returns (uint8 decimals); + function initialize( + address owner_, + address[] memory allowlist, + address router, + uint256 bridgeLimit + ) external; + function isRemotePool( + uint64 remoteChainSelector, + bytes memory remotePoolAddress + ) external view returns (bool); + function isSupportedChain(uint64 remoteChainSelector) external view returns (bool); + function isSupportedToken(address token) external view returns (bool); + function lockOrBurn( + IPool.LockOrBurnInV1 memory lockOrBurnIn + ) external returns (IPool.LockOrBurnOutV1 memory); + function owner() external view returns (address); + function provideLiquidity(uint256 amount) external; + function releaseOrMint( + IPool.ReleaseOrMintInV1 memory releaseOrMintIn + ) external returns (IPool.ReleaseOrMintOutV1 memory); + function removeRemotePool(uint64 remoteChainSelector, bytes memory remotePoolAddress) external; + function setBridgeLimit(uint256 newBridgeLimit) external; + function setCurrentBridgedAmount(uint256 newBridgedAmount) external; + function setBridgeLimitAdmin(address bridgeLimitAdmin) external; + function setChainRateLimiterConfig( + uint64 remoteChainSelector, + IRateLimiter.Config memory outboundConfig, + IRateLimiter.Config memory inboundConfig + ) external; + function setRateLimitAdmin(address rateLimitAdmin) external; + function setRebalancer(address rebalancer) external; + function setRouter(address newRouter) external; + function supportsInterface(bytes4 interfaceId) external pure returns (bool); + function transferLiquidity(address from, uint256 amount) external; + function transferOwnership(address to) external; + function typeAndVersion() external view returns (string memory); + function withdrawLiquidity(uint256 amount) external; +}