diff --git a/.gitmodules b/.gitmodules index f7316a1d6..07649de30 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,10 @@ [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.git + branch = feat/1_5_1_token_pool +[submodule "lib/gho-core"] + path = lib/gho-core + url = https://github.com/aave/gho-core.git diff --git a/diffs/AaveV3Arbitrum_GHOAvaxLaunch_20241106_before_AaveV3Arbitrum_GHOAvaxLaunch_20241106_after.md b/diffs/AaveV3Arbitrum_GHOAvaxLaunch_20241106_before_AaveV3Arbitrum_GHOAvaxLaunch_20241106_after.md new file mode 100644 index 000000000..4d686addb --- /dev/null +++ b/diffs/AaveV3Arbitrum_GHOAvaxLaunch_20241106_before_AaveV3Arbitrum_GHOAvaxLaunch_20241106_after.md @@ -0,0 +1,15 @@ +## Emodes changed + +### EMode: Stablecoins(id: 1) + + + +### EMode: ETH correlated(id: 2) + + + +## Raw diff + +```json +{} +``` \ No newline at end of file diff --git a/diffs/AaveV3Avalanche_GHOAvaxLaunch_20241106_before_AaveV3Avalanche_GHOAvaxLaunch_20241106_after.md b/diffs/AaveV3Avalanche_GHOAvaxLaunch_20241106_before_AaveV3Avalanche_GHOAvaxLaunch_20241106_after.md new file mode 100644 index 000000000..5e354a32d --- /dev/null +++ b/diffs/AaveV3Avalanche_GHOAvaxLaunch_20241106_before_AaveV3Avalanche_GHOAvaxLaunch_20241106_after.md @@ -0,0 +1,15 @@ +## Emodes changed + +### EMode: Stablecoins(id: 1) + + + +### EMode: AVAX correlated(id: 2) + + + +## Raw diff + +```json +{} +``` \ No newline at end of file diff --git a/diffs/AaveV3Ethereum_GHOAvaxLaunch_20241106_before_AaveV3Ethereum_GHOAvaxLaunch_20241106_after.md b/diffs/AaveV3Ethereum_GHOAvaxLaunch_20241106_before_AaveV3Ethereum_GHOAvaxLaunch_20241106_after.md new file mode 100644 index 000000000..95861275d --- /dev/null +++ b/diffs/AaveV3Ethereum_GHOAvaxLaunch_20241106_before_AaveV3Ethereum_GHOAvaxLaunch_20241106_after.md @@ -0,0 +1,19 @@ +## Emodes changed + +### EMode: ETH correlated(id: 1) + + + +### EMode: sUSDe Stablecoins(id: 2) + + + +### EMode: rsETH LST main(id: 3) + + + +## Raw diff + +```json +{} +``` \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 42e201d77..ee6bd7f5c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,7 +2,7 @@ src = 'src' test = 'tests' script = 'scripts' -solc = '0.8.20' +solc = '0.8.24' out = 'out' bytecode_hash = 'none' libs = ['lib'] diff --git a/lib/ccip b/lib/ccip new file mode 160000 index 000000000..1881f2070 --- /dev/null +++ b/lib/ccip @@ -0,0 +1 @@ +Subproject commit 1881f2070c7b34e6b2195e5b9fe26e771a0233b4 diff --git a/lib/gho-core b/lib/gho-core new file mode 160000 index 000000000..0a6fbd4d4 --- /dev/null +++ b/lib/gho-core @@ -0,0 +1 @@ +Subproject commit 0a6fbd4d4167f6e0e777aec0ae12bf4f18b4b0a8 diff --git a/remappings.txt b/remappings.txt index 9fefc7eea..8e4662a5e 100644 --- a/remappings.txt +++ b/remappings.txt @@ -3,3 +3,5 @@ aave-helpers/=lib/aave-helpers/ aave-v3-origin/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-origin/src/ forge-std/=lib/aave-helpers/lib/forge-std/src/ solidity-utils/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-origin/lib/solidity-utils/src +ccip/=lib/ccip/contracts/src/v0.8/ccip/ +gho-core/=lib/gho-core/src/contracts/ \ No newline at end of file diff --git a/src/20241106_Multi_GHOAvaxLaunch/AaveV3Arbitrum_GHOAvaxLaunch_20241106.sol b/src/20241106_Multi_GHOAvaxLaunch/AaveV3Arbitrum_GHOAvaxLaunch_20241106.sol new file mode 100644 index 000000000..d6662f02a --- /dev/null +++ b/src/20241106_Multi_GHOAvaxLaunch/AaveV3Arbitrum_GHOAvaxLaunch_20241106.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {RateLimiter} from 'ccip/libraries/RateLimiter.sol'; +import {IProposalGenericExecutor} from 'aave-helpers/src/interfaces/IProposalGenericExecutor.sol'; +import {MiscArbitrum} from 'aave-address-book/MiscArbitrum.sol'; +import {IUpgradeableTokenPool_1_4} from 'src/interfaces/ccip/IUpgradeableTokenPool_1_4.sol'; + +/** + * @title GHOAvaxLaunch + * @author Aave Labs + * - Snapshot: https://snapshot.org/#/aave.eth/proposal/0x2aed7eb8b03cb3f961cbf790bf2e2e1e449f841a4ad8bdbcdd223bb6ac69e719 + * - Discussion: https://governance.aave.com/t/arfc-launch-gho-on-avalanche-set-aci-as-emissions-manager-for-rewards/19339 + * @dev This payload configures the CCIP TokenPool for Avalanche + */ +contract AaveV3Arbitrum_GHOAvaxLaunch_20241106 is IProposalGenericExecutor { + address public constant CCIP_TOKEN_POOL = MiscArbitrum.GHO_CCIP_TOKEN_POOL; + uint64 public constant CCIP_AVAX_CHAIN_SELECTOR = 6433500567565415381; + + function execute() external { + _configureCcipTokenPool(CCIP_TOKEN_POOL, CCIP_AVAX_CHAIN_SELECTOR); + } + + function _configureCcipTokenPool(address tokenPool, uint64 chainSelector) internal { + IUpgradeableTokenPool_1_4.ChainUpdate[] + memory chainUpdates = new IUpgradeableTokenPool_1_4.ChainUpdate[](1); + RateLimiter.Config memory rateConfig = RateLimiter.Config({ + isEnabled: false, + capacity: 0, + rate: 0 + }); + chainUpdates[0] = IUpgradeableTokenPool_1_4.ChainUpdate({ + remoteChainSelector: chainSelector, + allowed: true, + outboundRateLimiterConfig: rateConfig, + inboundRateLimiterConfig: rateConfig + }); + IUpgradeableTokenPool_1_4(tokenPool).applyChainUpdates(chainUpdates); + } +} diff --git a/src/20241106_Multi_GHOAvaxLaunch/AaveV3Arbitrum_GHOAvaxLaunch_20241106.t.sol b/src/20241106_Multi_GHOAvaxLaunch/AaveV3Arbitrum_GHOAvaxLaunch_20241106.t.sol new file mode 100644 index 000000000..e92237812 --- /dev/null +++ b/src/20241106_Multi_GHOAvaxLaunch/AaveV3Arbitrum_GHOAvaxLaunch_20241106.t.sol @@ -0,0 +1,488 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {TransparentUpgradeableProxy} from 'solidity-utils/contracts/transparent-proxy/TransparentUpgradeableProxy.sol'; +import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; +import {UpgradeableBurnMintTokenPool} from 'ccip/pools/GHO/UpgradeableBurnMintTokenPool.sol'; +import {IPoolPriorTo1_5} from 'ccip/interfaces/IPoolPriorTo1_5.sol'; +import {IPriceRegistry} from 'ccip/interfaces/IPriceRegistry.sol'; +import {Internal} from 'ccip/libraries/Internal.sol'; +import {RateLimiter} from 'ccip/libraries/RateLimiter.sol'; +import {Client} from 'ccip/libraries/Client.sol'; +import {TokenAdminRegistry} from 'ccip/tokenAdminRegistry/TokenAdminRegistry.sol'; +import {EVM2EVMOnRamp} from 'ccip/onRamp/EVM2EVMOnRamp.sol'; +import {EVM2EVMOffRamp} from 'ccip/offRamp/EVM2EVMOffRamp.sol'; +import {Router} from 'ccip/Router.sol'; +import {ProtocolV3TestBase} from 'aave-helpers/src/ProtocolV3TestBase.sol'; +import {GovV3Helpers} from 'aave-helpers/src/GovV3Helpers.sol'; +import {AaveV3Arbitrum} from 'aave-address-book/AaveV3Arbitrum.sol'; +import {MiscArbitrum} from 'aave-address-book/MiscArbitrum.sol'; +import {AaveV3ArbitrumAssets} from 'aave-address-book/AaveV3Arbitrum.sol'; +import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; +import {GovernanceV3Avalanche} from 'aave-address-book/GovernanceV3Avalanche.sol'; +import {MiscAvalanche} from 'aave-address-book/MiscAvalanche.sol'; +import {UpgradeableGhoToken} from 'gho-core/gho/UpgradeableGhoToken.sol'; +import {IUpgradeableTokenPool_1_5} from 'src/interfaces/ccip/IUpgradeableTokenPool_1_5.sol'; +import {AaveV3Arbitrum_GHOAvaxLaunch_20241106} from './AaveV3Arbitrum_GHOAvaxLaunch_20241106.sol'; +import {AaveV3Avalanche_GHOAvaxLaunch_20241106} from './AaveV3Avalanche_GHOAvaxLaunch_20241106.sol'; + +/** + * @dev Test for AaveV3Arbitrum_GHOAvaxLaunch_20241106 + * command: FOUNDRY_PROFILE=arbitrum forge test --match-path=src/20241106_Multi_GHOAvaxLaunch/AaveV3Arbitrum_GHOAvaxLaunch_20241106.t.sol -vv + */ +contract AaveV3Arbitrum_GHOAvaxLaunch_20241106_Test is ProtocolV3TestBase { + AaveV3Arbitrum_GHOAvaxLaunch_20241106 internal proposal; + + UpgradeableBurnMintTokenPool public constant TOKEN_POOL = + UpgradeableBurnMintTokenPool(MiscArbitrum.GHO_CCIP_TOKEN_POOL); + address public constant GHO_TOKEN = AaveV3ArbitrumAssets.GHO_UNDERLYING; + UpgradeableGhoToken public GHO = UpgradeableGhoToken(GHO_TOKEN); + + address public constant TOKEN_ADMIN_REGISTRY = 0x39AE1032cF4B334a1Ed41cdD0833bdD7c7E7751E; + address public constant REGISTRY_ADMIN = 0x8a89770722c84B60cE02989Aedb22Ac4791F8C7f; + address public constant AVAX_GHO_TOKEN = 0xc0F850AfdeFF8E0292C638C3e237fB2168E703d0; + address public constant AVAX_TOKEN_POOL = 0x2e234DAe75C793f67A35089C9d99245E1C58470b; + address public constant AVAX_REGISTRY_ADMIN = 0xA3f32a07CCd8569f49cf350D4e61C016CA484644; + address public constant AVAX_TOKEN_ADMIN_REGISTRY = 0xc8df5D618c6a59Cc6A311E96a39450381001464F; + address public constant AVAX_RMN_PROXY = 0xcBD48A8eB077381c3c4Eb36b402d7283aB2b11Bc; + address public constant AVAX_ROUTER = 0xF4c7E640EdA248ef95972845a62bdC74237805dB; + address public constant ETH_TOKEN_POOL = MiscEthereum.GHO_CCIP_TOKEN_POOL; + address public constant CCIP_ARB_ETH_ON_RAMP = 0x67761742ac8A21Ec4D76CA18cbd701e5A6F3Bef3; + address public constant CCIP_ARB_ETH_OFF_RAMP = 0x91e46cc5590A4B9182e47f40006140A7077Dec31; + address public constant CCIP_ARB_AVAX_ON_RAMP = 0xe80cC83B895ada027b722b78949b296Bd1fC5639; + address public constant CCIP_ARB_AVAX_OFF_RAMP = 0x95095007d5Cc3E7517A1A03c9e228adA5D0bc376; + address public constant TOKEN_POOL_AND_PROXY = 0x26329558f08cbb40d6a4CCA0E0C67b29D64A8c50; + uint64 public constant CCIP_AVAX_CHAIN_SELECTOR = 6433500567565415381; + uint64 public constant CCIP_ETH_CHAIN_SELECTOR = 5009297550715157269; + + 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); + + function setUp() public { + // Execute Avax proposal to deploy Avax token pool + vm.createSelectFork(vm.rpcUrl('avalanche'), 53559217); + + _deployCcipTokenPool(); + + // TODO: Remove this (will be done on chainlink's side) + // Prank chainlink and set up admin role to be accepted on token registry + vm.startPrank(AVAX_REGISTRY_ADMIN); + TokenAdminRegistry(AVAX_TOKEN_ADMIN_REGISTRY).proposeAdministrator( + AVAX_GHO_TOKEN, + GovernanceV3Avalanche.EXECUTOR_LVL_1 + ); + vm.stopPrank(); + + AaveV3Avalanche_GHOAvaxLaunch_20241106 avaxProposal = new AaveV3Avalanche_GHOAvaxLaunch_20241106(); + GovV3Helpers.executePayload(vm, address(avaxProposal)); + + // Switch to Arbitrum and create proposal + vm.createSelectFork(vm.rpcUrl('arbitrum'), 279521658); + + // Configure TokenPoolAndProxy for Avalanche + // Prank Registry owner + vm.startPrank(REGISTRY_ADMIN); + _configureCcipTokenPool(TOKEN_POOL_AND_PROXY, CCIP_AVAX_CHAIN_SELECTOR); + vm.stopPrank(); + proposal = new AaveV3Arbitrum_GHOAvaxLaunch_20241106(); + } + + /** + * @dev executes the generic test suite including e2e and config snapshots + */ + function test_defaultProposalExecution() public { + defaultTest('AaveV3Arbitrum_GHOAvaxLaunch_20241106', AaveV3Arbitrum.POOL, address(proposal)); + + _validateCcipTokenPool(); + } + + /// @dev Test burn and mint actions, mocking CCIP calls + function test_ccipTokenPool() public { + GovV3Helpers.executePayload(vm, address(proposal)); + + // 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'); + + // ARB <> ETH + + // Mint + uint256 amount = 100e18; + uint256 startingFacilitatorLevel = _getFacilitatorLevel(address(TOKEN_POOL)); + uint256 startingGhoBalance = GHO.balanceOf(address(TOKEN_POOL)); + + 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); + + IPoolPriorTo1_5(address(TOKEN_POOL)).releaseOrMint( + bytes(''), + user, + amount, + CCIP_ETH_CHAIN_SELECTOR, + bytes('') + ); + + assertEq(_getFacilitatorLevel(address(TOKEN_POOL)), startingFacilitatorLevel + amount); + assertEq(GHO.balanceOf(address(TOKEN_POOL)), startingGhoBalance); + 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); + IPoolPriorTo1_5(address(TOKEN_POOL)).lockOrBurn( + user, + bytes(''), + amount, + CCIP_ETH_CHAIN_SELECTOR, + bytes('') + ); + + assertEq(_getFacilitatorLevel(address(TOKEN_POOL)), startingFacilitatorLevel); + assertEq(GHO.balanceOf(address(TOKEN_POOL)), startingGhoBalance); + + // ARB <> AVAX + + // Mint + 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); + + IPoolPriorTo1_5(address(TOKEN_POOL)).releaseOrMint( + bytes(''), + user, + amount, + CCIP_AVAX_CHAIN_SELECTOR, + bytes('') + ); + + assertEq(_getFacilitatorLevel(address(TOKEN_POOL)), startingFacilitatorLevel + amount); + assertEq(GHO.balanceOf(address(TOKEN_POOL)), startingGhoBalance); + 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); + IPoolPriorTo1_5(address(TOKEN_POOL)).lockOrBurn( + user, + bytes(''), + amount, + CCIP_AVAX_CHAIN_SELECTOR, + bytes('') + ); + + assertEq(_getFacilitatorLevel(address(TOKEN_POOL)), startingFacilitatorLevel); + assertEq(GHO.balanceOf(address(TOKEN_POOL)), startingGhoBalance); + } + + /// @dev CCIP e2e eth <> arb + function test_ccipE2E_ETH_ARB() public { + GovV3Helpers.executePayload(vm, address(proposal)); + + // Chainlink config + Router router = Router(TOKEN_POOL.getRouter()); + + { + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](1); + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](1); + // ETH -> ARB + onRampUpdates[0] = Router.OnRamp({ + destChainSelector: CCIP_ETH_CHAIN_SELECTOR, + onRamp: CCIP_ARB_ETH_ON_RAMP + }); + // ARB -> ETH + offRampUpdates[0] = Router.OffRamp({ + sourceChainSelector: CCIP_ETH_CHAIN_SELECTOR, + offRamp: CCIP_ARB_ETH_OFF_RAMP + }); + address routerOwner = router.owner(); + vm.startPrank(routerOwner); + router.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), offRampUpdates); + } + + { + // OnRamp Price Registry + EVM2EVMOnRamp.DynamicConfig memory onRampDynamicConfig = EVM2EVMOnRamp(CCIP_ARB_ETH_ON_RAMP) + .getDynamicConfig(); + Internal.PriceUpdates memory priceUpdate = _getSingleTokenPriceUpdateStruct( + address(GHO), + 1e18 + ); + + IPriceRegistry(onRampDynamicConfig.priceRegistry).updatePrices(priceUpdate); + // OffRamp Price Registry + EVM2EVMOffRamp.DynamicConfig memory offRampDynamicConfig = EVM2EVMOffRamp( + CCIP_ARB_ETH_OFF_RAMP + ).getDynamicConfig(); + IPriceRegistry(offRampDynamicConfig.priceRegistry).updatePrices(priceUpdate); + } + + // User executes ccipSend + address user = makeAddr('user'); + uint256 amount = 100e18; // 100 GHO + deal(user, 1e18); // 1 ETH + + uint256 startingGhoBalance = GHO.balanceOf(address(TOKEN_POOL)); + uint256 startingFacilitatorLevel = _getFacilitatorLevel(address(TOKEN_POOL)); + + // 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)), startingGhoBalance); + assertEq(_getFacilitatorLevel(address(TOKEN_POOL)), startingFacilitatorLevel + amount); + + vm.startPrank(user); + // Use address(0) to use native token as fee token + _sendCcip(router, address(GHO), amount, address(0), CCIP_ETH_CHAIN_SELECTOR, user); + + assertEq(GHO.balanceOf(user), 0); + assertEq(GHO.balanceOf(address(TOKEN_POOL)), startingGhoBalance); + assertEq(_getFacilitatorLevel(address(TOKEN_POOL)), startingFacilitatorLevel); + } + + /// @dev CCIP e2e avax <> arb + function test_ccipE2E_AVAX_ARB() public { + GovV3Helpers.executePayload(vm, address(proposal)); + + // Chainlink config + Router router = Router(TOKEN_POOL.getRouter()); + + { + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](1); + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](1); + // AVAX -> ARB + onRampUpdates[0] = Router.OnRamp({ + destChainSelector: CCIP_AVAX_CHAIN_SELECTOR, + onRamp: CCIP_ARB_AVAX_ON_RAMP + }); + // ARB -> AVAX + offRampUpdates[0] = Router.OffRamp({ + sourceChainSelector: CCIP_AVAX_CHAIN_SELECTOR, + offRamp: CCIP_ARB_AVAX_OFF_RAMP + }); + address routerOwner = router.owner(); + vm.startPrank(routerOwner); + router.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), offRampUpdates); + } + + { + // OnRamp Price Registry + EVM2EVMOnRamp.DynamicConfig memory onRampDynamicConfig = EVM2EVMOnRamp(CCIP_ARB_AVAX_ON_RAMP) + .getDynamicConfig(); + Internal.PriceUpdates memory priceUpdate = _getSingleTokenPriceUpdateStruct( + address(GHO), + 1e18 + ); + + IPriceRegistry(onRampDynamicConfig.priceRegistry).updatePrices(priceUpdate); + // OffRamp Price Registry + EVM2EVMOffRamp.DynamicConfig memory offRampDynamicConfig = EVM2EVMOffRamp( + CCIP_ARB_AVAX_OFF_RAMP + ).getDynamicConfig(); + IPriceRegistry(offRampDynamicConfig.priceRegistry).updatePrices(priceUpdate); + } + + // User executes ccipSend + address user = makeAddr('user'); + uint256 amount = 100e18; // 100 GHO + deal(user, 1e18); // 1 ETH + + uint256 startingGhoBalance = GHO.balanceOf(address(TOKEN_POOL)); + uint256 startingFacilitatorLevel = _getFacilitatorLevel(address(TOKEN_POOL)); + + // 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)), startingGhoBalance); + assertEq(_getFacilitatorLevel(address(TOKEN_POOL)), startingFacilitatorLevel + amount); + + vm.startPrank(user); + // Use address(0) to use native token as fee token + _sendCcip(router, address(GHO), amount, address(0), CCIP_AVAX_CHAIN_SELECTOR, user); + + assertEq(GHO.balanceOf(user), 0); + assertEq(GHO.balanceOf(address(TOKEN_POOL)), startingGhoBalance); + assertEq(_getFacilitatorLevel(address(TOKEN_POOL)), startingFacilitatorLevel); + } + + // --- + // Deployment + // --- + + function _deployGhoToken() internal returns (address) { + address imple = address(new UpgradeableGhoToken()); + + bytes memory ghoTokenInitParams = abi.encodeWithSignature( + 'initialize(address)', + GovernanceV3Avalanche.EXECUTOR_LVL_1 // owner + ); + return + address( + new TransparentUpgradeableProxy(imple, MiscAvalanche.PROXY_ADMIN, ghoTokenInitParams) + ); + } + + function _deployCcipTokenPool() internal returns (address) { + address imple = address( + new UpgradeableBurnMintTokenPool(AVAX_GHO_TOKEN, 18, AVAX_RMN_PROXY, false) + ); + + bytes memory tokenPoolInitParams = abi.encodeWithSignature( + 'initialize(address,address[],address)', + GovernanceV3Avalanche.EXECUTOR_LVL_1, // owner + new address[](0), // allowList + AVAX_ROUTER // router + ); + return + address( + new TransparentUpgradeableProxy( + imple, // logic + MiscAvalanche.PROXY_ADMIN, // proxy admin + tokenPoolInitParams // data + ) + ); + } + + // --- + // Test Helpers + // --- + + function _validateCcipTokenPool() internal view { + // Configs + uint64[] memory supportedChains = TOKEN_POOL.getSupportedChains(); + assertEq(supportedChains.length, 2); + + // ETH + assertEq(supportedChains[0], CCIP_ETH_CHAIN_SELECTOR); + + // AVAX + assertEq(supportedChains[1], CCIP_AVAX_CHAIN_SELECTOR); + RateLimiter.TokenBucket memory outboundRateLimit = TOKEN_POOL + .getCurrentOutboundRateLimiterState(CCIP_AVAX_CHAIN_SELECTOR); + RateLimiter.TokenBucket memory inboundRateLimit = TOKEN_POOL.getCurrentInboundRateLimiterState( + CCIP_AVAX_CHAIN_SELECTOR + ); + assertEq(outboundRateLimit.isEnabled, false); + assertEq(inboundRateLimit.isEnabled, false); + } + + // --- + // Utils + // --- + + function _configureCcipTokenPool(address tokenPool, uint64 chainSelector) internal { + IUpgradeableTokenPool_1_5.ChainUpdate[] + memory chainUpdates = new IUpgradeableTokenPool_1_5.ChainUpdate[](1); + RateLimiter.Config memory rateConfig = RateLimiter.Config({ + isEnabled: false, + capacity: 0, + rate: 0 + }); + chainUpdates[0] = IUpgradeableTokenPool_1_5.ChainUpdate({ + remoteChainSelector: chainSelector, + allowed: true, + remotePoolAddress: abi.encode(AVAX_TOKEN_POOL), + remoteTokenAddress: abi.encode(AVAX_GHO_TOKEN), + outboundRateLimiterConfig: rateConfig, + inboundRateLimiterConfig: rateConfig + }); + IUpgradeableTokenPool_1_5(tokenPool).applyChainUpdates(chainUpdates); + } + + 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 _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/20241106_Multi_GHOAvaxLaunch/AaveV3Avalanche_GHOAvaxLaunch_20241106.sol b/src/20241106_Multi_GHOAvaxLaunch/AaveV3Avalanche_GHOAvaxLaunch_20241106.sol new file mode 100644 index 000000000..9d05da15f --- /dev/null +++ b/src/20241106_Multi_GHOAvaxLaunch/AaveV3Avalanche_GHOAvaxLaunch_20241106.sol @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {TransparentUpgradeableProxy} from 'solidity-utils/contracts/transparent-proxy/TransparentUpgradeableProxy.sol'; +import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; +import {SafeERC20} from 'solidity-utils/contracts/oz-common/SafeERC20.sol'; +import {UpgradeableBurnMintTokenPool} from 'ccip/pools/GHO/UpgradeableBurnMintTokenPool.sol'; +import {UpgradeableTokenPool} from 'ccip/pools/GHO/UpgradeableTokenPool.sol'; +import {RateLimiter} from 'ccip/libraries/RateLimiter.sol'; +import {TokenAdminRegistry} from 'ccip/tokenAdminRegistry/TokenAdminRegistry.sol'; +import {IProposalGenericExecutor} from 'aave-helpers/src/interfaces/IProposalGenericExecutor.sol'; +import {AaveV3PayloadAvalanche} from 'aave-helpers/src/v3-config-engine/AaveV3PayloadAvalanche.sol'; +import {AaveV3Avalanche} from 'aave-address-book/AaveV3Avalanche.sol'; +import {AaveV3ArbitrumAssets} from 'aave-address-book/AaveV3Arbitrum.sol'; +import {GovernanceV3Avalanche} from 'aave-address-book/GovernanceV3Avalanche.sol'; +import {MiscAvalanche} from 'aave-address-book/MiscAvalanche.sol'; +import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; +import {MiscArbitrum} from 'aave-address-book/MiscArbitrum.sol'; +import {EngineFlags} from 'aave-v3-origin/contracts/extensions/v3-config-engine/EngineFlags.sol'; +import {IAaveV3ConfigEngine} from 'aave-v3-origin/contracts/extensions/v3-config-engine/IAaveV3ConfigEngine.sol'; +import {IGhoToken} from 'gho-core/gho/interfaces/IGhoToken.sol'; +import {UpgradeableGhoToken} from 'gho-core/gho/UpgradeableGhoToken.sol'; + +/** + * @title GHO Avax Launch + * @author Aave Labs + * - Snapshot: https://snapshot.org/#/aave.eth/proposal/0x2aed7eb8b03cb3f961cbf790bf2e2e1e449f841a4ad8bdbcdd223bb6ac69e719 + * - Discussion: https://governance.aave.com/t/arfc-launch-gho-on-avalanche-set-aci-as-emissions-manager-for-rewards/19339 + * @dev This payload consists of the following set of actions: + * 1. Deploy GHO + * 2. Accept ownership of CCIP TokenPool + * 3. Configure CCIP TokenPool for Ethereum + * 4. Configure CCIP TokenPool for Arbitrum + * 5. Add CCIP TokenPool as GHO Facilitator (allowing burn and mint) + * 6. Accept administrator role from Chainlink token admin registry + * 7. Link token to pool on Chainlink token admin registry + */ +contract AaveV3Avalanche_GHOAvaxLaunch_20241106 is IProposalGenericExecutor { + address public constant CCIP_RMN_PROXY = 0xcBD48A8eB077381c3c4Eb36b402d7283aB2b11Bc; + address public constant CCIP_ROUTER = 0xF4c7E640EdA248ef95972845a62bdC74237805dB; + address public constant CCIP_TOKEN_ADMIN_REGISTRY = 0xc8df5D618c6a59Cc6A311E96a39450381001464F; + address public constant GHO_TOKEN = 0xc0F850AfdeFF8E0292C638C3e237fB2168E703d0; + address public constant CCIP_TOKEN_POOL = 0x2e234DAe75C793f67A35089C9d99245E1C58470b; + address public constant ETH_TOKEN_POOL = MiscEthereum.GHO_CCIP_TOKEN_POOL; + address public constant ETH_GHO = MiscEthereum.GHO_TOKEN; + address public constant ARB_TOKEN_POOL = MiscArbitrum.GHO_CCIP_TOKEN_POOL; + address public constant ARB_GHO = AaveV3ArbitrumAssets.GHO_UNDERLYING; + uint256 public constant CCIP_BUCKET_CAPACITY = 25_000_000e18; // 25M + uint64 public constant CCIP_ETH_CHAIN_SELECTOR = 5009297550715157269; + uint64 public constant CCIP_ARB_CHAIN_SELECTOR = 4949039107694359620; + + function execute() external { + // 1. Deploy GHO + _deployGhoToken(); + + // 2. Accept TokenPool ownership + UpgradeableBurnMintTokenPool(CCIP_TOKEN_POOL).acceptOwnership(); + + // 3. Configure CCIP TokenPool for Ethereum + _configureCcipTokenPool(CCIP_TOKEN_POOL, CCIP_ETH_CHAIN_SELECTOR, ETH_TOKEN_POOL, ETH_GHO); + + // 4. Configure CCIP TokenPool for Arbitrum + _configureCcipTokenPool(CCIP_TOKEN_POOL, CCIP_ARB_CHAIN_SELECTOR, ARB_TOKEN_POOL, ARB_GHO); + + // 5. Add CCIP TokenPool as GHO Facilitator + IGhoToken(GHO_TOKEN).grantRole( + IGhoToken(GHO_TOKEN).FACILITATOR_MANAGER_ROLE(), + GovernanceV3Avalanche.EXECUTOR_LVL_1 + ); + IGhoToken(GHO_TOKEN).grantRole( + IGhoToken(GHO_TOKEN).BUCKET_MANAGER_ROLE(), + GovernanceV3Avalanche.EXECUTOR_LVL_1 + ); + IGhoToken(GHO_TOKEN).addFacilitator( + CCIP_TOKEN_POOL, + 'CCIP TokenPool', + uint128(CCIP_BUCKET_CAPACITY) + ); + + // 6. Accept administrator role from Chainlink token manager + TokenAdminRegistry(CCIP_TOKEN_ADMIN_REGISTRY).acceptAdminRole(GHO_TOKEN); + + // 7. Link token to pool on Chainlink token admin registry + TokenAdminRegistry(CCIP_TOKEN_ADMIN_REGISTRY).setPool(GHO_TOKEN, CCIP_TOKEN_POOL); + } + + function _deployGhoToken() internal returns (address) { + // Deterministically deploy the gho token using create2 + bytes memory bytecode = type(UpgradeableGhoToken).creationCode; + bytes32 salt = keccak256(abi.encodePacked(GovernanceV3Avalanche.EXECUTOR_LVL_1, 'AVAX-GHO')); + address imple; + assembly { + imple := create2(0, add(bytecode, 0x20), mload(bytecode), salt) + } + + // Deterministically deploy the proxy + bytes memory ghoTokenInitParams = abi.encodeWithSignature( + 'initialize(address)', + GovernanceV3Avalanche.EXECUTOR_LVL_1 // owner + ); + bytes memory proxyBytecode = abi.encodePacked( + type(TransparentUpgradeableProxy).creationCode, + abi.encode(imple, MiscAvalanche.PROXY_ADMIN, ghoTokenInitParams) + ); + bytes32 proxySalt = keccak256( + abi.encodePacked(GovernanceV3Avalanche.EXECUTOR_LVL_1, 'AVAX-GHO-PROXY') + ); + address proxy; + assembly { + proxy := create2(0, add(proxyBytecode, 0x20), mload(proxyBytecode), proxySalt) + } + + return proxy; + } + + function _configureCcipTokenPool( + address tokenPool, + uint64 chainSelector, + address remotePool, + address remoteToken + ) internal { + UpgradeableTokenPool.ChainUpdate[] memory chainUpdates = new UpgradeableTokenPool.ChainUpdate[]( + 1 + ); + RateLimiter.Config memory rateConfig = RateLimiter.Config({ + isEnabled: false, + capacity: 0, + rate: 0 + }); + bytes[] memory remotePools = new bytes[](1); + remotePools[0] = abi.encode(remotePool); + chainUpdates[0] = UpgradeableTokenPool.ChainUpdate({ + remoteChainSelector: chainSelector, + remotePoolAddresses: remotePools, + remoteTokenAddress: abi.encode(remoteToken), + outboundRateLimiterConfig: rateConfig, + inboundRateLimiterConfig: rateConfig + }); + UpgradeableBurnMintTokenPool(tokenPool).applyChainUpdates(new uint64[](0), chainUpdates); + } +} + +// TODO: Determine appropriate procedure to have these 2 as separate payload, same AIP +/* + * @dev This payload consists of the following set of actions: + * 1. List GHO on Avax in separate payload - because there is a delay to activate lane + * 2. Supply GHO to the Aave protocol + */ +contract GhoAvaxListing is AaveV3PayloadAvalanche { + using SafeERC20 for IERC20; + + uint256 public constant GHO_SEED_AMOUNT = 1_000_000e18; + address public immutable ghoToken; + + constructor(address gho) { + ghoToken = gho; + } + + function newListings() public view override returns (IAaveV3ConfigEngine.Listing[] memory) { + IAaveV3ConfigEngine.Listing[] memory listings = new IAaveV3ConfigEngine.Listing[](1); + + listings[0] = IAaveV3ConfigEngine.Listing({ + asset: ghoToken, + assetSymbol: 'GHO', + priceFeed: 0x076DE3812BDbdAe1330064fc01Adf7f4EAa123f3, + enabledToBorrow: EngineFlags.ENABLED, + borrowableInIsolation: EngineFlags.DISABLED, + withSiloedBorrowing: EngineFlags.DISABLED, + flashloanable: EngineFlags.ENABLED, + ltv: 0, + liqThreshold: 0, + liqBonus: 0, + reserveFactor: 10_00, + supplyCap: 5_000_000, + borrowCap: 4_500_000, + debtCeiling: 0, + liqProtocolFee: 0, + rateStrategyParams: IAaveV3ConfigEngine.InterestRateInputData({ + optimalUsageRatio: _bpsToRay(90_00), + baseVariableBorrowRate: _bpsToRay(0), + variableRateSlope1: _bpsToRay(12_00), + variableRateSlope2: _bpsToRay(65_00) + }) + }); + + return listings; + } + + function _postExecute() internal override { + IERC20(ghoToken).forceApprove(address(AaveV3Avalanche.POOL), GHO_SEED_AMOUNT); + AaveV3Avalanche.POOL.supply(ghoToken, GHO_SEED_AMOUNT, address(0), 0); + } +} diff --git a/src/20241106_Multi_GHOAvaxLaunch/AaveV3Avalanche_GHOAvaxLaunch_20241106.t.sol b/src/20241106_Multi_GHOAvaxLaunch/AaveV3Avalanche_GHOAvaxLaunch_20241106.t.sol new file mode 100644 index 000000000..e97f25ca3 --- /dev/null +++ b/src/20241106_Multi_GHOAvaxLaunch/AaveV3Avalanche_GHOAvaxLaunch_20241106.t.sol @@ -0,0 +1,509 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {TransparentUpgradeableProxy} from 'solidity-utils/contracts/transparent-proxy/TransparentUpgradeableProxy.sol'; +import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; +import {UpgradeableBurnMintTokenPool} from 'ccip/pools/GHO/UpgradeableBurnMintTokenPool.sol'; +import {TokenAdminRegistry} from 'ccip/tokenAdminRegistry/TokenAdminRegistry.sol'; +import {IPoolV1} from 'ccip/interfaces/IPool.sol'; +import {IPriceRegistry} from 'ccip/interfaces/IPriceRegistry.sol'; +import {Pool} from 'ccip/libraries/Pool.sol'; +import {Internal} from 'ccip/libraries/Internal.sol'; +import {RateLimiter} from 'ccip/libraries/RateLimiter.sol'; +import {Client} from 'ccip/libraries/Client.sol'; +import {EVM2EVMOnRamp} from 'ccip/onRamp/EVM2EVMOnRamp.sol'; +import {EVM2EVMOffRamp} from 'ccip/offRamp/EVM2EVMOffRamp.sol'; +import {Router} from 'ccip/Router.sol'; +import {ProtocolV3TestBase} from 'aave-helpers/src/ProtocolV3TestBase.sol'; +import {GovV3Helpers} from 'aave-helpers/src/GovV3Helpers.sol'; +import {AaveV3Avalanche} from 'aave-address-book/AaveV3Avalanche.sol'; +import {GovernanceV3Avalanche} from 'aave-address-book/GovernanceV3Avalanche.sol'; +import {MiscAvalanche} from 'aave-address-book/MiscAvalanche.sol'; +import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; +import {MiscArbitrum} from 'aave-address-book/MiscArbitrum.sol'; +import {UpgradeableGhoToken} from 'gho-core/gho/UpgradeableGhoToken.sol'; +import {AaveV3Avalanche_GHOAvaxLaunch_20241106} from './AaveV3Avalanche_GHOAvaxLaunch_20241106.sol'; + +/** + * @dev Test for AaveV3Avalanche_GHOAvaxLaunch_20241106 + * command: FOUNDRY_PROFILE=avalanche forge test --match-path=src/20241106_Multi_GHOAvaxLaunch/AaveV3Avalanche_GHOAvaxLaunch_20241106.t.sol -vv + */ +contract AaveV3Avalanche_GHOAvaxLaunch_20241106_Test is ProtocolV3TestBase { + AaveV3Avalanche_GHOAvaxLaunch_20241106 internal proposal; + + address internal constant CCIP_AVAX_ETH_ON_RAMP = 0xe8784c29c583C52FA89144b9e5DD91Df2a1C2587; + address internal constant CCIP_AVAX_ETH_OFF_RAMP = 0xE5F21F43937199D4D57876A83077b3923F68EB76; + address internal constant CCIP_AVAX_ARB_ON_RAMP = 0x4e910c8Bbe88DaDF90baa6c1B7850DbeA32c5B29; + address internal constant CCIP_AVAX_ARB_OFF_RAMP = 0x508Ea280D46E4796Ce0f1Acf8BEDa610c4238dB3; + + address public constant TOKEN_ADMIN_REGISTRY = 0xc8df5D618c6a59Cc6A311E96a39450381001464F; + address public constant REGISTRY_ADMIN = 0xA3f32a07CCd8569f49cf350D4e61C016CA484644; + address public constant CCIP_RMN_PROXY = 0xcBD48A8eB077381c3c4Eb36b402d7283aB2b11Bc; + address public constant CCIP_ROUTER = 0xF4c7E640EdA248ef95972845a62bdC74237805dB; + address public constant ETH_TOKEN_POOL = MiscEthereum.GHO_CCIP_TOKEN_POOL; + address public constant ARB_TOKEN_POOL = MiscArbitrum.GHO_CCIP_TOKEN_POOL; + address public constant GHO_TOKEN = 0xc0F850AfdeFF8E0292C638C3e237fB2168E703d0; + UpgradeableGhoToken public GHO = UpgradeableGhoToken(GHO_TOKEN); + UpgradeableBurnMintTokenPool public TOKEN_POOL; + + 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); + + function setUp() public { + vm.createSelectFork(vm.rpcUrl('avalanche'), 53559217); + + // Assume token pool deployed on Avalanche + address tokenPool = _deployCcipTokenPool(GHO_TOKEN); + TOKEN_POOL = UpgradeableBurnMintTokenPool(tokenPool); + + // TODO: Remove this (will be done on chainlink's side) + // Prank chainlink and set up admin role to be accepted on token registry + vm.startPrank(REGISTRY_ADMIN); + TokenAdminRegistry(TOKEN_ADMIN_REGISTRY).proposeAdministrator( + GHO_TOKEN, + GovernanceV3Avalanche.EXECUTOR_LVL_1 + ); + vm.stopPrank(); + + proposal = new AaveV3Avalanche_GHOAvaxLaunch_20241106(); + } + + /** + * @dev executes the generic test suite including e2e and config snapshots + */ + function test_defaultProposalExecution() public { + defaultTest('AaveV3Avalanche_GHOAvaxLaunch_20241106', AaveV3Avalanche.POOL, address(proposal)); + + _validateGhoDeployment(); + _validateCcipTokenPool(); + } + + /// @dev Test burn and mint actions, mocking CCIP calls + function test_ccipTokenPool() public { + GovV3Helpers.executePayload(vm, address(proposal)); + + // 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'); + + // AVAX <> ETH + + // Mint + uint256 amount = 500_000e18; // 500K GHO + uint64 ethChainSelector = proposal.CCIP_ETH_CHAIN_SELECTOR(); + assertEq(_getFacilitatorLevel(address(TOKEN_POOL)), 0); + assertEq(GHO.balanceOf(address(TOKEN_POOL)), 0); + + Pool.ReleaseOrMintInV1 memory releaseOrMintIn = Pool.ReleaseOrMintInV1({ + originalSender: bytes(''), + remoteChainSelector: ethChainSelector, + receiver: user, + amount: amount, + localToken: address(GHO), + sourcePoolAddress: abi.encode(ETH_TOKEN_POOL), + sourcePoolData: bytes(''), + offchainTokenData: bytes('') + }); + + 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(releaseOrMintIn); + + 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); + + Pool.LockOrBurnInV1 memory lockOrBurnIn = Pool.LockOrBurnInV1({ + receiver: bytes(''), + remoteChainSelector: ethChainSelector, + originalSender: user, + amount: amount, + localToken: address(GHO) + }); + + 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(lockOrBurnIn); + + assertEq(_getFacilitatorLevel(address(TOKEN_POOL)), 0); + assertEq(GHO.balanceOf(address(TOKEN_POOL)), 0); + + // AVAX <> ARB + + // Mint + uint64 arbChainSelector = proposal.CCIP_ARB_CHAIN_SELECTOR(); + assertEq(_getFacilitatorLevel(address(TOKEN_POOL)), 0); + assertEq(GHO.balanceOf(address(TOKEN_POOL)), 0); + + releaseOrMintIn = Pool.ReleaseOrMintInV1({ + originalSender: bytes(''), + remoteChainSelector: arbChainSelector, + receiver: user, + amount: amount, + localToken: address(GHO), + sourcePoolAddress: abi.encode(ARB_TOKEN_POOL), + sourcePoolData: bytes(''), + offchainTokenData: bytes('') + }); + + 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(releaseOrMintIn); + + 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); + + lockOrBurnIn = Pool.LockOrBurnInV1({ + receiver: bytes(''), + remoteChainSelector: arbChainSelector, + originalSender: user, + amount: amount, + localToken: address(GHO) + }); + + 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(lockOrBurnIn); + + assertEq(_getFacilitatorLevel(address(TOKEN_POOL)), 0); + assertEq(GHO.balanceOf(address(TOKEN_POOL)), 0); + } + + /// @dev CCIP e2e eth <> avax + function test_ccipE2E_ETH_AVAX() public { + GovV3Helpers.executePayload(vm, address(proposal)); + + uint64 ethChainSelector = proposal.CCIP_ETH_CHAIN_SELECTOR(); + + // Chainlink config + Router router = Router(TOKEN_POOL.getRouter()); + + { + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](1); + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](1); + // ETH -> AVAX + onRampUpdates[0] = Router.OnRamp({ + destChainSelector: ethChainSelector, + onRamp: CCIP_AVAX_ETH_ON_RAMP + }); + // AVAX -> ETH + offRampUpdates[0] = Router.OffRamp({ + sourceChainSelector: ethChainSelector, + offRamp: CCIP_AVAX_ETH_OFF_RAMP + }); + address routerOwner = router.owner(); + vm.startPrank(routerOwner); + router.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), offRampUpdates); + } + + { + // OnRamp Price Registry + EVM2EVMOnRamp.DynamicConfig memory onRampDynamicConfig = EVM2EVMOnRamp(CCIP_AVAX_ETH_ON_RAMP) + .getDynamicConfig(); + Internal.PriceUpdates memory priceUpdate = _getSingleTokenPriceUpdateStruct( + address(GHO), + 1e18 + ); + + IPriceRegistry(onRampDynamicConfig.priceRegistry).updatePrices(priceUpdate); + // OffRamp Price Registry + EVM2EVMOffRamp.DynamicConfig memory offRampDynamicConfig = EVM2EVMOffRamp( + CCIP_AVAX_ETH_OFF_RAMP + ).getDynamicConfig(); + IPriceRegistry(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, proposal.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, proposal.CCIP_BUCKET_CAPACITY()); + assertEq(level, 0); + } + + /// @dev CCIP e2e arb <> avax + function test_ccipE2E_ARB_AVAX() public { + GovV3Helpers.executePayload(vm, address(proposal)); + + uint64 arbChainSelector = proposal.CCIP_ARB_CHAIN_SELECTOR(); + + // Chainlink config + Router router = Router(TOKEN_POOL.getRouter()); + + { + Router.OnRamp[] memory onRampUpdates = new Router.OnRamp[](1); + Router.OffRamp[] memory offRampUpdates = new Router.OffRamp[](1); + // ARB -> AVAX + onRampUpdates[0] = Router.OnRamp({ + destChainSelector: arbChainSelector, + onRamp: CCIP_AVAX_ARB_ON_RAMP + }); + // AVAX -> ARB + offRampUpdates[0] = Router.OffRamp({ + sourceChainSelector: arbChainSelector, + offRamp: CCIP_AVAX_ARB_OFF_RAMP + }); + address routerOwner = router.owner(); + vm.startPrank(routerOwner); + router.applyRampUpdates(onRampUpdates, new Router.OffRamp[](0), offRampUpdates); + } + + { + // OnRamp Price Registry + EVM2EVMOnRamp.DynamicConfig memory onRampDynamicConfig = EVM2EVMOnRamp(CCIP_AVAX_ARB_ON_RAMP) + .getDynamicConfig(); + Internal.PriceUpdates memory priceUpdate = _getSingleTokenPriceUpdateStruct( + address(GHO), + 1e18 + ); + + IPriceRegistry(onRampDynamicConfig.priceRegistry).updatePrices(priceUpdate); + // OffRamp Price Registry + EVM2EVMOffRamp.DynamicConfig memory offRampDynamicConfig = EVM2EVMOffRamp( + CCIP_AVAX_ARB_OFF_RAMP + ).getDynamicConfig(); + IPriceRegistry(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, proposal.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), arbChainSelector, user); + + assertEq(GHO.balanceOf(user), 0); + assertEq(GHO.balanceOf(address(TOKEN_POOL)), 0); + (capacity, level) = GHO.getFacilitatorBucket(address(TOKEN_POOL)); + assertEq(capacity, proposal.CCIP_BUCKET_CAPACITY()); + assertEq(level, 0); + } + + // --- + // Deployment + // --- + + function _deployGhoToken() internal returns (address) { + address imple = address(new UpgradeableGhoToken()); + + bytes memory ghoTokenInitParams = abi.encodeWithSignature( + 'initialize(address)', + GovernanceV3Avalanche.EXECUTOR_LVL_1 // owner + ); + return + address( + new TransparentUpgradeableProxy(imple, MiscAvalanche.PROXY_ADMIN, ghoTokenInitParams) + ); + } + + function _deployCcipTokenPool(address ghoToken) internal returns (address) { + address imple = address(new UpgradeableBurnMintTokenPool(ghoToken, 18, CCIP_RMN_PROXY, false)); + + bytes memory tokenPoolInitParams = abi.encodeWithSignature( + 'initialize(address,address[],address)', + GovernanceV3Avalanche.EXECUTOR_LVL_1, // owner + new address[](0), // allowList + CCIP_ROUTER // router + ); + return + address( + new TransparentUpgradeableProxy( + imple, // logic + MiscAvalanche.PROXY_ADMIN, // proxy admin + tokenPoolInitParams // data + ) + ); + } + + // --- + // Test Helpers + // --- + + function _validateGhoDeployment() internal view { + assertEq(GHO.totalSupply(), 0); + assertEq(GHO.getFacilitatorsList().length, 1); + assertEq(_getProxyAdminAddress(address(GHO)), MiscAvalanche.PROXY_ADMIN); + assertTrue(GHO.hasRole(bytes32(0), GovernanceV3Avalanche.EXECUTOR_LVL_1)); + assertTrue(GHO.hasRole(GHO.FACILITATOR_MANAGER_ROLE(), GovernanceV3Avalanche.EXECUTOR_LVL_1)); + assertTrue(GHO.hasRole(GHO.BUCKET_MANAGER_ROLE(), GovernanceV3Avalanche.EXECUTOR_LVL_1)); + } + + function _validateCcipTokenPool() internal view { + // Deployment + assertEq(_getProxyAdminAddress(address(TOKEN_POOL)), MiscAvalanche.PROXY_ADMIN); + assertEq(TOKEN_POOL.owner(), GovernanceV3Avalanche.EXECUTOR_LVL_1); + assertEq(address(TOKEN_POOL.getToken()), address(GHO)); + assertEq(TOKEN_POOL.getRmnProxy(), CCIP_RMN_PROXY); + assertEq(TOKEN_POOL.getRouter(), CCIP_ROUTER); + + // Facilitator + (uint256 capacity, uint256 level) = GHO.getFacilitatorBucket(address(TOKEN_POOL)); + assertEq(capacity, proposal.CCIP_BUCKET_CAPACITY()); + assertEq(level, 0); + + // Configs + uint64[] memory supportedChains = TOKEN_POOL.getSupportedChains(); + assertEq(supportedChains.length, 2); + + // ETH + assertEq(supportedChains[0], proposal.CCIP_ETH_CHAIN_SELECTOR()); + RateLimiter.TokenBucket memory outboundRateLimit = TOKEN_POOL + .getCurrentOutboundRateLimiterState(proposal.CCIP_ETH_CHAIN_SELECTOR()); + RateLimiter.TokenBucket memory inboundRateLimit = TOKEN_POOL.getCurrentInboundRateLimiterState( + proposal.CCIP_ETH_CHAIN_SELECTOR() + ); + assertEq(outboundRateLimit.isEnabled, false); + assertEq(inboundRateLimit.isEnabled, false); + + // ARB + assertEq(supportedChains[1], proposal.CCIP_ARB_CHAIN_SELECTOR()); + outboundRateLimit = TOKEN_POOL.getCurrentOutboundRateLimiterState( + proposal.CCIP_ARB_CHAIN_SELECTOR() + ); + 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 _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 _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/20241106_Multi_GHOAvaxLaunch/AaveV3Ethereum_GHOAvaxLaunch_20241106.sol b/src/20241106_Multi_GHOAvaxLaunch/AaveV3Ethereum_GHOAvaxLaunch_20241106.sol new file mode 100644 index 000000000..b088a01ac --- /dev/null +++ b/src/20241106_Multi_GHOAvaxLaunch/AaveV3Ethereum_GHOAvaxLaunch_20241106.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IProposalGenericExecutor} from 'aave-helpers/src/interfaces/IProposalGenericExecutor.sol'; +import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; +import {RateLimiter} from 'ccip/libraries/RateLimiter.sol'; +import {IUpgradeableTokenPool_1_4} from 'src/interfaces/ccip/IUpgradeableTokenPool_1_4.sol'; + +/** + * @title GHO Avax Launch + * @author Aave Labs + * - Snapshot: https://snapshot.org/#/aave.eth/proposal/0x2aed7eb8b03cb3f961cbf790bf2e2e1e449f841a4ad8bdbcdd223bb6ac69e719 + * - Discussion: https://governance.aave.com/t/arfc-launch-gho-on-avalanche-set-aci-as-emissions-manager-for-rewards/19339 + * @dev This payload configures the CCIP TokenPool for Avalanche + */ +contract AaveV3Ethereum_GHOAvaxLaunch_20241106 is IProposalGenericExecutor { + address public constant CCIP_TOKEN_POOL = MiscEthereum.GHO_CCIP_TOKEN_POOL; + uint64 public constant CCIP_AVAX_CHAIN_SELECTOR = 6433500567565415381; + + function execute() external { + _configureCcipTokenPool(CCIP_TOKEN_POOL, CCIP_AVAX_CHAIN_SELECTOR); + } + + function _configureCcipTokenPool(address tokenPool, uint64 chainSelector) internal { + IUpgradeableTokenPool_1_4.ChainUpdate[] + memory chainUpdates = new IUpgradeableTokenPool_1_4.ChainUpdate[](1); + RateLimiter.Config memory rateConfig = RateLimiter.Config({ + isEnabled: false, + capacity: 0, + rate: 0 + }); + chainUpdates[0] = IUpgradeableTokenPool_1_4.ChainUpdate({ + remoteChainSelector: chainSelector, + allowed: true, + outboundRateLimiterConfig: rateConfig, + inboundRateLimiterConfig: rateConfig + }); + IUpgradeableTokenPool_1_4(tokenPool).applyChainUpdates(chainUpdates); + } +} diff --git a/src/20241106_Multi_GHOAvaxLaunch/AaveV3Ethereum_GHOAvaxLaunch_20241106.t.sol b/src/20241106_Multi_GHOAvaxLaunch/AaveV3Ethereum_GHOAvaxLaunch_20241106.t.sol new file mode 100644 index 000000000..a5b8c1e25 --- /dev/null +++ b/src/20241106_Multi_GHOAvaxLaunch/AaveV3Ethereum_GHOAvaxLaunch_20241106.t.sol @@ -0,0 +1,395 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {TransparentUpgradeableProxy} from 'solidity-utils/contracts/transparent-proxy/TransparentUpgradeableProxy.sol'; +import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; +import {UpgradeableBurnMintTokenPool} from 'ccip/pools/GHO/UpgradeableBurnMintTokenPool.sol'; +import {UpgradeableLockReleaseTokenPool} from 'ccip/pools/GHO/UpgradeableLockReleaseTokenPool.sol'; +import {IPoolPriorTo1_5} from 'ccip/interfaces/IPoolPriorTo1_5.sol'; +import {Internal} from 'ccip/libraries/Internal.sol'; +import {RateLimiter} from 'ccip/libraries/RateLimiter.sol'; +import {Client} from 'ccip/libraries/Client.sol'; +import {TokenAdminRegistry} from 'ccip/tokenAdminRegistry/TokenAdminRegistry.sol'; +import {Router} from 'ccip/Router.sol'; +import {ProtocolV3TestBase} from 'aave-helpers/src/ProtocolV3TestBase.sol'; +import {GovV3Helpers} from 'aave-helpers/src/GovV3Helpers.sol'; +import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; +import {GovernanceV3Arbitrum} from 'aave-address-book/GovernanceV3Arbitrum.sol'; +import {AaveV3Ethereum, AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; +import {GovernanceV3Avalanche} from 'aave-address-book/GovernanceV3Avalanche.sol'; +import {MiscAvalanche} from 'aave-address-book/MiscAvalanche.sol'; +import {UpgradeableGhoToken} from 'gho-core/gho/UpgradeableGhoToken.sol'; +import {IUpgradeableTokenPool_1_5} from 'src/interfaces/ccip/IUpgradeableTokenPool_1_5.sol'; +import {AaveV3Avalanche_GHOAvaxLaunch_20241106} from './AaveV3Avalanche_GHOAvaxLaunch_20241106.sol'; +import {AaveV3Ethereum_GHOAvaxLaunch_20241106} from './AaveV3Ethereum_GHOAvaxLaunch_20241106.sol'; + +/** + * @dev Test for AaveV3Ethereum_GHOAvaxLaunch_20241106 + * command: FOUNDRY_PROFILE=mainnet forge test --match-path=src/20241106_Multi_GHOAvaxLaunch/AaveV3Ethereum_GHOAvaxLaunch_20241106.t.sol -vv + */ +contract AaveV3Ethereum_GHOAvaxLaunch_20241106_Test is ProtocolV3TestBase { + AaveV3Ethereum_GHOAvaxLaunch_20241106 internal proposal; + + UpgradeableLockReleaseTokenPool public constant TOKEN_POOL = + UpgradeableLockReleaseTokenPool(MiscEthereum.GHO_CCIP_TOKEN_POOL); + address public constant GHO_TOKEN = AaveV3EthereumAssets.GHO_UNDERLYING; + UpgradeableGhoToken public GHO = UpgradeableGhoToken(GHO_TOKEN); + + address public constant AVAX_GHO_TOKEN = 0xc0F850AfdeFF8E0292C638C3e237fB2168E703d0; + address public constant AVAX_TOKEN_POOL = 0x2e234DAe75C793f67A35089C9d99245E1C58470b; + address public constant AVAX_REGISTRY_ADMIN = 0xA3f32a07CCd8569f49cf350D4e61C016CA484644; + address public constant AVAX_TOKEN_ADMIN_REGISTRY = 0xc8df5D618c6a59Cc6A311E96a39450381001464F; + address public constant AVAX_RMN_PROXY = 0xcBD48A8eB077381c3c4Eb36b402d7283aB2b11Bc; + address public constant AVAX_ROUTER = 0xF4c7E640EdA248ef95972845a62bdC74237805dB; + address public constant CCIP_ETH_ARB_ON_RAMP = 0x69eCC4E2D8ea56E2d0a05bF57f4Fd6aEE7f2c284; + address public constant CCIP_ETH_ARB_OFF_RAMP = 0xdf615eF8D4C64d0ED8Fd7824BBEd2f6a10245aC9; + address public constant CCIP_ETH_AVAX_ON_RAMP = 0xaFd31C0C78785aDF53E4c185670bfd5376249d8A; + address public constant CCIP_ETH_AVAX_OFF_RAMP = 0xd98E80C79a15E4dbaF4C40B6cCDF690fe619BFBb; + address public constant TOKEN_POOL_AND_PROXY = 0x9Ec9F9804733df96D1641666818eFb5198eC50f0; + address public constant REGISTRY_ADMIN = 0x44835bBBA9D40DEDa9b64858095EcFB2693c9449; + uint64 public constant CCIP_AVAX_CHAIN_SELECTOR = 6433500567565415381; + uint64 public constant CCIP_ARB_CHAIN_SELECTOR = 4949039107694359620; + + 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); + + function setUp() public { + // Execute Avax proposal to deploy Avax token pool + vm.createSelectFork(vm.rpcUrl('avalanche'), 53559217); + + // Assume token pool deployed on Avalanche + _deployCcipTokenPool(); + + // TODO: Remove this (will be done on chainlink's side) + // Prank chainlink and set up admin role to be accepted on token registry + vm.startPrank(AVAX_REGISTRY_ADMIN); + TokenAdminRegistry(AVAX_TOKEN_ADMIN_REGISTRY).proposeAdministrator( + AVAX_GHO_TOKEN, + GovernanceV3Avalanche.EXECUTOR_LVL_1 + ); + vm.stopPrank(); + + AaveV3Avalanche_GHOAvaxLaunch_20241106 avaxProposal = new AaveV3Avalanche_GHOAvaxLaunch_20241106(); + GovV3Helpers.executePayload(vm, address(avaxProposal)); + + // Switch to Ethereum and create proposal + vm.createSelectFork(vm.rpcUrl('mainnet'), 21436313); + + // Configure TokenPoolAndProxy for Avalanche + // Prank Registry owner + vm.startPrank(REGISTRY_ADMIN); + _configureCcipTokenPool(TOKEN_POOL_AND_PROXY, CCIP_AVAX_CHAIN_SELECTOR); + vm.stopPrank(); + + proposal = new AaveV3Ethereum_GHOAvaxLaunch_20241106(); + } + + /** + * @dev executes the generic test suite including e2e and config snapshots + */ + function test_defaultProposalExecution() public { + defaultTest('AaveV3Ethereum_GHOAvaxLaunch_20241106', AaveV3Ethereum.POOL, address(proposal)); + + _validateCcipTokenPool(); + } + + /// @dev Test burn and mint actions, mocking CCIP calls + function test_ccipTokenPool() public { + GovV3Helpers.executePayload(vm, address(proposal)); + + // 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'); + + // ETH <> ARB + + // Lock + uint256 amount = 100e18; // 100 GHO + deal(address(GHO), user, amount); + + uint256 startingGhoBalance = GHO.balanceOf(address(TOKEN_POOL)); + + // 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); + IPoolPriorTo1_5(address(TOKEN_POOL)).lockOrBurn( + user, + bytes(''), + amount, + CCIP_ARB_CHAIN_SELECTOR, + bytes('') + ); + assertEq(GHO.balanceOf(address(TOKEN_POOL)), startingGhoBalance + 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); + + IPoolPriorTo1_5(address(TOKEN_POOL)).releaseOrMint( + bytes(''), + user, + amount, + CCIP_ARB_CHAIN_SELECTOR, + bytes('') + ); + assertEq(GHO.balanceOf(address(TOKEN_POOL)), startingGhoBalance); + assertEq(GHO.balanceOf(user), amount); + + // ETH <> AVAX + + // Lock + deal(address(GHO), user, amount); + + startingGhoBalance = GHO.balanceOf(address(TOKEN_POOL)); + + // 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); + IPoolPriorTo1_5(address(TOKEN_POOL)).lockOrBurn( + user, + bytes(''), + amount, + CCIP_AVAX_CHAIN_SELECTOR, + bytes('') + ); + assertEq(GHO.balanceOf(address(TOKEN_POOL)), startingGhoBalance + 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); + + IPoolPriorTo1_5(address(TOKEN_POOL)).releaseOrMint( + bytes(''), + user, + amount, + CCIP_AVAX_CHAIN_SELECTOR, + bytes('') + ); + assertEq(GHO.balanceOf(address(TOKEN_POOL)), startingGhoBalance); + assertEq(GHO.balanceOf(user), amount); + } + + /// @dev CCIP e2e arb <> eth + function test_ccipE2E_ARB_ETH() public { + GovV3Helpers.executePayload(vm, address(proposal)); + + Router router = Router(TOKEN_POOL.getRouter()); + + // User executes ccipSend + address user = makeAddr('user'); + uint256 amount = 100e18; // 100 GHO + deal(user, 1e18); // 1 ETH + deal(address(GHO), user, amount); + + uint256 startingGhoBalance = GHO.balanceOf(address(TOKEN_POOL)); + uint256 startingBridgeLimit = TOKEN_POOL.getBridgeLimit(); + uint256 startingBridgedAmount = TOKEN_POOL.getCurrentBridgedAmount(); + + vm.startPrank(user); + // Use address(0) to use native token as fee token + _sendCcip(router, address(GHO), amount, address(0), CCIP_ARB_CHAIN_SELECTOR, user); + + assertEq(GHO.balanceOf(user), 0); + assertEq(GHO.balanceOf(address(TOKEN_POOL)), startingGhoBalance + amount); + assertEq(TOKEN_POOL.getBridgeLimit(), startingBridgeLimit); + assertEq(TOKEN_POOL.getCurrentBridgedAmount(), startingBridgedAmount + amount); + } + + /// @dev CCIP e2e avax <> eth + function test_ccipE2E_AVAX_ETH() public { + GovV3Helpers.executePayload(vm, address(proposal)); + + // Chainlink config + Router router = Router(TOKEN_POOL.getRouter()); + + // User executes ccipSend + address user = makeAddr('user'); + uint256 amount = 100e18; // 100 GHO + deal(user, 1e18); // 1 ETH + deal(address(GHO), user, amount); + + uint256 startingGhoBalance = GHO.balanceOf(address(TOKEN_POOL)); + uint256 startingBridgeLimit = TOKEN_POOL.getBridgeLimit(); + uint256 startingBridgedAmount = TOKEN_POOL.getCurrentBridgedAmount(); + + vm.startPrank(user); + // Use address(0) to use native token as fee token + _sendCcip(router, address(GHO), amount, address(0), CCIP_AVAX_CHAIN_SELECTOR, user); + + assertEq(GHO.balanceOf(user), 0); + assertEq(GHO.balanceOf(address(TOKEN_POOL)), startingGhoBalance + amount); + assertEq(TOKEN_POOL.getBridgeLimit(), startingBridgeLimit); + assertEq(TOKEN_POOL.getCurrentBridgedAmount(), startingBridgedAmount + amount); + } + + // --- + // Deployment + // --- + + function _deployGhoToken() internal returns (address) { + address imple = address(new UpgradeableGhoToken()); + + bytes memory ghoTokenInitParams = abi.encodeWithSignature( + 'initialize(address)', + GovernanceV3Avalanche.EXECUTOR_LVL_1 // owner + ); + return + address( + new TransparentUpgradeableProxy(imple, MiscAvalanche.PROXY_ADMIN, ghoTokenInitParams) + ); + } + + function _deployCcipTokenPool() internal returns (address) { + address imple = address( + new UpgradeableBurnMintTokenPool(AVAX_GHO_TOKEN, 18, AVAX_RMN_PROXY, false) + ); + + bytes memory tokenPoolInitParams = abi.encodeWithSignature( + 'initialize(address,address[],address)', + GovernanceV3Avalanche.EXECUTOR_LVL_1, // owner + new address[](0), // allowList + AVAX_ROUTER // router + ); + return + address( + new TransparentUpgradeableProxy( + imple, // logic + MiscAvalanche.PROXY_ADMIN, // proxy admin + tokenPoolInitParams // data + ) + ); + } + + // --- + // Test Helpers + // --- + + function _validateCcipTokenPool() internal view { + // Configs + uint64[] memory supportedChains = TOKEN_POOL.getSupportedChains(); + assertEq(supportedChains.length, 2); + + // ARB + assertEq(supportedChains[0], CCIP_ARB_CHAIN_SELECTOR); + + // AVAX + assertEq(supportedChains[1], CCIP_AVAX_CHAIN_SELECTOR); + RateLimiter.TokenBucket memory outboundRateLimit = TOKEN_POOL + .getCurrentOutboundRateLimiterState(CCIP_AVAX_CHAIN_SELECTOR); + RateLimiter.TokenBucket memory inboundRateLimit = TOKEN_POOL.getCurrentInboundRateLimiterState( + CCIP_AVAX_CHAIN_SELECTOR + ); + assertEq(outboundRateLimit.isEnabled, false); + assertEq(inboundRateLimit.isEnabled, false); + } + + // --- + // Utils + // --- + + function _configureCcipTokenPool(address tokenPool, uint64 chainSelector) internal { + IUpgradeableTokenPool_1_5.ChainUpdate[] + memory chainUpdates = new IUpgradeableTokenPool_1_5.ChainUpdate[](1); + RateLimiter.Config memory rateConfig = RateLimiter.Config({ + isEnabled: false, + capacity: 0, + rate: 0 + }); + chainUpdates[0] = IUpgradeableTokenPool_1_5.ChainUpdate({ + remoteChainSelector: chainSelector, + allowed: true, + remotePoolAddress: abi.encode(AVAX_TOKEN_POOL), + remoteTokenAddress: abi.encode(AVAX_GHO_TOKEN), + outboundRateLimiterConfig: rateConfig, + inboundRateLimiterConfig: rateConfig + }); + IUpgradeableTokenPool_1_5(tokenPool).applyChainUpdates(chainUpdates); + } + + 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 _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/20241106_Multi_GHOAvaxLaunch/GHOAvaxLaunch.md b/src/20241106_Multi_GHOAvaxLaunch/GHOAvaxLaunch.md new file mode 100644 index 000000000..bde189fcc --- /dev/null +++ b/src/20241106_Multi_GHOAvaxLaunch/GHOAvaxLaunch.md @@ -0,0 +1,23 @@ +--- +title: "GHOAvaxLaunch" +author: "Aave Labs" +discussions: "https://governance.aave.com/t/arfc-launch-gho-on-avalanche-set-aci-as-emissions-manager-for-rewards/19339" +snapshot: "https://snapshot.org/#/aave.eth/proposal/0x2aed7eb8b03cb3f961cbf790bf2e2e1e449f841a4ad8bdbcdd223bb6ac69e719" +--- + +## Simple Summary + +## Motivation + +## Specification + +## References + +- Implementation: [AaveV3Ethereum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20241106_Multi_GHOAvaxLaunch/AaveV3Ethereum_GHOAvaxLaunch_20241106.sol), [AaveV3Avalanche](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20241106_Multi_GHOAvaxLaunch/AaveV3Avalanche_GHOAvaxLaunch_20241106.sol), [AaveV3Arbitrum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20241106_Multi_GHOAvaxLaunch/AaveV3Arbitrum_GHOAvaxLaunch_20241106.sol) +- Tests: [AaveV3Ethereum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20241106_Multi_GHOAvaxLaunch/AaveV3Ethereum_GHOAvaxLaunch_20241106.t.sol), [AaveV3Avalanche](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20241106_Multi_GHOAvaxLaunch/AaveV3Avalanche_GHOAvaxLaunch_20241106.t.sol), [AaveV3Arbitrum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20241106_Multi_GHOAvaxLaunch/AaveV3Arbitrum_GHOAvaxLaunch_20241106.t.sol) +- [Snapshot](https://snapshot.org/#/aave.eth/proposal/0x2aed7eb8b03cb3f961cbf790bf2e2e1e449f841a4ad8bdbcdd223bb6ac69e719) +- [Discussion](https://governance.aave.com/t/arfc-launch-gho-on-avalanche-set-aci-as-emissions-manager-for-rewards/19339) + +## Copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). diff --git a/src/20241106_Multi_GHOAvaxLaunch/GHOAvaxLaunch_20241106.s.sol b/src/20241106_Multi_GHOAvaxLaunch/GHOAvaxLaunch_20241106.s.sol new file mode 100644 index 000000000..f28d3b3db --- /dev/null +++ b/src/20241106_Multi_GHOAvaxLaunch/GHOAvaxLaunch_20241106.s.sol @@ -0,0 +1,117 @@ +// 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, AvalancheScript, ArbitrumScript} from 'solidity-utils/contracts/utils/ScriptUtils.sol'; +import {AaveV3Ethereum_GHOAvaxLaunch_20241106} from './AaveV3Ethereum_GHOAvaxLaunch_20241106.sol'; +import {AaveV3Avalanche_GHOAvaxLaunch_20241106} from './AaveV3Avalanche_GHOAvaxLaunch_20241106.sol'; +import {AaveV3Arbitrum_GHOAvaxLaunch_20241106} from './AaveV3Arbitrum_GHOAvaxLaunch_20241106.sol'; + +/** + * @dev Deploy Ethereum + * deploy-command: make deploy-ledger contract=src/20241106_Multi_GHOAvaxLaunch/GHOAvaxLaunch_20241106.s.sol:DeployEthereum chain=mainnet + * verify-command: FOUNDRY_PROFILE=mainnet npx catapulta-verify -b broadcast/GHOAvaxLaunch_20241106.s.sol/1/run-latest.json + */ +contract DeployEthereum is EthereumScript { + function run() external broadcast { + // deploy payloads + address payload0 = GovV3Helpers.deployDeterministic( + type(AaveV3Ethereum_GHOAvaxLaunch_20241106).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 Avalanche + * deploy-command: make deploy-ledger contract=src/20241106_Multi_GHOAvaxLaunch/GHOAvaxLaunch_20241106.s.sol:DeployAvalanche chain=avalanche + * verify-command: FOUNDRY_PROFILE=avalanche npx catapulta-verify -b broadcast/GHOAvaxLaunch_20241106.s.sol/43114/run-latest.json + */ +contract DeployAvalanche is AvalancheScript { + function run() external broadcast { + // deploy payloads + address payload0 = GovV3Helpers.deployDeterministic( + type(AaveV3Avalanche_GHOAvaxLaunch_20241106).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/20241106_Multi_GHOAvaxLaunch/GHOAvaxLaunch_20241106.s.sol:DeployArbitrum chain=arbitrum + * verify-command: FOUNDRY_PROFILE=arbitrum npx catapulta-verify -b broadcast/GHOAvaxLaunch_20241106.s.sol/42161/run-latest.json + */ +contract DeployArbitrum is ArbitrumScript { + function run() external broadcast { + // deploy payloads + address payload0 = GovV3Helpers.deployDeterministic( + type(AaveV3Arbitrum_GHOAvaxLaunch_20241106).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/20241106_Multi_GHOAvaxLaunch/GHOAvaxLaunch_20241106.s.sol:CreateProposal chain=mainnet + */ +contract CreateProposal is EthereumScript { + function run() external { + // create payloads + PayloadsControllerUtils.Payload[] memory payloads = new PayloadsControllerUtils.Payload[](3); + + // compose actions for validation + IPayloadsControllerCore.ExecutionAction[] + memory actionsEthereum = new IPayloadsControllerCore.ExecutionAction[](1); + actionsEthereum[0] = GovV3Helpers.buildAction( + type(AaveV3Ethereum_GHOAvaxLaunch_20241106).creationCode + ); + payloads[0] = GovV3Helpers.buildMainnetPayload(vm, actionsEthereum); + + IPayloadsControllerCore.ExecutionAction[] + memory actionsAvalanche = new IPayloadsControllerCore.ExecutionAction[](1); + actionsAvalanche[0] = GovV3Helpers.buildAction( + type(AaveV3Avalanche_GHOAvaxLaunch_20241106).creationCode + ); + payloads[1] = GovV3Helpers.buildAvalanchePayload(vm, actionsAvalanche); + + IPayloadsControllerCore.ExecutionAction[] + memory actionsArbitrum = new IPayloadsControllerCore.ExecutionAction[](1); + actionsArbitrum[0] = GovV3Helpers.buildAction( + type(AaveV3Arbitrum_GHOAvaxLaunch_20241106).creationCode + ); + payloads[2] = GovV3Helpers.buildArbitrumPayload(vm, actionsArbitrum); + + // create proposal + vm.startBroadcast(); + GovV3Helpers.createProposal( + vm, + payloads, + GovernanceV3Ethereum.VOTING_PORTAL_ETH_POL, + GovV3Helpers.ipfsHashFile(vm, 'src/20241106_Multi_GHOAvaxLaunch/GHOAvaxLaunch.md') + ); + } +} diff --git a/src/20241106_Multi_GHOAvaxLaunch/config.ts b/src/20241106_Multi_GHOAvaxLaunch/config.ts new file mode 100644 index 000000000..efff73fff --- /dev/null +++ b/src/20241106_Multi_GHOAvaxLaunch/config.ts @@ -0,0 +1,20 @@ +import {ConfigFile} from '../../generator/types'; +export const config: ConfigFile = { + rootOptions: { + title: 'GHO Avax Launch', + author: 'Aave Labs', + discussion: + 'https://governance.aave.com/t/arfc-launch-gho-on-avalanche-set-aci-as-emissions-manager-for-rewards/19339', + snapshot: + 'https://snapshot.org/#/aave.eth/proposal/0x2aed7eb8b03cb3f961cbf790bf2e2e1e449f841a4ad8bdbcdd223bb6ac69e719', + pools: ['AaveV3Ethereum', 'AaveV3Avalanche', 'AaveV3Arbitrum'], + shortName: 'GHOAvaxLaunch', + date: '20241106', + votingNetwork: 'POLYGON', + }, + poolOptions: { + AaveV3Ethereum: {configs: {OTHERS: {}}, cache: {blockNumber: 21133428}}, + AaveV3Avalanche: {configs: {OTHERS: {}}, cache: {blockNumber: 52758592}}, + AaveV3Arbitrum: {configs: {OTHERS: {}}, cache: {blockNumber: 271862002}}, + }, +}; diff --git a/src/interfaces/ccip/IUpgradeableTokenPool_1_4.sol b/src/interfaces/ccip/IUpgradeableTokenPool_1_4.sol new file mode 100644 index 000000000..3d83c1d9f --- /dev/null +++ b/src/interfaces/ccip/IUpgradeableTokenPool_1_4.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {RateLimiter} from 'ccip/libraries/RateLimiter.sol'; + +interface IUpgradeableTokenPool_1_4 { + struct ChainUpdate { + uint64 remoteChainSelector; + bool allowed; + RateLimiter.Config outboundRateLimiterConfig; + RateLimiter.Config inboundRateLimiterConfig; + } + + function applyChainUpdates(ChainUpdate[] calldata updates) external; +} diff --git a/src/interfaces/ccip/IUpgradeableTokenPool_1_5.sol b/src/interfaces/ccip/IUpgradeableTokenPool_1_5.sol new file mode 100644 index 000000000..7ef9cbd0c --- /dev/null +++ b/src/interfaces/ccip/IUpgradeableTokenPool_1_5.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {RateLimiter} from 'ccip/libraries/RateLimiter.sol'; + +interface IUpgradeableTokenPool_1_5 { + struct ChainUpdate { + uint64 remoteChainSelector; + bool allowed; + bytes remotePoolAddress; + bytes remoteTokenAddress; + RateLimiter.Config outboundRateLimiterConfig; + RateLimiter.Config inboundRateLimiterConfig; + } + + function applyChainUpdates(ChainUpdate[] calldata updates) external; +}