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/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/src/20241209_Multi_GHOCCIP151Upgrade/AaveV3Arbitrum_GHOCCIP151Upgrade_20241209.sol b/src/20241209_Multi_GHOCCIP151Upgrade/AaveV3Arbitrum_GHOCCIP151Upgrade_20241209.sol new file mode 100644 index 000000000..455a652ee --- /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: https://governance.aave.com/t/technical-maintenance-proposals/15274/59 + */ +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..faca9a22f --- /dev/null +++ b/src/20241209_Multi_GHOCCIP151Upgrade/AaveV3Arbitrum_GHOCCIP151Upgrade_20241209.t.sol @@ -0,0 +1,735 @@ +// 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'), 293994020); + proposal = new AaveV3Arbitrum_GHOCCIP151Upgrade_20241209(); + _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..b0695928d --- /dev/null +++ b/src/20241209_Multi_GHOCCIP151Upgrade/AaveV3E2E_GHOCCIP151Upgrade_20241209.t.sol @@ -0,0 +1,623 @@ +// 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'), 21594804); + l2.c.forkId = vm.createFork(vm.rpcUrl('arbitrum'), 293994020); + + 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}); + } + + 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 _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..c06228b86 --- /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: https://governance.aave.com/t/technical-maintenance-proposals/15274/59 + */ +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..3e6fde3f3 --- /dev/null +++ b/src/20241209_Multi_GHOCCIP151Upgrade/AaveV3Ethereum_GHOCCIP151Upgrade_20241209.t.sol @@ -0,0 +1,749 @@ +// 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'), 21594804); + proposal = new AaveV3Ethereum_GHOCCIP151Upgrade_20241209(); + _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..231fb9852 --- /dev/null +++ b/src/20241209_Multi_GHOCCIP151Upgrade/GHOCCIP151Upgrade.md @@ -0,0 +1,61 @@ +--- +title: "GHO Risk Stewards Update and GHO CCIP Integration Upgrade" +author: "Aave Labs" +discussions: "https://governance.aave.com/t/technical-maintenance-proposals/15274/59" +--- + +## Simple Summary + +This AIP proposes to: + +1. update the GHO Risk Steward contracts to enhance the Risk Council’s user experience and align the design of the Risk Stewards implementations throughout the Aave Protocol. +2. update the GHO CCIP Token Pools on Arbitrum and Ethereum to integrate them with latest version of CCIP (1.5.1) to leverage the full functionality of CCIP and prepare for future expansions to other chains. + +## Motivation + +This AIP seeks to enhance the Aave user experience and align the design of the Risk Stewards implementation across the Aave Protocol. Additionally, the CCIP was recently upgraded to version 1.5.1, introducing a number of enhancements for cross-chain pool management. Currently, GHO CCIP Token Pools are based on version 1.4, though still compatible with 1.5.1. + +Aave Labs will provide technical support to maintain the GHO CCIP Token Pools functional, secured, and aligned with the latest updates, enabling GHO expansion to other networks when needed. + +## Specification + +The proposal includes the following actions: + +Risk Stewards update: + +1. GhoAaveSteward: Remove the max cap of 25% configured by `GHO_BORROW_RATE_MAX`. While this limitation was sensible when applied to the Ethereum reserve only, it is not necessary for different instances of GHO when implemented as a regular reserve. Additionally, the Risk Stewards already have limitations and sanity checks in place to restrict capabilities during rates update. +2. GhoCcipSteward: Add a missing getter for the timelock state of the CCIP. +3. GhoBucketSteward: No modification, configure new token pool and retire permissions for the existing token pool. + +GHO CCIP Token Pools upgrade: + +1. Ownership maintenance of contracts: + 1. Accept ownership of new token pool contracts for GHO on each network. + 2. Assume Admin role for the GHO token in the CCIP TokenAdminRegistry contract on each network. + 3. Take ownership of the existing proxy pools (even though they'll be deprecated). +2. Migrate Liquidity Between Old and New Token Pools: + 1. On Ethereum: Transfer locked GHO liquidity from the old LockReleaseTokenPool contract to the new one, and properly initialize the new contract to reflect the correct amount of bridged liquidity. + 2. On Arbitrum: Mint tokens on the new BurnMintTokenPool contract and burn tokens from the old pool using the newly introduced `directMint` and `directBurn` methods. This is necessary to offboard the old pool as a facilitator and enable the new pool to handle bridge transactions. +3. Setup a token rate limit of 300,000 GHO capacity and 60 GHO per second refill rate (216,000 GHO per hour), as recommended by the Risk Provider ChaosLabs in the previous maintenance upgrade to v1.5, see [here](https://governance.aave.com/t/technical-maintenance-proposals/15274/54). +4. Keep GhoStewards functional by validating they can execute actions over the new CCIP lane and remain fully operational. + +## References + +- Implementation: [GhoAaveSteward](https://github.com/aave/gho-core/blob/cf6ee42adc8b2e9ac8ffd1d70bd5b52f06e536b6/src/contracts/misc/GhoAaveSteward.sol), [GhoCcipSteward](https://github.com/aave/gho-core/blob/cf6ee42adc8b2e9ac8ffd1d70bd5b52f06e536b6/src/contracts/misc/GhoCcipSteward.sol), [UpgradeableLockReleaseTokenPool](https://github.com/aave/ccip/blob/d5c6cedde6fbca9890a92a55f2db80e94793d0ec/contracts/src/v0.8/ccip/pools/GHO/UpgradeableLockReleaseTokenPool.sol), [UpgradeableTokenPool](https://github.com/aave/ccip/blob/d5c6cedde6fbca9890a92a55f2db80e94793d0ec/contracts/src/v0.8/ccip/pools/GHO/UpgradeableTokenPool.sol) + +- Contracts: + + - Ethereum + - [UpgradeableLockReleaseTokenPool](https://etherscan.io/address/0x06179f7C1be40863405f374E7f5F8806c728660A) + - [GhoAaveSteward](https://etherscan.io/address/0x98217A06721Ebf727f2C8d9aD7718ec28b7aAe34) + - [GhoCcipSteward](https://etherscan.io/address/0xC5BcC58BE6172769ca1a78B8A45752E3C5059c39) + - Arbitrum + - [UpgradeableBurnMintTokenPool](https://arbiscan.io/address/0xB94Ab28c6869466a46a42abA834ca2B3cECCA5eB) + - [GhoAaveSteward](https://arbiscan.io/address/0xd2D586f849620ef042FE3aF52eAa10e9b78bf7De) + - [GhoCcipSteward](https://arbiscan.io/address/0xCd5ab470AaC5c13e1063ee700503f3346b7C90Db) + +- Discussion: [GHO CCIP Integration Maintenance (CCIP v1.5.1 upgrade)](https://governance.aave.com/t/technical-maintenance-proposals/15274/59), [Update GHO Risk Stewards](https://governance.aave.com/t/technical-maintenance-proposals/15274/60) + +## 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..c340e7540 --- /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: 'https://governance.aave.com/t/technical-maintenance-proposals/15274/59', + snapshot: 'Direct-to-AIP', + votingNetwork: 'POLYGON', + }, + poolOptions: { + AaveV3Ethereum: {configs: {OTHERS: {}}, cache: {blockNumber: 21594804}}, + AaveV3Arbitrum: {configs: {OTHERS: {}}, cache: {blockNumber: 293994020}}, + }, +}; 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/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 ab21aefbe..d37fb1643 100644 --- a/src/interfaces/IGhoToken.sol +++ b/src/interfaces/IGhoToken.sol @@ -1,13 +1,32 @@ // 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; } + /** + * @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 @@ -21,7 +40,12 @@ interface IGhoToken { uint128 bucketCapacity ) external; - function balanceOf(address user) external returns (uint256); + /** + * @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 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; +}