diff --git a/.assets/02a040db3fc4a88618783a9156e7fb51928e1ede.svg b/.assets/02a040db3fc4a88618783a9156e7fb51928e1ede.svg new file mode 100644 index 000000000..db81caacb --- /dev/null +++ b/.assets/02a040db3fc4a88618783a9156e7fb51928e1ede.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%20%40%60%80%Optimal utilization 90%Optimal utilization 90% \ No newline at end of file diff --git a/.assets/03dc42930b2b94af37ef869d012c4734aedb52e6.svg b/.assets/03dc42930b2b94af37ef869d012c4734aedb52e6.svg new file mode 100644 index 000000000..95de9c4ed --- /dev/null +++ b/.assets/03dc42930b2b94af37ef869d012c4734aedb52e6.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%100%200%300%Optimal utilization 30%Optimal utilization 30% \ No newline at end of file diff --git a/.assets/0afe490affc1bc77fe6675127b18e9aeeb4b1404.svg b/.assets/0afe490affc1bc77fe6675127b18e9aeeb4b1404.svg new file mode 100644 index 000000000..85a5bf372 --- /dev/null +++ b/.assets/0afe490affc1bc77fe6675127b18e9aeeb4b1404.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%20%40%60%80%Optimal utilization 90%Optimal utilization 90% \ No newline at end of file diff --git a/.assets/0fcfb1e84443509c42e21db9e4320c00e1db6098.svg b/.assets/0fcfb1e84443509c42e21db9e4320c00e1db6098.svg new file mode 100644 index 000000000..5283877c6 --- /dev/null +++ b/.assets/0fcfb1e84443509c42e21db9e4320c00e1db6098.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%50%100%Optimal utilization 80%Optimal utilization 80% \ No newline at end of file diff --git a/.assets/1899e5ae4f4fd5b0d2565edd912a160acb2d0b78.svg b/.assets/1899e5ae4f4fd5b0d2565edd912a160acb2d0b78.svg new file mode 100644 index 000000000..de0657eb7 --- /dev/null +++ b/.assets/1899e5ae4f4fd5b0d2565edd912a160acb2d0b78.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%20%40%60%80%Optimal utilization 80%Optimal utilization 80% \ No newline at end of file diff --git a/.assets/18e6ba583d4d35e38cb678120492b2b4d52d19ea.svg b/.assets/18e6ba583d4d35e38cb678120492b2b4d52d19ea.svg new file mode 100644 index 000000000..db81caacb --- /dev/null +++ b/.assets/18e6ba583d4d35e38cb678120492b2b4d52d19ea.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%20%40%60%80%Optimal utilization 90%Optimal utilization 90% \ No newline at end of file diff --git a/.assets/1aa0fb636d34a35d35ce67919bc0d7d2e36d3839.svg b/.assets/1aa0fb636d34a35d35ce67919bc0d7d2e36d3839.svg new file mode 100644 index 000000000..f05c3fb39 --- /dev/null +++ b/.assets/1aa0fb636d34a35d35ce67919bc0d7d2e36d3839.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%50%100%Optimal utilization 90%Optimal utilization 90% \ No newline at end of file diff --git a/.assets/1c1fbd79406b4b3a8c64ec79ef429f7c422d0387.svg b/.assets/1c1fbd79406b4b3a8c64ec79ef429f7c422d0387.svg new file mode 100644 index 000000000..d003381d1 --- /dev/null +++ b/.assets/1c1fbd79406b4b3a8c64ec79ef429f7c422d0387.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%Optimal utilization 90%Optimal utilization 90% \ No newline at end of file diff --git a/.assets/2e9d5ba5e949c7896bc79e7fdd98872cb43375b0.svg b/.assets/2e9d5ba5e949c7896bc79e7fdd98872cb43375b0.svg new file mode 100644 index 000000000..c752baefc --- /dev/null +++ b/.assets/2e9d5ba5e949c7896bc79e7fdd98872cb43375b0.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%2%4%Optimal utilization 99%Optimal utilization 99% \ No newline at end of file diff --git a/.assets/39a3707c0ded91bbfde8953567853db55452a227.svg b/.assets/39a3707c0ded91bbfde8953567853db55452a227.svg new file mode 100644 index 000000000..01584d9c0 --- /dev/null +++ b/.assets/39a3707c0ded91bbfde8953567853db55452a227.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%50%100%Optimal utilization 80%Optimal utilization 80% \ No newline at end of file diff --git a/.assets/39d5ff798c0771402486c6f1ca63f2a1868d80d9.svg b/.assets/39d5ff798c0771402486c6f1ca63f2a1868d80d9.svg new file mode 100644 index 000000000..df2260094 --- /dev/null +++ b/.assets/39d5ff798c0771402486c6f1ca63f2a1868d80d9.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%100%200%300%Optimal utilization 45%Optimal utilization 45% \ No newline at end of file diff --git a/.assets/4820a9e881eaa3f3def4916e47e0c330b34ab151.svg b/.assets/4820a9e881eaa3f3def4916e47e0c330b34ab151.svg new file mode 100644 index 000000000..403a5e0f3 --- /dev/null +++ b/.assets/4820a9e881eaa3f3def4916e47e0c330b34ab151.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%50%100%Optimal utilization 80%Optimal utilization 80% \ No newline at end of file diff --git a/.assets/5dbd5b9335471c83b44949206c9b2186657b836f.svg b/.assets/5dbd5b9335471c83b44949206c9b2186657b836f.svg new file mode 100644 index 000000000..d348fb2d6 --- /dev/null +++ b/.assets/5dbd5b9335471c83b44949206c9b2186657b836f.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%50%100%Optimal utilization 80%Optimal utilization 80% \ No newline at end of file diff --git a/.assets/5f455cedf818a43aba043e71714721fd1e844501.svg b/.assets/5f455cedf818a43aba043e71714721fd1e844501.svg new file mode 100644 index 000000000..5dff1554b --- /dev/null +++ b/.assets/5f455cedf818a43aba043e71714721fd1e844501.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%100%200%300%Optimal utilization 45%Optimal utilization 45% \ No newline at end of file diff --git a/.assets/7bc797fb2b1f421fa40a6d9a0028d1f78db71dc0.svg b/.assets/7bc797fb2b1f421fa40a6d9a0028d1f78db71dc0.svg new file mode 100644 index 000000000..e673fbc39 --- /dev/null +++ b/.assets/7bc797fb2b1f421fa40a6d9a0028d1f78db71dc0.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%20%40%60%80%Optimal utilization 92%Optimal utilization 92% \ No newline at end of file diff --git a/.assets/7ee454df22dbd82f0c1929bc14104d54c7081a49.svg b/.assets/7ee454df22dbd82f0c1929bc14104d54c7081a49.svg new file mode 100644 index 000000000..3c15ce1a6 --- /dev/null +++ b/.assets/7ee454df22dbd82f0c1929bc14104d54c7081a49.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%100%200%300%Optimal utilization 45%Optimal utilization 45% \ No newline at end of file diff --git a/.assets/8aa25c38caec024924773d1a5c7c63ab45c4eecf.svg b/.assets/8aa25c38caec024924773d1a5c7c63ab45c4eecf.svg new file mode 100644 index 000000000..5283877c6 --- /dev/null +++ b/.assets/8aa25c38caec024924773d1a5c7c63ab45c4eecf.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%50%100%Optimal utilization 80%Optimal utilization 80% \ No newline at end of file diff --git a/.assets/8fc705464b3515a654de9893168a1b1321feccb4.svg b/.assets/8fc705464b3515a654de9893168a1b1321feccb4.svg new file mode 100644 index 000000000..5212f2d4a --- /dev/null +++ b/.assets/8fc705464b3515a654de9893168a1b1321feccb4.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%50%100%Optimal utilization 45%Optimal utilization 45% \ No newline at end of file diff --git a/.assets/9c784d12784a084406c3794fbe177e93da4c3479.svg b/.assets/9c784d12784a084406c3794fbe177e93da4c3479.svg new file mode 100644 index 000000000..081891402 --- /dev/null +++ b/.assets/9c784d12784a084406c3794fbe177e93da4c3479.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%20%40%60%80%Optimal utilization 90%Optimal utilization 90% \ No newline at end of file diff --git a/.assets/a4faa118ed690be4e95fae01b8535036cda67e3d.svg b/.assets/a4faa118ed690be4e95fae01b8535036cda67e3d.svg new file mode 100644 index 000000000..93fde991f --- /dev/null +++ b/.assets/a4faa118ed690be4e95fae01b8535036cda67e3d.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%50%100%150%200%Optimal utilization 80%Optimal utilization 80% \ No newline at end of file diff --git a/.assets/a9a0275265838d424840d721917e0a4e3a5d8044.svg b/.assets/a9a0275265838d424840d721917e0a4e3a5d8044.svg new file mode 100644 index 000000000..6af590b85 --- /dev/null +++ b/.assets/a9a0275265838d424840d721917e0a4e3a5d8044.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%50%100%Optimal utilization 90%Optimal utilization 90% \ No newline at end of file diff --git a/.assets/ac33ca939f6fc30c2fd799aaa6f59b0521c19e9f.svg b/.assets/ac33ca939f6fc30c2fd799aaa6f59b0521c19e9f.svg new file mode 100644 index 000000000..3c15ce1a6 --- /dev/null +++ b/.assets/ac33ca939f6fc30c2fd799aaa6f59b0521c19e9f.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%100%200%300%Optimal utilization 45%Optimal utilization 45% \ No newline at end of file diff --git a/.assets/c229e34ea67f12eea11bf5403763207eeab38cff.svg b/.assets/c229e34ea67f12eea11bf5403763207eeab38cff.svg new file mode 100644 index 000000000..771e3e22e --- /dev/null +++ b/.assets/c229e34ea67f12eea11bf5403763207eeab38cff.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%20%40%60%80%Optimal utilization 90%Optimal utilization 90% \ No newline at end of file diff --git a/.assets/c277de9f9a155f6a0e47e0443845a3c9de5f6b84.svg b/.assets/c277de9f9a155f6a0e47e0443845a3c9de5f6b84.svg new file mode 100644 index 000000000..5212f2d4a --- /dev/null +++ b/.assets/c277de9f9a155f6a0e47e0443845a3c9de5f6b84.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%50%100%Optimal utilization 45%Optimal utilization 45% \ No newline at end of file diff --git a/.assets/ceb223a18f5aad36d9f6087d1d468b4dd5ba56a1.svg b/.assets/ceb223a18f5aad36d9f6087d1d468b4dd5ba56a1.svg new file mode 100644 index 000000000..95de9c4ed --- /dev/null +++ b/.assets/ceb223a18f5aad36d9f6087d1d468b4dd5ba56a1.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%100%200%300%Optimal utilization 30%Optimal utilization 30% \ No newline at end of file diff --git a/.assets/d8d51b9f960b17304a3bdcaf8922d3ae84b3c30f.svg b/.assets/d8d51b9f960b17304a3bdcaf8922d3ae84b3c30f.svg new file mode 100644 index 000000000..ad84fcb3b --- /dev/null +++ b/.assets/d8d51b9f960b17304a3bdcaf8922d3ae84b3c30f.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%50%100%Optimal utilization 80%Optimal utilization 80% \ No newline at end of file diff --git a/.assets/dcedb30554ccb68f317b952113777790df5ca547.svg b/.assets/dcedb30554ccb68f317b952113777790df5ca547.svg new file mode 100644 index 000000000..b5810667c --- /dev/null +++ b/.assets/dcedb30554ccb68f317b952113777790df5ca547.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%20%40%60%80%Optimal utilization 92%Optimal utilization 92% \ No newline at end of file diff --git a/.assets/e6c7eef36a7e70a1f1b35633097516fe5e21508b.svg b/.assets/e6c7eef36a7e70a1f1b35633097516fe5e21508b.svg new file mode 100644 index 000000000..6af590b85 --- /dev/null +++ b/.assets/e6c7eef36a7e70a1f1b35633097516fe5e21508b.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%50%100%Optimal utilization 90%Optimal utilization 90% \ No newline at end of file diff --git a/.assets/ec2b4dc8236ac87f8058f121d86de1d99e029c5c.svg b/.assets/ec2b4dc8236ac87f8058f121d86de1d99e029c5c.svg new file mode 100644 index 000000000..f9bccbdd6 --- /dev/null +++ b/.assets/ec2b4dc8236ac87f8058f121d86de1d99e029c5c.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%100%200%300%Optimal utilization 70%Optimal utilization 70% \ No newline at end of file diff --git a/.assets/fb6ebf3fa05cf980f374598b56757dbc5cae3662.svg b/.assets/fb6ebf3fa05cf980f374598b56757dbc5cae3662.svg new file mode 100644 index 000000000..df2260094 --- /dev/null +++ b/.assets/fb6ebf3fa05cf980f374598b56757dbc5cae3662.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%100%200%300%Optimal utilization 45%Optimal utilization 45% \ No newline at end of file diff --git a/.assets/fe79e1c7a3e943262824d4f683059c85d91a233d.svg b/.assets/fe79e1c7a3e943262824d4f683059c85d91a233d.svg new file mode 100644 index 000000000..5dff1554b --- /dev/null +++ b/.assets/fe79e1c7a3e943262824d4f683059c85d91a233d.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%100%200%300%Optimal utilization 45%Optimal utilization 45% \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0f186f228..977839ab4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,19 @@ jobs: test-sol: uses: bgd-labs/github-workflows/.github/workflows/foundry-test.yml@main - secrets: inherit + secrets: + RPC_MAINNET: "https://eth-mainnet.g.alchemy.com/v2/${{ secrets.ALCHEMY_KEY }}" + RPC_ARBITRUM: "https://arb-mainnet.g.alchemy.com/v2/${{ secrets.ALCHEMY_KEY }}" + RPC_POLYGON: "https://polygon-mainnet.g.alchemy.com/v2/${{ secrets.ALCHEMY_KEY }}" + RPC_AVALANCHE: "https://avax-mainnet.g.alchemy.com/v2/${{ secrets.ALCHEMY_KEY }}" + RPC_OPTIMISM: "https://opt-mainnet.g.alchemy.com/v2/${{ secrets.ALCHEMY_KEY }}" + RPC_METIS: "https://metis-mainnet.g.alchemy.com/v2/${{ secrets.ALCHEMY_KEY }}" + RPC_BASE: "https://base-mainnet.g.alchemy.com/v2/${{ secrets.ALCHEMY_KEY }}" + RPC_GNOSIS: "https://gnosis-mainnet.g.alchemy.com/v2/${{ secrets.ALCHEMY_KEY }}" + RPC_BNB: "https://bnb-mainnet.g.alchemy.com/v2/${{ secrets.ALCHEMY_KEY }}" + RPC_ZKEVM: "https://polygonzkevm-mainnet.g.alchemy.com/v2/${{ secrets.ALCHEMY_KEY }}" + RPC_SCROLL: "https://scroll-mainnet.g.alchemy.com/v2/${{ secrets.ALCHEMY_KEY }}" + RPC_ZKSYNC: "https://zksync-mainnet.g.alchemy.com/v2/${{ secrets.ALCHEMY_KEY }}" with: mode: "CHANGED" diff --git a/.gitmodules b/.gitmodules index f7316a1d6..d34bc9729 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "lib/aave-helpers"] path = lib/aave-helpers url = https://github.com/bgd-labs/aave-helpers +[submodule "lib/aave-ccip"] + path = lib/aave-ccip + url = https://github.com/aave/ccip diff --git a/diffs/AaveV3Arbitrum_GHOCCIP150Upgrade_20241021_before_AaveV3Arbitrum_GHOCCIP150Upgrade_20241021_after.md b/diffs/AaveV3Arbitrum_GHOCCIP150Upgrade_20241021_before_AaveV3Arbitrum_GHOCCIP150Upgrade_20241021_after.md new file mode 100644 index 000000000..dbd03dce4 --- /dev/null +++ b/diffs/AaveV3Arbitrum_GHOCCIP150Upgrade_20241021_before_AaveV3Arbitrum_GHOCCIP150Upgrade_20241021_after.md @@ -0,0 +1,7 @@ +## Emodes changes + +## Raw diff + +```json +{} +``` \ No newline at end of file diff --git a/diffs/AaveV3Ethereum_GHOCCIP150Upgrade_20241021_before_AaveV3Ethereum_GHOCCIP150Upgrade_20241021_after.md b/diffs/AaveV3Ethereum_GHOCCIP150Upgrade_20241021_before_AaveV3Ethereum_GHOCCIP150Upgrade_20241021_after.md new file mode 100644 index 000000000..dbd03dce4 --- /dev/null +++ b/diffs/AaveV3Ethereum_GHOCCIP150Upgrade_20241021_before_AaveV3Ethereum_GHOCCIP150Upgrade_20241021_after.md @@ -0,0 +1,7 @@ +## Emodes changes + +## Raw diff + +```json +{} +``` \ No newline at end of file diff --git a/lib/aave-ccip b/lib/aave-ccip new file mode 160000 index 000000000..47a535bb3 --- /dev/null +++ b/lib/aave-ccip @@ -0,0 +1 @@ +Subproject commit 47a535bb3b5829c6d015fcdf8ccdc1357520a2c1 diff --git a/remappings.txt b/remappings.txt index 9fefc7eea..5900f0f71 100644 --- a/remappings.txt +++ b/remappings.txt @@ -3,3 +3,4 @@ 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 +aave-ccip/=lib/aave-ccip/contracts/src/ diff --git a/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Arbitrum_GHOCCIP150Upgrade_20241021.sol b/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Arbitrum_GHOCCIP150Upgrade_20241021.sol new file mode 100644 index 000000000..493504d02 --- /dev/null +++ b/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Arbitrum_GHOCCIP150Upgrade_20241021.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {TransparentUpgradeableProxy} from 'solidity-utils/contracts/transparent-proxy/TransparentUpgradeableProxy.sol'; +import {ProxyAdmin} from 'solidity-utils/contracts/transparent-proxy/ProxyAdmin.sol'; +import {IProposalGenericExecutor} from 'aave-helpers/src/interfaces/IProposalGenericExecutor.sol'; +import {MiscArbitrum} from 'aave-address-book/MiscArbitrum.sol'; +import {UpgradeableBurnMintTokenPool} from 'aave-ccip/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPool.sol'; +import {RateLimiter} from 'aave-ccip/v0.8/ccip/libraries/RateLimiter.sol'; + +/** + * @title GHO CCIP 1.50 Upgrade + * @author Aave Labs + * - Discussion: https://governance.aave.com/t/bgd-technical-maintenance-proposals/15274/51 + */ +contract AaveV3Arbitrum_GHOCCIP150Upgrade_20241021 is IProposalGenericExecutor { + address public constant GHO_CCIP_PROXY_POOL = 0x26329558f08cbb40d6a4CCA0E0C67b29D64A8c50; + uint64 public constant ETH_CHAIN_SELECTOR = 5009297550715157269; + + function execute() external { + UpgradeableBurnMintTokenPool tokenPoolProxy = UpgradeableBurnMintTokenPool( + MiscArbitrum.GHO_CCIP_TOKEN_POOL + ); + + // Deploy new tokenPool implementation, retain existing immutable configuration + address tokenPoolImpl = address( + new UpgradeableBurnMintTokenPool( + address(tokenPoolProxy.getToken()), + tokenPoolProxy.getArmProxy(), + tokenPoolProxy.getAllowListEnabled() + ) + ); + + ProxyAdmin(MiscArbitrum.PROXY_ADMIN).upgrade( + TransparentUpgradeableProxy(payable(address(tokenPoolProxy))), + tokenPoolImpl + ); + + // Update proxyPool address + tokenPoolProxy.setProxyPool(GHO_CCIP_PROXY_POOL); + + // Set rate limit + tokenPoolProxy.setChainRateLimiterConfig( + ETH_CHAIN_SELECTOR, + getOutBoundRateLimiterConfig(), + getInBoundRateLimiterConfig() + ); + } + + /// @notice Returns the rate limiter configuration for the outbound rate limiter + /// The onRamp rate limit for ARB => ETH will be as follows: + /// Capacity: 350_000 GHO + /// Rate: 100 GHO per second (=> 360_000 GHO per hour) + /// @return The rate limiter configuration + function getOutBoundRateLimiterConfig() public pure returns (RateLimiter.Config memory) { + return RateLimiter.Config({isEnabled: true, capacity: 350_000e18, rate: 100e18}); + } + + /// @notice Returns the rate limiter configuration for the inbound rate limiter + /// The offRamp rate limit for ETH => ARB will be as follows: + /// Capacity: 350_000 GHO + /// Rate: 100 GHO per second (=> 360_000 GHO per hour) + /// @return The rate limiter configuration + function getInBoundRateLimiterConfig() public pure returns (RateLimiter.Config memory) { + return RateLimiter.Config({isEnabled: true, capacity: 350_000e18, rate: 100e18}); + } +} diff --git a/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Arbitrum_GHOCCIP150Upgrade_20241021.t.sol b/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Arbitrum_GHOCCIP150Upgrade_20241021.t.sol new file mode 100644 index 000000000..795a34dea --- /dev/null +++ b/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Arbitrum_GHOCCIP150Upgrade_20241021.t.sol @@ -0,0 +1,507 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; +import {AaveV3Arbitrum, AaveV3ArbitrumAssets} from 'aave-address-book/AaveV3Arbitrum.sol'; +import {MiscArbitrum} from 'aave-address-book/MiscArbitrum.sol'; +import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; +import {ProtocolV3TestBase} from 'aave-helpers/src/ProtocolV3TestBase.sol'; +import {UpgradeableBurnMintTokenPool} from 'aave-ccip/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPool.sol'; +import {RateLimiter} from 'aave-ccip/v0.8/ccip/libraries/RateLimiter.sol'; +import {IClient} from 'src/interfaces/ccip/IClient.sol'; +import {IInternal} from 'src/interfaces/ccip/IInternal.sol'; +import {IRouter} from 'src/interfaces/ccip/IRouter.sol'; +import {ITypeAndVersion} from 'src/interfaces/ccip/ITypeAndVersion.sol'; +import {IProxyPool} from 'src/interfaces/ccip/IProxyPool.sol'; +import {IRateLimiter} from 'src/interfaces/ccip/IRateLimiter.sol'; +import {ITokenAdminRegistry} from 'src/interfaces/ccip/ITokenAdminRegistry.sol'; +import {IGhoToken} from 'src/interfaces/IGhoToken.sol'; +import {IGhoCcipSteward} from 'src/interfaces/IGhoCcipSteward.sol'; +import {CCIPUtils} from './utils/CCIPUtils.sol'; + +import {AaveV3Arbitrum_GHOCCIP150Upgrade_20241021} from './AaveV3Arbitrum_GHOCCIP150Upgrade_20241021.sol'; + +/** + * @dev Test for AaveV3Arbitrum_GHOCCIP150Upgrade_20241021 + * command: FOUNDRY_PROFILE=arbitrum forge test --match-path=src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Arbitrum_GHOCCIP150Upgrade_20241021.t.sol -vv + */ +contract AaveV3Arbitrum_GHOCCIP150Upgrade_20241021_Test is ProtocolV3TestBase { + struct CCIPSendParams { + IRouter router; + uint256 amount; + bool migrated; + } + + AaveV3Arbitrum_GHOCCIP150Upgrade_20241021 internal proposal; + UpgradeableBurnMintTokenPool internal ghoTokenPool; + IProxyPool internal proxyPool; + + address internal alice = makeAddr('alice'); + + uint64 internal constant ETH_CHAIN_SELECTOR = 5009297550715157269; + uint64 internal constant ARB_CHAIN_SELECTOR = 4949039107694359620; + address internal constant ARB_GHO_TOKEN = AaveV3ArbitrumAssets.GHO_UNDERLYING; + address internal constant ETH_PROXY_POOL = 0x9Ec9F9804733df96D1641666818eFb5198eC50f0; + ITokenAdminRegistry internal constant TOKEN_ADMIN_REGISTRY = + ITokenAdminRegistry(0x39AE1032cF4B334a1Ed41cdD0833bdD7c7E7751E); + + address internal constant ON_RAMP_1_2 = 0xCe11020D56e5FDbfE46D9FC3021641FfbBB5AdEE; + address internal constant ON_RAMP_1_5 = 0x67761742ac8A21Ec4D76CA18cbd701e5A6F3Bef3; + address internal constant OFF_RAMP_1_2 = 0x542ba1902044069330e8c5b36A84EC503863722f; + address internal constant OFF_RAMP_1_5 = 0x91e46cc5590A4B9182e47f40006140A7077Dec31; + + IGhoCcipSteward internal constant GHO_CCIP_STEWARD = + IGhoCcipSteward(0xb329CEFF2c362F315900d245eC88afd24C4949D5); + + event Burned(address indexed sender, uint256 amount); + event Minted(address indexed sender, address indexed recipient, uint256 amount); + event CCIPSendRequested(IInternal.EVM2EVMMessage message); + + error CallerIsNotARampOnRouter(address caller); + error NotACompatiblePool(address pool); + + function setUp() public { + vm.createSelectFork(vm.rpcUrl('arbitrum'), 267660907); + proposal = new AaveV3Arbitrum_GHOCCIP150Upgrade_20241021(); + ghoTokenPool = UpgradeableBurnMintTokenPool(MiscArbitrum.GHO_CCIP_TOKEN_POOL); + proxyPool = IProxyPool(proposal.GHO_CCIP_PROXY_POOL()); + + _validateConstants(); + } + + /** + * @dev executes the generic test suite including e2e and config snapshots + */ + function test_defaultProposalExecution() public { + assertEq( + abi.encode( + _tokenBucketToConfig(ghoTokenPool.getCurrentInboundRateLimiterState(ETH_CHAIN_SELECTOR)) + ), + abi.encode(_getDisabledConfig()) + ); + assertEq( + abi.encode( + _tokenBucketToConfig(ghoTokenPool.getCurrentOutboundRateLimiterState(ETH_CHAIN_SELECTOR)) + ), + abi.encode(_getDisabledConfig()) + ); + + bytes memory dynamicParamsBefore = _getDynamicParams(); + bytes memory staticParamsBefore = _getStaticParams(); + + defaultTest( + 'AaveV3Arbitrum_GHOCCIP150Upgrade_20241021', + AaveV3Arbitrum.POOL, + address(proposal) + ); + + assertEq(keccak256(_getDynamicParams()), keccak256(dynamicParamsBefore)); + assertEq(keccak256(_getStaticParams()), keccak256(staticParamsBefore)); + + assertEq( + abi.encode( + _tokenBucketToConfig(ghoTokenPool.getCurrentInboundRateLimiterState(ETH_CHAIN_SELECTOR)) + ), + abi.encode(proposal.getInBoundRateLimiterConfig()) + ); + assertEq( + abi.encode( + _tokenBucketToConfig(ghoTokenPool.getCurrentOutboundRateLimiterState(ETH_CHAIN_SELECTOR)) + ), + abi.encode(proposal.getOutBoundRateLimiterConfig()) + ); + } + + function test_getProxyPool() public { + // proxyPool getter does not exist before the upgrade + vm.expectRevert(); + ghoTokenPool.getProxyPool(); + + executePayload(vm, address(proposal)); + + assertEq(ghoTokenPool.getProxyPool(), address(proxyPool)); + } + + function test_getRateLimitAdmin() public { + // rateLimitAdmin getter does not exist before the upgrade on BurnMintTokenPool + vm.expectRevert(); + ghoTokenPool.getRateLimitAdmin(); + + executePayload(vm, address(proposal)); + + // we currently do not set the rate limit admin + assertEq(ghoTokenPool.getRateLimitAdmin(), address(0)); + } + + function test_tokenPoolCannotBeInitializedAgain() public { + vm.expectRevert('Initializable: contract is already initialized'); + ghoTokenPool.initialize(makeAddr('owner'), new address[](0), makeAddr('router')); + /// proxy implementation is initialized + assertEq(_readInitialized(address(ghoTokenPool)), 1); + assertEq(_readInitialized(_getImplementation(address(ghoTokenPool))), 255); + + executePayload(vm, address(proposal)); + + vm.expectRevert('Initializable: contract is already initialized'); + ghoTokenPool.initialize(makeAddr('owner'), new address[](0), makeAddr('router')); + assertEq(_readInitialized(address(ghoTokenPool)), 1); + /// proxy implementation is initialized + assertEq(_readInitialized(_getImplementation(address(ghoTokenPool))), 255); + } + + function test_sendMessagePreCCIPMigration() public { + executePayload(vm, address(proposal)); + + IRouter router = IRouter(ghoTokenPool.getRouter()); + uint256 amount = 150_000e18; + uint256 facilitatorLevelBefore = _getFacilitatorLevel(address(ghoTokenPool)); + + // wait for the rate limiter to refill + skip(_getOutboundRefillTime(amount)); + + vm.prank(alice); + IERC20(ARB_GHO_TOKEN).approve(address(router), amount); + deal(ARB_GHO_TOKEN, alice, amount); + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage(CCIPSendParams({router: router, amount: amount, migrated: false})); + + vm.expectEmit(address(ghoTokenPool)); + emit Burned(ON_RAMP_1_2, amount); + vm.expectEmit(ON_RAMP_1_2); + emit CCIPSendRequested(eventArg); + vm.prank(alice); + router.ccipSend{value: eventArg.feeTokenAmount}(ETH_CHAIN_SELECTOR, message); + + assertEq(IERC20(ARB_GHO_TOKEN).balanceOf(alice), 0); + assertEq(_getFacilitatorLevel(address(ghoTokenPool)), facilitatorLevelBefore - amount); + } + + function test_sendMessagePostCCIPMigration() public { + executePayload(vm, address(proposal)); + + _mockCCIPMigration(); + + IRouter router = IRouter(ghoTokenPool.getRouter()); + uint256 amount = 350_000e18; + uint256 facilitatorLevelBefore = _getFacilitatorLevel(address(ghoTokenPool)); + + // wait for the rate limiter to refill + skip(_getOutboundRefillTime(amount)); + + vm.prank(alice); + IERC20(ARB_GHO_TOKEN).approve(address(router), amount); + deal(ARB_GHO_TOKEN, alice, amount); + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage(CCIPSendParams({router: router, amount: amount, migrated: true})); + + vm.expectEmit(address(ghoTokenPool)); + emit Burned(address(proxyPool), amount); + vm.expectEmit(address(proxyPool)); + emit Burned(ON_RAMP_1_5, amount); + vm.expectEmit(ON_RAMP_1_5); + emit CCIPSendRequested(eventArg); + vm.prank(alice); + router.ccipSend{value: eventArg.feeTokenAmount}(ETH_CHAIN_SELECTOR, message); + + assertEq(IERC20(ARB_GHO_TOKEN).balanceOf(alice), 0); + assertEq(_getFacilitatorLevel(address(ghoTokenPool)), facilitatorLevelBefore - amount); + } + + function test_executeMessagePreCCIPMigration() public { + executePayload(vm, address(proposal)); + + uint256 amount = 350_000e18; + uint256 facilitatorLevelBefore = _getFacilitatorLevel(address(ghoTokenPool)); + + // wait for the rate limiter to refill + skip(_getInboundRefillTime(amount)); + + vm.expectEmit(address(ghoTokenPool)); + emit Minted(OFF_RAMP_1_2, alice, amount); + vm.prank(OFF_RAMP_1_2); + ghoTokenPool.releaseOrMint(abi.encode(alice), alice, amount, ETH_CHAIN_SELECTOR, ''); + + assertEq(IERC20(ARB_GHO_TOKEN).balanceOf(alice), amount); + assertEq(_getFacilitatorLevel(address(ghoTokenPool)), facilitatorLevelBefore + amount); + } + + function test_executeMessagePostCCIPMigration() public { + executePayload(vm, address(proposal)); + + _mockCCIPMigration(); + + uint256 amount = 350_000e18; + uint256 facilitatorLevelBefore = _getFacilitatorLevel(address(ghoTokenPool)); + + // wait for the rate limiter to refill + skip(_getInboundRefillTime(amount)); + + vm.expectEmit(address(ghoTokenPool)); + emit Minted(OFF_RAMP_1_5, alice, amount); + vm.prank(OFF_RAMP_1_5); + ghoTokenPool.releaseOrMint(abi.encode(alice), alice, amount, ETH_CHAIN_SELECTOR, ''); + + assertEq(IERC20(ARB_GHO_TOKEN).balanceOf(alice), amount); + assertEq(_getFacilitatorLevel(address(ghoTokenPool)), facilitatorLevelBefore + amount); + } + + function test_executeMessagePostCCIPMigrationViaLegacyOffRamp() public { + executePayload(vm, address(proposal)); + + _mockCCIPMigration(); + + uint256 amount = 350_000e18; + uint256 facilitatorLevelBefore = _getFacilitatorLevel(address(ghoTokenPool)); + + // wait for the rate limiter to refill + skip(_getInboundRefillTime(amount)); + + vm.expectEmit(address(ghoTokenPool)); + emit Minted(OFF_RAMP_1_2, alice, amount); + vm.prank(OFF_RAMP_1_2); + ghoTokenPool.releaseOrMint(abi.encode(alice), alice, amount, ETH_CHAIN_SELECTOR, ''); + + assertEq(IERC20(ARB_GHO_TOKEN).balanceOf(alice), amount); + assertEq(_getFacilitatorLevel(address(ghoTokenPool)), facilitatorLevelBefore + amount); + } + + function test_proxyPoolCanOnRamp() public { + uint256 amount = 1337e18; + + vm.expectRevert(abi.encodeWithSelector(CallerIsNotARampOnRouter.selector, proxyPool)); + vm.prank(address(proxyPool)); + ghoTokenPool.lockOrBurn(alice, abi.encode(alice), amount, ETH_CHAIN_SELECTOR, new bytes(0)); + + executePayload(vm, address(proposal)); + // router is responsible for transferring liquidity, so we mock router.token.transferFrom(user, tokenPool) + deal(ARB_GHO_TOKEN, address(ghoTokenPool), amount); + uint256 facilitatorLevelBefore = _getFacilitatorLevel(address(ghoTokenPool)); + + // wait for the rate limiter to refill + skip(_getOutboundRefillTime(amount)); + + vm.expectEmit(address(ghoTokenPool)); + emit Burned(address(proxyPool), amount); + vm.prank(address(proxyPool)); + ghoTokenPool.lockOrBurn(alice, abi.encode(alice), amount, ETH_CHAIN_SELECTOR, new bytes(0)); + + assertEq(IERC20(ARB_GHO_TOKEN).balanceOf(alice), 0); + assertEq(_getFacilitatorLevel(address(ghoTokenPool)), facilitatorLevelBefore - amount); + } + + function test_proxyPoolCanOffRamp() public { + uint256 amount = 1337e18; + + vm.expectRevert(abi.encodeWithSelector(CallerIsNotARampOnRouter.selector, proxyPool)); + vm.prank(address(proxyPool)); + ghoTokenPool.releaseOrMint(abi.encode(alice), alice, amount, ETH_CHAIN_SELECTOR, new bytes(0)); + + executePayload(vm, address(proposal)); + uint256 facilitatorLevelBefore = _getFacilitatorLevel(address(ghoTokenPool)); + + // wait for the rate limiter to refill + skip(_getInboundRefillTime(amount)); + + vm.expectEmit(address(ghoTokenPool)); + emit Minted(address(proxyPool), alice, amount); + vm.prank(address(proxyPool)); + ghoTokenPool.releaseOrMint(abi.encode(alice), alice, amount, ETH_CHAIN_SELECTOR, new bytes(0)); + + assertEq(IERC20(ARB_GHO_TOKEN).balanceOf(alice), amount); + assertEq(_getFacilitatorLevel(address(ghoTokenPool)), facilitatorLevelBefore + amount); + } + + function test_stewardCanDisableRateLimit() public { + executePayload(vm, address(proposal)); + + vm.prank(ghoTokenPool.owner()); + ghoTokenPool.setRateLimitAdmin(address(GHO_CCIP_STEWARD)); + + vm.prank(GHO_CCIP_STEWARD.RISK_COUNCIL()); + GHO_CCIP_STEWARD.updateRateLimit(ETH_CHAIN_SELECTOR, false, 0, 0, false, 0, 0); + + assertEq( + abi.encode( + _tokenBucketToConfig(ghoTokenPool.getCurrentInboundRateLimiterState(ETH_CHAIN_SELECTOR)) + ), + abi.encode(_getDisabledConfig()) + ); + assertEq( + abi.encode( + _tokenBucketToConfig(ghoTokenPool.getCurrentOutboundRateLimiterState(ETH_CHAIN_SELECTOR)) + ), + abi.encode(_getDisabledConfig()) + ); + } + + function test_ownershipTransferOfGhoProxyPool() public { + executePayload(vm, address(proposal)); + _mockCCIPMigration(); + + assertEq(ghoTokenPool.owner(), AaveV3Arbitrum.ACL_ADMIN); + + // CLL team transfers ownership of proxyPool and GHO token in TokenAdminRegistry + vm.prank(proxyPool.owner()); + proxyPool.transferOwnership(AaveV3Arbitrum.ACL_ADMIN); + vm.prank(TOKEN_ADMIN_REGISTRY.owner()); + TOKEN_ADMIN_REGISTRY.transferAdminRole(ARB_GHO_TOKEN, AaveV3Arbitrum.ACL_ADMIN); + + // new AIP to accept ownership + vm.startPrank(AaveV3Arbitrum.ACL_ADMIN); + proxyPool.acceptOwnership(); + TOKEN_ADMIN_REGISTRY.acceptAdminRole(ARB_GHO_TOKEN); + vm.stopPrank(); + + assertEq(proxyPool.owner(), AaveV3Arbitrum.ACL_ADMIN); + assertTrue(TOKEN_ADMIN_REGISTRY.isAdministrator(ARB_GHO_TOKEN, AaveV3Arbitrum.ACL_ADMIN)); + } + + function _mockCCIPMigration() private { + IRouter router = IRouter(ghoTokenPool.getRouter()); + // token registry not set for 1.5 migration + assertEq(TOKEN_ADMIN_REGISTRY.getPool(ARB_GHO_TOKEN), address(0)); + vm.startPrank(TOKEN_ADMIN_REGISTRY.owner()); + TOKEN_ADMIN_REGISTRY.proposeAdministrator(ARB_GHO_TOKEN, TOKEN_ADMIN_REGISTRY.owner()); + TOKEN_ADMIN_REGISTRY.acceptAdminRole(ARB_GHO_TOKEN); + TOKEN_ADMIN_REGISTRY.setPool(ARB_GHO_TOKEN, address(proxyPool)); + vm.stopPrank(); + assertEq(TOKEN_ADMIN_REGISTRY.getPool(ARB_GHO_TOKEN), address(proxyPool)); + + assertEq(proxyPool.getRouter(), address(router)); + + IProxyPool.ChainUpdate[] memory chains = new IProxyPool.ChainUpdate[](1); + chains[0] = IProxyPool.ChainUpdate({ + remoteChainSelector: ETH_CHAIN_SELECTOR, + remotePoolAddress: abi.encode(ETH_PROXY_POOL), + remoteTokenAddress: abi.encode(address(MiscEthereum.GHO_TOKEN)), + allowed: true, + outboundRateLimiterConfig: IRateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}), + inboundRateLimiterConfig: IRateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}) + }); + + vm.prank(proxyPool.owner()); + proxyPool.applyChainUpdates(chains); + + assertTrue(proxyPool.isSupportedChain(ETH_CHAIN_SELECTOR)); + + IRouter.OnRamp[] memory onRampUpdates = new IRouter.OnRamp[](1); + onRampUpdates[0] = IRouter.OnRamp({ + destChainSelector: ETH_CHAIN_SELECTOR, + onRamp: ON_RAMP_1_5 // new onRamp + }); + IRouter.OffRamp[] memory offRampUpdates = new IRouter.OffRamp[](1); + offRampUpdates[0] = IRouter.OffRamp({ + sourceChainSelector: ARB_CHAIN_SELECTOR, + offRamp: OFF_RAMP_1_5 // new offRamp + }); + + vm.prank(router.owner()); + router.applyRampUpdates(onRampUpdates, new IRouter.OffRamp[](0), offRampUpdates); + } + + function _getTokenMessage( + CCIPSendParams memory params + ) internal returns (IClient.EVM2AnyMessage memory, IInternal.EVM2EVMMessage memory) { + IClient.EVM2AnyMessage memory message = CCIPUtils.generateMessage(alice, 1); + message.tokenAmounts[0] = IClient.EVMTokenAmount({token: ARB_GHO_TOKEN, amount: params.amount}); + + uint256 feeAmount = params.router.getFee(ETH_CHAIN_SELECTOR, message); + deal(alice, feeAmount); + + IInternal.EVM2EVMMessage memory eventArg = CCIPUtils.messageToEvent( + CCIPUtils.MessageToEventParams({ + message: message, + router: params.router, + sourceChainSelector: ARB_CHAIN_SELECTOR, + feeTokenAmount: feeAmount, + originalSender: alice, + destinationToken: MiscEthereum.GHO_TOKEN, + migrated: params.migrated + }) + ); + + return (message, eventArg); + } + + function _getImplementation(address proxy) private view returns (address) { + bytes32 slot = bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1); + return address(uint160(uint256(vm.load(proxy, slot)))); + } + + function _readInitialized(address proxy) private view returns (uint8) { + /// slot 0 + // <1 byte ^ 1 byte ^ ---------- 20 bytes ----------> + // initialized initializing owner + return uint8(uint256(vm.load(proxy, bytes32(0)))); + } + + function _getFacilitatorLevel(address f) internal view returns (uint256) { + (, uint256 level) = IGhoToken(ARB_GHO_TOKEN).getFacilitatorBucket(f); + return level; + } + + function _getStaticParams() private view returns (bytes memory) { + return + abi.encode( + address(ghoTokenPool.getToken()), + ghoTokenPool.getAllowList(), + ghoTokenPool.getArmProxy(), + ghoTokenPool.getRouter() + ); + } + + function _getDynamicParams() private view returns (bytes memory) { + return + abi.encode( + ghoTokenPool.owner(), + ghoTokenPool.getSupportedChains(), + ghoTokenPool.getAllowListEnabled() + ); + } + + function _validateConstants() private view { + assertEq(TOKEN_ADMIN_REGISTRY.typeAndVersion(), 'TokenAdminRegistry 1.5.0'); + assertEq(proxyPool.typeAndVersion(), 'BurnMintTokenPoolAndProxy 1.5.0'); + assertEq(ITypeAndVersion(ON_RAMP_1_2).typeAndVersion(), 'EVM2EVMOnRamp 1.2.0'); + assertEq(ITypeAndVersion(ON_RAMP_1_5).typeAndVersion(), 'EVM2EVMOnRamp 1.5.0'); + assertEq(ITypeAndVersion(OFF_RAMP_1_2).typeAndVersion(), 'EVM2EVMOffRamp 1.2.0'); + assertEq(ITypeAndVersion(OFF_RAMP_1_5).typeAndVersion(), 'EVM2EVMOffRamp 1.5.0'); + + assertEq(GHO_CCIP_STEWARD.GHO_TOKEN(), ARB_GHO_TOKEN); + assertEq(GHO_CCIP_STEWARD.GHO_TOKEN_POOL(), address(ghoTokenPool)); + } + + function _getOutboundRefillTime(uint256 amount) private view returns (uint256) { + uint128 rate = proposal.getOutBoundRateLimiterConfig().rate; + assertNotEq(rate, 0); + return amount / uint256(rate) + 1; // account for rounding + } + + function _getInboundRefillTime(uint256 amount) private view returns (uint256) { + uint128 rate = proposal.getInBoundRateLimiterConfig().rate; + assertNotEq(rate, 0); + return amount / uint256(rate) + 1; // account for rounding + } + + function _tokenBucketToConfig( + RateLimiter.TokenBucket memory bucket + ) private pure returns (RateLimiter.Config memory) { + return + RateLimiter.Config({ + isEnabled: bucket.isEnabled, + capacity: bucket.capacity, + rate: bucket.rate + }); + } + + function _getDisabledConfig() private pure returns (RateLimiter.Config memory) { + return RateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}); + } +} diff --git a/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3E2E_GHOCCIP150Upgrade_20241021.t.sol b/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3E2E_GHOCCIP150Upgrade_20241021.t.sol new file mode 100644 index 000000000..e62080247 --- /dev/null +++ b/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3E2E_GHOCCIP150Upgrade_20241021.t.sol @@ -0,0 +1,852 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; +import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; +import {MiscArbitrum} from 'aave-address-book/MiscArbitrum.sol'; +import {ProtocolV3TestBase} from 'aave-helpers/src/ProtocolV3TestBase.sol'; +import {UpgradeableLockReleaseTokenPool} from 'aave-ccip/v0.8/ccip/pools/GHO/UpgradeableLockReleaseTokenPool.sol'; +import {UpgradeableBurnMintTokenPool} from 'aave-ccip/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPool.sol'; +import {IClient} from 'src/interfaces/ccip/IClient.sol'; +import {IInternal} from 'src/interfaces/ccip/IInternal.sol'; +import {IRouter} from 'src/interfaces/ccip/IRouter.sol'; +import {IEVM2EVMOnRamp} from 'src/interfaces/ccip/IEVM2EVMOnRamp.sol'; +import {ITypeAndVersion} from 'src/interfaces/ccip/ITypeAndVersion.sol'; +import {IProxyPool} from 'src/interfaces/ccip/IProxyPool.sol'; +import {IRateLimiter} from 'src/interfaces/ccip/IRateLimiter.sol'; +import {IEVM2EVMOffRamp_1_2, IEVM2EVMOffRamp_1_5} from 'src/interfaces/ccip/IEVM2EVMOffRamp.sol'; +import {ITokenAdminRegistry} from 'src/interfaces/ccip/ITokenAdminRegistry.sol'; +import {CCIPUtils} from './utils/CCIPUtils.sol'; +import {AaveV3Ethereum_GHOCCIP150Upgrade_20241021} from './AaveV3Ethereum_GHOCCIP150Upgrade_20241021.sol'; +import {AaveV3Arbitrum_GHOCCIP150Upgrade_20241021} from './AaveV3Arbitrum_GHOCCIP150Upgrade_20241021.sol'; + +/** + * @dev Test for AaveV3E2E_GHOCCIP150Upgrade_20241021 + * command: FOUNDRY_PROFILE=mainnet forge test --match-path=src/20241021_Multi_GHOCCIP150Upgrade/AaveV3E2E_GHOCCIP150Upgrade_20241021.t.sol -vv + */ +contract AaveV3E2E_GHOCCIP150Upgrade_20241021_Base is ProtocolV3TestBase { + struct CCIPSendParams { + IRouter router; + IERC20 token; + uint256 amount; + uint64 sourceChainSelector; + uint64 destinationChainSelector; + address sender; + bool migrated; + } + + struct Common { + IRouter router; + IERC20 token; + IEVM2EVMOnRamp EVM2EVMOnRamp1_2; + IEVM2EVMOnRamp EVM2EVMOnRamp1_5; + IEVM2EVMOffRamp_1_2 EVM2EVMOffRamp1_2; + IEVM2EVMOffRamp_1_5 EVM2EVMOffRamp1_5; + ITokenAdminRegistry tokenAdminRegistry; + IProxyPool proxyPool; + uint64 chainSelector; + uint256 forkId; + } + + struct L1 { + AaveV3Ethereum_GHOCCIP150Upgrade_20241021 proposal; + UpgradeableLockReleaseTokenPool tokenPool; + Common c; + } + + struct L2 { + AaveV3Arbitrum_GHOCCIP150Upgrade_20241021 proposal; + UpgradeableBurnMintTokenPool tokenPool; + Common c; + } + + L1 internal l1; + L2 internal l2; + address internal alice = makeAddr('alice'); + + event CCIPSendRequested(IInternal.EVM2EVMMessage message); + event Locked(address indexed sender, uint256 amount); + event Burned(address indexed sender, uint256 amount); + event Released(address indexed sender, address indexed recipient, uint256 amount); + event Minted(address indexed sender, address indexed recipient, uint256 amount); + + error CallerIsNotARampOnRouter(address caller); + error NotACompatiblePool(address pool); + + function setUp() public virtual { + l1.c.forkId = vm.createFork(vm.rpcUrl('mainnet'), 21045560); + l2.c.forkId = vm.createFork(vm.rpcUrl('arbitrum'), 267660907); + + vm.selectFork(l1.c.forkId); + l1.proposal = new AaveV3Ethereum_GHOCCIP150Upgrade_20241021(); + l1.c.proxyPool = IProxyPool(l1.proposal.GHO_CCIP_PROXY_POOL()); + l1.tokenPool = UpgradeableLockReleaseTokenPool(MiscEthereum.GHO_CCIP_TOKEN_POOL); + l1.c.router = IRouter(l1.tokenPool.getRouter()); + l2.c.chainSelector = l1.tokenPool.getSupportedChains()[0]; + l1.c.token = IERC20(address(l1.tokenPool.getToken())); + l1.c.EVM2EVMOnRamp1_2 = IEVM2EVMOnRamp(l1.c.router.getOnRamp(l2.c.chainSelector)); + l1.c.EVM2EVMOnRamp1_5 = IEVM2EVMOnRamp(0x69eCC4E2D8ea56E2d0a05bF57f4Fd6aEE7f2c284); // new onramp + l1.c.EVM2EVMOffRamp1_2 = IEVM2EVMOffRamp_1_2(0xeFC4a18af59398FF23bfe7325F2401aD44286F4d); + l1.c.EVM2EVMOffRamp1_5 = IEVM2EVMOffRamp_1_5(0xdf615eF8D4C64d0ED8Fd7824BBEd2f6a10245aC9); // new offramp + l1.c.tokenAdminRegistry = ITokenAdminRegistry(0xb22764f98dD05c789929716D677382Df22C05Cb6); + + vm.selectFork(l2.c.forkId); + l2.proposal = new AaveV3Arbitrum_GHOCCIP150Upgrade_20241021(); + l2.c.proxyPool = IProxyPool(l2.proposal.GHO_CCIP_PROXY_POOL()); + l2.tokenPool = UpgradeableBurnMintTokenPool(MiscArbitrum.GHO_CCIP_TOKEN_POOL); + l2.c.router = IRouter(l2.tokenPool.getRouter()); + l1.c.chainSelector = l2.tokenPool.getSupportedChains()[0]; + l2.c.token = IERC20(address(l2.tokenPool.getToken())); + l2.c.EVM2EVMOnRamp1_2 = IEVM2EVMOnRamp(l2.c.router.getOnRamp(l1.c.chainSelector)); + l2.c.EVM2EVMOnRamp1_5 = IEVM2EVMOnRamp(0x67761742ac8A21Ec4D76CA18cbd701e5A6F3Bef3); // new onramp + l2.c.EVM2EVMOffRamp1_2 = IEVM2EVMOffRamp_1_2(0x542ba1902044069330e8c5b36A84EC503863722f); + l2.c.EVM2EVMOffRamp1_5 = IEVM2EVMOffRamp_1_5(0x91e46cc5590A4B9182e47f40006140A7077Dec31); // new offramp + l2.c.tokenAdminRegistry = ITokenAdminRegistry(0x39AE1032cF4B334a1Ed41cdD0833bdD7c7E7751E); + + _validateConfig({migrated: false}); + } + + function _getTokenMessage( + CCIPSendParams memory params + ) internal returns (IClient.EVM2AnyMessage memory, IInternal.EVM2EVMMessage memory) { + IClient.EVM2AnyMessage memory message = CCIPUtils.generateMessage(params.sender, 1); + message.tokenAmounts[0] = IClient.EVMTokenAmount({ + token: address(params.token), + amount: params.amount + }); + + uint256 feeAmount = params.router.getFee(params.destinationChainSelector, message); + deal(params.sender, feeAmount); + + IInternal.EVM2EVMMessage memory eventArg = CCIPUtils.messageToEvent( + CCIPUtils.MessageToEventParams({ + message: message, + router: params.router, + sourceChainSelector: params.sourceChainSelector, + feeTokenAmount: feeAmount, + originalSender: params.sender, + destinationToken: address(params.token == l1.c.token ? l2.c.token : l1.c.token), + migrated: params.migrated + }) + ); + + return (message, eventArg); + } + + function _validateConfig(bool migrated) internal { + vm.selectFork(l1.c.forkId); + assertEq(l1.c.chainSelector, 5009297550715157269); + assertEq(address(l1.c.token), MiscEthereum.GHO_TOKEN); + assertEq(ITypeAndVersion(address(l1.c.router)).typeAndVersion(), 'Router 1.2.0'); + assertEq( + ITypeAndVersion(address(l1.c.EVM2EVMOnRamp1_2)).typeAndVersion(), + 'EVM2EVMOnRamp 1.2.0' + ); + assertEq( + ITypeAndVersion(address(l1.c.EVM2EVMOnRamp1_5)).typeAndVersion(), + 'EVM2EVMOnRamp 1.5.0' + ); + assertEq( + ITypeAndVersion(address(l1.c.EVM2EVMOffRamp1_2)).typeAndVersion(), + 'EVM2EVMOffRamp 1.2.0' + ); + assertEq( + ITypeAndVersion(address(l1.c.EVM2EVMOffRamp1_5)).typeAndVersion(), + 'EVM2EVMOffRamp 1.5.0' + ); + assertEq(l1.c.proxyPool.typeAndVersion(), 'LockReleaseTokenPoolAndProxy 1.5.0'); + assertEq(l1.tokenPool.typeAndVersion(), 'LockReleaseTokenPool 1.4.0'); + assertEq(l1.c.tokenAdminRegistry.typeAndVersion(), 'TokenAdminRegistry 1.5.0'); + assertTrue(l1.c.router.isOffRamp(l2.c.chainSelector, address(l1.c.EVM2EVMOffRamp1_2))); + assertTrue(l1.c.router.isOffRamp(l2.c.chainSelector, address(l1.c.EVM2EVMOffRamp1_5))); + + // ensure only 1.2 & 1.5 offRamps are configured + IRouter.OffRamp[] memory offRamps = l1.c.router.getOffRamps(); + for (uint256 i; i < offRamps.length; ++i) { + if (offRamps[i].sourceChainSelector == l2.c.chainSelector) { + assertTrue( + offRamps[i].offRamp == address(l1.c.EVM2EVMOffRamp1_2) || + offRamps[i].offRamp == address(l1.c.EVM2EVMOffRamp1_5) + ); + } + } + + if (migrated) { + assertEq(l1.c.router.getOnRamp(l2.c.chainSelector), address(l1.c.EVM2EVMOnRamp1_5)); + } else { + assertEq(l1.c.router.getOnRamp(l2.c.chainSelector), address(l1.c.EVM2EVMOnRamp1_2)); + } + + vm.selectFork(l2.c.forkId); + assertEq(l2.c.chainSelector, 4949039107694359620); + assertEq(address(l2.c.token), 0x7dfF72693f6A4149b17e7C6314655f6A9F7c8B33); + assertEq(ITypeAndVersion(address(l2.c.router)).typeAndVersion(), 'Router 1.2.0'); + assertEq( + ITypeAndVersion(address(l2.c.EVM2EVMOnRamp1_2)).typeAndVersion(), + 'EVM2EVMOnRamp 1.2.0' + ); + assertEq( + ITypeAndVersion(address(l2.c.EVM2EVMOnRamp1_5)).typeAndVersion(), + 'EVM2EVMOnRamp 1.5.0' + ); + assertEq( + ITypeAndVersion(address(l2.c.EVM2EVMOffRamp1_2)).typeAndVersion(), + 'EVM2EVMOffRamp 1.2.0' + ); + assertEq( + ITypeAndVersion(address(l2.c.EVM2EVMOffRamp1_5)).typeAndVersion(), + 'EVM2EVMOffRamp 1.5.0' + ); + assertEq(l2.c.proxyPool.typeAndVersion(), 'BurnMintTokenPoolAndProxy 1.5.0'); + assertEq(l2.tokenPool.typeAndVersion(), 'BurnMintTokenPool 1.4.0'); + assertEq(l2.c.tokenAdminRegistry.typeAndVersion(), 'TokenAdminRegistry 1.5.0'); + assertTrue(l2.c.router.isOffRamp(l1.c.chainSelector, address(l2.c.EVM2EVMOffRamp1_2))); + assertTrue(l2.c.router.isOffRamp(l1.c.chainSelector, address(l2.c.EVM2EVMOffRamp1_5))); + + // ensure only 1.2 & 1.5 offRamps are configured + offRamps = l2.c.router.getOffRamps(); + for (uint256 i; i < offRamps.length; ++i) { + if (offRamps[i].sourceChainSelector == l1.c.chainSelector) { + assertTrue( + offRamps[i].offRamp == address(l2.c.EVM2EVMOffRamp1_2) || + offRamps[i].offRamp == address(l2.c.EVM2EVMOffRamp1_5) + ); + } + } + + if (migrated) { + assertEq(l2.c.router.getOnRamp(l1.c.chainSelector), address(l2.c.EVM2EVMOnRamp1_5)); + } else { + assertEq(l2.c.router.getOnRamp(l1.c.chainSelector), address(l2.c.EVM2EVMOnRamp1_2)); + } + } + + function _mockCCIPMigration(Common memory src, Common memory dest) internal { + vm.selectFork(src.forkId); + + // token registry not set for 1.5 migration + assertEq(src.tokenAdminRegistry.getPool(address(src.token)), address(0)); + vm.startPrank(src.tokenAdminRegistry.owner()); + src.tokenAdminRegistry.proposeAdministrator(address(src.token), src.tokenAdminRegistry.owner()); + src.tokenAdminRegistry.acceptAdminRole(address(src.token)); + src.tokenAdminRegistry.setPool(address(src.token), address(src.proxyPool)); + vm.stopPrank(); + assertEq(src.tokenAdminRegistry.getPool(address(src.token)), address(src.proxyPool)); + + assertEq(src.proxyPool.getRouter(), address(src.router)); + + IProxyPool.ChainUpdate[] memory chains = new IProxyPool.ChainUpdate[](1); + chains[0] = IProxyPool.ChainUpdate({ + remoteChainSelector: dest.chainSelector, + remotePoolAddress: abi.encode(address(dest.proxyPool)), + remoteTokenAddress: abi.encode(address(dest.token)), + allowed: true, + outboundRateLimiterConfig: IRateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}), + inboundRateLimiterConfig: IRateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}) + }); + + vm.prank(src.proxyPool.owner()); + src.proxyPool.applyChainUpdates(chains); + + assertTrue(src.proxyPool.isSupportedChain(dest.chainSelector)); + + IRouter.OnRamp[] memory onRampUpdates = new IRouter.OnRamp[](1); + onRampUpdates[0] = IRouter.OnRamp({ + destChainSelector: dest.chainSelector, + onRamp: address(src.EVM2EVMOnRamp1_5) // new onRamp + }); + IRouter.OffRamp[] memory offRampUpdates = new IRouter.OffRamp[](1); + offRampUpdates[0] = IRouter.OffRamp({ + sourceChainSelector: dest.chainSelector, + offRamp: address(src.EVM2EVMOffRamp1_5) // new offRamp + }); + + vm.prank(src.router.owner()); + src.router.applyRampUpdates(onRampUpdates, new IRouter.OffRamp[](0), offRampUpdates); + } +} + +contract AaveV3E2E_GHOCCIP150Upgrade_20241021_PreCCIPMigration is + AaveV3E2E_GHOCCIP150Upgrade_20241021_Base +{ + function setUp() public override { + super.setUp(); + + vm.selectFork(l1.c.forkId); + executePayload(vm, address(l1.proposal)); + vm.selectFork(l2.c.forkId); + executePayload(vm, address(l2.proposal)); + + _validateConfig({migrated: false}); + } + + function test_E2E() public { + uint256 amount = 500e18; + // ETH (=> ARB) sendMessage + { + vm.selectFork(l1.c.forkId); + deal(address(l1.c.token), alice, amount, true); + vm.prank(alice); + l1.c.token.approve(address(l1.c.router), amount); + + uint128 outBoundRate = l1.proposal.getOutBoundRateLimiterConfig().rate; + // wait for the rate limiter to refill + skip(amount / uint256(outBoundRate) + 1); // rate is non zero + + uint256 tokenPoolBalance = l1.c.token.balanceOf(address(l1.tokenPool)); + uint256 bridgedAmount = l1.tokenPool.getCurrentBridgedAmount(); + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({ + router: l1.c.router, + token: l1.c.token, + amount: amount, + sourceChainSelector: l1.c.chainSelector, + destinationChainSelector: l2.c.chainSelector, + sender: alice, + migrated: false + }) + ); + + vm.expectEmit(address(l1.tokenPool)); + emit Locked(address(l1.c.EVM2EVMOnRamp1_2), amount); + vm.expectEmit(address(l1.c.EVM2EVMOnRamp1_2)); + emit CCIPSendRequested(eventArg); + vm.prank(alice); + l1.c.router.ccipSend{value: eventArg.feeTokenAmount}(l2.c.chainSelector, message); + + assertEq(l1.c.token.balanceOf(address(l1.tokenPool)), tokenPoolBalance + amount); + assertEq(l1.tokenPool.getCurrentBridgedAmount(), bridgedAmount + amount); + + // ARB executeMessage + vm.selectFork(l2.c.forkId); + + uint256 aliceBalanceBefore = l2.c.token.balanceOf(alice); + + uint128 inBoundRate = l2.proposal.getInBoundRateLimiterConfig().rate; + // wait for the rate limiter to refill + skip(amount / uint256(inBoundRate) + 1); // rate is non zero + + vm.expectEmit(address(l2.tokenPool)); + emit Minted(address(l2.c.EVM2EVMOffRamp1_2), alice, amount); + vm.prank(address(l2.c.EVM2EVMOffRamp1_2)); + l2.c.EVM2EVMOffRamp1_2.executeSingleMessage( + eventArg, + new bytes[](message.tokenAmounts.length) + ); + + assertEq(l2.c.token.balanceOf(alice), aliceBalanceBefore + amount); + } + + // ARB (=> ETH) sendMessage + { + vm.selectFork(l2.c.forkId); + + vm.prank(alice); + l2.c.token.approve(address(l2.c.router), amount); + uint128 outBoundRate = l2.proposal.getOutBoundRateLimiterConfig().rate; + // wait for the rate limiter to refill + skip(amount / uint256(outBoundRate) + 1); // rate is non zero + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({ + router: l2.c.router, + token: l2.c.token, + amount: amount, + sourceChainSelector: l2.c.chainSelector, + destinationChainSelector: l1.c.chainSelector, + sender: alice, + migrated: false + }) + ); + + vm.expectEmit(address(l2.tokenPool)); + emit Burned(address(l2.c.EVM2EVMOnRamp1_2), amount); + vm.expectEmit(address(l2.c.EVM2EVMOnRamp1_2)); + emit CCIPSendRequested(eventArg); + vm.prank(alice); + l2.c.router.ccipSend{value: eventArg.feeTokenAmount}(l1.c.chainSelector, message); + + assertEq(l2.c.token.balanceOf(alice), 0); + + // ETH executeMessage + vm.selectFork(l1.c.forkId); + + uint256 tokenPoolBalanceBefore = l1.c.token.balanceOf(address(l1.tokenPool)); + + uint128 inBoundRate = l1.proposal.getInBoundRateLimiterConfig().rate; + // wait for the rate limiter to refill + skip(amount / uint256(inBoundRate) + 1); // rate is non zero + + vm.expectEmit(address(l1.tokenPool)); + emit Released(address(l1.c.EVM2EVMOffRamp1_2), alice, amount); + vm.prank(address(l1.c.EVM2EVMOffRamp1_2)); + l1.c.EVM2EVMOffRamp1_2.executeSingleMessage( + eventArg, + new bytes[](message.tokenAmounts.length) + ); + + assertEq(l1.c.token.balanceOf(address(l1.tokenPool)), tokenPoolBalanceBefore - amount); + assertEq(l1.c.token.balanceOf(alice), amount); + } + } +} + +contract AaveV3E2E_GHOCCIP150Upgrade_20241021_PostCCIPMigration is + AaveV3E2E_GHOCCIP150Upgrade_20241021_Base +{ + function setUp() public override { + super.setUp(); + + // execute proposal + vm.selectFork(l1.c.forkId); + executePayload(vm, address(l1.proposal)); + vm.selectFork(l2.c.forkId); + executePayload(vm, address(l2.proposal)); + + _mockCCIPMigration(l1.c, l2.c); + _mockCCIPMigration(l2.c, l1.c); + + _validateConfig({migrated: true}); + } + + function test_E2E() public { + uint256 amount = 500e18; + // ETH (=> ARB) sendMessage + { + vm.selectFork(l1.c.forkId); + deal(address(l1.c.token), alice, amount, true); + vm.prank(alice); + l1.c.token.approve(address(l1.c.router), amount); + + uint128 outBoundRate = l1.proposal.getOutBoundRateLimiterConfig().rate; + // wait for the rate limiter to refill + skip(amount / uint256(outBoundRate) + 1); // rate is non zero + + uint256 tokenPoolBalance = l1.c.token.balanceOf(address(l1.tokenPool)); + uint256 bridgedAmount = l1.tokenPool.getCurrentBridgedAmount(); + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({ + router: l1.c.router, + token: l1.c.token, + amount: amount, + sourceChainSelector: l1.c.chainSelector, + destinationChainSelector: l2.c.chainSelector, + sender: alice, + migrated: true + }) + ); + + /// expected flow: router => onRamp => proxyPool => tokenPool + vm.expectEmit(address(l1.tokenPool)); + emit Locked(address(l1.c.proxyPool), amount); + + vm.expectEmit(address(l1.c.proxyPool)); + emit Locked(address(l1.c.EVM2EVMOnRamp1_5), amount); + + vm.expectEmit(address(l1.c.EVM2EVMOnRamp1_5)); // @dev caller is now 1.5 onRamp + emit CCIPSendRequested(eventArg); + vm.prank(alice); + l1.c.router.ccipSend{value: eventArg.feeTokenAmount}(l2.c.chainSelector, message); + + assertEq(l1.c.token.balanceOf(address(l1.tokenPool)), tokenPoolBalance + amount); + assertEq(l1.tokenPool.getCurrentBridgedAmount(), bridgedAmount + amount); + + // ARB executeMessage + vm.selectFork(l2.c.forkId); + + uint128 inBoundRate = l2.proposal.getInBoundRateLimiterConfig().rate; + // wait for the rate limiter to refill + skip(amount / uint256(inBoundRate) + 1); // rate is non zero + + vm.expectEmit(address(l2.tokenPool)); + emit Minted(address(l2.c.EVM2EVMOffRamp1_2), alice, amount); + vm.prank(address(l2.c.EVM2EVMOffRamp1_2)); + l2.c.EVM2EVMOffRamp1_2.executeSingleMessage( + eventArg, + new bytes[](message.tokenAmounts.length) + ); + + // wait for the rate limiter to refill + skip(amount / uint256(inBoundRate) + 1); // rate is non zero + + vm.expectEmit(address(l2.c.proxyPool)); // emitter is proxyPool for 1.5 on ramp + emit Minted(address(l2.c.EVM2EVMOffRamp1_5), alice, amount); + vm.prank(address(l2.c.EVM2EVMOffRamp1_5)); + l2.c.EVM2EVMOffRamp1_5.executeSingleMessage( + eventArg, + new bytes[](message.tokenAmounts.length), + new uint32[](1) // tokenGasOverrides + ); + } + + // ARB (=> ETH) sendMessage + { + vm.selectFork(l2.c.forkId); + + vm.prank(alice); + l2.c.token.approve(address(l2.c.router), amount); + uint128 outBoundRate = l2.proposal.getOutBoundRateLimiterConfig().rate; + // wait for the rate limiter to refill + skip(amount / uint256(outBoundRate) + 1); // rate is non zero + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({ + router: l2.c.router, + token: l2.c.token, + amount: amount, + sourceChainSelector: l2.c.chainSelector, + destinationChainSelector: l1.c.chainSelector, + sender: alice, + migrated: true + }) + ); + + /// expected flow: router => onRamp => proxyPool => tokenPool + vm.expectEmit(address(l2.tokenPool)); + emit Burned(address(l2.c.proxyPool), amount); // @dev caller is now 1.5 onRamp + + vm.expectEmit(address(l2.c.proxyPool)); + emit Burned(address(l2.c.EVM2EVMOnRamp1_5), amount); // @dev caller is now 1.5 onRamp + + vm.expectEmit(address(l2.c.EVM2EVMOnRamp1_5)); + emit CCIPSendRequested(eventArg); + vm.prank(alice); + l2.c.router.ccipSend{value: eventArg.feeTokenAmount}(l1.c.chainSelector, message); + + // ETH executeMessage + vm.selectFork(l1.c.forkId); + + uint256 tokenPoolBalanceBefore = l1.c.token.balanceOf(address(l1.tokenPool)); + + uint128 inBoundRate = l1.proposal.getInBoundRateLimiterConfig().rate; + // wait for the rate limiter to refill + skip(amount / uint256(inBoundRate) + 1); // rate is non zero + + vm.expectEmit(address(l1.c.proxyPool)); // emitter is proxyPool for 1.5 off ramp + emit Released(address(l1.c.EVM2EVMOffRamp1_5), alice, amount); + vm.prank(address(l1.c.EVM2EVMOffRamp1_5)); + l1.c.EVM2EVMOffRamp1_5.executeSingleMessage( + eventArg, + new bytes[](message.tokenAmounts.length), + new uint32[](1) // tokenGasOverrides + ); + + assertEq(l1.c.token.balanceOf(address(l1.tokenPool)), tokenPoolBalanceBefore - amount); + assertEq(l1.c.token.balanceOf(alice), amount); + } + } + + function test_SendRevertsWithoutUpgrade() public { + { + vm.selectFork(l1.c.forkId); + uint256 amount = 500e18; + deal(address(l1.c.token), alice, amount, true); + vm.prank(alice); + l1.c.token.approve(address(l1.c.router), amount); + uint128 rate = l1.proposal.getOutBoundRateLimiterConfig().rate; + // wait for the rate limiter to refill + skip(amount / uint256(rate) + 1); // rate is non zero + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({ + router: l1.c.router, + token: l1.c.token, + amount: amount, + sourceChainSelector: l1.c.chainSelector, + destinationChainSelector: l2.c.chainSelector, + sender: alice, + migrated: true + }) + ); + + // mock undo upgrade by setting proxy pool to random address + vm.prank(l1.tokenPool.owner()); + l1.tokenPool.setProxyPool(address(1337)); + + vm.expectRevert(abi.encodeWithSelector(CallerIsNotARampOnRouter.selector, l1.c.proxyPool)); + vm.prank(alice); + l1.c.router.ccipSend{value: eventArg.feeTokenAmount}(l2.c.chainSelector, message); + } + + { + vm.selectFork(l2.c.forkId); + uint256 amount = 500e18; + deal(address(l2.c.token), alice, amount, true); + vm.prank(alice); + l2.c.token.approve(address(l2.c.router), amount); + uint128 rate = l2.proposal.getOutBoundRateLimiterConfig().rate; + // wait for the rate limiter to refill + skip(amount / uint256(rate) + 1); // rate is non zero + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({ + router: l2.c.router, + token: l2.c.token, + amount: amount, + sourceChainSelector: l2.c.chainSelector, + destinationChainSelector: l1.c.chainSelector, + sender: alice, + migrated: true + }) + ); + + // mock undo upgrade by setting proxy pool to random address + vm.prank(l2.tokenPool.owner()); + l2.tokenPool.setProxyPool(address(1337)); + + vm.expectRevert(abi.encodeWithSelector(CallerIsNotARampOnRouter.selector, l2.c.proxyPool)); + vm.prank(alice); + l2.c.router.ccipSend{value: eventArg.feeTokenAmount}(l1.c.chainSelector, message); + } + } + + function test_ExecuteRevertsWithoutUpgrade() public { + { + vm.selectFork(l1.c.forkId); + uint256 amount = 500e18; + deal(address(l1.c.token), alice, amount, true); + vm.prank(alice); + l1.c.token.approve(address(l1.c.router), amount); + + uint128 inBoundRate = l1.proposal.getInBoundRateLimiterConfig().rate; + // wait for the rate limiter to refill + skip(amount / uint256(inBoundRate) + 1); // rate is non zero + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({ + router: l1.c.router, + token: l1.c.token, + amount: amount, + sourceChainSelector: l1.c.chainSelector, + destinationChainSelector: l2.c.chainSelector, + sender: alice, + migrated: true + }) + ); + + // mock undo upgrade by setting proxy pool to random address + vm.prank(l1.tokenPool.owner()); + l1.tokenPool.setProxyPool(address(1337)); + + vm.expectRevert(abi.encodeWithSelector(NotACompatiblePool.selector, address(0))); + vm.prank(address(l1.c.EVM2EVMOffRamp1_5)); + l1.c.EVM2EVMOffRamp1_5.executeSingleMessage( + eventArg, + new bytes[](message.tokenAmounts.length), + new uint32[](1) // tokenGasOverrides + ); + } + + { + vm.selectFork(l2.c.forkId); + uint256 amount = 500e18; + deal(address(l2.c.token), alice, amount, true); + vm.prank(alice); + l2.c.token.approve(address(l2.c.router), amount); + + uint128 inBoundRate = l2.proposal.getInBoundRateLimiterConfig().rate; + // wait for the rate limiter to refill + skip(amount / uint256(inBoundRate) + 1); // rate is non zero + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({ + router: l2.c.router, + token: l2.c.token, + amount: amount, + sourceChainSelector: l2.c.chainSelector, + destinationChainSelector: l1.c.chainSelector, + sender: alice, + migrated: true + }) + ); + + // mock undo upgrade by setting proxy pool to random address + vm.prank(l2.tokenPool.owner()); + l2.tokenPool.setProxyPool(address(1337)); + + vm.expectRevert(abi.encodeWithSelector(NotACompatiblePool.selector, address(0))); + vm.prank(address(l2.c.EVM2EVMOffRamp1_5)); + l2.c.EVM2EVMOffRamp1_5.executeSingleMessage( + eventArg, + new bytes[](message.tokenAmounts.length), + new uint32[](1) // tokenGasOverrides + ); + } + } +} + +// sendMsg => CCIP Migration => executeMsg +contract AaveV3E2E_GHOCCIP150Upgrade_20241021_InFlightCCIPMigration is + AaveV3E2E_GHOCCIP150Upgrade_20241021_Base +{ + function setUp() public override { + super.setUp(); + + // execute proposal + vm.selectFork(l1.c.forkId); + executePayload(vm, address(l1.proposal)); + vm.selectFork(l2.c.forkId); + executePayload(vm, address(l2.proposal)); + + _validateConfig({migrated: false}); + } + + function test_SendFlowInFlightCCIPMigrationFromEth() public { + // ETH => ARB, ccipSend 1.4; CCIP migration, Destination executeMessage + { + vm.selectFork(l1.c.forkId); + uint256 amount = 500e18; + deal(address(l1.c.token), alice, amount, true); + vm.prank(alice); + l1.c.token.approve(address(l1.c.router), amount); + uint128 rate = l1.proposal.getOutBoundRateLimiterConfig().rate; + // wait for the rate limiter to refill + skip(amount / uint256(rate) + 1); // rate is non zero + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({ + router: l1.c.router, + token: l1.c.token, + amount: amount, + sourceChainSelector: l1.c.chainSelector, + destinationChainSelector: l2.c.chainSelector, + sender: alice, + migrated: false + }) + ); + + vm.expectEmit(address(l1.tokenPool)); + emit Locked(address(l1.c.EVM2EVMOnRamp1_2), amount); + vm.expectEmit(address(l1.c.EVM2EVMOnRamp1_2)); + emit CCIPSendRequested(eventArg); + vm.prank(alice); + l1.c.router.ccipSend{value: eventArg.feeTokenAmount}(l2.c.chainSelector, message); + + assertEq(l1.c.token.balanceOf(alice), 0); + + // CCIP Migration + _mockCCIPMigration(l1.c, l2.c); + _mockCCIPMigration(l2.c, l1.c); + + vm.selectFork(l2.c.forkId); + + uint128 inBoundRate = l2.proposal.getInBoundRateLimiterConfig().rate; + // wait for the rate limiter to refill + skip(amount / uint256(inBoundRate) + 1); // rate is non zero + + // reverts with 1.5 off ramp because eventArg is in CCIP 1.4 message format + vm.expectRevert(); + vm.prank(address(l2.c.EVM2EVMOffRamp1_5)); + l2.c.EVM2EVMOffRamp1_5.executeSingleMessage( + eventArg, + new bytes[](message.tokenAmounts.length), + new uint32[](0) // tokenGasOverrides + ); + + // system can use legacy 1.2 off ramp after migration + vm.expectEmit(address(l2.tokenPool)); + emit Minted(address(l2.c.EVM2EVMOffRamp1_2), alice, amount); + vm.prank(address(l2.c.EVM2EVMOffRamp1_2)); + l2.c.EVM2EVMOffRamp1_2.executeSingleMessage( + eventArg, + new bytes[](message.tokenAmounts.length) + ); + } + } + + function test_SendFlowInFlightCCIPMigrationFromArb() public { + // ARB => ETH, ccipSend 1.4; CCIP migration, Destination executeMessage + { + vm.selectFork(l2.c.forkId); + uint256 amount = 500e18; + deal(address(l2.c.token), alice, amount, true); + vm.prank(alice); + l2.c.token.approve(address(l2.c.router), amount); + uint128 outBoundRate = l2.proposal.getOutBoundRateLimiterConfig().rate; + // wait for the rate limiter to refill + skip(amount / uint256(outBoundRate) + 1); // rate is non zero + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({ + router: l2.c.router, + token: l2.c.token, + amount: amount, + sourceChainSelector: l2.c.chainSelector, + destinationChainSelector: l1.c.chainSelector, + sender: alice, + migrated: false + }) + ); + + vm.expectEmit(address(l2.tokenPool)); + emit Burned(address(l2.c.EVM2EVMOnRamp1_2), amount); + vm.expectEmit(address(l2.c.EVM2EVMOnRamp1_2)); + emit CCIPSendRequested(eventArg); + vm.prank(alice); + l2.c.router.ccipSend{value: eventArg.feeTokenAmount}(l1.c.chainSelector, message); + + assertEq(l2.c.token.balanceOf(alice), 0); + + // CCIP Migration + _mockCCIPMigration(l1.c, l2.c); + _mockCCIPMigration(l2.c, l1.c); + + vm.selectFork(l1.c.forkId); + + uint128 inBoundRate = l1.proposal.getInBoundRateLimiterConfig().rate; + // wait for the rate limiter to refill + skip(amount / uint256(inBoundRate) + 1); // rate is non zero + + // reverts with 1.5 off ramp + vm.expectRevert(); + vm.prank(address(l1.c.EVM2EVMOffRamp1_5)); + l1.c.EVM2EVMOffRamp1_5.executeSingleMessage( + eventArg, + new bytes[](message.tokenAmounts.length), + new uint32[](0) // tokenGasOverrides + ); + + // system can use legacy 1.2 off ramp after migration + vm.expectEmit(address(l1.tokenPool)); + emit Released(address(l1.c.EVM2EVMOffRamp1_2), alice, amount); + vm.prank(address(l1.c.EVM2EVMOffRamp1_2)); + l1.c.EVM2EVMOffRamp1_2.executeSingleMessage( + eventArg, + new bytes[](message.tokenAmounts.length) + ); + } + } +} diff --git a/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Ethereum_GHOCCIP150Upgrade_20241021.sol b/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Ethereum_GHOCCIP150Upgrade_20241021.sol new file mode 100644 index 000000000..00ae3566f --- /dev/null +++ b/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Ethereum_GHOCCIP150Upgrade_20241021.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {TransparentUpgradeableProxy} from 'solidity-utils/contracts/transparent-proxy/TransparentUpgradeableProxy.sol'; +import {ProxyAdmin} from 'solidity-utils/contracts/transparent-proxy/ProxyAdmin.sol'; +import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; +import {IProposalGenericExecutor} from 'aave-helpers/src/interfaces/IProposalGenericExecutor.sol'; +import {UpgradeableLockReleaseTokenPool} from 'aave-ccip/v0.8/ccip/pools/GHO/UpgradeableLockReleaseTokenPool.sol'; +import {RateLimiter} from 'aave-ccip/v0.8/ccip/libraries/RateLimiter.sol'; + +/** + * @title GHO CCIP 1.50 Upgrade + * @author Aave Labs + * - Discussion: https://governance.aave.com/t/bgd-technical-maintenance-proposals/15274/51 + */ +contract AaveV3Ethereum_GHOCCIP150Upgrade_20241021 is IProposalGenericExecutor { + address public constant GHO_CCIP_PROXY_POOL = 0x9Ec9F9804733df96D1641666818eFb5198eC50f0; + uint64 public constant ARB_CHAIN_SELECTOR = 4949039107694359620; + + function execute() external { + UpgradeableLockReleaseTokenPool tokenPoolProxy = UpgradeableLockReleaseTokenPool( + MiscEthereum.GHO_CCIP_TOKEN_POOL + ); + + // Deploy new tokenPool implementation, retain existing immutable configuration + address tokenPoolImpl = address( + new UpgradeableLockReleaseTokenPool( + address(tokenPoolProxy.getToken()), + tokenPoolProxy.getArmProxy(), + tokenPoolProxy.getAllowListEnabled(), + tokenPoolProxy.canAcceptLiquidity() + ) + ); + + ProxyAdmin(MiscEthereum.PROXY_ADMIN).upgrade( + TransparentUpgradeableProxy(payable(address(tokenPoolProxy))), + tokenPoolImpl + ); + + // Update proxyPool address + tokenPoolProxy.setProxyPool(GHO_CCIP_PROXY_POOL); + + // Set rate limit + tokenPoolProxy.setChainRateLimiterConfig( + ARB_CHAIN_SELECTOR, + getOutBoundRateLimiterConfig(), + getInBoundRateLimiterConfig() + ); + } + + /// @notice Returns the rate limiter configuration for the outbound rate limiter + /// The onRamp rate limit for ETH => ARB will be as follows: + /// Capacity: 350_000 GHO + /// Rate: 100 GHO per second (=> 360_000 GHO per hour) + /// @return The rate limiter configuration + function getOutBoundRateLimiterConfig() public pure returns (RateLimiter.Config memory) { + return RateLimiter.Config({isEnabled: true, capacity: 350_000e18, rate: 100e18}); + } + + /// @notice Returns the rate limiter configuration for the inbound rate limiter + /// The offRamp rate limit for ARB=>ETH will be as follows: + /// Capacity: 350_000 GHO + /// Rate: 100 GHO per second (=> 360_000 GHO per hour) + /// @return The rate limiter configuration + function getInBoundRateLimiterConfig() public pure returns (RateLimiter.Config memory) { + return RateLimiter.Config({isEnabled: true, capacity: 350_000e18, rate: 100e18}); + } +} diff --git a/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Ethereum_GHOCCIP150Upgrade_20241021.t.sol b/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Ethereum_GHOCCIP150Upgrade_20241021.t.sol new file mode 100644 index 000000000..79dd28897 --- /dev/null +++ b/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Ethereum_GHOCCIP150Upgrade_20241021.t.sol @@ -0,0 +1,502 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; +import {AaveV3Ethereum} from 'aave-address-book/AaveV3Ethereum.sol'; +import {AaveV3ArbitrumAssets} from 'aave-address-book/AaveV3Arbitrum.sol'; +import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; +import {ProtocolV3TestBase} from 'aave-helpers/src/ProtocolV3TestBase.sol'; +import {UpgradeableLockReleaseTokenPool} from 'aave-ccip/v0.8/ccip/pools/GHO/UpgradeableLockReleaseTokenPool.sol'; +import {RateLimiter} from 'aave-ccip/v0.8/ccip/libraries/RateLimiter.sol'; +import {IClient} from 'src/interfaces/ccip/IClient.sol'; +import {IInternal} from 'src/interfaces/ccip/IInternal.sol'; +import {IRouter} from 'src/interfaces/ccip/IRouter.sol'; +import {ITypeAndVersion} from 'src/interfaces/ccip/ITypeAndVersion.sol'; +import {IProxyPool} from 'src/interfaces/ccip/IProxyPool.sol'; +import {IRateLimiter} from 'src/interfaces/ccip/IRateLimiter.sol'; +import {ITokenAdminRegistry} from 'src/interfaces/ccip/ITokenAdminRegistry.sol'; +import {IGhoCcipSteward} from 'src/interfaces/IGhoCcipSteward.sol'; +import {CCIPUtils} from './utils/CCIPUtils.sol'; +import {AaveV3Ethereum_GHOCCIP150Upgrade_20241021} from './AaveV3Ethereum_GHOCCIP150Upgrade_20241021.sol'; + +/** + * @dev Test for AaveV3Ethereum_GHOCCIP150Upgrade_20241021 + * command: FOUNDRY_PROFILE=mainnet forge test --match-path=src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Ethereum_GHOCCIP150Upgrade_20241021.t.sol -vv + */ +contract AaveV3Ethereum_GHOCCIP150Upgrade_20241021_Test is ProtocolV3TestBase { + struct CCIPSendParams { + IRouter router; + uint256 amount; + bool migrated; + } + + AaveV3Ethereum_GHOCCIP150Upgrade_20241021 internal proposal; + UpgradeableLockReleaseTokenPool internal ghoTokenPool; + IProxyPool internal proxyPool; + + address internal alice = makeAddr('alice'); + + uint64 internal constant ETH_CHAIN_SELECTOR = 5009297550715157269; + uint64 internal constant ARB_CHAIN_SELECTOR = 4949039107694359620; + address internal constant ARB_PROXY_POOL = 0x26329558f08cbb40d6a4CCA0E0C67b29D64A8c50; + ITokenAdminRegistry internal constant TOKEN_ADMIN_REGISTRY = + ITokenAdminRegistry(0xb22764f98dD05c789929716D677382Df22C05Cb6); + + address internal constant ON_RAMP_1_2 = 0x925228D7B82d883Dde340A55Fe8e6dA56244A22C; + address internal constant ON_RAMP_1_5 = 0x69eCC4E2D8ea56E2d0a05bF57f4Fd6aEE7f2c284; + address internal constant OFF_RAMP_1_2 = 0xeFC4a18af59398FF23bfe7325F2401aD44286F4d; + address internal constant OFF_RAMP_1_5 = 0xdf615eF8D4C64d0ED8Fd7824BBEd2f6a10245aC9; + + IGhoCcipSteward internal constant GHO_CCIP_STEWARD = + IGhoCcipSteward(0x101Efb7b9Beb073B1219Cd5473a7C8A2f2EB84f4); + + event Locked(address indexed sender, uint256 amount); + event Released(address indexed sender, address indexed recipient, uint256 amount); + event CCIPSendRequested(IInternal.EVM2EVMMessage message); + + error CallerIsNotARampOnRouter(address caller); + + function setUp() public { + vm.createSelectFork(vm.rpcUrl('mainnet'), 21045560); + proposal = new AaveV3Ethereum_GHOCCIP150Upgrade_20241021(); + ghoTokenPool = UpgradeableLockReleaseTokenPool(MiscEthereum.GHO_CCIP_TOKEN_POOL); + proxyPool = IProxyPool(proposal.GHO_CCIP_PROXY_POOL()); + + _validateConstants(); + } + + /** + * @dev executes the generic test suite including e2e and config snapshots + */ + function test_defaultProposalExecution() public { + assertEq( + abi.encode( + _tokenBucketToConfig(ghoTokenPool.getCurrentInboundRateLimiterState(ARB_CHAIN_SELECTOR)) + ), + abi.encode(_getDisabledConfig()) + ); + assertEq( + abi.encode( + _tokenBucketToConfig(ghoTokenPool.getCurrentOutboundRateLimiterState(ARB_CHAIN_SELECTOR)) + ), + abi.encode(_getDisabledConfig()) + ); + + bytes memory dynamicParamsBefore = _getDynamicParams(); + bytes memory staticParamsBefore = _getStaticParams(); + + defaultTest( + 'AaveV3Ethereum_GHOCCIP150Upgrade_20241021', + AaveV3Ethereum.POOL, + address(proposal) + ); + + assertEq(keccak256(_getDynamicParams()), keccak256(dynamicParamsBefore)); + assertEq(keccak256(_getStaticParams()), keccak256(staticParamsBefore)); + + assertEq( + abi.encode( + _tokenBucketToConfig(ghoTokenPool.getCurrentInboundRateLimiterState(ARB_CHAIN_SELECTOR)) + ), + abi.encode(proposal.getInBoundRateLimiterConfig()) + ); + assertEq( + abi.encode( + _tokenBucketToConfig(ghoTokenPool.getCurrentOutboundRateLimiterState(ARB_CHAIN_SELECTOR)) + ), + abi.encode(proposal.getOutBoundRateLimiterConfig()) + ); + } + + function test_getProxyPool() public { + // proxyPool getter does not exist before the upgrade + vm.expectRevert(); + ghoTokenPool.getProxyPool(); + + executePayload(vm, address(proposal)); + + assertEq(ghoTokenPool.getProxyPool(), address(proxyPool)); + } + + function test_tokenPoolCannotBeInitializedAgain() public { + vm.expectRevert('Initializable: contract is already initialized'); + ghoTokenPool.initialize(makeAddr('owner'), new address[](0), makeAddr('router'), 0); + assertEq(_readInitialized(address(ghoTokenPool)), 1); + /// proxy implementation is initialized + assertEq(_readInitialized(_getImplementation(address(ghoTokenPool))), 255); + + executePayload(vm, address(proposal)); + + vm.expectRevert('Initializable: contract is already initialized'); + ghoTokenPool.initialize(makeAddr('owner'), new address[](0), makeAddr('router'), 0); + assertEq(_readInitialized(address(ghoTokenPool)), 1); + /// proxy implementation is initialized + assertEq(_readInitialized(_getImplementation(address(ghoTokenPool))), 255); + } + + function test_sendMessagePreCCIPMigration() public { + executePayload(vm, address(proposal)); + + IERC20 gho = IERC20(address(ghoTokenPool.getToken())); + IRouter router = IRouter(ghoTokenPool.getRouter()); + uint256 amount = 150_000e18; + + // wait for the rate limiter to refill + skip(_getOutboundRefillTime(amount)); + + vm.prank(alice); + gho.approve(address(router), amount); + deal(address(gho), alice, amount); + + uint256 tokenPoolBalance = gho.balanceOf(address(ghoTokenPool)); + uint256 bridgedAmount = ghoTokenPool.getCurrentBridgedAmount(); + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage(CCIPSendParams({router: router, amount: amount, migrated: false})); + + vm.expectEmit(address(ghoTokenPool)); + emit Locked(ON_RAMP_1_2, amount); + vm.expectEmit(ON_RAMP_1_2); + emit CCIPSendRequested(eventArg); + vm.prank(alice); + router.ccipSend{value: eventArg.feeTokenAmount}(ARB_CHAIN_SELECTOR, message); + + assertEq(gho.balanceOf(address(ghoTokenPool)), tokenPoolBalance + amount); + assertEq(ghoTokenPool.getCurrentBridgedAmount(), bridgedAmount + amount); + } + + function test_sendMessagePostCCIPMigration() public { + executePayload(vm, address(proposal)); + + _mockCCIPMigration(); + + IERC20 gho = IERC20(address(ghoTokenPool.getToken())); + IRouter router = IRouter(ghoTokenPool.getRouter()); + uint256 amount = 350_000e18; + + // wait for the rate limiter to refill + skip(_getOutboundRefillTime(amount)); + + vm.prank(alice); + gho.approve(address(router), amount); + deal(address(gho), alice, amount); + + uint256 tokenPoolBalance = gho.balanceOf(address(ghoTokenPool)); + uint256 bridgedAmount = ghoTokenPool.getCurrentBridgedAmount(); + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage(CCIPSendParams({router: router, amount: amount, migrated: true})); + + vm.expectEmit(address(ghoTokenPool)); + emit Locked(address(proxyPool), amount); + vm.expectEmit(address(proxyPool)); + emit Locked(ON_RAMP_1_5, amount); + vm.expectEmit(ON_RAMP_1_5); + emit CCIPSendRequested(eventArg); + vm.prank(alice); + router.ccipSend{value: eventArg.feeTokenAmount}(ARB_CHAIN_SELECTOR, message); + + assertEq(gho.balanceOf(address(ghoTokenPool)), tokenPoolBalance + amount); + assertEq(ghoTokenPool.getCurrentBridgedAmount(), bridgedAmount + amount); + } + + function test_executeMessagePreCCIPMigration() public { + executePayload(vm, address(proposal)); + + IERC20 gho = IERC20(address(ghoTokenPool.getToken())); + uint256 amount = 350_000e18; + + // wait for the rate limiter to refill + skip(_getInboundRefillTime(amount)); + // mock previously locked gho + deal(address(gho), address(ghoTokenPool), amount); + + vm.expectEmit(address(ghoTokenPool)); + emit Released(OFF_RAMP_1_2, alice, amount); + vm.prank(OFF_RAMP_1_2); + ghoTokenPool.releaseOrMint(abi.encode(alice), alice, amount, ARB_CHAIN_SELECTOR, ''); + + assertEq(gho.balanceOf(alice), amount); + } + + function test_executeMessagePostCCIPMigration() public { + executePayload(vm, address(proposal)); + + _mockCCIPMigration(); + + IERC20 gho = IERC20(address(ghoTokenPool.getToken())); + uint256 amount = 350_000e18; + // wait for the rate limiter to refill + skip(_getInboundRefillTime(amount)); + // mock previously locked gho + deal(address(gho), address(ghoTokenPool), amount); + + vm.expectEmit(address(ghoTokenPool)); + emit Released(OFF_RAMP_1_5, alice, amount); + vm.prank(OFF_RAMP_1_5); + ghoTokenPool.releaseOrMint(abi.encode(alice), alice, amount, ARB_CHAIN_SELECTOR, ''); + + assertEq(gho.balanceOf(alice), amount); + } + + function test_executeMessagePostCCIPMigrationViaLegacyOffRamp() public { + executePayload(vm, address(proposal)); + + _mockCCIPMigration(); + + IERC20 gho = IERC20(address(ghoTokenPool.getToken())); + uint256 amount = 350_000e18; + + // wait for the rate limiter to refill + skip(_getInboundRefillTime(amount)); + // mock previously locked gho + deal(address(gho), address(ghoTokenPool), amount); + + vm.expectEmit(address(ghoTokenPool)); + emit Released(OFF_RAMP_1_2, alice, amount); + vm.prank(OFF_RAMP_1_2); + ghoTokenPool.releaseOrMint(abi.encode(alice), alice, amount, ARB_CHAIN_SELECTOR, ''); + + assertEq(gho.balanceOf(alice), amount); + } + + function test_proxyPoolCanOnRamp() public { + uint256 amount = 1337e18; + + uint256 bridgedAmount = ghoTokenPool.getCurrentBridgedAmount(); + + vm.expectRevert(abi.encodeWithSelector(CallerIsNotARampOnRouter.selector, proxyPool)); + vm.prank(address(proxyPool)); + ghoTokenPool.lockOrBurn(alice, abi.encode(alice), amount, ARB_CHAIN_SELECTOR, new bytes(0)); + + executePayload(vm, address(proposal)); + + // wait for the rate limiter to refill + skip(_getOutboundRefillTime(amount)); + + vm.expectEmit(address(ghoTokenPool)); + emit Locked(address(proxyPool), amount); + vm.prank(address(proxyPool)); + ghoTokenPool.lockOrBurn(alice, abi.encode(alice), amount, ARB_CHAIN_SELECTOR, new bytes(0)); + + assertEq(ghoTokenPool.getCurrentBridgedAmount(), bridgedAmount + amount); + } + + function test_proxyPoolCanOffRamp() public { + uint256 amount = 1337e18; + + vm.expectRevert(abi.encodeWithSelector(CallerIsNotARampOnRouter.selector, proxyPool)); + vm.prank(address(proxyPool)); + ghoTokenPool.releaseOrMint(abi.encode(alice), alice, amount, ARB_CHAIN_SELECTOR, new bytes(0)); + + executePayload(vm, address(proposal)); + // mock previously locked gho + deal(MiscEthereum.GHO_TOKEN, address(ghoTokenPool), amount); + // wait for the rate limiter to refill + skip(_getInboundRefillTime(amount)); + + vm.expectEmit(address(ghoTokenPool)); + emit Released(address(proxyPool), alice, amount); + vm.prank(address(proxyPool)); + ghoTokenPool.releaseOrMint(abi.encode(alice), alice, amount, ARB_CHAIN_SELECTOR, new bytes(0)); + } + + function test_stewardCanDisableRateLimit() public { + executePayload(vm, address(proposal)); + + vm.prank(ghoTokenPool.owner()); + ghoTokenPool.setRateLimitAdmin(address(GHO_CCIP_STEWARD)); + + vm.prank(GHO_CCIP_STEWARD.RISK_COUNCIL()); + GHO_CCIP_STEWARD.updateRateLimit(ARB_CHAIN_SELECTOR, false, 0, 0, false, 0, 0); + + assertEq( + abi.encode( + _tokenBucketToConfig(ghoTokenPool.getCurrentInboundRateLimiterState(ARB_CHAIN_SELECTOR)) + ), + abi.encode(_getDisabledConfig()) + ); + assertEq( + abi.encode( + _tokenBucketToConfig(ghoTokenPool.getCurrentOutboundRateLimiterState(ARB_CHAIN_SELECTOR)) + ), + abi.encode(_getDisabledConfig()) + ); + } + + function test_ownershipTransferOfGhoProxyPool() public { + executePayload(vm, address(proposal)); + _mockCCIPMigration(); + + assertEq(ghoTokenPool.owner(), AaveV3Ethereum.ACL_ADMIN); + + // CLL team transfers ownership of proxyPool and GHO token in TokenAdminRegistry + vm.prank(proxyPool.owner()); + proxyPool.transferOwnership(AaveV3Ethereum.ACL_ADMIN); + vm.prank(TOKEN_ADMIN_REGISTRY.owner()); + TOKEN_ADMIN_REGISTRY.transferAdminRole(MiscEthereum.GHO_TOKEN, AaveV3Ethereum.ACL_ADMIN); + + // new AIP to accept ownership + vm.startPrank(AaveV3Ethereum.ACL_ADMIN); + proxyPool.acceptOwnership(); + TOKEN_ADMIN_REGISTRY.acceptAdminRole(MiscEthereum.GHO_TOKEN); + vm.stopPrank(); + + assertEq(proxyPool.owner(), AaveV3Ethereum.ACL_ADMIN); + assertTrue( + TOKEN_ADMIN_REGISTRY.isAdministrator(MiscEthereum.GHO_TOKEN, AaveV3Ethereum.ACL_ADMIN) + ); + } + + function _mockCCIPMigration() private { + IRouter router = IRouter(ghoTokenPool.getRouter()); + // token registry not set for 1.5 migration + assertEq(TOKEN_ADMIN_REGISTRY.getPool(MiscEthereum.GHO_TOKEN), address(0)); + vm.startPrank(TOKEN_ADMIN_REGISTRY.owner()); + TOKEN_ADMIN_REGISTRY.proposeAdministrator(MiscEthereum.GHO_TOKEN, TOKEN_ADMIN_REGISTRY.owner()); + TOKEN_ADMIN_REGISTRY.acceptAdminRole(MiscEthereum.GHO_TOKEN); + TOKEN_ADMIN_REGISTRY.setPool(MiscEthereum.GHO_TOKEN, address(proxyPool)); + vm.stopPrank(); + assertEq(TOKEN_ADMIN_REGISTRY.getPool(MiscEthereum.GHO_TOKEN), address(proxyPool)); + + assertEq(proxyPool.getRouter(), address(router)); + + IProxyPool.ChainUpdate[] memory chains = new IProxyPool.ChainUpdate[](1); + chains[0] = IProxyPool.ChainUpdate({ + remoteChainSelector: ARB_CHAIN_SELECTOR, + remotePoolAddress: abi.encode(ARB_PROXY_POOL), + remoteTokenAddress: abi.encode(address(AaveV3ArbitrumAssets.GHO_UNDERLYING)), + allowed: true, + outboundRateLimiterConfig: IRateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}), + inboundRateLimiterConfig: IRateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}) + }); + + vm.prank(proxyPool.owner()); + proxyPool.applyChainUpdates(chains); + + assertTrue(proxyPool.isSupportedChain(ARB_CHAIN_SELECTOR)); + + IRouter.OnRamp[] memory onRampUpdates = new IRouter.OnRamp[](1); + onRampUpdates[0] = IRouter.OnRamp({ + destChainSelector: ARB_CHAIN_SELECTOR, + onRamp: ON_RAMP_1_5 // new onRamp + }); + IRouter.OffRamp[] memory offRampUpdates = new IRouter.OffRamp[](1); + offRampUpdates[0] = IRouter.OffRamp({ + sourceChainSelector: ARB_CHAIN_SELECTOR, + offRamp: OFF_RAMP_1_5 // new offRamp + }); + + vm.prank(router.owner()); + router.applyRampUpdates(onRampUpdates, new IRouter.OffRamp[](0), offRampUpdates); + } + + function _getTokenMessage( + CCIPSendParams memory params + ) internal returns (IClient.EVM2AnyMessage memory, IInternal.EVM2EVMMessage memory) { + IClient.EVM2AnyMessage memory message = CCIPUtils.generateMessage(alice, 1); + message.tokenAmounts[0] = IClient.EVMTokenAmount({ + token: MiscEthereum.GHO_TOKEN, + amount: params.amount + }); + + uint256 feeAmount = params.router.getFee(ARB_CHAIN_SELECTOR, message); + deal(alice, feeAmount); + + IInternal.EVM2EVMMessage memory eventArg = CCIPUtils.messageToEvent( + CCIPUtils.MessageToEventParams({ + message: message, + router: params.router, + sourceChainSelector: ETH_CHAIN_SELECTOR, + feeTokenAmount: feeAmount, + originalSender: alice, + destinationToken: AaveV3ArbitrumAssets.GHO_UNDERLYING, + migrated: params.migrated + }) + ); + + return (message, eventArg); + } + + function _getImplementation(address proxy) private view returns (address) { + bytes32 slot = bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1); + return address(uint160(uint256(vm.load(proxy, slot)))); + } + + function _readInitialized(address proxy) private view returns (uint8) { + /// slot 0 + // <1 byte ^ 1 byte ^ ---------- 20 bytes ----------> + // initialized initializing owner + return uint8(uint256(vm.load(proxy, bytes32(0)))); + } + + function _getStaticParams() private view returns (bytes memory) { + return + abi.encode( + address(ghoTokenPool.getToken()), + ghoTokenPool.getAllowList(), + ghoTokenPool.getArmProxy(), + ghoTokenPool.canAcceptLiquidity(), + ghoTokenPool.getRouter() + ); + } + + function _getDynamicParams() private view returns (bytes memory) { + return + abi.encode( + ghoTokenPool.owner(), + ghoTokenPool.getBridgeLimitAdmin(), + ghoTokenPool.getRateLimitAdmin(), + ghoTokenPool.getRebalancer(), + ghoTokenPool.getSupportedChains(), + ghoTokenPool.getLockReleaseInterfaceId(), + ghoTokenPool.getBridgeLimit(), + ghoTokenPool.getCurrentBridgedAmount(), + ghoTokenPool.getRateLimitAdmin() + ); + } + + function _validateConstants() private view { + assertEq(TOKEN_ADMIN_REGISTRY.typeAndVersion(), 'TokenAdminRegistry 1.5.0'); + assertEq(proxyPool.typeAndVersion(), 'LockReleaseTokenPoolAndProxy 1.5.0'); + assertEq(ITypeAndVersion(ON_RAMP_1_2).typeAndVersion(), 'EVM2EVMOnRamp 1.2.0'); + assertEq(ITypeAndVersion(ON_RAMP_1_5).typeAndVersion(), 'EVM2EVMOnRamp 1.5.0'); + assertEq(ITypeAndVersion(OFF_RAMP_1_2).typeAndVersion(), 'EVM2EVMOffRamp 1.2.0'); + assertEq(ITypeAndVersion(OFF_RAMP_1_5).typeAndVersion(), 'EVM2EVMOffRamp 1.5.0'); + + assertEq(GHO_CCIP_STEWARD.GHO_TOKEN(), MiscEthereum.GHO_TOKEN); + assertEq(GHO_CCIP_STEWARD.GHO_TOKEN_POOL(), address(ghoTokenPool)); + } + + function _getOutboundRefillTime(uint256 amount) private view returns (uint256) { + uint128 rate = proposal.getOutBoundRateLimiterConfig().rate; + assertNotEq(rate, 0); + return amount / uint256(rate) + 1; // account for rounding + } + + function _getInboundRefillTime(uint256 amount) private view returns (uint256) { + uint128 rate = proposal.getInBoundRateLimiterConfig().rate; + assertNotEq(rate, 0); + return amount / uint256(rate) + 1; // account for rounding + } + + function _tokenBucketToConfig( + RateLimiter.TokenBucket memory bucket + ) private pure returns (RateLimiter.Config memory) { + return + RateLimiter.Config({ + isEnabled: bucket.isEnabled, + capacity: bucket.capacity, + rate: bucket.rate + }); + } + + function _getDisabledConfig() private pure returns (RateLimiter.Config memory) { + return RateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}); + } +} diff --git a/src/20241021_Multi_GHOCCIP150Upgrade/GHOCCIP150Upgrade.md b/src/20241021_Multi_GHOCCIP150Upgrade/GHOCCIP150Upgrade.md new file mode 100644 index 000000000..de8f4b8b5 --- /dev/null +++ b/src/20241021_Multi_GHOCCIP150Upgrade/GHOCCIP150Upgrade.md @@ -0,0 +1,22 @@ +--- +title: "GHO CCIP 1.50 Upgrade" +author: "Aave Labs" +discussions: "https://governance.aave.com/t/bgd-technical-maintenance-proposals/15274/51" +--- + +## Simple Summary + +## Motivation + +## Specification + +## References + +- Implementation: [AaveV3Ethereum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Ethereum_GHOCCIP150Upgrade_20241021.sol), [AaveV3Arbitrum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Arbitrum_GHOCCIP150Upgrade_20241021.sol) +- Tests: [AaveV3Ethereum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Ethereum_GHOCCIP150Upgrade_20241021.t.sol), [AaveV3Arbitrum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Arbitrum_GHOCCIP150Upgrade_20241021.t.sol) +- [Snapshot](TODO) +- [Discussion](https://governance.aave.com/t/bgd-technical-maintenance-proposals/15274/51) + +## Copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). diff --git a/src/20241021_Multi_GHOCCIP150Upgrade/GHOCCIP150Upgrade_20241021.s.sol b/src/20241021_Multi_GHOCCIP150Upgrade/GHOCCIP150Upgrade_20241021.s.sol new file mode 100644 index 000000000..728f912ec --- /dev/null +++ b/src/20241021_Multi_GHOCCIP150Upgrade/GHOCCIP150Upgrade_20241021.s.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {GovV3Helpers, IPayloadsControllerCore, PayloadsControllerUtils} from 'aave-helpers/src/GovV3Helpers.sol'; +import {GovernanceV3Ethereum} from 'aave-address-book/GovernanceV3Ethereum.sol'; +import {EthereumScript, ArbitrumScript} from 'solidity-utils/contracts/utils/ScriptUtils.sol'; +import {AaveV3Ethereum_GHOCCIP150Upgrade_20241021} from './AaveV3Ethereum_GHOCCIP150Upgrade_20241021.sol'; +import {AaveV3Arbitrum_GHOCCIP150Upgrade_20241021} from './AaveV3Arbitrum_GHOCCIP150Upgrade_20241021.sol'; + +/** + * @dev Deploy Ethereum + * deploy-command: make deploy-ledger contract=src/20241021_Multi_GHOCCIP150Upgrade/GHOCCIP150Upgrade_20241021.s.sol:DeployEthereum chain=mainnet + * verify-command: FOUNDRY_PROFILE=mainnet npx catapulta-verify -b broadcast/GHOCCIP150Upgrade_20241021.s.sol/1/run-latest.json + */ +contract DeployEthereum is EthereumScript { + function run() external broadcast { + // deploy payloads + address payload0 = GovV3Helpers.deployDeterministic( + type(AaveV3Ethereum_GHOCCIP150Upgrade_20241021).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/20241021_Multi_GHOCCIP150Upgrade/GHOCCIP150Upgrade_20241021.s.sol:DeployArbitrum chain=arbitrum + * verify-command: FOUNDRY_PROFILE=arbitrum npx catapulta-verify -b broadcast/GHOCCIP150Upgrade_20241021.s.sol/42161/run-latest.json + */ +contract DeployArbitrum is ArbitrumScript { + function run() external broadcast { + // deploy payloads + address payload0 = GovV3Helpers.deployDeterministic( + type(AaveV3Arbitrum_GHOCCIP150Upgrade_20241021).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/20241021_Multi_GHOCCIP150Upgrade/GHOCCIP150Upgrade_20241021.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_GHOCCIP150Upgrade_20241021).creationCode + ); + payloads[0] = GovV3Helpers.buildMainnetPayload(vm, actionsEthereum); + + IPayloadsControllerCore.ExecutionAction[] + memory actionsArbitrum = new IPayloadsControllerCore.ExecutionAction[](1); + actionsArbitrum[0] = GovV3Helpers.buildAction( + type(AaveV3Arbitrum_GHOCCIP150Upgrade_20241021).creationCode + ); + payloads[1] = GovV3Helpers.buildArbitrumPayload(vm, actionsArbitrum); + + // create proposal + vm.startBroadcast(); + GovV3Helpers.createProposal( + vm, + payloads, + GovernanceV3Ethereum.VOTING_PORTAL_ETH_POL, + GovV3Helpers.ipfsHashFile(vm, 'src/20241021_Multi_GHOCCIP150Upgrade/GHOCCIP150Upgrade.md') + ); + } +} diff --git a/src/20241021_Multi_GHOCCIP150Upgrade/config.ts b/src/20241021_Multi_GHOCCIP150Upgrade/config.ts new file mode 100644 index 000000000..6c7e1fdad --- /dev/null +++ b/src/20241021_Multi_GHOCCIP150Upgrade/config.ts @@ -0,0 +1,17 @@ +import {ConfigFile} from '../../generator/types'; +export const config: ConfigFile = { + rootOptions: { + pools: ['AaveV3Ethereum', 'AaveV3Arbitrum'], + title: 'GHO CCIP 1.5.0 Upgrade', + shortName: 'GHOCCIP150Upgrade', + date: '20241021', + author: 'Aave Labs', + discussion: 'https://governance.aave.com/t/bgd-technical-maintenance-proposals/15274/51', + snapshot: '', + votingNetwork: 'POLYGON', + }, + poolOptions: { + AaveV3Ethereum: {configs: {OTHERS: {}}, cache: {blockNumber: 21045560}}, + AaveV3Arbitrum: {configs: {OTHERS: {}}, cache: {blockNumber: 267660907}}, + }, +}; diff --git a/src/20241021_Multi_GHOCCIP150Upgrade/utils/CCIPUtils.sol b/src/20241021_Multi_GHOCCIP150Upgrade/utils/CCIPUtils.sol new file mode 100644 index 000000000..c73b30175 --- /dev/null +++ b/src/20241021_Multi_GHOCCIP150Upgrade/utils/CCIPUtils.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IClient} from 'src/interfaces/ccip/IClient.sol'; +import {IRouter} from 'src/interfaces/ccip/IRouter.sol'; +import {IInternal} from 'src/interfaces/ccip/IInternal.sol'; +import {IEVM2EVMOnRamp} from 'src/interfaces/ccip/IEVM2EVMOnRamp.sol'; + +library CCIPUtils { + uint64 internal constant ETH_CHAIN_SELECTOR = 5009297550715157269; + uint64 internal constant ARB_CHAIN_SELECTOR = 4949039107694359620; + + bytes32 internal constant LEAF_DOMAIN_SEPARATOR = + 0x0000000000000000000000000000000000000000000000000000000000000000; + bytes32 internal constant INTERNAL_DOMAIN_SEPARATOR = + 0x0000000000000000000000000000000000000000000000000000000000000001; + bytes32 internal constant EVM_2_EVM_MESSAGE_HASH = keccak256('EVM2EVMMessageHashV2'); + bytes4 public constant EVM_EXTRA_ARGS_V1_TAG = 0x97a657c9; + + struct SourceTokenData { + bytes sourcePoolAddress; + bytes destTokenAddress; + bytes extraData; + uint32 destGasAmount; + } + + struct MessageToEventParams { + IClient.EVM2AnyMessage message; + IRouter router; + uint64 sourceChainSelector; + uint256 feeTokenAmount; + address originalSender; + address destinationToken; + bool migrated; + } + + function generateMessage( + address receiver, + uint256 tokenAmountsLength + ) internal pure returns (IClient.EVM2AnyMessage memory) { + return + IClient.EVM2AnyMessage({ + receiver: abi.encode(receiver), + data: '', + tokenAmounts: new IClient.EVMTokenAmount[](tokenAmountsLength), + feeToken: address(0), + extraArgs: argsToBytes(IClient.EVMExtraArgsV1({gasLimit: 0})) + }); + } + + function messageToEvent( + MessageToEventParams memory params + ) public view returns (IInternal.EVM2EVMMessage memory) { + uint64 destChainSelector = params.sourceChainSelector == ETH_CHAIN_SELECTOR + ? ARB_CHAIN_SELECTOR + : ETH_CHAIN_SELECTOR; + IEVM2EVMOnRamp onRamp = IEVM2EVMOnRamp(params.router.getOnRamp(destChainSelector)); + + bytes memory args = new bytes(params.message.extraArgs.length - 4); + for (uint256 i = 4; i < params.message.extraArgs.length; ++i) { + args[i - 4] = params.message.extraArgs[i]; + } + + IInternal.EVM2EVMMessage memory messageEvent = IInternal.EVM2EVMMessage({ + sequenceNumber: onRamp.getExpectedNextSequenceNumber(), + feeTokenAmount: params.feeTokenAmount, + sender: params.originalSender, + nonce: onRamp.getSenderNonce(params.originalSender) + 1, + gasLimit: abi.decode(args, (IClient.EVMExtraArgsV1)).gasLimit, + strict: false, + sourceChainSelector: params.sourceChainSelector, + receiver: abi.decode(params.message.receiver, (address)), + data: params.message.data, + tokenAmounts: params.message.tokenAmounts, + sourceTokenData: new bytes[](params.message.tokenAmounts.length), + feeToken: params.router.getWrappedNative(), + messageId: '' + }); + + // change introduced in CCIP 1.5, hence we only apply if CCIP has migrated to 1.5 + if (params.migrated) { + for (uint256 i; i < params.message.tokenAmounts.length; ++i) { + messageEvent.sourceTokenData[i] = abi.encode( + SourceTokenData({ + sourcePoolAddress: abi.encode( + onRamp.getPoolBySourceToken(destChainSelector, params.message.tokenAmounts[i].token) + ), + destTokenAddress: abi.encode(params.destinationToken), + extraData: '', + destGasAmount: getDestGasAmount(onRamp, params.message.tokenAmounts[i].token) + }) + ); + } + } + + messageEvent.messageId = hash( + messageEvent, + generateMetadataHash(params.sourceChainSelector, destChainSelector, address(onRamp)) + ); + return messageEvent; + } + + function generateMetadataHash( + uint64 sourceChainSelector, + uint64 destChainSelector, + address onRamp + ) internal pure returns (bytes32) { + return + keccak256(abi.encode(EVM_2_EVM_MESSAGE_HASH, sourceChainSelector, destChainSelector, onRamp)); + } + + function argsToBytes( + IClient.EVMExtraArgsV1 memory extraArgs + ) internal pure returns (bytes memory bts) { + return abi.encodeWithSelector(EVM_EXTRA_ARGS_V1_TAG, extraArgs); + } + + /// @dev Used to hash messages for single-lane ramps. + /// OnRamp hash(EVM2EVMMessage) = OffRamp hash(EVM2EVMMessage) + /// The EVM2EVMMessage's messageId is expected to be the output of this hash function + /// @param original Message to hash + /// @param metadataHash Immutable metadata hash representing a lane with a fixed OnRamp + /// @return hashedMessage hashed message as a keccak256 + function hash( + IInternal.EVM2EVMMessage memory original, + bytes32 metadataHash + ) internal pure returns (bytes32) { + // Fixed-size message fields are included in nested hash to reduce stack pressure. + // This hashing scheme is also used by RMN. If changing it, please notify the RMN maintainers. + return + keccak256( + abi.encode( + LEAF_DOMAIN_SEPARATOR, + metadataHash, + keccak256( + abi.encode( + original.sender, + original.receiver, + original.sequenceNumber, + original.gasLimit, + original.strict, + original.nonce, + original.feeToken, + original.feeTokenAmount + ) + ), + keccak256(original.data), + keccak256(abi.encode(original.tokenAmounts)), + keccak256(abi.encode(original.sourceTokenData)) + ) + ); + } + + function getDestGasAmount(IEVM2EVMOnRamp onRamp, address token) internal view returns (uint32) { + IEVM2EVMOnRamp.TokenTransferFeeConfig memory config = onRamp.getTokenTransferFeeConfig(token); + return + config.isEnabled + ? config.destGasOverhead + : onRamp.getDynamicConfig().defaultTokenDestGasOverhead; + } +} diff --git a/src/interfaces/ccip/IEVM2EVMOffRamp.sol b/src/interfaces/ccip/IEVM2EVMOffRamp.sol index b900be64b..7f8b84385 100644 --- a/src/interfaces/ccip/IEVM2EVMOffRamp.sol +++ b/src/interfaces/ccip/IEVM2EVMOffRamp.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import {IInternal} from 'src/interfaces/ccip/IInternal.sol'; -interface IEVM2EVMOffRamp { +interface IEVM2EVMOffRamp_1_2 { /// @notice Execute a single message. /// @param message The message that will be executed. /// @param offchainTokenData Token transfer data to be passed to TokenPool. @@ -17,3 +17,18 @@ interface IEVM2EVMOffRamp { bytes[] memory offchainTokenData ) external; } + +interface IEVM2EVMOffRamp_1_5 { + /// @notice Execute a single message. + /// @param message The message that will be executed. + /// @param offchainTokenData Token transfer data to be passed to TokenPool. + /// @dev We make this external and callable by the contract itself, in order to try/catch + /// its execution and enforce atomicity among successful message processing and token transfer. + /// @dev We use ERC-165 to check for the ccipReceive interface to permit sending tokens to contracts + /// (for example smart contract wallets) without an associated message. + function executeSingleMessage( + IInternal.EVM2EVMMessage calldata message, + bytes[] calldata offchainTokenData, + uint32[] memory tokenGasOverrides + ) external; +} diff --git a/src/interfaces/ccip/IEVM2EVMOnRamp.sol b/src/interfaces/ccip/IEVM2EVMOnRamp.sol new file mode 100644 index 000000000..20efb0cfd --- /dev/null +++ b/src/interfaces/ccip/IEVM2EVMOnRamp.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import {IInternal} from './IInternal.sol'; + +interface IEVM2EVMOnRamp { + struct TokenTransferFeeConfig { + uint32 minFeeUSDCents; // ──────────╮ Minimum fee to charge per token transfer, multiples of 0.01 USD + uint32 maxFeeUSDCents; // │ Maximum fee to charge per token transfer, multiples of 0.01 USD + uint16 deciBps; // │ Basis points charged on token transfers, multiples of 0.1bps, or 1e-5 + uint32 destGasOverhead; // │ Gas charged to execute the token transfer on the destination chain + // │ Extra data availability bytes that are returned from the source pool and sent + uint32 destBytesOverhead; // │ to the destination pool. Must be >= Pool.CCIP_LOCK_OR_BURN_V1_RET_BYTES + bool aggregateRateLimitEnabled; // │ Whether this transfer token is to be included in Aggregate Rate Limiting + bool isEnabled; // ─────────────────╯ Whether this token has custom transfer fees + } + + struct DynamicConfig { + address router; // ──────────────────────────╮ Router address + uint16 maxNumberOfTokensPerMsg; // │ Maximum number of distinct ERC20 token transferred per message + uint32 destGasOverhead; // │ Gas charged on top of the gasLimit to cover destination chain costs + uint16 destGasPerPayloadByte; // │ Destination chain gas charged for passing each byte of `data` payload to receiver + uint32 destDataAvailabilityOverheadGas; // ──╯ Extra data availability gas charged on top of the message, e.g. for OCR + uint16 destGasPerDataAvailabilityByte; // ───╮ Amount of gas to charge per byte of message data that needs availability + uint16 destDataAvailabilityMultiplierBps; // │ Multiplier for data availability gas, multiples of bps, or 0.0001 + address priceRegistry; // │ Price registry address + uint32 maxDataBytes; // │ Maximum payload data size in bytes + uint32 maxPerMsgGasLimit; // ────────────────╯ Maximum gas limit for messages targeting EVMs + // │ + // The following three properties are defaults, they can be overridden by setting the TokenTransferFeeConfig for a token + uint16 defaultTokenFeeUSDCents; // ──────────╮ Default token fee charged per token transfer + uint32 defaultTokenDestGasOverhead; // │ Default gas charged to execute the token transfer on the destination chain + bool enforceOutOfOrder; // ──────────────────╯ Whether to enforce the allowOutOfOrderExecution extraArg value to be true. + } + + /// @notice Gets the next sequence number to be used in the onRamp + /// @return the next sequence number to be used + function getExpectedNextSequenceNumber() external view returns (uint64); + + /// @notice Get the next nonce for a given sender + /// @param sender The sender to get the nonce for + /// @return nonce The next nonce for the sender + function getSenderNonce(address sender) external view returns (uint64 nonce); + + /// @notice Adds and removed token pools. + /// @param removes The tokens and pools to be removed + /// @param adds The tokens and pools to be added. + function applyPoolUpdates( + IInternal.PoolUpdate[] memory removes, + IInternal.PoolUpdate[] memory adds + ) external; + + /// @notice Get the pool for a specific token + /// @param destChainSelector The destination chain selector + /// @param sourceToken The source chain token to get the pool for + /// @return pool Token pool + function getPoolBySourceToken( + uint64 destChainSelector, + address sourceToken + ) external view returns (address); + + /// @notice Gets the transfer fee config for a given token. + function getTokenTransferFeeConfig( + address token + ) external view returns (TokenTransferFeeConfig memory tokenTransferFeeConfig); + + /// @notice Returns the dynamic onRamp config. + /// @return dynamicConfig the configuration. + function getDynamicConfig() external view returns (DynamicConfig memory dynamicConfig); +} diff --git a/src/interfaces/ccip/IInternal.sol b/src/interfaces/ccip/IInternal.sol index fa8c78f6d..062ee93da 100644 --- a/src/interfaces/ccip/IInternal.sol +++ b/src/interfaces/ccip/IInternal.sol @@ -5,6 +5,11 @@ pragma solidity ^0.8.0; import {IClient} from 'src/interfaces/ccip/IClient.sol'; interface IInternal { + struct PoolUpdate { + address token; // The IERC20 token address + address pool; // The token pool address + } + /// @notice The cross chain message that gets committed to EVM chains. /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. struct EVM2EVMMessage { diff --git a/src/interfaces/ccip/IProxyPool.sol b/src/interfaces/ccip/IProxyPool.sol new file mode 100644 index 000000000..a5696ffd3 --- /dev/null +++ b/src/interfaces/ccip/IProxyPool.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {ITypeAndVersion} from './ITypeAndVersion.sol'; +import {IRateLimiter} from './IRateLimiter.sol'; + +interface IProxyPool is ITypeAndVersion { + struct ChainUpdate { + uint64 remoteChainSelector; + bool allowed; + bytes remotePoolAddress; + bytes remoteTokenAddress; + IRateLimiter.Config inboundRateLimiterConfig; + IRateLimiter.Config outboundRateLimiterConfig; + } + + function owner() external view returns (address); + function transferOwnership(address newOwner) external; + function acceptOwnership() external; + function getRouter() external view returns (address); + function setRouter(address router) external; + function getRemotePool(uint64 chainSelector) external view returns (bytes memory); + function applyChainUpdates(ChainUpdate[] memory updates) external; + function isSupportedChain(uint64 chainSelector) external view returns (bool); +} diff --git a/src/interfaces/ccip/IRateLimiter.sol b/src/interfaces/ccip/IRateLimiter.sol new file mode 100644 index 000000000..d4cb9ffa4 --- /dev/null +++ b/src/interfaces/ccip/IRateLimiter.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +interface IRateLimiter { + struct TokenBucket { + uint128 tokens; // ──────╮ Current number of tokens that are in the bucket. + uint32 lastUpdated; // │ Timestamp in seconds of the last token refill, good for 100+ years. + bool isEnabled; // ──────╯ Indication whether the rate limiting is enabled or not + uint128 capacity; // ────╮ Maximum number of tokens that can be in the bucket. + uint128 rate; // ────────╯ Number of tokens per second that the bucket is refilled. + } + + struct Config { + bool isEnabled; // Indication whether the rate limiting should be enabled + uint128 capacity; // ────╮ Specifies the capacity of the rate limiter + uint128 rate; // ───────╯ Specifies the rate of the rate limiter + } +} diff --git a/src/interfaces/ccip/IRouter.sol b/src/interfaces/ccip/IRouter.sol index 4e524b42f..89c9275b2 100644 --- a/src/interfaces/ccip/IRouter.sol +++ b/src/interfaces/ccip/IRouter.sol @@ -5,23 +5,38 @@ pragma solidity ^0.8.0; import {IClient} from 'src/interfaces/ccip/IClient.sol'; interface IRouter { - /// @notice Request a message to be sent to the destination chain - /// @param destinationChainSelector The destination chain ID - /// @param message The cross-chain CCIP message including data and/or tokens - /// @return messageId The message ID - /// @dev Note if msg.value is larger than the required fee (from getFee) we accept - /// the overpayment with no refund. - /// @dev Reverts with appropriate reason upon invalid message. + error UnsupportedDestinationChain(uint64 destChainSelector); + error InsufficientFeeTokenAmount(); + error InvalidMsgValue(); + + struct OnRamp { + uint64 destChainSelector; + address onRamp; + } + struct OffRamp { + uint64 sourceChainSelector; + address offRamp; + } + + function owner() external view returns (address); + function getWrappedNative() external view returns (address); + function getOffRamps() external view returns (OffRamp[] memory); + function getOnRamp(uint64 destChainSelector) external view returns (address onRampAddress); + function isOffRamp( + uint64 sourceChainSelector, + address offRamp + ) external view returns (bool isOffRamp); + function applyRampUpdates( + OnRamp[] calldata onRampUpdates, + OffRamp[] calldata offRampRemoves, + OffRamp[] calldata offRampAdds + ) external; + function isChainSupported(uint64 chainSelector) external view returns (bool supported); + function getSupportedTokens(uint64 chainSelector) external view returns (address[] memory tokens); function ccipSend( uint64 destinationChainSelector, IClient.EVM2AnyMessage memory message ) external payable returns (bytes32); - - /// @param destinationChainSelector The destination chainSelector - /// @param message The cross-chain CCIP message including data and/or tokens - /// @return fee returns execution fee for the message - /// delivery to destination chain, denominated in the feeToken specified in the message. - /// @dev Reverts with appropriate reason upon invalid message. function getFee( uint64 destinationChainSelector, IClient.EVM2AnyMessage memory message diff --git a/src/interfaces/ccip/ITokenAdminRegistry.sol b/src/interfaces/ccip/ITokenAdminRegistry.sol new file mode 100644 index 000000000..05667ccba --- /dev/null +++ b/src/interfaces/ccip/ITokenAdminRegistry.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {ITypeAndVersion} from './ITypeAndVersion.sol'; +interface ITokenAdminRegistry is ITypeAndVersion { + function owner() external view returns (address); + function acceptAdminRole(address from) external; + function proposeAdministrator(address localToken, address administrator) external; + function transferAdminRole(address localToken, address newAdministrator) external; + function isAdministrator(address localToken, address administrator) external view returns (bool); + function getPool(address token) external view returns (address); + function setPool(address source, address pool) external; +} diff --git a/src/interfaces/ccip/ITypeAndVersion.sol b/src/interfaces/ccip/ITypeAndVersion.sol new file mode 100644 index 000000000..135f6d0ae --- /dev/null +++ b/src/interfaces/ccip/ITypeAndVersion.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface ITypeAndVersion { + function typeAndVersion() external pure returns (string memory); +}