From c016f11d2d921748459bb0aaca37f462b2639e1a Mon Sep 17 00:00:00 2001 From: miguelmtz <36620902+miguelmtzinf@users.noreply.github.com> Date: Thu, 7 Nov 2024 11:27:06 +0100 Subject: [PATCH] GHO CCIP 1.5 Maintenance Upgrade (#512) * Add proposal for Gho Incident Report 20231113 (#1) * chore: add payload and deploy script for update of GHO variable debt token * forge install: gho-core * chore: add gho-core to dependency * test: Add tests for update of gho variable token * test: Add tests for update of gho variable token * fix: add modifier in method of interface * fix: remove gho dependency from repo and fix test * fix: Remove unnecesary dependency * fix: Add latest details --------- Co-authored-by: miguelmtzinf * fix: Make new impl constant (#3) * fix: Amend AIP text (#4) * fix: Make new impl constant * fix: Fix AIP text * test: Tweak default tests with borrow cap update (#5) * fix: lint issue (#6) * test: Add diffs from test running (#7) * fix: Add payload address (#8) * fix: Fix payload address in script (#9) * fix: Remove unneeded diff file (#10) * feat: install aave-ccip at latest commit on aave/ccip#18 * chore: upd remappings, lib/aave-helpers, .assets * new: add proposal for eth & arb * fix: arb test supply locked invariant * fix: rename, add discussion link * upd: aave-ccip module * feat: add proxy pool deployments * feat: add e2e tests * chore: fix license, explicit senders for expectEmit * ci: override rpc url in secrets * test: proxy pool can off ramp * chore: reorder deps * nit: utilize existing ierc20 import, style guide, use arb assets for gho addy * feat: add rate limit, use gho fascilitator for tests * feat: set in bound rate limit as well * chore: cleanup * test: check proxy initialized version * chore: inBetween => inFlight" * chore: test name consistency * test: dynamically fetch dest gas amt * test: execMsg post migration using legacy off ramp * test: ccip steward can disable rate limit * chore: cleanup * doc: add comment for fetching all offramps, use RISK_COUNCIL for clarity * test: ownership transfer after migration * chore: rm _readRiskAdmin, fix config * fix: ci test setup to stay compatible with bdg upstream * feat: rm aave-ccip dep * rft: use deployed token pools * tmp: fix ci for aave org, to rm for bdg * fix: ci to bdg setup * fix: update rate limit * test: fuzz amounts * chore: snapshot docs * chore: const cap rate * chore: fix type * fix: Fix rate limit config constants * fix: tests, impl correct solc * fix: tests * Update src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Ethereum_GHOCCIP150Upgrade_20241021.sol Co-authored-by: Harsh Pandey * Update src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Arbitrum_GHOCCIP150Upgrade_20241021.sol Co-authored-by: Harsh Pandey * doc: reference token pool contracts * doc: upgrade spec * doc: formatting * Update src/20241021_Multi_GHOCCIP150Upgrade/GHOCCIP150Upgrade.md Co-authored-by: miguelmtz <36620902+miguelmtzinf@users.noreply.github.com> * Update src/20241021_Multi_GHOCCIP150Upgrade/GHOCCIP150Upgrade.md Co-authored-by: miguelmtz <36620902+miguelmtzinf@users.noreply.github.com> * test: Refresh block number with ghostewards aip executed * fix: Remove unneeded script --------- Co-authored-by: Parth Patel Co-authored-by: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Co-authored-by: Harsh Pandey --- ...a040db3fc4a88618783a9156e7fb51928e1ede.svg | 1 + ...dc42930b2b94af37ef869d012c4734aedb52e6.svg | 1 + ...fe490affc1bc77fe6675127b18e9aeeb4b1404.svg | 1 + ...cfb1e84443509c42e21db9e4320c00e1db6098.svg | 1 + ...99e5ae4f4fd5b0d2565edd912a160acb2d0b78.svg | 1 + ...e6ba583d4d35e38cb678120492b2b4d52d19ea.svg | 1 + ...a0fb636d34a35d35ce67919bc0d7d2e36d3839.svg | 1 + ...1fbd79406b4b3a8c64ec79ef429f7c422d0387.svg | 1 + ...9d5ba5e949c7896bc79e7fdd98872cb43375b0.svg | 1 + ...a3707c0ded91bbfde8953567853db55452a227.svg | 1 + ...d5ff798c0771402486c6f1ca63f2a1868d80d9.svg | 1 + ...20a9e881eaa3f3def4916e47e0c330b34ab151.svg | 1 + ...bd5b9335471c83b44949206c9b2186657b836f.svg | 1 + ...455cedf818a43aba043e71714721fd1e844501.svg | 1 + ...c797fb2b1f421fa40a6d9a0028d1f78db71dc0.svg | 1 + ...e454df22dbd82f0c1929bc14104d54c7081a49.svg | 1 + ...a25c38caec024924773d1a5c7c63ab45c4eecf.svg | 1 + ...c705464b3515a654de9893168a1b1321feccb4.svg | 1 + ...784d12784a084406c3794fbe177e93da4c3479.svg | 1 + ...faa118ed690be4e95fae01b8535036cda67e3d.svg | 1 + ...a0275265838d424840d721917e0a4e3a5d8044.svg | 1 + ...33ca939f6fc30c2fd799aaa6f59b0521c19e9f.svg | 1 + ...29e34ea67f12eea11bf5403763207eeab38cff.svg | 1 + ...77de9f9a155f6a0e47e0443845a3c9de5f6b84.svg | 1 + ...b223a18f5aad36d9f6087d1d468b4dd5ba56a1.svg | 1 + ...d51b9f960b17304a3bdcaf8922d3ae84b3c30f.svg | 1 + ...edb30554ccb68f317b952113777790df5ca547.svg | 1 + ...c7eef36a7e70a1f1b35633097516fe5e21508b.svg | 1 + ...2b4dc8236ac87f8058f121d86de1d99e029c5c.svg | 1 + ...6ebf3fa05cf980f374598b56757dbc5cae3662.svg | 1 + ...79e1c7a3e943262824d4f683059c85d91a233d.svg | 1 + ...bitrum_GHOCCIP150Upgrade_20241021_after.md | 7 + ...hereum_GHOCCIP150Upgrade_20241021_after.md | 7 + ...eV3Arbitrum_GHOCCIP150Upgrade_20241021.sol | 51 + ...3Arbitrum_GHOCCIP150Upgrade_20241021.t.sol | 493 ++++++++++ ...AaveV3E2E_GHOCCIP150Upgrade_20241021.t.sol | 873 ++++++++++++++++++ ...eV3Ethereum_GHOCCIP150Upgrade_20241021.sol | 51 + ...3Ethereum_GHOCCIP150Upgrade_20241021.t.sol | 501 ++++++++++ .../GHOCCIP150Upgrade.md | 35 + .../GHOCCIP150Upgrade_20241021.s.sol | 87 ++ .../config.ts | 17 + .../utils/CCIPUtils.sol | 161 ++++ src/interfaces/ccip/IEVM2EVMOffRamp.sol | 17 +- src/interfaces/ccip/IEVM2EVMOnRamp.sol | 71 ++ src/interfaces/ccip/IInternal.sol | 5 + src/interfaces/ccip/IProxyPool.sol | 33 + src/interfaces/ccip/IRateLimiter.sol | 19 + src/interfaces/ccip/IRouter.sol | 41 +- src/interfaces/ccip/ITokenAdminRegistry.sol | 13 + src/interfaces/ccip/ITypeAndVersion.sol | 6 + .../ccip/IUpgradeableBurnMintTokenPool.sol | 93 ++ .../ccip/IUpgradeableLockReleaseTokenPool.sol | 133 ++- 52 files changed, 2703 insertions(+), 42 deletions(-) create mode 100644 .assets/02a040db3fc4a88618783a9156e7fb51928e1ede.svg create mode 100644 .assets/03dc42930b2b94af37ef869d012c4734aedb52e6.svg create mode 100644 .assets/0afe490affc1bc77fe6675127b18e9aeeb4b1404.svg create mode 100644 .assets/0fcfb1e84443509c42e21db9e4320c00e1db6098.svg create mode 100644 .assets/1899e5ae4f4fd5b0d2565edd912a160acb2d0b78.svg create mode 100644 .assets/18e6ba583d4d35e38cb678120492b2b4d52d19ea.svg create mode 100644 .assets/1aa0fb636d34a35d35ce67919bc0d7d2e36d3839.svg create mode 100644 .assets/1c1fbd79406b4b3a8c64ec79ef429f7c422d0387.svg create mode 100644 .assets/2e9d5ba5e949c7896bc79e7fdd98872cb43375b0.svg create mode 100644 .assets/39a3707c0ded91bbfde8953567853db55452a227.svg create mode 100644 .assets/39d5ff798c0771402486c6f1ca63f2a1868d80d9.svg create mode 100644 .assets/4820a9e881eaa3f3def4916e47e0c330b34ab151.svg create mode 100644 .assets/5dbd5b9335471c83b44949206c9b2186657b836f.svg create mode 100644 .assets/5f455cedf818a43aba043e71714721fd1e844501.svg create mode 100644 .assets/7bc797fb2b1f421fa40a6d9a0028d1f78db71dc0.svg create mode 100644 .assets/7ee454df22dbd82f0c1929bc14104d54c7081a49.svg create mode 100644 .assets/8aa25c38caec024924773d1a5c7c63ab45c4eecf.svg create mode 100644 .assets/8fc705464b3515a654de9893168a1b1321feccb4.svg create mode 100644 .assets/9c784d12784a084406c3794fbe177e93da4c3479.svg create mode 100644 .assets/a4faa118ed690be4e95fae01b8535036cda67e3d.svg create mode 100644 .assets/a9a0275265838d424840d721917e0a4e3a5d8044.svg create mode 100644 .assets/ac33ca939f6fc30c2fd799aaa6f59b0521c19e9f.svg create mode 100644 .assets/c229e34ea67f12eea11bf5403763207eeab38cff.svg create mode 100644 .assets/c277de9f9a155f6a0e47e0443845a3c9de5f6b84.svg create mode 100644 .assets/ceb223a18f5aad36d9f6087d1d468b4dd5ba56a1.svg create mode 100644 .assets/d8d51b9f960b17304a3bdcaf8922d3ae84b3c30f.svg create mode 100644 .assets/dcedb30554ccb68f317b952113777790df5ca547.svg create mode 100644 .assets/e6c7eef36a7e70a1f1b35633097516fe5e21508b.svg create mode 100644 .assets/ec2b4dc8236ac87f8058f121d86de1d99e029c5c.svg create mode 100644 .assets/fb6ebf3fa05cf980f374598b56757dbc5cae3662.svg create mode 100644 .assets/fe79e1c7a3e943262824d4f683059c85d91a233d.svg create mode 100644 diffs/AaveV3Arbitrum_GHOCCIP150Upgrade_20241021_before_AaveV3Arbitrum_GHOCCIP150Upgrade_20241021_after.md create mode 100644 diffs/AaveV3Ethereum_GHOCCIP150Upgrade_20241021_before_AaveV3Ethereum_GHOCCIP150Upgrade_20241021_after.md create mode 100644 src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Arbitrum_GHOCCIP150Upgrade_20241021.sol create mode 100644 src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Arbitrum_GHOCCIP150Upgrade_20241021.t.sol create mode 100644 src/20241021_Multi_GHOCCIP150Upgrade/AaveV3E2E_GHOCCIP150Upgrade_20241021.t.sol create mode 100644 src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Ethereum_GHOCCIP150Upgrade_20241021.sol create mode 100644 src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Ethereum_GHOCCIP150Upgrade_20241021.t.sol create mode 100644 src/20241021_Multi_GHOCCIP150Upgrade/GHOCCIP150Upgrade.md create mode 100644 src/20241021_Multi_GHOCCIP150Upgrade/GHOCCIP150Upgrade_20241021.s.sol create mode 100644 src/20241021_Multi_GHOCCIP150Upgrade/config.ts create mode 100644 src/20241021_Multi_GHOCCIP150Upgrade/utils/CCIPUtils.sol create mode 100644 src/interfaces/ccip/IEVM2EVMOnRamp.sol create mode 100644 src/interfaces/ccip/IProxyPool.sol create mode 100644 src/interfaces/ccip/IRateLimiter.sol create mode 100644 src/interfaces/ccip/ITokenAdminRegistry.sol create mode 100644 src/interfaces/ccip/ITypeAndVersion.sol create mode 100644 src/interfaces/ccip/IUpgradeableBurnMintTokenPool.sol diff --git a/.assets/02a040db3fc4a88618783a9156e7fb51928e1ede.svg b/.assets/02a040db3fc4a88618783a9156e7fb51928e1ede.svg new file mode 100644 index 000000000..db81caacb --- /dev/null +++ b/.assets/02a040db3fc4a88618783a9156e7fb51928e1ede.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%20%40%60%80%Optimal utilization 90%Optimal utilization 90% \ No newline at end of file diff --git a/.assets/03dc42930b2b94af37ef869d012c4734aedb52e6.svg b/.assets/03dc42930b2b94af37ef869d012c4734aedb52e6.svg new file mode 100644 index 000000000..95de9c4ed --- /dev/null +++ b/.assets/03dc42930b2b94af37ef869d012c4734aedb52e6.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%100%200%300%Optimal utilization 30%Optimal utilization 30% \ No newline at end of file diff --git a/.assets/0afe490affc1bc77fe6675127b18e9aeeb4b1404.svg b/.assets/0afe490affc1bc77fe6675127b18e9aeeb4b1404.svg new file mode 100644 index 000000000..85a5bf372 --- /dev/null +++ b/.assets/0afe490affc1bc77fe6675127b18e9aeeb4b1404.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%20%40%60%80%Optimal utilization 90%Optimal utilization 90% \ No newline at end of file diff --git a/.assets/0fcfb1e84443509c42e21db9e4320c00e1db6098.svg b/.assets/0fcfb1e84443509c42e21db9e4320c00e1db6098.svg new file mode 100644 index 000000000..5283877c6 --- /dev/null +++ b/.assets/0fcfb1e84443509c42e21db9e4320c00e1db6098.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%50%100%Optimal utilization 80%Optimal utilization 80% \ No newline at end of file diff --git a/.assets/1899e5ae4f4fd5b0d2565edd912a160acb2d0b78.svg b/.assets/1899e5ae4f4fd5b0d2565edd912a160acb2d0b78.svg new file mode 100644 index 000000000..de0657eb7 --- /dev/null +++ b/.assets/1899e5ae4f4fd5b0d2565edd912a160acb2d0b78.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%20%40%60%80%Optimal utilization 80%Optimal utilization 80% \ No newline at end of file diff --git a/.assets/18e6ba583d4d35e38cb678120492b2b4d52d19ea.svg b/.assets/18e6ba583d4d35e38cb678120492b2b4d52d19ea.svg new file mode 100644 index 000000000..db81caacb --- /dev/null +++ b/.assets/18e6ba583d4d35e38cb678120492b2b4d52d19ea.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%20%40%60%80%Optimal utilization 90%Optimal utilization 90% \ No newline at end of file diff --git a/.assets/1aa0fb636d34a35d35ce67919bc0d7d2e36d3839.svg b/.assets/1aa0fb636d34a35d35ce67919bc0d7d2e36d3839.svg new file mode 100644 index 000000000..f05c3fb39 --- /dev/null +++ b/.assets/1aa0fb636d34a35d35ce67919bc0d7d2e36d3839.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%50%100%Optimal utilization 90%Optimal utilization 90% \ No newline at end of file diff --git a/.assets/1c1fbd79406b4b3a8c64ec79ef429f7c422d0387.svg b/.assets/1c1fbd79406b4b3a8c64ec79ef429f7c422d0387.svg new file mode 100644 index 000000000..d003381d1 --- /dev/null +++ b/.assets/1c1fbd79406b4b3a8c64ec79ef429f7c422d0387.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%Optimal utilization 90%Optimal utilization 90% \ No newline at end of file diff --git a/.assets/2e9d5ba5e949c7896bc79e7fdd98872cb43375b0.svg b/.assets/2e9d5ba5e949c7896bc79e7fdd98872cb43375b0.svg new file mode 100644 index 000000000..c752baefc --- /dev/null +++ b/.assets/2e9d5ba5e949c7896bc79e7fdd98872cb43375b0.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%2%4%Optimal utilization 99%Optimal utilization 99% \ No newline at end of file diff --git a/.assets/39a3707c0ded91bbfde8953567853db55452a227.svg b/.assets/39a3707c0ded91bbfde8953567853db55452a227.svg new file mode 100644 index 000000000..01584d9c0 --- /dev/null +++ b/.assets/39a3707c0ded91bbfde8953567853db55452a227.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%50%100%Optimal utilization 80%Optimal utilization 80% \ No newline at end of file diff --git a/.assets/39d5ff798c0771402486c6f1ca63f2a1868d80d9.svg b/.assets/39d5ff798c0771402486c6f1ca63f2a1868d80d9.svg new file mode 100644 index 000000000..df2260094 --- /dev/null +++ b/.assets/39d5ff798c0771402486c6f1ca63f2a1868d80d9.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%100%200%300%Optimal utilization 45%Optimal utilization 45% \ No newline at end of file diff --git a/.assets/4820a9e881eaa3f3def4916e47e0c330b34ab151.svg b/.assets/4820a9e881eaa3f3def4916e47e0c330b34ab151.svg new file mode 100644 index 000000000..403a5e0f3 --- /dev/null +++ b/.assets/4820a9e881eaa3f3def4916e47e0c330b34ab151.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%50%100%Optimal utilization 80%Optimal utilization 80% \ No newline at end of file diff --git a/.assets/5dbd5b9335471c83b44949206c9b2186657b836f.svg b/.assets/5dbd5b9335471c83b44949206c9b2186657b836f.svg new file mode 100644 index 000000000..d348fb2d6 --- /dev/null +++ b/.assets/5dbd5b9335471c83b44949206c9b2186657b836f.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%50%100%Optimal utilization 80%Optimal utilization 80% \ No newline at end of file diff --git a/.assets/5f455cedf818a43aba043e71714721fd1e844501.svg b/.assets/5f455cedf818a43aba043e71714721fd1e844501.svg new file mode 100644 index 000000000..5dff1554b --- /dev/null +++ b/.assets/5f455cedf818a43aba043e71714721fd1e844501.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%100%200%300%Optimal utilization 45%Optimal utilization 45% \ No newline at end of file diff --git a/.assets/7bc797fb2b1f421fa40a6d9a0028d1f78db71dc0.svg b/.assets/7bc797fb2b1f421fa40a6d9a0028d1f78db71dc0.svg new file mode 100644 index 000000000..e673fbc39 --- /dev/null +++ b/.assets/7bc797fb2b1f421fa40a6d9a0028d1f78db71dc0.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%20%40%60%80%Optimal utilization 92%Optimal utilization 92% \ No newline at end of file diff --git a/.assets/7ee454df22dbd82f0c1929bc14104d54c7081a49.svg b/.assets/7ee454df22dbd82f0c1929bc14104d54c7081a49.svg new file mode 100644 index 000000000..3c15ce1a6 --- /dev/null +++ b/.assets/7ee454df22dbd82f0c1929bc14104d54c7081a49.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%100%200%300%Optimal utilization 45%Optimal utilization 45% \ No newline at end of file diff --git a/.assets/8aa25c38caec024924773d1a5c7c63ab45c4eecf.svg b/.assets/8aa25c38caec024924773d1a5c7c63ab45c4eecf.svg new file mode 100644 index 000000000..5283877c6 --- /dev/null +++ b/.assets/8aa25c38caec024924773d1a5c7c63ab45c4eecf.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%50%100%Optimal utilization 80%Optimal utilization 80% \ No newline at end of file diff --git a/.assets/8fc705464b3515a654de9893168a1b1321feccb4.svg b/.assets/8fc705464b3515a654de9893168a1b1321feccb4.svg new file mode 100644 index 000000000..5212f2d4a --- /dev/null +++ b/.assets/8fc705464b3515a654de9893168a1b1321feccb4.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%50%100%Optimal utilization 45%Optimal utilization 45% \ No newline at end of file diff --git a/.assets/9c784d12784a084406c3794fbe177e93da4c3479.svg b/.assets/9c784d12784a084406c3794fbe177e93da4c3479.svg new file mode 100644 index 000000000..081891402 --- /dev/null +++ b/.assets/9c784d12784a084406c3794fbe177e93da4c3479.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%20%40%60%80%Optimal utilization 90%Optimal utilization 90% \ No newline at end of file diff --git a/.assets/a4faa118ed690be4e95fae01b8535036cda67e3d.svg b/.assets/a4faa118ed690be4e95fae01b8535036cda67e3d.svg new file mode 100644 index 000000000..93fde991f --- /dev/null +++ b/.assets/a4faa118ed690be4e95fae01b8535036cda67e3d.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%50%100%150%200%Optimal utilization 80%Optimal utilization 80% \ No newline at end of file diff --git a/.assets/a9a0275265838d424840d721917e0a4e3a5d8044.svg b/.assets/a9a0275265838d424840d721917e0a4e3a5d8044.svg new file mode 100644 index 000000000..6af590b85 --- /dev/null +++ b/.assets/a9a0275265838d424840d721917e0a4e3a5d8044.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%50%100%Optimal utilization 90%Optimal utilization 90% \ No newline at end of file diff --git a/.assets/ac33ca939f6fc30c2fd799aaa6f59b0521c19e9f.svg b/.assets/ac33ca939f6fc30c2fd799aaa6f59b0521c19e9f.svg new file mode 100644 index 000000000..3c15ce1a6 --- /dev/null +++ b/.assets/ac33ca939f6fc30c2fd799aaa6f59b0521c19e9f.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%100%200%300%Optimal utilization 45%Optimal utilization 45% \ No newline at end of file diff --git a/.assets/c229e34ea67f12eea11bf5403763207eeab38cff.svg b/.assets/c229e34ea67f12eea11bf5403763207eeab38cff.svg new file mode 100644 index 000000000..771e3e22e --- /dev/null +++ b/.assets/c229e34ea67f12eea11bf5403763207eeab38cff.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%20%40%60%80%Optimal utilization 90%Optimal utilization 90% \ No newline at end of file diff --git a/.assets/c277de9f9a155f6a0e47e0443845a3c9de5f6b84.svg b/.assets/c277de9f9a155f6a0e47e0443845a3c9de5f6b84.svg new file mode 100644 index 000000000..5212f2d4a --- /dev/null +++ b/.assets/c277de9f9a155f6a0e47e0443845a3c9de5f6b84.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%50%100%Optimal utilization 45%Optimal utilization 45% \ No newline at end of file diff --git a/.assets/ceb223a18f5aad36d9f6087d1d468b4dd5ba56a1.svg b/.assets/ceb223a18f5aad36d9f6087d1d468b4dd5ba56a1.svg new file mode 100644 index 000000000..95de9c4ed --- /dev/null +++ b/.assets/ceb223a18f5aad36d9f6087d1d468b4dd5ba56a1.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%100%200%300%Optimal utilization 30%Optimal utilization 30% \ No newline at end of file diff --git a/.assets/d8d51b9f960b17304a3bdcaf8922d3ae84b3c30f.svg b/.assets/d8d51b9f960b17304a3bdcaf8922d3ae84b3c30f.svg new file mode 100644 index 000000000..ad84fcb3b --- /dev/null +++ b/.assets/d8d51b9f960b17304a3bdcaf8922d3ae84b3c30f.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%50%100%Optimal utilization 80%Optimal utilization 80% \ No newline at end of file diff --git a/.assets/dcedb30554ccb68f317b952113777790df5ca547.svg b/.assets/dcedb30554ccb68f317b952113777790df5ca547.svg new file mode 100644 index 000000000..b5810667c --- /dev/null +++ b/.assets/dcedb30554ccb68f317b952113777790df5ca547.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%20%40%60%80%Optimal utilization 92%Optimal utilization 92% \ No newline at end of file diff --git a/.assets/e6c7eef36a7e70a1f1b35633097516fe5e21508b.svg b/.assets/e6c7eef36a7e70a1f1b35633097516fe5e21508b.svg new file mode 100644 index 000000000..6af590b85 --- /dev/null +++ b/.assets/e6c7eef36a7e70a1f1b35633097516fe5e21508b.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%50%100%Optimal utilization 90%Optimal utilization 90% \ No newline at end of file diff --git a/.assets/ec2b4dc8236ac87f8058f121d86de1d99e029c5c.svg b/.assets/ec2b4dc8236ac87f8058f121d86de1d99e029c5c.svg new file mode 100644 index 000000000..f9bccbdd6 --- /dev/null +++ b/.assets/ec2b4dc8236ac87f8058f121d86de1d99e029c5c.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%100%200%300%Optimal utilization 70%Optimal utilization 70% \ No newline at end of file diff --git a/.assets/fb6ebf3fa05cf980f374598b56757dbc5cae3662.svg b/.assets/fb6ebf3fa05cf980f374598b56757dbc5cae3662.svg new file mode 100644 index 000000000..df2260094 --- /dev/null +++ b/.assets/fb6ebf3fa05cf980f374598b56757dbc5cae3662.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%100%200%300%Optimal utilization 45%Optimal utilization 45% \ No newline at end of file diff --git a/.assets/fe79e1c7a3e943262824d4f683059c85d91a233d.svg b/.assets/fe79e1c7a3e943262824d4f683059c85d91a233d.svg new file mode 100644 index 000000000..5dff1554b --- /dev/null +++ b/.assets/fe79e1c7a3e943262824d4f683059c85d91a233d.svg @@ -0,0 +1 @@ + Borrow APR, variableBorrow APR, stable0%25%50%75%100%0%100%200%300%Optimal utilization 45%Optimal utilization 45% \ No newline at end of file diff --git a/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/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Arbitrum_GHOCCIP150Upgrade_20241021.sol b/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Arbitrum_GHOCCIP150Upgrade_20241021.sol new file mode 100644 index 000000000..7daeb4d52 --- /dev/null +++ b/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Arbitrum_GHOCCIP150Upgrade_20241021.sol @@ -0,0 +1,51 @@ +// 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 {IUpgradeableBurnMintTokenPool} from 'src/interfaces/ccip/IUpgradeableBurnMintTokenPool.sol'; +import {IRateLimiter} from 'src/interfaces/ccip/IRateLimiter.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 { + uint64 public constant ETH_CHAIN_SELECTOR = 5009297550715157269; + + // https://arbiscan.io/address/0xfc421aD3C883Bf9E7C4f42dE845C4e4405799e73 + address public constant TOKEN_POOL_IMPL = 0xfc421aD3C883Bf9E7C4f42dE845C4e4405799e73; + // https://arbiscan.io/address/0x26329558f08cbb40d6a4CCA0E0C67b29D64A8c50 + address public constant GHO_CCIP_PROXY_POOL = 0x26329558f08cbb40d6a4CCA0E0C67b29D64A8c50; + + /// @dev Token Rate Limit Capacity: 300_000 GHO + uint128 public constant CCIP_RATE_LIMIT_CAPACITY = 300_000e18; + + /// @dev Token Rate Limit Refill Rate: 60 GHO per second (=> 216_000 GHO per hour) + uint128 public constant CCIP_RATE_LIMIT_REFILL_RATE = 60e18; + + function execute() external { + IUpgradeableBurnMintTokenPool tokenPoolProxy = IUpgradeableBurnMintTokenPool( + MiscArbitrum.GHO_CCIP_TOKEN_POOL + ); + + ProxyAdmin(MiscArbitrum.PROXY_ADMIN).upgrade( + TransparentUpgradeableProxy(payable(address(tokenPoolProxy))), + TOKEN_POOL_IMPL + ); + + // Update proxyPool address + tokenPoolProxy.setProxyPool(GHO_CCIP_PROXY_POOL); + + // Set rate limit + IRateLimiter.Config memory rateLimitConfig = IRateLimiter.Config({ + isEnabled: true, + capacity: CCIP_RATE_LIMIT_CAPACITY, + rate: CCIP_RATE_LIMIT_REFILL_RATE + }); + tokenPoolProxy.setChainRateLimiterConfig(ETH_CHAIN_SELECTOR, rateLimitConfig, rateLimitConfig); + } +} 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..79e12a8d7 --- /dev/null +++ b/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Arbitrum_GHOCCIP150Upgrade_20241021.t.sol @@ -0,0 +1,493 @@ +// 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 {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 {IUpgradeableBurnMintTokenPool} from 'src/interfaces/ccip/IUpgradeableBurnMintTokenPool.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; + IUpgradeableBurnMintTokenPool 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'), 271788784); + proposal = new AaveV3Arbitrum_GHOCCIP150Upgrade_20241021(); + ghoTokenPool = IUpgradeableBurnMintTokenPool(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( + ghoTokenPool.getCurrentInboundRateLimiterState(ETH_CHAIN_SELECTOR), + _getDisabledConfig() + ); + assertEq( + ghoTokenPool.getCurrentOutboundRateLimiterState(ETH_CHAIN_SELECTOR), + _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( + ghoTokenPool.getCurrentInboundRateLimiterState(ETH_CHAIN_SELECTOR), + _getRateLimiterConfig() + ); + assertEq( + ghoTokenPool.getCurrentOutboundRateLimiterState(ETH_CHAIN_SELECTOR), + _getRateLimiterConfig() + ); + } + + 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')); + /// 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(uint256 amount) public { + executePayload(vm, address(proposal)); + + IRouter router = IRouter(ghoTokenPool.getRouter()); + amount = bound(amount, 1, _getRateLimiterConfig().capacity); + 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(uint256 amount) public { + executePayload(vm, address(proposal)); + + _mockCCIPMigration(); + + IRouter router = IRouter(ghoTokenPool.getRouter()); + amount = bound(amount, 1, _getRateLimiterConfig().capacity); + 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(uint256 amount) public { + executePayload(vm, address(proposal)); + + amount = bound(amount, 1, _getRateLimiterConfig().capacity); + 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(uint256 amount) public { + executePayload(vm, address(proposal)); + + _mockCCIPMigration(); + + amount = bound(amount, 1, _getRateLimiterConfig().capacity); + 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(uint256 amount) public { + executePayload(vm, address(proposal)); + + _mockCCIPMigration(); + + amount = bound(amount, 1, _getRateLimiterConfig().capacity); + 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(uint256 amount) public { + amount = bound(amount, 1, _getRateLimiterConfig().capacity); + + 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(uint256 amount) public { + amount = bound(amount, 1, _getRateLimiterConfig().capacity); + + 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)); + + assertEq(ghoTokenPool.getRateLimitAdmin(), 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()); + + assertEq(TOKEN_ADMIN_REGISTRY.getPool(ARB_GHO_TOKEN), address(proxyPool)); + + assertEq(proxyPool.getRouter(), address(router)); + + assertTrue(proxyPool.isSupportedChain(ETH_CHAIN_SELECTOR)); + assertEq(proxyPool.getCurrentInboundRateLimiterState(ETH_CHAIN_SELECTOR), _getDisabledConfig()); + assertEq( + proxyPool.getCurrentOutboundRateLimiterState(ETH_CHAIN_SELECTOR), + _getDisabledConfig() + ); + assertEq(proxyPool.getRemotePool(ETH_CHAIN_SELECTOR), abi.encode(ETH_PROXY_POOL)); + assertEq(proxyPool.getRemoteToken(ETH_CHAIN_SELECTOR), abi.encode(MiscEthereum.GHO_TOKEN)); + + 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(), + ghoTokenPool.getRateLimitAdmin() + ); + } + + 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)); + + assertEq(proxyPool.getPreviousPool(), address(ghoTokenPool)); + } + + function _getOutboundRefillTime(uint256 amount) private view returns (uint256) { + uint128 rate = _getRateLimiterConfig().rate; + assertNotEq(rate, 0); + return amount / uint256(rate) + 1; // account for rounding + } + + function _getInboundRefillTime(uint256 amount) private view returns (uint256) { + uint128 rate = _getRateLimiterConfig().rate; + assertNotEq(rate, 0); + return amount / uint256(rate) + 1; // account for rounding + } + + function _tokenBucketToConfig( + IRateLimiter.TokenBucket memory bucket + ) private pure returns (IRateLimiter.Config memory) { + return + IRateLimiter.Config({ + isEnabled: bucket.isEnabled, + capacity: bucket.capacity, + rate: bucket.rate + }); + } + + function _getDisabledConfig() private pure returns (IRateLimiter.Config memory) { + return IRateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}); + } + + function assertEq( + IRateLimiter.TokenBucket memory bucket, + IRateLimiter.Config memory config + ) internal pure { + assertEq(abi.encode(_tokenBucketToConfig(bucket)), abi.encode(config)); + } + + function _getRateLimiterConfig() internal view returns (IRateLimiter.Config memory) { + return + IRateLimiter.Config({ + isEnabled: true, + capacity: proposal.CCIP_RATE_LIMIT_CAPACITY(), + rate: proposal.CCIP_RATE_LIMIT_REFILL_RATE() + }); + } +} 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..e64678526 --- /dev/null +++ b/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3E2E_GHOCCIP150Upgrade_20241021.t.sol @@ -0,0 +1,873 @@ +// 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 {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 {IUpgradeableBurnMintTokenPool} from 'src/interfaces/ccip/IUpgradeableBurnMintTokenPool.sol'; +import {IUpgradeableLockReleaseTokenPool} from 'src/interfaces/ccip/IUpgradeableLockReleaseTokenPool.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; + IUpgradeableLockReleaseTokenPool tokenPool; + IRateLimiter.Config rateLimitConfig; + Common c; + } + + struct L2 { + AaveV3Arbitrum_GHOCCIP150Upgrade_20241021 proposal; + IUpgradeableBurnMintTokenPool tokenPool; + IRateLimiter.Config rateLimitConfig; + 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'), 21131872); + l2.c.forkId = vm.createFork(vm.rpcUrl('arbitrum'), 271788784); + + vm.selectFork(l1.c.forkId); + l1.proposal = new AaveV3Ethereum_GHOCCIP150Upgrade_20241021(); + l1.c.proxyPool = IProxyPool(l1.proposal.GHO_CCIP_PROXY_POOL()); + l1.tokenPool = IUpgradeableLockReleaseTokenPool(MiscEthereum.GHO_CCIP_TOKEN_POOL); + l1.rateLimitConfig = IRateLimiter.Config({ + isEnabled: true, + capacity: l1.proposal.CCIP_RATE_LIMIT_CAPACITY(), + rate: l1.proposal.CCIP_RATE_LIMIT_REFILL_RATE() + }); + 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 = IUpgradeableBurnMintTokenPool(MiscArbitrum.GHO_CCIP_TOKEN_POOL); + l2.rateLimitConfig = IRateLimiter.Config({ + isEnabled: true, + capacity: l2.proposal.CCIP_RATE_LIMIT_CAPACITY(), + rate: l2.proposal.CCIP_RATE_LIMIT_REFILL_RATE() + }); + 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); + assertEq(src.tokenAdminRegistry.getPool(address(src.token)), address(src.proxyPool)); + assertEq(src.proxyPool.getRouter(), address(src.router)); + assertTrue(src.proxyPool.isSupportedChain(dest.chainSelector)); + assertEq( + src.proxyPool.getCurrentInboundRateLimiterState(dest.chainSelector), + _getDisabledConfig() + ); + assertEq( + src.proxyPool.getCurrentOutboundRateLimiterState(dest.chainSelector), + _getDisabledConfig() + ); + assertEq(src.proxyPool.getRemotePool(dest.chainSelector), abi.encode(dest.proxyPool)); + assertEq(src.proxyPool.getRemoteToken(dest.chainSelector), abi.encode(dest.token)); + + 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); + } + + function _getDisabledConfig() private pure returns (IRateLimiter.Config memory) { + return IRateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}); + } + + function _tokenBucketToConfig( + IRateLimiter.TokenBucket memory bucket + ) private pure returns (IRateLimiter.Config memory) { + return + IRateLimiter.Config({ + isEnabled: bucket.isEnabled, + capacity: bucket.capacity, + rate: bucket.rate + }); + } + + function assertEq( + IRateLimiter.TokenBucket memory bucket, + IRateLimiter.Config memory config + ) internal pure { + assertEq(abi.encode(_tokenBucketToConfig(bucket)), abi.encode(config)); + } +} + +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 = l1.rateLimitConfig.capacity; + // 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.rateLimitConfig.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.rateLimitConfig.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.rateLimitConfig.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.rateLimitConfig.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 = l1.rateLimitConfig.capacity; + // 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.rateLimitConfig.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.rateLimitConfig.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.rateLimitConfig.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.rateLimitConfig.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 = l1.rateLimitConfig.capacity; + deal(address(l1.c.token), alice, amount, true); + vm.prank(alice); + l1.c.token.approve(address(l1.c.router), amount); + uint128 rate = l1.rateLimitConfig.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 = l1.rateLimitConfig.capacity; + deal(address(l2.c.token), alice, amount, true); + vm.prank(alice); + l2.c.token.approve(address(l2.c.router), amount); + uint128 rate = l2.rateLimitConfig.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 = l1.rateLimitConfig.capacity; + deal(address(l1.c.token), alice, amount, true); + vm.prank(alice); + l1.c.token.approve(address(l1.c.router), amount); + + uint128 inBoundRate = l1.rateLimitConfig.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 = l1.rateLimitConfig.capacity; + deal(address(l2.c.token), alice, amount, true); + vm.prank(alice); + l2.c.token.approve(address(l2.c.router), amount); + + uint128 inBoundRate = l2.rateLimitConfig.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 = l1.rateLimitConfig.capacity; + deal(address(l1.c.token), alice, amount, true); + vm.prank(alice); + l1.c.token.approve(address(l1.c.router), amount); + uint128 rate = l1.rateLimitConfig.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.rateLimitConfig.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 = l1.rateLimitConfig.capacity; + deal(address(l2.c.token), alice, amount, true); + vm.prank(alice); + l2.c.token.approve(address(l2.c.router), amount); + uint128 outBoundRate = l2.rateLimitConfig.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.rateLimitConfig.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..7d6a405fe --- /dev/null +++ b/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Ethereum_GHOCCIP150Upgrade_20241021.sol @@ -0,0 +1,51 @@ +// 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 {IUpgradeableLockReleaseTokenPool} from 'src/interfaces/ccip/IUpgradeableLockReleaseTokenPool.sol'; +import {IRateLimiter} from 'src/interfaces/ccip/IRateLimiter.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 { + uint64 public constant ARB_CHAIN_SELECTOR = 4949039107694359620; + + // https://etherscan.io/address/0xb77E872A68C62CfC0dFb02C067Ecc3DA23B4bbf3 + address public constant TOKEN_POOL_IMPL = 0xb77E872A68C62CfC0dFb02C067Ecc3DA23B4bbf3; + // https://etherscan.io/address/0x9Ec9F9804733df96D1641666818eFb5198eC50f0 + address public constant GHO_CCIP_PROXY_POOL = 0x9Ec9F9804733df96D1641666818eFb5198eC50f0; + + /// @dev Token Rate Limit Capacity: 300_000 GHO + uint128 public constant CCIP_RATE_LIMIT_CAPACITY = 300_000e18; + + /// @dev Token Rate Limit Refill Rate: 60 GHO per second (=> 216_000 GHO per hour) + uint128 public constant CCIP_RATE_LIMIT_REFILL_RATE = 60e18; + + function execute() external { + IUpgradeableLockReleaseTokenPool tokenPoolProxy = IUpgradeableLockReleaseTokenPool( + MiscEthereum.GHO_CCIP_TOKEN_POOL + ); + + ProxyAdmin(MiscEthereum.PROXY_ADMIN).upgrade( + TransparentUpgradeableProxy(payable(address(tokenPoolProxy))), + TOKEN_POOL_IMPL + ); + + // Update proxyPool address + tokenPoolProxy.setProxyPool(GHO_CCIP_PROXY_POOL); + + // Set rate limit + IRateLimiter.Config memory rateLimitConfig = IRateLimiter.Config({ + isEnabled: true, + capacity: CCIP_RATE_LIMIT_CAPACITY, + rate: CCIP_RATE_LIMIT_REFILL_RATE + }); + tokenPoolProxy.setChainRateLimiterConfig(ARB_CHAIN_SELECTOR, rateLimitConfig, rateLimitConfig); + } +} 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..c9b0b63d8 --- /dev/null +++ b/src/20241021_Multi_GHOCCIP150Upgrade/AaveV3Ethereum_GHOCCIP150Upgrade_20241021.t.sol @@ -0,0 +1,501 @@ +// 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 {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 {IUpgradeableLockReleaseTokenPool} from 'src/interfaces/ccip/IUpgradeableLockReleaseTokenPool.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; + IUpgradeableLockReleaseTokenPool 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'), 21131872); + proposal = new AaveV3Ethereum_GHOCCIP150Upgrade_20241021(); + ghoTokenPool = IUpgradeableLockReleaseTokenPool(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( + ghoTokenPool.getCurrentInboundRateLimiterState(ARB_CHAIN_SELECTOR), + _getDisabledConfig() + ); + assertEq( + ghoTokenPool.getCurrentOutboundRateLimiterState(ARB_CHAIN_SELECTOR), + _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( + ghoTokenPool.getCurrentInboundRateLimiterState(ARB_CHAIN_SELECTOR), + _getRateLimiterConfig() + ); + assertEq( + ghoTokenPool.getCurrentOutboundRateLimiterState(ARB_CHAIN_SELECTOR), + _getRateLimiterConfig() + ); + } + + 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(uint256 amount) public { + executePayload(vm, address(proposal)); + + IERC20 gho = IERC20(address(ghoTokenPool.getToken())); + IRouter router = IRouter(ghoTokenPool.getRouter()); + amount = bound(amount, 1, _getRateLimiterConfig().capacity); + + // 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(uint256 amount) public { + executePayload(vm, address(proposal)); + + _mockCCIPMigration(); + + IERC20 gho = IERC20(address(ghoTokenPool.getToken())); + IRouter router = IRouter(ghoTokenPool.getRouter()); + amount = bound(amount, 1, _getRateLimiterConfig().capacity); + + // 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(uint256 amount) public { + executePayload(vm, address(proposal)); + + IERC20 gho = IERC20(address(ghoTokenPool.getToken())); + amount = bound(amount, 1, _getRateLimiterConfig().capacity); + + // 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(uint256 amount) public { + executePayload(vm, address(proposal)); + + _mockCCIPMigration(); + + IERC20 gho = IERC20(address(ghoTokenPool.getToken())); + amount = bound(amount, 1, _getRateLimiterConfig().capacity); + // 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(uint256 amount) public { + executePayload(vm, address(proposal)); + + _mockCCIPMigration(); + + IERC20 gho = IERC20(address(ghoTokenPool.getToken())); + amount = bound(amount, 1, _getRateLimiterConfig().capacity); + + // 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(uint256 amount) public { + amount = bound(amount, 1, _getRateLimiterConfig().capacity); + + 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(uint256 amount) public { + amount = bound(amount, 1, _getRateLimiterConfig().capacity); + + 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)); + + assertEq(ghoTokenPool.getRateLimitAdmin(), 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()); + + assertEq(TOKEN_ADMIN_REGISTRY.getPool(MiscEthereum.GHO_TOKEN), address(proxyPool)); + + assertEq(proxyPool.getRouter(), address(router)); + + assertTrue(proxyPool.isSupportedChain(ARB_CHAIN_SELECTOR)); + assertEq(proxyPool.getCurrentInboundRateLimiterState(ARB_CHAIN_SELECTOR), _getDisabledConfig()); + assertEq( + proxyPool.getCurrentOutboundRateLimiterState(ARB_CHAIN_SELECTOR), + _getDisabledConfig() + ); + assertEq(proxyPool.getRemotePool(ARB_CHAIN_SELECTOR), abi.encode(ARB_PROXY_POOL)); + assertEq( + proxyPool.getRemoteToken(ARB_CHAIN_SELECTOR), + abi.encode(AaveV3ArbitrumAssets.GHO_UNDERLYING) + ); + + 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.getSupportedChains(), + ghoTokenPool.getAllowListEnabled(), + ghoTokenPool.getRateLimitAdmin(), + ghoTokenPool.getBridgeLimitAdmin(), + ghoTokenPool.getRebalancer(), + ghoTokenPool.getLockReleaseInterfaceId(), + ghoTokenPool.getBridgeLimit(), + ghoTokenPool.getCurrentBridgedAmount() + ); + } + + 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)); + + assertEq(proxyPool.getPreviousPool(), address(ghoTokenPool)); + } + + function _getOutboundRefillTime(uint256 amount) private view returns (uint256) { + uint128 rate = _getRateLimiterConfig().rate; + assertNotEq(rate, 0); + return amount / uint256(rate) + 1; // account for rounding + } + + function _getInboundRefillTime(uint256 amount) private view returns (uint256) { + uint128 rate = _getRateLimiterConfig().rate; + assertNotEq(rate, 0); + return amount / uint256(rate) + 1; // account for rounding + } + + function _tokenBucketToConfig( + IRateLimiter.TokenBucket memory bucket + ) private pure returns (IRateLimiter.Config memory) { + return + IRateLimiter.Config({ + isEnabled: bucket.isEnabled, + capacity: bucket.capacity, + rate: bucket.rate + }); + } + + function _getDisabledConfig() private pure returns (IRateLimiter.Config memory) { + return IRateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}); + } + + function assertEq( + IRateLimiter.TokenBucket memory bucket, + IRateLimiter.Config memory config + ) internal pure { + assertEq(abi.encode(_tokenBucketToConfig(bucket)), abi.encode(config)); + } + + function _getRateLimiterConfig() internal view returns (IRateLimiter.Config memory) { + return + IRateLimiter.Config({ + isEnabled: true, + capacity: proposal.CCIP_RATE_LIMIT_CAPACITY(), + rate: proposal.CCIP_RATE_LIMIT_REFILL_RATE() + }); + } +} diff --git a/src/20241021_Multi_GHOCCIP150Upgrade/GHOCCIP150Upgrade.md b/src/20241021_Multi_GHOCCIP150Upgrade/GHOCCIP150Upgrade.md new file mode 100644 index 000000000..ea56acb50 --- /dev/null +++ b/src/20241021_Multi_GHOCCIP150Upgrade/GHOCCIP150Upgrade.md @@ -0,0 +1,35 @@ +--- +title: "GHO CCIP Integration Maintenance (CCIP v1.5 upgrade)" +author: "Aave Labs" +discussions: "https://governance.aave.com/t/bgd-technical-maintenance-proposals/15274/51" +--- + +## Simple Summary + +Proposal to update GHO CCIP Token Pools to ensure GHO’s CCIP integration remains functional during and after the upcoming migration to CCIP version 1.5. + +## Motivation + +The Chainlink Cross-Chain Interoperability Protocol (CCIP) is upgrading to version 1.5 in Q4 2024, introducing several new features and enhancements. We are expecting that a detailed description of this new version will be announced by Chainlink soon. +Currently, GHO CCIP Token Pools are based on version 1.4, and they are not compatible in their current form with CCIP 1.5. The Chainlink CCIP team cannot migrate the GHO CCIP Token Pools as part of the system-wide upgrade, as these are fully controlled by the Aave DAO. +Aave Labs will provide technical support to ensure that GHO CCIP Token Pools remain functional, secure, and aligned with the latest updates, enabling GHO to expand to other networks if needed. + +## Specification + +Below actions are taken in order to make GHO token pools work with the new CCIP 1.5 protocol by allowing the "Intermediary Contract" (proxyPool) to translate the interface between 1.4 & 1.5 OnRamps and OffRamps: + +- Configure an Intermediary Contract: This contract translates execution calls between the new OnRamp and the existing GHO Token Pools. It was developed and deployed by the Chainlink CCIP team on behalf of the Aave DAO. +- Upgrade the Existing GHO Token Pools: The existing Token Pools are adjusted to allow calls from both the old and new versions of the OnRamp and OffRamp, through the intermediary contract. +- Setup a Token Rate Limit: A token rate limit of 300,000 GHO as limit capacity and 60 GHO per second as refill rate is set for both directions of Ethereum <> Arbitrum lane. This is a temporary security measure that can be lifted later via AIP or by GHO Stewards. + +## 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 - Direct-to-AIP +- [Discussion](https://governance.aave.com/t/bgd-technical-maintenance-proposals/15274/51) +- Token Pool Contracts - [UpgradeableLockReleaseTokenPool](https://github.com/aave/ccip/blob/bc0561e6a9615f410086d4766839eaf3ca9b9f49/contracts/src/v0.8/ccip/pools/GHO/UpgradeableLockReleaseTokenPool.sol), [UpgradeableBurnMintTokenPool](https://github.com/aave/ccip/blob/bc0561e6a9615f410086d4766839eaf3ca9b9f49/contracts/src/v0.8/ccip/pools/GHO/UpgradeableBurnMintTokenPool.sol) + +## 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..f1a49b82f --- /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: 'Direct-to-AIP', + votingNetwork: 'POLYGON', + }, + poolOptions: { + AaveV3Ethereum: {configs: {OTHERS: {}}, cache: {blockNumber: 21131872}}, + AaveV3Arbitrum: {configs: {OTHERS: {}}, cache: {blockNumber: 271788784}}, + }, +}; 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..4810b9e9d --- /dev/null +++ b/src/interfaces/ccip/IProxyPool.sol @@ -0,0 +1,33 @@ +// 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 applyChainUpdates(ChainUpdate[] memory updates) external; + function isSupportedChain(uint64 chainSelector) external view returns (bool); + function getPreviousPool() external view returns (address); + function getCurrentInboundRateLimiterState( + uint64 chainSelector + ) external view returns (IRateLimiter.TokenBucket memory); + function getCurrentOutboundRateLimiterState( + uint64 chainSelector + ) external view returns (IRateLimiter.TokenBucket memory); + function getRemotePool(uint64 remoteChainSelector) external view returns (bytes memory); + function getRemoteToken(uint64 remoteChainSelector) external view returns (bytes memory); +} 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); +} diff --git a/src/interfaces/ccip/IUpgradeableBurnMintTokenPool.sol b/src/interfaces/ccip/IUpgradeableBurnMintTokenPool.sol new file mode 100644 index 000000000..09c322ccc --- /dev/null +++ b/src/interfaces/ccip/IUpgradeableBurnMintTokenPool.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IRateLimiter} from './IRateLimiter.sol'; + +interface IUpgradeableBurnMintTokenPool { + error AggregateValueMaxCapacityExceeded(uint256 capacity, uint256 requested); + error AggregateValueRateLimitReached(uint256 minWaitInSeconds, uint256 available); + error AllowListNotEnabled(); + error BadARMSignal(); + error BucketOverfilled(); + error CallerIsNotARampOnRouter(address caller); + error ChainAlreadyExists(uint64 chainSelector); + error ChainNotAllowed(uint64 remoteChainSelector); + error DisabledNonZeroRateLimit(IRateLimiter.Config config); + error InvalidRatelimitRate(IRateLimiter.Config rateLimiterConfig); + error NonExistentChain(uint64 remoteChainSelector); + error RateLimitMustBeDisabled(); + error SenderNotAllowed(address sender); + error TokenMaxCapacityExceeded(uint256 capacity, uint256 requested, address tokenAddress); + error TokenRateLimitReached(uint256 minWaitInSeconds, uint256 available, address tokenAddress); + error Unauthorized(address caller); + error ZeroAddressNotAllowed(); + + event AllowListAdd(address sender); + event AllowListRemove(address sender); + event Burned(address indexed sender, uint256 amount); + event ChainAdded( + uint64 remoteChainSelector, + IRateLimiter.Config outboundRateLimiterConfig, + IRateLimiter.Config inboundRateLimiterConfig + ); + event ChainConfigured( + uint64 remoteChainSelector, + IRateLimiter.Config outboundRateLimiterConfig, + IRateLimiter.Config inboundRateLimiterConfig + ); + event ChainRemoved(uint64 remoteChainSelector); + event ConfigChanged(IRateLimiter.Config config); + event Initialized(uint8 version); + event Locked(address indexed sender, uint256 amount); + event Minted(address indexed sender, address indexed recipient, uint256 amount); + event OwnershipTransferRequested(address indexed from, address indexed to); + event OwnershipTransferred(address indexed from, address indexed to); + event Released(address indexed sender, address indexed recipient, uint256 amount); + event RouterUpdated(address oldRouter, address newRouter); + event TokensConsumed(uint256 tokens); + + function acceptOwnership() external; + function applyAllowListUpdates(address[] memory removes, address[] memory adds) external; + function getAllowList() external view returns (address[] memory); + function getAllowListEnabled() external view returns (bool); + function getArmProxy() external view returns (address armProxy); + function getCurrentInboundRateLimiterState( + uint64 remoteChainSelector + ) external view returns (IRateLimiter.TokenBucket memory); + function getCurrentOutboundRateLimiterState( + uint64 remoteChainSelector + ) external view returns (IRateLimiter.TokenBucket memory); + function getProxyPool() external view returns (address proxyPool); + function getRateLimitAdmin() external view returns (address); + function getRouter() external view returns (address router); + function getSupportedChains() external view returns (uint64[] memory); + function getToken() external view returns (address token); + function initialize(address owner, address[] memory allowlist, address router) external; + function isSupportedChain(uint64 remoteChainSelector) external view returns (bool); + function lockOrBurn( + address originalSender, + bytes memory, + uint256 amount, + uint64 remoteChainSelector, + bytes memory + ) external returns (bytes memory); + function owner() external view returns (address); + function releaseOrMint( + bytes memory, + address receiver, + uint256 amount, + uint64 remoteChainSelector, + bytes memory + ) external; + function setChainRateLimiterConfig( + uint64 remoteChainSelector, + IRateLimiter.Config memory outboundConfig, + IRateLimiter.Config memory inboundConfig + ) external; + function setProxyPool(address proxyPool) external; + function setRateLimitAdmin(address rateLimitAdmin) external; + function setRouter(address newRouter) external; + function supportsInterface(bytes4 interfaceId) external pure returns (bool); + function transferOwnership(address to) external; + function typeAndVersion() external view returns (string memory); +} diff --git a/src/interfaces/ccip/IUpgradeableLockReleaseTokenPool.sol b/src/interfaces/ccip/IUpgradeableLockReleaseTokenPool.sol index 02252f92c..bdc364e79 100644 --- a/src/interfaces/ccip/IUpgradeableLockReleaseTokenPool.sol +++ b/src/interfaces/ccip/IUpgradeableLockReleaseTokenPool.sol @@ -1,40 +1,117 @@ // SPDX-License-Identifier: MIT - pragma solidity ^0.8.0; -import {IClient} from 'src/interfaces/ccip/IClient.sol'; +import {IRateLimiter} from './IRateLimiter.sol'; interface IUpgradeableLockReleaseTokenPool { + error AggregateValueMaxCapacityExceeded(uint256 capacity, uint256 requested); + error AggregateValueRateLimitReached(uint256 minWaitInSeconds, uint256 available); + error AllowListNotEnabled(); + error BadARMSignal(); error BridgeLimitExceeded(uint256 bridgeLimit); + error BucketOverfilled(); + error CallerIsNotARampOnRouter(address caller); + error ChainAlreadyExists(uint64 chainSelector); + error ChainNotAllowed(uint64 remoteChainSelector); + error DisabledNonZeroRateLimit(IRateLimiter.Config config); + error InsufficientLiquidity(); + error InvalidRatelimitRate(IRateLimiter.Config rateLimiterConfig); + error LiquidityNotAccepted(); + error NonExistentChain(uint64 remoteChainSelector); + error NotEnoughBridgedAmount(); + error RateLimitMustBeDisabled(); + error SenderNotAllowed(address sender); + error TokenMaxCapacityExceeded(uint256 capacity, uint256 requested, address tokenAddress); + error TokenRateLimitReached(uint256 minWaitInSeconds, uint256 available, address tokenAddress); + error Unauthorized(address caller); + error ZeroAddressNotAllowed(); - /// @dev Initializer - /// @dev The address passed as `owner` must accept ownership after initialization. - /// @dev The `allowlist` is only effective if pool is set to access-controlled mode - /// @param owner The address of the owner - /// @param allowlist A set of addresses allowed to trigger lockOrBurn as original senders - /// @param router The address of the router - function initialize(address owner, address[] memory allowlist, address router) external; + event AllowListAdd(address sender); + event AllowListRemove(address sender); + event BridgeLimitAdminUpdated(address indexed oldAdmin, address indexed newAdmin); + event BridgeLimitUpdated(uint256 oldBridgeLimit, uint256 newBridgeLimit); + event Burned(address indexed sender, uint256 amount); + event ChainAdded( + uint64 remoteChainSelector, + IRateLimiter.Config outboundRateLimiterConfig, + IRateLimiter.Config inboundRateLimiterConfig + ); + event ChainConfigured( + uint64 remoteChainSelector, + IRateLimiter.Config outboundRateLimiterConfig, + IRateLimiter.Config inboundRateLimiterConfig + ); + event ChainRemoved(uint64 remoteChainSelector); + event ConfigChanged(IRateLimiter.Config config); + event Initialized(uint8 version); + event LiquidityAdded(address indexed provider, uint256 indexed amount); + event LiquidityRemoved(address indexed provider, uint256 indexed amount); + event Locked(address indexed sender, uint256 amount); + event Minted(address indexed sender, address indexed recipient, uint256 amount); + event OwnershipTransferRequested(address indexed from, address indexed to); + event OwnershipTransferred(address indexed from, address indexed to); + event Released(address indexed sender, address indexed recipient, uint256 amount); + event RouterUpdated(address oldRouter, address newRouter); + event TokensConsumed(uint256 tokens); - /// @dev Ownable + function acceptOwnership() external; + function applyAllowListUpdates(address[] memory removes, address[] memory adds) external; + function canAcceptLiquidity() external view returns (bool); + function getAllowList() external view returns (address[] memory); + function getAllowListEnabled() external view returns (bool); + function getArmProxy() external view returns (address armProxy); + function getBridgeLimit() external view returns (uint256); + function getBridgeLimitAdmin() external view returns (address); + function getCurrentBridgedAmount() external view returns (uint256); + function getCurrentInboundRateLimiterState( + uint64 remoteChainSelector + ) external view returns (IRateLimiter.TokenBucket memory); + function getCurrentOutboundRateLimiterState( + uint64 remoteChainSelector + ) external view returns (IRateLimiter.TokenBucket memory); + function getLockReleaseInterfaceId() external pure returns (bytes4); + function getProxyPool() external view returns (address proxyPool); + function getRateLimitAdmin() external view returns (address); + function getRebalancer() external view returns (address); + function getRouter() external view returns (address router); + function getSupportedChains() external view returns (uint64[] memory); + function getToken() external view returns (address token); + function initialize( + address owner, + address[] memory allowlist, + address router, + uint256 bridgeLimit + ) external; + function isSupportedChain(uint64 remoteChainSelector) external view returns (bool); + function lockOrBurn( + address originalSender, + bytes memory, + uint256 amount, + uint64 remoteChainSelector, + bytes memory + ) external returns (bytes memory); function owner() external view returns (address); - - /// @notice Sets the bridge limit - /// @param limit The new limit - function setBridgeLimit(uint256 limit) external; - - /// @notice Sets the bridge limit admin address. - /// @dev Only callable by the owner. - /// @param bridgeLimitAdmin The new bridge limit admin address. + function provideLiquidity(uint256 amount) external; + function releaseOrMint( + bytes memory, + address receiver, + uint256 amount, + uint64 remoteChainSelector, + bytes memory + ) external; + function setBridgeLimit(uint256 newBridgeLimit) external; function setBridgeLimitAdmin(address bridgeLimitAdmin) external; - - /// @notice Sets the rate limiter admin address. - /// @dev Only callable by the owner. - /// @param rateLimitAdmin The new rate limiter admin address. + function setChainRateLimiterConfig( + uint64 remoteChainSelector, + IRateLimiter.Config memory outboundConfig, + IRateLimiter.Config memory inboundConfig + ) external; + function setProxyPool(address proxyPool) external; function setRateLimitAdmin(address rateLimitAdmin) external; - - /// @notice Gets the bridge limiter admin address. - function getBridgeLimitAdmin() external view returns (address); - - /// @notice Gets the rate limiter admin address. - function getRateLimitAdmin() external view returns (address); + function setRebalancer(address rebalancer) external; + function setRouter(address newRouter) external; + function supportsInterface(bytes4 interfaceId) external pure returns (bool); + function transferOwnership(address to) external; + function typeAndVersion() external view returns (string memory); + function withdrawLiquidity(uint256 amount) external; }