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 @@
+
\ 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);
+}