From 7725647f98f1ae815c207bf7f3d26f933f538baa Mon Sep 17 00:00:00 2001 From: miguelmtz <36620902+miguelmtzinf@users.noreply.github.com> Date: Fri, 14 Jun 2024 16:47:28 +0200 Subject: [PATCH] Add AIP for GHO Cross-Chain Launch (#350) * Add proposal for Gho Incident Report 20231113 (#1) * chore: add payload and deploy script for update of GHO variable debt token * forge install: gho-core * chore: add gho-core to dependency * test: Add tests for update of gho variable token * test: Add tests for update of gho variable token * fix: add modifier in method of interface * fix: remove gho dependency from repo and fix test * fix: Remove unnecesary dependency * fix: Add latest details --------- Co-authored-by: miguelmtzinf * fix: Make new impl constant (#3) * fix: Amend AIP text (#4) * fix: Make new impl constant * fix: Fix AIP text * test: Tweak default tests with borrow cap update (#5) * fix: lint issue (#6) * test: Add diffs from test running (#7) * fix: Add payload address (#8) * fix: Fix payload address in script (#9) * fix: Remove unneeded diff file (#10) * feat: Add AIP code * fix: Fix AIP docs and imports * fix: Remove unnecesary test * fix: Updates risk params * test: Add ccip e2e tests * fix: Fix Arbitrum payload max bytecode size exceeded * feat: Add sophisticated defensive aave seed (#13) * feat: Add sophhisticated defensive aave seed * fix: Allows AaveDefensive contract remove itself as facilitator * fix: Add docs and reduce bytecode size * fix: Reduce Utils lib bytecode size * fix: Fix error in Utils * fix: Add Capped gho usd price oracle * fix: Add AIP draft text * fix: Update block number for tests * fix: Remove unneeded files * fix: Install latest version of gho and ccip repos * fix: Remove unused helper function * fix: Fix AIP text * fix: Remove defensive and use create2 * fix: Add fixed price oracle * fix: Remove unnecessary memory variables * fix: Use create1 and move listing out * fix: Remove listing aip --------- Co-authored-by: Parth Patel --- .gitmodules | 6 + ...trum_GHOCrossChainLaunch_20240528_after.md | 5 + ...reum_GHOCrossChainLaunch_20240528_after.md | 5 + lib/ccip | 1 + lib/gho-core | 1 + remappings.txt | 4 +- ...3Arbitrum_GHOCrossChainLaunch_20240528.sol | 110 ++++ ...rbitrum_GHOCrossChainLaunch_20240528.t.sol | 388 ++++++++++++++ ...veV3E2E_GHOCrossChainLaunch_20240528.t.sol | 481 ++++++++++++++++++ ...3Ethereum_GHOCrossChainLaunch_20240528.sol | 74 +++ ...thereum_GHOCrossChainLaunch_20240528.t.sol | 339 ++++++++++++ .../GHOCrossChainLaunch.md | 53 ++ .../GHOCrossChainLaunch_20240528.s.sol | 87 ++++ .../config.ts | 21 + 14 files changed, 1574 insertions(+), 1 deletion(-) create mode 100644 diffs/AaveV3Arbitrum_GHOCrossChainLaunch_20240528_before_AaveV3Arbitrum_GHOCrossChainLaunch_20240528_after.md create mode 100644 diffs/AaveV3Ethereum_GHOCrossChainLaunch_20240528_before_AaveV3Ethereum_GHOCrossChainLaunch_20240528_after.md create mode 160000 lib/ccip create mode 160000 lib/gho-core create mode 100644 src/20240528_Multi_GHOCrossChainLaunch/AaveV3Arbitrum_GHOCrossChainLaunch_20240528.sol create mode 100644 src/20240528_Multi_GHOCrossChainLaunch/AaveV3Arbitrum_GHOCrossChainLaunch_20240528.t.sol create mode 100644 src/20240528_Multi_GHOCrossChainLaunch/AaveV3E2E_GHOCrossChainLaunch_20240528.t.sol create mode 100644 src/20240528_Multi_GHOCrossChainLaunch/AaveV3Ethereum_GHOCrossChainLaunch_20240528.sol create mode 100644 src/20240528_Multi_GHOCrossChainLaunch/AaveV3Ethereum_GHOCrossChainLaunch_20240528.t.sol create mode 100644 src/20240528_Multi_GHOCrossChainLaunch/GHOCrossChainLaunch.md create mode 100644 src/20240528_Multi_GHOCrossChainLaunch/GHOCrossChainLaunch_20240528.s.sol create mode 100644 src/20240528_Multi_GHOCrossChainLaunch/config.ts diff --git a/.gitmodules b/.gitmodules index f7316a1d6..c58871825 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,9 @@ [submodule "lib/aave-helpers"] path = lib/aave-helpers url = https://github.com/bgd-labs/aave-helpers +[submodule "lib/ccip"] + path = lib/ccip + url = https://github.com/aave/ccip +[submodule "lib/gho-core"] + path = lib/gho-core + url = https://github.com/aave/gho-core diff --git a/diffs/AaveV3Arbitrum_GHOCrossChainLaunch_20240528_before_AaveV3Arbitrum_GHOCrossChainLaunch_20240528_after.md b/diffs/AaveV3Arbitrum_GHOCrossChainLaunch_20240528_before_AaveV3Arbitrum_GHOCrossChainLaunch_20240528_after.md new file mode 100644 index 000000000..c15d3e2bc --- /dev/null +++ b/diffs/AaveV3Arbitrum_GHOCrossChainLaunch_20240528_before_AaveV3Arbitrum_GHOCrossChainLaunch_20240528_after.md @@ -0,0 +1,5 @@ +## Raw diff + +```json +{} +``` \ No newline at end of file diff --git a/diffs/AaveV3Ethereum_GHOCrossChainLaunch_20240528_before_AaveV3Ethereum_GHOCrossChainLaunch_20240528_after.md b/diffs/AaveV3Ethereum_GHOCrossChainLaunch_20240528_before_AaveV3Ethereum_GHOCrossChainLaunch_20240528_after.md new file mode 100644 index 000000000..c15d3e2bc --- /dev/null +++ b/diffs/AaveV3Ethereum_GHOCrossChainLaunch_20240528_before_AaveV3Ethereum_GHOCrossChainLaunch_20240528_after.md @@ -0,0 +1,5 @@ +## Raw diff + +```json +{} +``` \ No newline at end of file diff --git a/lib/ccip b/lib/ccip new file mode 160000 index 000000000..d6cb9dab3 --- /dev/null +++ b/lib/ccip @@ -0,0 +1 @@ +Subproject commit d6cb9dab3eb7bff1cd3bfb0e1d2fb634f594f120 diff --git a/lib/gho-core b/lib/gho-core new file mode 160000 index 000000000..a9647e1b5 --- /dev/null +++ b/lib/gho-core @@ -0,0 +1 @@ +Subproject commit a9647e1b581c7f781d36ec34fb5174d29a1ec7ba diff --git a/remappings.txt b/remappings.txt index 620a76fc2..d5607d016 100644 --- a/remappings.txt +++ b/remappings.txt @@ -6,4 +6,6 @@ aave-v3-core/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-core/ aave-v3-periphery/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-periphery/ ds-test/=lib/aave-helpers/lib/forge-std/lib/ds-test/src/ forge-std/=lib/aave-helpers/lib/forge-std/src/ -solidity-utils/=lib/aave-helpers/lib/solidity-utils/src/ \ No newline at end of file +solidity-utils/=lib/aave-helpers/lib/solidity-utils/src/ +ccip/=lib/ccip/contracts/src/ +gho-core/=lib/gho-core/src/contracts/ \ No newline at end of file diff --git a/src/20240528_Multi_GHOCrossChainLaunch/AaveV3Arbitrum_GHOCrossChainLaunch_20240528.sol b/src/20240528_Multi_GHOCrossChainLaunch/AaveV3Arbitrum_GHOCrossChainLaunch_20240528.sol new file mode 100644 index 000000000..9ff4126dc --- /dev/null +++ b/src/20240528_Multi_GHOCrossChainLaunch/AaveV3Arbitrum_GHOCrossChainLaunch_20240528.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IProposalGenericExecutor} from 'aave-helpers/interfaces/IProposalGenericExecutor.sol'; +import {GovernanceV3Arbitrum} from 'aave-address-book/GovernanceV3Arbitrum.sol'; +import {MiscArbitrum} from 'aave-address-book/MiscArbitrum.sol'; +import {TransparentUpgradeableProxy} from 'solidity-utils/contracts/transparent-proxy/TransparentUpgradeableProxy.sol'; +import {UpgradeableBurnMintTokenPool} from 'ccip/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPool.sol'; +import {UpgradeableTokenPool} from 'ccip/v0.8/ccip/pools/GHO/UpgradeableTokenPool.sol'; +import {RateLimiter} from 'ccip/v0.8/ccip/libraries/RateLimiter.sol'; +import {UpgradeableGhoToken} from 'gho-core/gho/UpgradeableGhoToken.sol'; +import {IGhoToken} from 'gho-core/gho/interfaces/IGhoToken.sol'; + +library Utils { + address public constant CCIP_ARM_PROXY = 0xC311a21e6fEf769344EB1515588B9d535662a145; + address public constant CCIP_ROUTER = 0x141fa059441E0ca23ce184B6A78bafD2A517DdE8; + uint256 public constant CCIP_BUCKET_CAPACITY = 1_000_000e18; // 1M + uint64 public constant CCIP_ETH_CHAIN_SELECTOR = 5009297550715157269; + + function deployGhoToken() internal returns (address) { + address imple = address(new UpgradeableGhoToken()); + + bytes memory ghoTokenInitParams = abi.encodeWithSignature( + 'initialize(address)', + GovernanceV3Arbitrum.EXECUTOR_LVL_1 // owner + ); + return + address(new TransparentUpgradeableProxy(imple, MiscArbitrum.PROXY_ADMIN, ghoTokenInitParams)); + } + + function deployCcipTokenPool(address ghoToken) external returns (address) { + address imple = address(new UpgradeableBurnMintTokenPool(ghoToken, CCIP_ARM_PROXY, false)); + + bytes memory tokenPoolInitParams = abi.encodeWithSignature( + 'initialize(address,address[],address)', + GovernanceV3Arbitrum.EXECUTOR_LVL_1, // owner + new address[](0), // allowList + CCIP_ROUTER // router + ); + return + address( + new TransparentUpgradeableProxy( + imple, // logic + MiscArbitrum.PROXY_ADMIN, // proxy admin + tokenPoolInitParams // data + ) + ); + } +} + +/** + * @title GHO Cross-Chain Launch + * @author Aave Labs + * - Snapshot: https://snapshot.org/#/aave.eth/proposal/0x2a6ffbcff41a5ef98b7542f99b207af9c1e79e61f859d0a62f3bf52d3280877a + * - Discussion: https://governance.aave.com/t/arfc-gho-cross-chain-launch/17616 + * @dev This payload consists of the following set of actions: + * 1. Deploy GHO + * 2. Deploy BurnMintTokenPool + * 3. Accept ownership of CCIP TokenPool + * 4. Configure CCIP TokenPool + * 5. Add CCIP TokenPool as GHO Facilitator + */ +contract AaveV3Arbitrum_GHOCrossChainLaunch_20240528 is IProposalGenericExecutor { + function execute() external { + // 1. Deploy GHO + address ghoToken = Utils.deployGhoToken(); + + // 2. Deploy BurnMintTokenPool + address tokenPool = Utils.deployCcipTokenPool(ghoToken); + + // 3. Accept TokenPool ownership + UpgradeableBurnMintTokenPool(tokenPool).acceptOwnership(); + + // 4. Configure CCIP TokenPool + _configureCcipTokenPool(tokenPool); + + // 5. Add CCIP TokenPool as GHO Facilitator + IGhoToken(ghoToken).grantRole( + IGhoToken(ghoToken).FACILITATOR_MANAGER_ROLE(), + GovernanceV3Arbitrum.EXECUTOR_LVL_1 + ); + IGhoToken(ghoToken).grantRole( + IGhoToken(ghoToken).BUCKET_MANAGER_ROLE(), + GovernanceV3Arbitrum.EXECUTOR_LVL_1 + ); + IGhoToken(ghoToken).addFacilitator( + tokenPool, + 'CCIP TokenPool', + uint128(Utils.CCIP_BUCKET_CAPACITY) + ); + } + + function _configureCcipTokenPool(address tokenPool) internal { + UpgradeableTokenPool.ChainUpdate[] memory chainUpdates = new UpgradeableTokenPool.ChainUpdate[]( + 1 + ); + RateLimiter.Config memory rateConfig = RateLimiter.Config({ + isEnabled: false, + capacity: 0, + rate: 0 + }); + chainUpdates[0] = UpgradeableTokenPool.ChainUpdate({ + remoteChainSelector: Utils.CCIP_ETH_CHAIN_SELECTOR, + allowed: true, + outboundRateLimiterConfig: rateConfig, + inboundRateLimiterConfig: rateConfig + }); + UpgradeableBurnMintTokenPool(tokenPool).applyChainUpdates(chainUpdates); + } +} diff --git a/src/20240528_Multi_GHOCrossChainLaunch/AaveV3Arbitrum_GHOCrossChainLaunch_20240528.t.sol b/src/20240528_Multi_GHOCrossChainLaunch/AaveV3Arbitrum_GHOCrossChainLaunch_20240528.t.sol new file mode 100644 index 000000000..5e57ca610 --- /dev/null +++ b/src/20240528_Multi_GHOCrossChainLaunch/AaveV3Arbitrum_GHOCrossChainLaunch_20240528.t.sol @@ -0,0 +1,388 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {GovV3Helpers} from 'aave-helpers/GovV3Helpers.sol'; +import {ProtocolV3TestBase, ReserveConfig} from 'aave-helpers/ProtocolV3TestBase.sol'; +import {AaveV3Arbitrum} from 'aave-address-book/AaveV3Arbitrum.sol'; +import {GovernanceV3Arbitrum} from 'aave-address-book/GovernanceV3Arbitrum.sol'; +import {MiscArbitrum} from 'aave-address-book/MiscArbitrum.sol'; +import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; +import {RateLimiter} from 'ccip/v0.8/ccip/libraries/RateLimiter.sol'; +import {Internal} from 'ccip/v0.8/ccip/libraries/Internal.sol'; +import {Client} from 'ccip/v0.8/ccip/libraries/Client.sol'; +import {Router} from 'ccip/v0.8/ccip/Router.sol'; +import {PriceRegistry} from 'ccip/v0.8/ccip/PriceRegistry.sol'; +import {EVM2EVMOnRamp} from 'ccip/v0.8/ccip/onRamp/EVM2EVMOnRamp.sol'; +import {EVM2EVMOffRamp} from 'ccip/v0.8/ccip/offRamp/EVM2EVMOffRamp.sol'; +import {IPool} from 'ccip/v0.8/ccip/interfaces/pools/IPool.sol'; + +import {UpgradeableBurnMintTokenPool} from 'ccip/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPool.sol'; +import {IGhoToken} from 'gho-core/gho/interfaces/IGhoToken.sol'; + +import {AaveV3Arbitrum_GHOCrossChainLaunch_20240528, Utils} from './AaveV3Arbitrum_GHOCrossChainLaunch_20240528.sol'; + +/** + * @dev Test for AaveV3Arbitrum_GHOCrossChainLaunch_20240528 + * command: FOUNDRY_PROFILE=arbitrum forge test --match-path=src/20240528_Multi_GHOCrossChainLaunch/AaveV3Arbitrum_GHOCrossChainLaunch_20240528.t.sol -vv + */ +contract AaveV3Arbitrum_GHOCrossChainLaunch_20240528_Test is ProtocolV3TestBase { + AaveV3Arbitrum_GHOCrossChainLaunch_20240528 internal proposal; + + IGhoToken internal GHO; + UpgradeableBurnMintTokenPool internal TOKEN_POOL; + + address internal constant CCIP_ARB_ON_RAMP = 0xCe11020D56e5FDbfE46D9FC3021641FfbBB5AdEE; + address internal constant CCIP_ARB_OFF_RAMP = 0x542ba1902044069330e8c5b36A84EC503863722f; + + event Minted(address indexed sender, address indexed recipient, uint256 amount); + event Burned(address indexed sender, uint256 amount); + event Transfer(address indexed from, address indexed to, uint256 value); + event Initialized(uint8 version); + + function setUp() public { + vm.createSelectFork(vm.rpcUrl('arbitrum'), 220652440); + proposal = new AaveV3Arbitrum_GHOCrossChainLaunch_20240528(); + } + + /** + * @dev executes the generic test suite including e2e and config snapshots + */ + function test_defaultProposalExecution() public { + vm.recordLogs(); + + defaultTest( + 'AaveV3Arbitrum_GHOCrossChainLaunch_20240528', + AaveV3Arbitrum.POOL, + address(proposal) + ); + + // Fetch addresses + Vm.Log[] memory entries = vm.getRecordedLogs(); + address ghoAddress; + address ccipAddress; + for (uint256 i = 0; i < entries.length; i++) { + if (entries[i].topics[0] == Initialized.selector) { + uint8 version = abi.decode(entries[i].data, (uint8)); + if (version == 1) { + if (ghoAddress == address(0)) { + // ghoAddress is the first one + ghoAddress = entries[i].emitter; + } else { + // ccip is the second one + ccipAddress = entries[i].emitter; + break; + } + } + } + } + GHO = IGhoToken(ghoAddress); + TOKEN_POOL = UpgradeableBurnMintTokenPool(ccipAddress); + + _validateGhoDeployment(); + _validateCcipTokenPool(); + } + + /// @dev Test burn and mint actions, mocking CCIP calls + function test_ccipTokenPool() public { + vm.recordLogs(); + + GovV3Helpers.executePayload(vm, address(proposal)); + + // Fetch addresses + Vm.Log[] memory entries = vm.getRecordedLogs(); + address ghoAddress; + address ccipAddress; + for (uint256 i = 0; i < entries.length; i++) { + if (entries[i].topics[0] == Initialized.selector) { + uint8 version = abi.decode(entries[i].data, (uint8)); + if (version == 1) { + if (ghoAddress == address(0)) { + // ghoAddress is the first one + ghoAddress = entries[i].emitter; + } else { + // ccip is the second one + ccipAddress = entries[i].emitter; + break; + } + } + } + } + GHO = IGhoToken(ghoAddress); + TOKEN_POOL = UpgradeableBurnMintTokenPool(ccipAddress); + + // Mock calls + address router = TOKEN_POOL.getRouter(); + address ramp = makeAddr('ramp'); + vm.mockCall( + router, + abi.encodeWithSelector(bytes4(keccak256('getOnRamp(uint64)'))), + abi.encode(ramp) + ); + vm.mockCall( + router, + abi.encodeWithSelector(bytes4(keccak256('isOffRamp(uint64,address)'))), + abi.encode(true) + ); + + // Prank user + address user = makeAddr('user'); + + // Mint + uint256 amount = 500_000e18; // 500K GHO + uint64 ethChainSelector = Utils.CCIP_ETH_CHAIN_SELECTOR; + assertEq(_getFacilitatorLevel(address(TOKEN_POOL)), 0); + assertEq(GHO.balanceOf(address(TOKEN_POOL)), 0); + + vm.expectEmit(true, true, true, true, address(GHO)); + emit Transfer(address(0), user, amount); + + vm.expectEmit(false, true, true, true, address(TOKEN_POOL)); + emit Minted(address(0), user, amount); + + TOKEN_POOL.releaseOrMint(bytes(''), user, amount, ethChainSelector, bytes('')); + assertEq(_getFacilitatorLevel(address(TOKEN_POOL)), amount); + assertEq(GHO.balanceOf(address(TOKEN_POOL)), 0); + assertEq(GHO.balanceOf(user), amount); + + // Burn + // mock router transfer of funds from user to token pool + vm.prank(user); + GHO.transfer(address(TOKEN_POOL), amount); + + vm.expectEmit(true, true, true, true, address(GHO)); + emit Transfer(address(TOKEN_POOL), address(0), amount); + + vm.expectEmit(false, true, false, true, address(TOKEN_POOL)); + emit Burned(address(0), amount); + + vm.prank(ramp); + TOKEN_POOL.lockOrBurn(user, bytes(''), amount, ethChainSelector, bytes('')); + assertEq(_getFacilitatorLevel(address(TOKEN_POOL)), 0); + assertEq(GHO.balanceOf(address(TOKEN_POOL)), 0); + } + + /// @dev CCIP e2e + function test_ccipE2E() public { + vm.recordLogs(); + + GovV3Helpers.executePayload(vm, address(proposal)); + + // Fetch addresses + Vm.Log[] memory entries = vm.getRecordedLogs(); + address ghoAddress; + address ccipAddress; + for (uint256 i = 0; i < entries.length; i++) { + if (entries[i].topics[0] == Initialized.selector) { + uint8 version = abi.decode(entries[i].data, (uint8)); + if (version == 1) { + if (ghoAddress == address(0)) { + // ghoAddress is the first one + ghoAddress = entries[i].emitter; + } else { + // ccip is the second one + ccipAddress = entries[i].emitter; + break; + } + } + } + } + GHO = IGhoToken(ghoAddress); + TOKEN_POOL = UpgradeableBurnMintTokenPool(ccipAddress); + + uint64 ethChainSelector = Utils.CCIP_ETH_CHAIN_SELECTOR; + + // Chainlink config + Router router = Router(Utils.CCIP_ROUTER); + + { + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](1); + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](1); + // ETH -> ARB + onRampUpdates[0] = Router.OnRamp({ + destChainSelector: ethChainSelector, + onRamp: CCIP_ARB_ON_RAMP + }); + // ARB -> ETH + offRampUpdates[0] = Router.OffRamp({ + sourceChainSelector: ethChainSelector, + offRamp: CCIP_ARB_OFF_RAMP + }); + address routerOwner = router.owner(); + vm.startPrank(routerOwner); + router.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), offRampUpdates); + } + + { + // Add TokenPool to OnRamp + address[] memory tokens = new address[](1); + IPool[] memory pools = new IPool[](1); + tokens[0] = address(GHO); + pools[0] = IPool(address(TOKEN_POOL)); + address onRampOwner = EVM2EVMOnRamp(CCIP_ARB_ON_RAMP).owner(); + vm.startPrank(onRampOwner); + EVM2EVMOnRamp(CCIP_ARB_ON_RAMP).applyPoolUpdates( + new Internal.PoolUpdate[](0), + _getTokensAndPools(tokens, pools) + ); + } + + { + // OnRamp Price Registry + EVM2EVMOnRamp.DynamicConfig memory onRampDynamicConfig = EVM2EVMOnRamp(CCIP_ARB_ON_RAMP) + .getDynamicConfig(); + Internal.PriceUpdates memory priceUpdate = _getSingleTokenPriceUpdateStruct( + address(GHO), + 1e18 + ); + + PriceRegistry(onRampDynamicConfig.priceRegistry).updatePrices(priceUpdate); + // OffRamp Price Registry + EVM2EVMOffRamp.DynamicConfig memory offRampDynamicConfig = EVM2EVMOffRamp(CCIP_ARB_OFF_RAMP) + .getDynamicConfig(); + PriceRegistry(offRampDynamicConfig.priceRegistry).updatePrices(priceUpdate); + } + + // User executes ccipSend + address user = makeAddr('user'); + uint256 amount = 500_000e18; // 500K GHO + deal(user, 1e18); // 1 ETH + + // Mint tokens to user so can burn and bridge out + vm.startPrank(address(TOKEN_POOL)); + GHO.mint(user, amount); + + assertEq(GHO.balanceOf(address(TOKEN_POOL)), 0); + (uint256 capacity, uint256 level) = GHO.getFacilitatorBucket(address(TOKEN_POOL)); + assertEq(capacity, Utils.CCIP_BUCKET_CAPACITY); + assertEq(level, amount); + + vm.startPrank(user); + // Use address(0) to use native token as fee token + _sendCcip(router, address(GHO), amount, address(0), ethChainSelector, user); + + assertEq(GHO.balanceOf(user), 0); + assertEq(GHO.balanceOf(address(TOKEN_POOL)), 0); + (capacity, level) = GHO.getFacilitatorBucket(address(TOKEN_POOL)); + assertEq(capacity, Utils.CCIP_BUCKET_CAPACITY); + assertEq(level, 0); + } + + // --- + // Test Helpers + // --- + + function _validateGhoDeployment() internal { + assertEq(GHO.totalSupply(), 0); + assertEq(GHO.getFacilitatorsList().length, 1); + assertEq(_getProxyAdminAddress(address(GHO)), MiscArbitrum.PROXY_ADMIN); + assertTrue(GHO.hasRole(bytes32(0), GovernanceV3Arbitrum.EXECUTOR_LVL_1)); + assertTrue(GHO.hasRole(GHO.FACILITATOR_MANAGER_ROLE(), GovernanceV3Arbitrum.EXECUTOR_LVL_1)); + assertTrue(GHO.hasRole(GHO.BUCKET_MANAGER_ROLE(), GovernanceV3Arbitrum.EXECUTOR_LVL_1)); + } + + function _validateCcipTokenPool() internal { + // Deployment + assertEq(_getProxyAdminAddress(address(TOKEN_POOL)), MiscArbitrum.PROXY_ADMIN); + assertEq(TOKEN_POOL.owner(), GovernanceV3Arbitrum.EXECUTOR_LVL_1); + assertEq(address(TOKEN_POOL.getToken()), address(GHO)); + assertEq(TOKEN_POOL.getArmProxy(), Utils.CCIP_ARM_PROXY); + assertEq(TOKEN_POOL.getRouter(), Utils.CCIP_ROUTER); + + // Facilitator + (uint256 capacity, uint256 level) = GHO.getFacilitatorBucket(address(TOKEN_POOL)); + assertEq(capacity, Utils.CCIP_BUCKET_CAPACITY); + assertEq(level, 0); + + // Config + uint64[] memory supportedChains = TOKEN_POOL.getSupportedChains(); + assertEq(supportedChains.length, 1); + assertEq(supportedChains[0], Utils.CCIP_ETH_CHAIN_SELECTOR); + RateLimiter.TokenBucket memory outboundRateLimit = TOKEN_POOL + .getCurrentOutboundRateLimiterState(Utils.CCIP_ETH_CHAIN_SELECTOR); + RateLimiter.TokenBucket memory inboundRateLimit = TOKEN_POOL.getCurrentInboundRateLimiterState( + Utils.CCIP_ETH_CHAIN_SELECTOR + ); + assertEq(outboundRateLimit.isEnabled, false); + assertEq(inboundRateLimit.isEnabled, false); + } + + // --- + // Utils + // -- + + function _getProxyAdminAddress(address proxy) internal view returns (address) { + bytes32 ERC1967_ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + bytes32 adminSlot = vm.load(proxy, ERC1967_ADMIN_SLOT); + return address(uint160(uint256(adminSlot))); + } + + function _getFacilitatorLevel(address f) internal view returns (uint256) { + (, uint256 level) = GHO.getFacilitatorBucket(f); + return level; + } + + function _sendCcip( + Router router, + address token, + uint256 amount, + address feeToken, + uint64 destChainSelector, + address receiver + ) internal { + Client.EVM2AnyMessage memory message = _generateSingleTokenMessage( + receiver, + token, + amount, + feeToken + ); + uint256 expectedFee = router.getFee(destChainSelector, message); + + IERC20(token).approve(address(router), amount); + router.ccipSend{value: expectedFee}(destChainSelector, message); + } + + function _generateSingleTokenMessage( + address receiver, + address token, + uint256 amount, + address feeToken + ) public pure returns (Client.EVM2AnyMessage memory) { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({token: token, amount: amount}); + return + Client.EVM2AnyMessage({ + receiver: abi.encode(receiver), + data: '', + tokenAmounts: tokenAmounts, + feeToken: feeToken, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 200_000})) + }); + } + + function _getTokensAndPools( + address[] memory tokens, + IPool[] memory pools + ) internal pure returns (Internal.PoolUpdate[] memory) { + Internal.PoolUpdate[] memory tokensAndPools = new Internal.PoolUpdate[](tokens.length); + for (uint256 i = 0; i < tokens.length; ++i) { + tokensAndPools[i] = Internal.PoolUpdate({token: tokens[i], pool: address(pools[i])}); + } + return tokensAndPools; + } + + function _getSingleTokenPriceUpdateStruct( + address token, + uint224 price + ) internal pure returns (Internal.PriceUpdates memory) { + Internal.TokenPriceUpdate[] memory tokenPriceUpdates = new Internal.TokenPriceUpdate[](1); + tokenPriceUpdates[0] = Internal.TokenPriceUpdate({sourceToken: token, usdPerToken: price}); + + Internal.PriceUpdates memory priceUpdates = Internal.PriceUpdates({ + tokenPriceUpdates: tokenPriceUpdates, + gasPriceUpdates: new Internal.GasPriceUpdate[](0) + }); + + return priceUpdates; + } +} diff --git a/src/20240528_Multi_GHOCrossChainLaunch/AaveV3E2E_GHOCrossChainLaunch_20240528.t.sol b/src/20240528_Multi_GHOCrossChainLaunch/AaveV3E2E_GHOCrossChainLaunch_20240528.t.sol new file mode 100644 index 000000000..886ee7bd7 --- /dev/null +++ b/src/20240528_Multi_GHOCrossChainLaunch/AaveV3E2E_GHOCrossChainLaunch_20240528.t.sol @@ -0,0 +1,481 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {GovV3Helpers} from 'aave-helpers/GovV3Helpers.sol'; +import {ProtocolV3TestBase, ReserveConfig} from 'aave-helpers/ProtocolV3TestBase.sol'; +import {AaveV3Ethereum} from 'aave-address-book/AaveV3Ethereum.sol'; +import {AaveV3Arbitrum} 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 {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; +import {RateLimiter} from 'ccip/v0.8/ccip/libraries/RateLimiter.sol'; +import {Internal} from 'ccip/v0.8/ccip/libraries/Internal.sol'; +import {Client} from 'ccip/v0.8/ccip/libraries/Client.sol'; +import {Router} from 'ccip/v0.8/ccip/Router.sol'; +import {PriceRegistry} from 'ccip/v0.8/ccip/PriceRegistry.sol'; +import {EVM2EVMOnRamp} from 'ccip/v0.8/ccip/onRamp/EVM2EVMOnRamp.sol'; +import {EVM2EVMOffRamp} from 'ccip/v0.8/ccip/offRamp/EVM2EVMOffRamp.sol'; +import {IPool} from 'ccip/v0.8/ccip/interfaces/pools/IPool.sol'; + +import {UpgradeableLockReleaseTokenPool} from 'ccip/v0.8/ccip/pools/GHO/UpgradeableLockReleaseTokenPool.sol'; +import {UpgradeableBurnMintTokenPool} from 'ccip/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPool.sol'; + +import {IGhoToken} from 'gho-core/gho/interfaces/IGhoToken.sol'; + +import {AaveV3Ethereum_GHOCrossChainLaunch_20240528} from './AaveV3Ethereum_GHOCrossChainLaunch_20240528.sol'; +import {AaveV3Arbitrum_GHOCrossChainLaunch_20240528, Utils as ArbUtils} from './AaveV3Arbitrum_GHOCrossChainLaunch_20240528.sol'; + +/** + * @dev Test for AaveV3E2E_GHOCrossChainLaunch_20240528 + * command: FOUNDRY_PROFILE=mainnet forge test --match-path=src/20240528_Multi_GHOCrossChainLaunch/AaveV3E2E_GHOCrossChainLaunch_20240528.t.sol -vv + */ +contract AaveV3E2E_GHOCrossChainLaunch_20240528_Test is ProtocolV3TestBase { + using Internal for Internal.EVM2EVMMessage; + + AaveV3Ethereum_GHOCrossChainLaunch_20240528 internal ethProposal; + AaveV3Arbitrum_GHOCrossChainLaunch_20240528 internal arbProposal; + + UpgradeableLockReleaseTokenPool internal ETH_TOKEN_POOL; + UpgradeableBurnMintTokenPool internal ARB_TOKEN_POOL; + IGhoToken internal ETH_GHO; + IGhoToken internal ARB_GHO; + + Router internal ETH_ROUTER; + Router internal ARB_ROUTER; + + uint64 internal ETH_ARB_CHAIN_SELECTOR; + uint64 internal ARB_ETH_CHAIN_SELECTOR; + + address internal constant CCIP_ETH_ON_RAMP = 0x925228D7B82d883Dde340A55Fe8e6dA56244A22C; + address internal constant CCIP_ETH_OFF_RAMP = 0xeFC4a18af59398FF23bfe7325F2401aD44286F4d; + address internal constant CCIP_ARB_ON_RAMP = 0xCe11020D56e5FDbfE46D9FC3021641FfbBB5AdEE; + address internal constant CCIP_ARB_OFF_RAMP = 0x542ba1902044069330e8c5b36A84EC503863722f; + + event Released(address indexed sender, address indexed recipient, uint256 amount); + event Locked(address indexed sender, uint256 amount); + event Minted(address indexed sender, address indexed recipient, uint256 amount); + event Burned(address indexed sender, uint256 amount); + event CCIPSendRequested(Internal.EVM2EVMMessage message); + event Transfer(address indexed from, address indexed to, uint256 value); + event Initialized(uint8 version); + + uint256 internal ethereumFork; + uint256 internal arbitrumFork; + + function setUp() public { + ethereumFork = vm.createFork(vm.rpcUrl('mainnet'), 20067000); + arbitrumFork = vm.createFork(vm.rpcUrl('arbitrum'), 220652440); + + // Proposal creation + vm.selectFork(ethereumFork); + ethProposal = new AaveV3Ethereum_GHOCrossChainLaunch_20240528(); + ETH_GHO = IGhoToken(MiscEthereum.GHO_TOKEN); + ETH_ROUTER = Router(ethProposal.CCIP_ROUTER()); + ETH_ARB_CHAIN_SELECTOR = ethProposal.CCIP_ARB_CHAIN_SELECTOR(); + + vm.selectFork(arbitrumFork); + arbProposal = new AaveV3Arbitrum_GHOCrossChainLaunch_20240528(); + ARB_ROUTER = Router(ArbUtils.CCIP_ROUTER); + ARB_ETH_CHAIN_SELECTOR = ArbUtils.CCIP_ETH_CHAIN_SELECTOR; + + // AIP execution + vm.selectFork(ethereumFork); + vm.recordLogs(); + + GovV3Helpers.executePayload(vm, address(ethProposal)); + + // Fetch addresses + Vm.Log[] memory entries = vm.getRecordedLogs(); + for (uint256 i = 0; i < entries.length; i++) { + if (entries[i].topics[0] == Initialized.selector) { + uint8 version = abi.decode(entries[i].data, (uint8)); + if (version == 1) { + ETH_TOKEN_POOL = UpgradeableLockReleaseTokenPool(entries[i].emitter); + break; + } + } + } + + vm.selectFork(arbitrumFork); + vm.recordLogs(); + + GovV3Helpers.executePayload(vm, address(arbProposal)); + + // Fetch addresses + entries = vm.getRecordedLogs(); + for (uint256 i = 0; i < entries.length; i++) { + if (entries[i].topics[0] == Initialized.selector) { + uint8 version = abi.decode(entries[i].data, (uint8)); + if (version == 1) { + if (address(ARB_GHO) == address(0)) { + // ghoAddress is the first one + ARB_GHO = IGhoToken(entries[i].emitter); + } else { + // ccip is the second one + ARB_TOKEN_POOL = UpgradeableBurnMintTokenPool(entries[i].emitter); + break; + } + } + } + } + + // Chainlink execution + vm.selectFork(ethereumFork); + { + // OnRamp and OffRamp + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](1); + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](1); + // ARB -> ETH + onRampUpdates[0] = Router.OnRamp({ + destChainSelector: ETH_ARB_CHAIN_SELECTOR, + onRamp: CCIP_ETH_ON_RAMP + }); + // ETH -> ARB + offRampUpdates[0] = Router.OffRamp({ + sourceChainSelector: ETH_ARB_CHAIN_SELECTOR, + offRamp: CCIP_ETH_OFF_RAMP + }); + address routerOwner = ETH_ROUTER.owner(); + vm.startPrank(routerOwner); + ETH_ROUTER.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), offRampUpdates); + } + + { + // Add TokenPool to OnRamp + address[] memory tokens = new address[](1); + IPool[] memory pools = new IPool[](1); + tokens[0] = address(ETH_GHO); + pools[0] = IPool(address(ETH_TOKEN_POOL)); + address onRampOwner = EVM2EVMOnRamp(CCIP_ETH_ON_RAMP).owner(); + vm.startPrank(onRampOwner); + EVM2EVMOnRamp(CCIP_ETH_ON_RAMP).applyPoolUpdates( + new Internal.PoolUpdate[](0), + _getTokensAndPools(tokens, pools) + ); + + // Match Arbitrum GHO token with Ethereum TokenPool + tokens[0] = address(ARB_GHO); + EVM2EVMOffRamp(CCIP_ETH_OFF_RAMP).applyPoolUpdates( + new Internal.PoolUpdate[](0), + _getTokensAndPools(tokens, pools) + ); + } + + { + // OnRamp Price Registry + EVM2EVMOnRamp.DynamicConfig memory onRampDynamicConfig = EVM2EVMOnRamp(CCIP_ETH_ON_RAMP) + .getDynamicConfig(); + Internal.PriceUpdates memory priceUpdate = _getSingleTokenPriceUpdateStruct( + address(ETH_GHO), + 1e18 + ); + + PriceRegistry(onRampDynamicConfig.priceRegistry).updatePrices(priceUpdate); + // OffRamp Price Registry + EVM2EVMOffRamp.DynamicConfig memory offRampDynamicConfig = EVM2EVMOffRamp(CCIP_ETH_OFF_RAMP) + .getDynamicConfig(); + PriceRegistry(offRampDynamicConfig.priceRegistry).updatePrices(priceUpdate); + } + + vm.selectFork(arbitrumFork); + { + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](1); + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](1); + // ETH -> ARB + onRampUpdates[0] = Router.OnRamp({ + destChainSelector: ARB_ETH_CHAIN_SELECTOR, + onRamp: CCIP_ARB_ON_RAMP + }); + // ARB -> ETH + offRampUpdates[0] = Router.OffRamp({ + sourceChainSelector: ARB_ETH_CHAIN_SELECTOR, + offRamp: CCIP_ARB_OFF_RAMP + }); + address routerOwner = ARB_ROUTER.owner(); + vm.startPrank(routerOwner); + ARB_ROUTER.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), offRampUpdates); + } + + { + // Add TokenPool to OnRamp + address[] memory tokens = new address[](1); + IPool[] memory pools = new IPool[](1); + tokens[0] = address(ARB_GHO); + pools[0] = IPool(address(ARB_TOKEN_POOL)); + address onRampOwner = EVM2EVMOnRamp(CCIP_ARB_ON_RAMP).owner(); + vm.startPrank(onRampOwner); + EVM2EVMOnRamp(CCIP_ARB_ON_RAMP).applyPoolUpdates( + new Internal.PoolUpdate[](0), + _getTokensAndPools(tokens, pools) + ); + + // Match Ethereum GHO token with Arbitrum TokenPool + tokens[0] = address(ETH_GHO); + EVM2EVMOffRamp(CCIP_ARB_OFF_RAMP).applyPoolUpdates( + new Internal.PoolUpdate[](0), + _getTokensAndPools(tokens, pools) + ); + } + + { + // OnRamp Price Registry + EVM2EVMOnRamp.DynamicConfig memory onRampDynamicConfig = EVM2EVMOnRamp(CCIP_ARB_ON_RAMP) + .getDynamicConfig(); + Internal.PriceUpdates memory priceUpdate = _getSingleTokenPriceUpdateStruct( + address(ARB_GHO), + 1e18 + ); + + PriceRegistry(onRampDynamicConfig.priceRegistry).updatePrices(priceUpdate); + // OffRamp Price Registry + EVM2EVMOffRamp.DynamicConfig memory offRampDynamicConfig = EVM2EVMOffRamp(CCIP_ARB_OFF_RAMP) + .getDynamicConfig(); + PriceRegistry(offRampDynamicConfig.priceRegistry).updatePrices(priceUpdate); + } + } + + /// @dev Full E2E Test: transfer from Ethereum to Arbitrum and way back + function test_ccipFullE22() public { + // CCIP Transfer from Ethereum to Arbitrum + // Ethereum execution (origin) + vm.selectFork(ethereumFork); + address user = makeAddr('user'); + uint256 amount = 500_000e18; // 500K ETH_GHO + deal(user, 1e18); // 1 ETH + deal(address(ETH_GHO), user, amount); + + assertEq(ETH_GHO.balanceOf(address(ETH_TOKEN_POOL)), 0); + assertEq(ETH_TOKEN_POOL.getBridgeLimit(), ethProposal.CCIP_BRIDGE_LIMIT()); + assertEq(ETH_TOKEN_POOL.getCurrentBridgedAmount(), 0); + + vm.startPrank(user); + // Use address(0) to use native token as fee token + Internal.EVM2EVMMessage memory message = _sendCcip( + SendCcipParams({ + expectedSeqNum: 1, + router: ETH_ROUTER, + onRamp: CCIP_ETH_ON_RAMP, + token: address(ETH_GHO), + amount: amount, + feeToken: address(0), + sourceChainSelector: ARB_ETH_CHAIN_SELECTOR, + destChainSelector: ETH_ARB_CHAIN_SELECTOR, + sender: user, + receiver: user + }) + ); + + assertEq(ETH_GHO.balanceOf(user), 0); + assertEq(ETH_GHO.balanceOf(address(ETH_TOKEN_POOL)), amount); + assertEq(ETH_TOKEN_POOL.getBridgeLimit(), ethProposal.CCIP_BRIDGE_LIMIT()); + assertEq(ETH_TOKEN_POOL.getCurrentBridgedAmount(), amount); + + // Arbitrum execution (destination) + vm.selectFork(arbitrumFork); + + assertEq(ARB_GHO.balanceOf(address(ARB_TOKEN_POOL)), 0); + (uint256 capacity, uint256 level) = ARB_GHO.getFacilitatorBucket(address(ARB_TOKEN_POOL)); + assertEq(capacity, ArbUtils.CCIP_BUCKET_CAPACITY); + assertEq(level, 0); + + // Mock off ramp + vm.startPrank(CCIP_ARB_OFF_RAMP); + bytes[] memory emptyData = new bytes[](1); + EVM2EVMOffRamp(CCIP_ARB_OFF_RAMP).executeSingleMessage(message, emptyData); + + assertEq(ARB_GHO.balanceOf(address(ARB_TOKEN_POOL)), 0); + (capacity, level) = ARB_GHO.getFacilitatorBucket(address(ARB_TOKEN_POOL)); + assertEq(capacity, ArbUtils.CCIP_BUCKET_CAPACITY); + assertEq(level, amount); + + // CCIP Transfer from Arbitrum to Ethereum + // Arbitrum execution (origin) + vm.selectFork(arbitrumFork); + deal(user, 1e18); // 1 ETH + + assertEq(ARB_GHO.balanceOf(address(ARB_TOKEN_POOL)), 0); + (capacity, level) = ARB_GHO.getFacilitatorBucket(address(ARB_TOKEN_POOL)); + assertEq(capacity, ArbUtils.CCIP_BUCKET_CAPACITY); + assertEq(level, amount); + + vm.startPrank(user); + // Use address(0) to use native token as fee token + message = _sendCcip( + SendCcipParams({ + expectedSeqNum: 1, + router: ARB_ROUTER, + onRamp: CCIP_ARB_ON_RAMP, + token: address(ARB_GHO), + amount: amount, + feeToken: address(0), + sourceChainSelector: ETH_ARB_CHAIN_SELECTOR, + destChainSelector: ARB_ETH_CHAIN_SELECTOR, + sender: user, + receiver: user + }) + ); + + assertEq(ARB_GHO.balanceOf(user), 0); + assertEq(ARB_GHO.balanceOf(address(ARB_TOKEN_POOL)), 0); + (capacity, level) = ARB_GHO.getFacilitatorBucket(address(ARB_TOKEN_POOL)); + assertEq(capacity, ArbUtils.CCIP_BUCKET_CAPACITY); + assertEq(level, 0); + + // Ethereum execution (destination) + vm.selectFork(ethereumFork); + + assertEq(ETH_GHO.balanceOf(user), 0); + assertEq(ETH_GHO.balanceOf(address(ETH_TOKEN_POOL)), amount); + assertEq(ETH_TOKEN_POOL.getBridgeLimit(), ethProposal.CCIP_BRIDGE_LIMIT()); + assertEq(ETH_TOKEN_POOL.getCurrentBridgedAmount(), amount); + + // Mock off ramp + vm.startPrank(CCIP_ETH_OFF_RAMP); + EVM2EVMOffRamp(CCIP_ETH_OFF_RAMP).executeSingleMessage(message, emptyData); + + assertEq(ETH_GHO.balanceOf(user), amount); + assertEq(ETH_GHO.balanceOf(address(ETH_TOKEN_POOL)), 0); + assertEq(ETH_TOKEN_POOL.getBridgeLimit(), ethProposal.CCIP_BRIDGE_LIMIT()); + assertEq(ETH_TOKEN_POOL.getCurrentBridgedAmount(), 0); + } + + // --- + // Utils + // -- + + struct SendCcipParams { + uint64 expectedSeqNum; + Router router; + address onRamp; + address token; + uint256 amount; + address feeToken; + uint64 sourceChainSelector; + uint64 destChainSelector; + address sender; + address receiver; + } + + function _sendCcip( + SendCcipParams memory params + ) internal returns (Internal.EVM2EVMMessage memory) { + Client.EVM2AnyMessage memory message = _generateSingleTokenMessage( + params.receiver, + params.token, + params.amount, + params.feeToken + ); + uint256 expectedFee = params.router.getFee(params.destChainSelector, message); + + bytes32 metadataHash = keccak256( + abi.encode( + Internal.EVM_2_EVM_MESSAGE_HASH, + params.sourceChainSelector, + params.destChainSelector, + params.onRamp + ) + ); + + Internal.EVM2EVMMessage memory geEvent = _messageToEvent( + message, + params.expectedSeqNum, + params.expectedSeqNum, + expectedFee, + params.sender, + params.sourceChainSelector, + metadataHash + ); + + IERC20(params.token).approve(address(params.router), params.amount); + params.router.ccipSend{value: expectedFee}(params.destChainSelector, message); + + return geEvent; + } + + function _generateSingleTokenMessage( + address receiver, + address token, + uint256 amount, + address feeToken + ) public pure returns (Client.EVM2AnyMessage memory) { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({token: token, amount: amount}); + return + Client.EVM2AnyMessage({ + receiver: abi.encode(receiver), + data: '', + tokenAmounts: tokenAmounts, + feeToken: feeToken, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 200_000})) + }); + } + + function _messageToEvent( + Client.EVM2AnyMessage memory message, + uint64 seqNum, + uint64 nonce, + uint256 feeTokenAmount, + address originalSender, + uint64 sourceChainSelector, + bytes32 metadataHash + ) public pure returns (Internal.EVM2EVMMessage memory) { + // Slicing is only available for calldata. So we have to build a new bytes array. + bytes memory args = new bytes(message.extraArgs.length - 4); + for (uint256 i = 4; i < message.extraArgs.length; ++i) { + args[i - 4] = message.extraArgs[i]; + } + Client.EVMExtraArgsV1 memory extraArgs = abi.decode(args, (Client.EVMExtraArgsV1)); + Internal.EVM2EVMMessage memory messageEvent = Internal.EVM2EVMMessage({ + sequenceNumber: seqNum, + feeTokenAmount: feeTokenAmount, + sender: originalSender, + nonce: nonce, + gasLimit: extraArgs.gasLimit, + strict: false, + sourceChainSelector: sourceChainSelector, + receiver: abi.decode(message.receiver, (address)), + data: message.data, + tokenAmounts: message.tokenAmounts, + sourceTokenData: new bytes[](message.tokenAmounts.length), + feeToken: message.feeToken, + messageId: '' + }); + + messageEvent.messageId = Internal._hash(messageEvent, metadataHash); + return messageEvent; + } + + function _getTokensAndPools( + address[] memory tokens, + IPool[] memory pools + ) internal pure returns (Internal.PoolUpdate[] memory) { + Internal.PoolUpdate[] memory tokensAndPools = new Internal.PoolUpdate[](tokens.length); + for (uint256 i = 0; i < tokens.length; ++i) { + tokensAndPools[i] = Internal.PoolUpdate({token: tokens[i], pool: address(pools[i])}); + } + return tokensAndPools; + } + + function _getSingleTokenPriceUpdateStruct( + address token, + uint224 price + ) internal pure returns (Internal.PriceUpdates memory) { + Internal.TokenPriceUpdate[] memory tokenPriceUpdates = new Internal.TokenPriceUpdate[](1); + tokenPriceUpdates[0] = Internal.TokenPriceUpdate({sourceToken: token, usdPerToken: price}); + + Internal.PriceUpdates memory priceUpdates = Internal.PriceUpdates({ + tokenPriceUpdates: tokenPriceUpdates, + gasPriceUpdates: new Internal.GasPriceUpdate[](0) + }); + + return priceUpdates; + } + + function _getFacilitatorLevel(address f) internal view returns (uint256) { + (, uint256 level) = ARB_GHO.getFacilitatorBucket(f); + return level; + } +} diff --git a/src/20240528_Multi_GHOCrossChainLaunch/AaveV3Ethereum_GHOCrossChainLaunch_20240528.sol b/src/20240528_Multi_GHOCrossChainLaunch/AaveV3Ethereum_GHOCrossChainLaunch_20240528.sol new file mode 100644 index 000000000..9a5bf98ed --- /dev/null +++ b/src/20240528_Multi_GHOCrossChainLaunch/AaveV3Ethereum_GHOCrossChainLaunch_20240528.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IProposalGenericExecutor} from 'aave-helpers/interfaces/IProposalGenericExecutor.sol'; +import {GovernanceV3Ethereum} from 'aave-address-book/GovernanceV3Ethereum.sol'; +import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; +import {TransparentUpgradeableProxy} from 'solidity-utils/contracts/transparent-proxy/TransparentUpgradeableProxy.sol'; +import {UpgradeableLockReleaseTokenPool} from 'ccip/v0.8/ccip/pools/GHO/UpgradeableLockReleaseTokenPool.sol'; +import {UpgradeableTokenPool} from 'ccip/v0.8/ccip/pools/GHO/UpgradeableTokenPool.sol'; +import {RateLimiter} from 'ccip/v0.8/ccip/libraries/RateLimiter.sol'; + +/** + * @title GHO Cross-Chain Launch + * @author Aave Labs + * - Snapshot: https://snapshot.org/#/aave.eth/proposal/0x2a6ffbcff41a5ef98b7542f99b207af9c1e79e61f859d0a62f3bf52d3280877a + * - Discussion: https://governance.aave.com/t/arfc-gho-cross-chain-launch/17616 + * @dev This payload consists of the following set of actions: + * 1. Deploy LockReleaseTokenPool + * 2. Accept ownership of CCIP TokenPool + * 3. Configure CCIP TokenPool + */ +contract AaveV3Ethereum_GHOCrossChainLaunch_20240528 is IProposalGenericExecutor { + address public constant CCIP_ARM_PROXY = 0x411dE17f12D1A34ecC7F45f49844626267c75e81; + address public constant CCIP_ROUTER = 0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D; + uint256 public constant CCIP_BRIDGE_LIMIT = 1_000_000e18; // 1M + uint64 public constant CCIP_ARB_CHAIN_SELECTOR = 4949039107694359620; + + function execute() external { + // 1. Deploy LockReleaseTokenPool + address tokenPool = _deployCcipTokenPool(); + + // 2. Accept TokenPool ownership + UpgradeableLockReleaseTokenPool(tokenPool).acceptOwnership(); + + // 3. Configure CCIP + _configureCcipTokenPool(tokenPool); + } + + function _deployCcipTokenPool() internal returns (address) { + address imple = address( + new UpgradeableLockReleaseTokenPool(MiscEthereum.GHO_TOKEN, CCIP_ARM_PROXY, false, true) + ); + + bytes memory tokenPoolInitParams = abi.encodeWithSignature( + 'initialize(address,address[],address,uint256)', + GovernanceV3Ethereum.EXECUTOR_LVL_1, // owner + new address[](0), // allowList + CCIP_ROUTER, // router + CCIP_BRIDGE_LIMIT // bridgeLimit + ); + return + address( + new TransparentUpgradeableProxy(imple, MiscEthereum.PROXY_ADMIN, tokenPoolInitParams) + ); + } + + function _configureCcipTokenPool(address tokenPool) internal { + UpgradeableTokenPool.ChainUpdate[] memory chainUpdates = new UpgradeableTokenPool.ChainUpdate[]( + 1 + ); + RateLimiter.Config memory rateConfig = RateLimiter.Config({ + isEnabled: false, + capacity: 0, + rate: 0 + }); + chainUpdates[0] = UpgradeableTokenPool.ChainUpdate({ + remoteChainSelector: CCIP_ARB_CHAIN_SELECTOR, + allowed: true, + outboundRateLimiterConfig: rateConfig, + inboundRateLimiterConfig: rateConfig + }); + UpgradeableLockReleaseTokenPool(tokenPool).applyChainUpdates(chainUpdates); + } +} diff --git a/src/20240528_Multi_GHOCrossChainLaunch/AaveV3Ethereum_GHOCrossChainLaunch_20240528.t.sol b/src/20240528_Multi_GHOCrossChainLaunch/AaveV3Ethereum_GHOCrossChainLaunch_20240528.t.sol new file mode 100644 index 000000000..4ada050e0 --- /dev/null +++ b/src/20240528_Multi_GHOCrossChainLaunch/AaveV3Ethereum_GHOCrossChainLaunch_20240528.t.sol @@ -0,0 +1,339 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {GovV3Helpers} from 'aave-helpers/GovV3Helpers.sol'; +import {ProtocolV3TestBase, ReserveConfig} from 'aave-helpers/ProtocolV3TestBase.sol'; +import {AaveV3Ethereum} from 'aave-address-book/AaveV3Ethereum.sol'; +import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; +import {GovernanceV3Ethereum} from 'aave-address-book/GovernanceV3Ethereum.sol'; +import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; +import {RateLimiter} from 'ccip/v0.8/ccip/libraries/RateLimiter.sol'; +import {Internal} from 'ccip/v0.8/ccip/libraries/Internal.sol'; +import {Client} from 'ccip/v0.8/ccip/libraries/Client.sol'; +import {Router} from 'ccip/v0.8/ccip/Router.sol'; +import {PriceRegistry} from 'ccip/v0.8/ccip/PriceRegistry.sol'; +import {EVM2EVMOnRamp} from 'ccip/v0.8/ccip/onRamp/EVM2EVMOnRamp.sol'; +import {EVM2EVMOffRamp} from 'ccip/v0.8/ccip/offRamp/EVM2EVMOffRamp.sol'; +import {IPool} from 'ccip/v0.8/ccip/interfaces/pools/IPool.sol'; + +import {UpgradeableLockReleaseTokenPool} from 'ccip/v0.8/ccip/pools/GHO/UpgradeableLockReleaseTokenPool.sol'; +import {IGhoToken} from 'gho-core/gho/interfaces/IGhoToken.sol'; + +import {AaveV3Ethereum_GHOCrossChainLaunch_20240528} from './AaveV3Ethereum_GHOCrossChainLaunch_20240528.sol'; + +/** + * @dev Test for AaveV3Ethereum_GHOCrossChainLaunch_20240528 + * command: FOUNDRY_PROFILE=mainnet forge test --match-path=src/20240528_Multi_GHOCrossChainLaunch/AaveV3Ethereum_GHOCrossChainLaunch_20240528.t.sol -vv + */ +contract AaveV3Ethereum_GHOCrossChainLaunch_20240528_Test is ProtocolV3TestBase { + using Internal for Internal.EVM2EVMMessage; + + AaveV3Ethereum_GHOCrossChainLaunch_20240528 internal proposal; + + UpgradeableLockReleaseTokenPool internal TOKEN_POOL; + IGhoToken internal GHO; + + address internal constant CCIP_ETH_ON_RAMP = 0x925228D7B82d883Dde340A55Fe8e6dA56244A22C; + address internal constant CCIP_ETH_OFF_RAMP = 0xeFC4a18af59398FF23bfe7325F2401aD44286F4d; + + event Released(address indexed sender, address indexed recipient, uint256 amount); + event Locked(address indexed sender, uint256 amount); + event CCIPSendRequested(Internal.EVM2EVMMessage message); + event Transfer(address indexed from, address indexed to, uint256 value); + event Initialized(uint8 version); + + function setUp() public { + vm.createSelectFork(vm.rpcUrl('mainnet'), 20067000); + proposal = new AaveV3Ethereum_GHOCrossChainLaunch_20240528(); + GHO = IGhoToken(MiscEthereum.GHO_TOKEN); + } + + /// @dev General test of the proposal + function test_defaultProposalExecution() public { + vm.recordLogs(); + + defaultTest( + 'AaveV3Ethereum_GHOCrossChainLaunch_20240528', + AaveV3Ethereum.POOL, + address(proposal) + ); + + // Fetch addresses + Vm.Log[] memory entries = vm.getRecordedLogs(); + address ccipAddress; + for (uint256 i = 0; i < entries.length; i++) { + if (entries[i].topics[0] == Initialized.selector) { + uint8 version = abi.decode(entries[i].data, (uint8)); + if (version == 1) { + ccipAddress = entries[i].emitter; + break; + } + } + } + TOKEN_POOL = UpgradeableLockReleaseTokenPool(ccipAddress); + + _validateCcipTokenPool(); + } + + /// @dev Test lock and release actions, mocking CCIP calls + function test_ccipTokenPool() public { + vm.recordLogs(); + + GovV3Helpers.executePayload(vm, address(proposal)); + + // Fetch addresses + Vm.Log[] memory entries = vm.getRecordedLogs(); + address ccipAddress; + for (uint256 i = 0; i < entries.length; i++) { + if (entries[i].topics[0] == Initialized.selector) { + uint8 version = abi.decode(entries[i].data, (uint8)); + if (version == 1) { + ccipAddress = entries[i].emitter; + break; + } + } + } + TOKEN_POOL = UpgradeableLockReleaseTokenPool(ccipAddress); + + // Mock calls + address router = TOKEN_POOL.getRouter(); + address ramp = makeAddr('ramp'); + vm.mockCall( + router, + abi.encodeWithSelector(bytes4(keccak256('getOnRamp(uint64)'))), + abi.encode(ramp) + ); + vm.mockCall( + router, + abi.encodeWithSelector(bytes4(keccak256('isOffRamp(uint64,address)'))), + abi.encode(true) + ); + + // Prank user + address user = makeAddr('user'); + + // Lock + uint256 amount = 500_000e18; // 500K GHO + deal(address(GHO), user, amount); + uint64 arbChainSelector = proposal.CCIP_ARB_CHAIN_SELECTOR(); + + assertEq(GHO.balanceOf(address(TOKEN_POOL)), 0); + + // mock router transfer of funds from user to token pool + vm.prank(user); + GHO.transfer(address(TOKEN_POOL), amount); + + vm.expectEmit(false, true, false, true, address(TOKEN_POOL)); + emit Locked(address(0), amount); + + vm.prank(ramp); + TOKEN_POOL.lockOrBurn(user, bytes(''), amount, arbChainSelector, bytes('')); + assertEq(GHO.balanceOf(address(TOKEN_POOL)), amount); + assertEq(GHO.balanceOf(user), 0); + + // Release + vm.expectEmit(true, true, true, true, address(GHO)); + emit Transfer(address(TOKEN_POOL), user, amount); + + vm.expectEmit(false, true, true, true, address(TOKEN_POOL)); + emit Released(address(0), user, amount); + + TOKEN_POOL.releaseOrMint(bytes(''), user, amount, arbChainSelector, bytes('')); + assertEq(GHO.balanceOf(address(TOKEN_POOL)), 0); + assertEq(GHO.balanceOf(user), amount); + } + + /// @dev CCIP e2e + function test_ccipE2E() public { + vm.recordLogs(); + + GovV3Helpers.executePayload(vm, address(proposal)); + + // Fetch addresses + Vm.Log[] memory entries = vm.getRecordedLogs(); + address ccipAddress; + for (uint256 i = 0; i < entries.length; i++) { + if (entries[i].topics[0] == Initialized.selector) { + uint8 version = abi.decode(entries[i].data, (uint8)); + if (version == 1) { + ccipAddress = entries[i].emitter; + break; + } + } + } + TOKEN_POOL = UpgradeableLockReleaseTokenPool(ccipAddress); + + uint64 arbChainSelector = proposal.CCIP_ARB_CHAIN_SELECTOR(); + + // Chainlink config + Router router = Router(proposal.CCIP_ROUTER()); + + { + // OnRamp and OffRamp + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](1); + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](1); + // ARB -> ETH + onRampUpdates[0] = Router.OnRamp({ + destChainSelector: arbChainSelector, + onRamp: CCIP_ETH_ON_RAMP + }); + // ETH -> ARB + offRampUpdates[0] = Router.OffRamp({ + sourceChainSelector: arbChainSelector, + offRamp: CCIP_ETH_OFF_RAMP + }); + address routerOwner = router.owner(); + vm.startPrank(routerOwner); + router.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), offRampUpdates); + } + + { + // Add TokenPool to OnRamp + address[] memory tokens = new address[](1); + IPool[] memory pools = new IPool[](1); + tokens[0] = address(GHO); + pools[0] = IPool(address(TOKEN_POOL)); + address onRampOwner = EVM2EVMOnRamp(CCIP_ETH_ON_RAMP).owner(); + vm.startPrank(onRampOwner); + EVM2EVMOnRamp(CCIP_ETH_ON_RAMP).applyPoolUpdates( + new Internal.PoolUpdate[](0), + _getTokensAndPools(tokens, pools) + ); + } + + { + // OnRamp Price Registry + EVM2EVMOnRamp.DynamicConfig memory onRampDynamicConfig = EVM2EVMOnRamp(CCIP_ETH_ON_RAMP) + .getDynamicConfig(); + Internal.PriceUpdates memory priceUpdate = _getSingleTokenPriceUpdateStruct( + address(GHO), + 1e18 + ); + + PriceRegistry(onRampDynamicConfig.priceRegistry).updatePrices(priceUpdate); + // OffRamp Price Registry + EVM2EVMOffRamp.DynamicConfig memory offRampDynamicConfig = EVM2EVMOffRamp(CCIP_ETH_OFF_RAMP) + .getDynamicConfig(); + PriceRegistry(offRampDynamicConfig.priceRegistry).updatePrices(priceUpdate); + } + + // User executes ccipSend + address user = makeAddr('user'); + uint256 amount = 500_000e18; // 500K GHO + deal(user, 1e18); // 1 ETH + deal(address(GHO), user, amount); + + assertEq(GHO.balanceOf(address(TOKEN_POOL)), 0); + assertEq(TOKEN_POOL.getBridgeLimit(), proposal.CCIP_BRIDGE_LIMIT()); + assertEq(TOKEN_POOL.getCurrentBridgedAmount(), 0); + + vm.startPrank(user); + // Use address(0) to use native token as fee token + _sendCcip(router, address(GHO), amount, address(0), arbChainSelector, user); + + assertEq(GHO.balanceOf(user), 0); + assertEq(GHO.balanceOf(address(TOKEN_POOL)), amount); + assertEq(TOKEN_POOL.getBridgeLimit(), proposal.CCIP_BRIDGE_LIMIT()); + assertEq(TOKEN_POOL.getCurrentBridgedAmount(), amount); + } + + // --- + // Test Helpers + // --- + + function _validateCcipTokenPool() internal { + // Deployment + assertEq(_getProxyAdminAddress(address(TOKEN_POOL)), MiscEthereum.PROXY_ADMIN); + assertEq(address(TOKEN_POOL.getToken()), address(GHO)); + assertEq(TOKEN_POOL.owner(), GovernanceV3Ethereum.EXECUTOR_LVL_1); + assertEq(TOKEN_POOL.getArmProxy(), proposal.CCIP_ARM_PROXY()); + assertEq(TOKEN_POOL.getRouter(), proposal.CCIP_ROUTER()); + + // Config + uint64[] memory supportedChains = TOKEN_POOL.getSupportedChains(); + assertEq(supportedChains.length, 1); + assertEq(supportedChains[0], proposal.CCIP_ARB_CHAIN_SELECTOR()); + RateLimiter.TokenBucket memory outboundRateLimit = TOKEN_POOL + .getCurrentOutboundRateLimiterState(proposal.CCIP_ARB_CHAIN_SELECTOR()); + RateLimiter.TokenBucket memory inboundRateLimit = TOKEN_POOL.getCurrentInboundRateLimiterState( + proposal.CCIP_ARB_CHAIN_SELECTOR() + ); + assertEq(outboundRateLimit.isEnabled, false); + assertEq(inboundRateLimit.isEnabled, false); + } + + // --- + // Utils + // -- + + function _getProxyAdminAddress(address proxy) internal view returns (address) { + bytes32 ERC1967_ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + bytes32 adminSlot = vm.load(proxy, ERC1967_ADMIN_SLOT); + return address(uint160(uint256(adminSlot))); + } + + function _sendCcip( + Router router, + address token, + uint256 amount, + address feeToken, + uint64 destChainSelector, + address receiver + ) internal { + Client.EVM2AnyMessage memory message = _generateSingleTokenMessage( + receiver, + token, + amount, + feeToken + ); + uint256 expectedFee = router.getFee(destChainSelector, message); + + IERC20(token).approve(address(router), amount); + router.ccipSend{value: expectedFee}(destChainSelector, message); + } + + function _generateSingleTokenMessage( + address receiver, + address token, + uint256 amount, + address feeToken + ) public pure returns (Client.EVM2AnyMessage memory) { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({token: token, amount: amount}); + return + Client.EVM2AnyMessage({ + receiver: abi.encode(receiver), + data: '', + tokenAmounts: tokenAmounts, + feeToken: feeToken, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 200_000})) + }); + } + + function _getTokensAndPools( + address[] memory tokens, + IPool[] memory pools + ) internal pure returns (Internal.PoolUpdate[] memory) { + Internal.PoolUpdate[] memory tokensAndPools = new Internal.PoolUpdate[](tokens.length); + for (uint256 i = 0; i < tokens.length; ++i) { + tokensAndPools[i] = Internal.PoolUpdate({token: tokens[i], pool: address(pools[i])}); + } + return tokensAndPools; + } + + function _getSingleTokenPriceUpdateStruct( + address token, + uint224 price + ) internal pure returns (Internal.PriceUpdates memory) { + Internal.TokenPriceUpdate[] memory tokenPriceUpdates = new Internal.TokenPriceUpdate[](1); + tokenPriceUpdates[0] = Internal.TokenPriceUpdate({sourceToken: token, usdPerToken: price}); + + Internal.PriceUpdates memory priceUpdates = Internal.PriceUpdates({ + tokenPriceUpdates: tokenPriceUpdates, + gasPriceUpdates: new Internal.GasPriceUpdate[](0) + }); + + return priceUpdates; + } +} diff --git a/src/20240528_Multi_GHOCrossChainLaunch/GHOCrossChainLaunch.md b/src/20240528_Multi_GHOCrossChainLaunch/GHOCrossChainLaunch.md new file mode 100644 index 000000000..bf5d69d9c --- /dev/null +++ b/src/20240528_Multi_GHOCrossChainLaunch/GHOCrossChainLaunch.md @@ -0,0 +1,53 @@ +--- +title: "GHO Cross-Chain" +author: "Aave Labs" +discussions: "https://governance.aave.com/t/arfc-gho-cross-chain-launch/17616" +snapshot: "https://snapshot.org/#/aave.eth/proposal/0x2a6ffbcff41a5ef98b7542f99b207af9c1e79e61f859d0a62f3bf52d3280877a" +--- + +## Simple Summary + +This AIP proposes the cross-chain implementation for the GHO stablecoin, the native asset of the Aave Protocol, beginning with the initial expansion to the Arbitrum network utilizing the Chainlink Cross-Chain Interoperability Protocol (CCIP). + +The smart contracts have been refined through multiple stages of design, development, testing, and implementation. For security validations, collaborations with DAO service providers Certora and BGD Labs were established to conduct code reviews. + +Following extensive community discussion, this AIP proposes the deployment of cross-chain GHO, adopting risk parameters formulated by Chaos Labs. + +## Motivation + +Transitioning to a cross-chain model beyond Ethereum Mainnet enhances GHO's accessibility and the overall user experience with faster transactions and reduced costs. This shift also presents new opportunities within the ecosystem of each network, allowing access to a wide array of integrations with other protocols and tools, enriching GHO's utility potential. + +## Specification + +This AIP addresses the implementation of the GHO cross-chain strategy. The following smart contracts have been developed: + +- Upgradable GHO Token: This contract version allows the DAO to adjust the logic of the token. +- Modified CCIP Contracts: Tailored versions of the Chainlink Cross-Chain Interoperability Protocol (CCIP) contracts, designed to support the GHO cross-chain implementation. + +All smart contracts, including the code for this AIP, have been reviewed by BGD Labs for alignment with the Aave Protocol and by Certora for security aspects, including both manual and formal verification. + +Proposed implementation actions are the following: + +Ethereum: + +- Deployment of CCIP LockReleaseTokenPool Contract: GHO reserve contract backs up liquidity across different chains. A "bridge limit" control enables the DAO to regulate the outflow of GHO liquidity to other networks. The limit is set at the minimum bucket capacity of the bridges across networks to ensure proper validation of GHO transfers on the source chain to facilitate transfers between chains. +- Transfer of ownership of the CCIP LockReleaseTokenPool contract to the DAO: The DAO controls and owns the contract logic and configuration parameters, including the outbound/inbound rate limit and the bridge limit. +- Configuration of CCIP LockReleaseTokenPool contract: token pool rate limit will be disabled. + +Arbitrum: + +- Deployment of UpgradeableGHO: The contract is configured to be deployed by the DAO upon passing of this AIP. +- Deployment of CCIP BurnMintTokenPool contract: The contract handles the minting and burning processes, based on the liquidity backed from Ethereum. +- Transfer of ownership of the CCIP BurnMintTokenPool contract to the DAO: The DAO will take control of the contract logic and configuration of outbound/inbound rate limit. +- Configuration of CCIP BurnMintTokenPool contract: token pool rate limit will be disabled. + +## References + +- Implementation: [AaveV3Ethereum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20240528_Multi_GHOCrossChainLaunch/AaveV3Ethereum_GHOCrossChainLaunch_20240528.sol), [AaveV3Arbitrum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20240528_Multi_GHOCrossChainLaunch/AaveV3Arbitrum_GHOCrossChainLaunch_20240528.sol) +- Tests: [AaveV3Ethereum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20240528_Multi_GHOCrossChainLaunch/AaveV3Ethereum_GHOCrossChainLaunch_20240528.t.sol), [AaveV3Arbitrum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20240528_Multi_GHOCrossChainLaunch/AaveV3Arbitrum_GHOCrossChainLaunch_20240528.t.sol) +- [Snapshot](https://snapshot.org/#/aave.eth/proposal/0x2a6ffbcff41a5ef98b7542f99b207af9c1e79e61f859d0a62f3bf52d3280877a) +- [Discussion](https://governance.aave.com/t/arfc-gho-cross-chain-launch/17616) + +## Copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). diff --git a/src/20240528_Multi_GHOCrossChainLaunch/GHOCrossChainLaunch_20240528.s.sol b/src/20240528_Multi_GHOCrossChainLaunch/GHOCrossChainLaunch_20240528.s.sol new file mode 100644 index 000000000..5abaab012 --- /dev/null +++ b/src/20240528_Multi_GHOCrossChainLaunch/GHOCrossChainLaunch_20240528.s.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {GovV3Helpers, IPayloadsControllerCore, PayloadsControllerUtils} from 'aave-helpers/GovV3Helpers.sol'; +import {GovernanceV3Ethereum} from 'aave-address-book/GovernanceV3Ethereum.sol'; +import {EthereumScript, ArbitrumScript} from 'aave-helpers/ScriptUtils.sol'; +import {AaveV3Ethereum_GHOCrossChainLaunch_20240528} from './AaveV3Ethereum_GHOCrossChainLaunch_20240528.sol'; +import {AaveV3Arbitrum_GHOCrossChainLaunch_20240528} from './AaveV3Arbitrum_GHOCrossChainLaunch_20240528.sol'; + +/** + * @dev Deploy Ethereum + * deploy-command: make deploy-ledger contract=src/20240528_Multi_GHOCrossChainLaunch/GHOCrossChainLaunch_20240528.s.sol:DeployEthereum chain=mainnet + * verify-command: FOUNDRY_PROFILE=mainnet npx catapulta-verify -b broadcast/GHOCrossChainLaunch_20240528.s.sol/1/run-latest.json + */ +contract DeployEthereum is EthereumScript { + function run() external broadcast { + // deploy payloads + address payload0 = GovV3Helpers.deployDeterministic( + type(AaveV3Ethereum_GHOCrossChainLaunch_20240528).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/20240528_Multi_GHOCrossChainLaunch/GHOCrossChainLaunch_20240528.s.sol:DeployArbitrum chain=arbitrum + * verify-command: FOUNDRY_PROFILE=arbitrum npx catapulta-verify -b broadcast/GHOCrossChainLaunch_20240528.s.sol/42161/run-latest.json + */ +contract DeployArbitrum is ArbitrumScript { + function run() external broadcast { + // deploy payloads + address payload0 = GovV3Helpers.deployDeterministic( + type(AaveV3Arbitrum_GHOCrossChainLaunch_20240528).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/20240528_Multi_GHOCrossChainLaunch/GHOCrossChainLaunch_20240528.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_GHOCrossChainLaunch_20240528).creationCode + ); + payloads[0] = GovV3Helpers.buildMainnetPayload(vm, actionsEthereum); + + IPayloadsControllerCore.ExecutionAction[] + memory actionsArbitrum = new IPayloadsControllerCore.ExecutionAction[](1); + actionsArbitrum[0] = GovV3Helpers.buildAction( + type(AaveV3Arbitrum_GHOCrossChainLaunch_20240528).creationCode + ); + payloads[1] = GovV3Helpers.buildArbitrumPayload(vm, actionsArbitrum); + + // create proposal + vm.startBroadcast(); + GovV3Helpers.createProposal( + vm, + payloads, + GovernanceV3Ethereum.VOTING_PORTAL_ETH_POL, + GovV3Helpers.ipfsHashFile(vm, 'src/20240528_Multi_GHOCrossChainLaunch/GHOCrossChainLaunch.md') + ); + } +} diff --git a/src/20240528_Multi_GHOCrossChainLaunch/config.ts b/src/20240528_Multi_GHOCrossChainLaunch/config.ts new file mode 100644 index 000000000..c8de36b18 --- /dev/null +++ b/src/20240528_Multi_GHOCrossChainLaunch/config.ts @@ -0,0 +1,21 @@ +import {ConfigFile} from '../../generator/types'; +export const config: ConfigFile = { + rootOptions: { + pools: ['AaveV3Ethereum', 'AaveV3Arbitrum'], + title: 'GHO Cross-Chain Launch', + shortName: 'GHOCrossChainLaunch', + date: '20240528', + author: 'Aave Labs', + discussion: 'https://governance.aave.com/t/arfc-gho-cross-chain-launch/17616', + snapshot: + 'https://snapshot.org/#/aave.eth/proposal/0x2a6ffbcff41a5ef98b7542f99b207af9c1e79e61f859d0a62f3bf52d3280877a', + votingNetwork: 'POLYGON', + }, + poolOptions: { + AaveV3Ethereum: {configs: {}, cache: {blockNumber: 19967293}}, + AaveV3Arbitrum: { + configs: {}, + cache: {blockNumber: 215853041}, + }, + }, +};