diff --git a/diffs/AaveV3Base_GHOBaseLaunch_20241223_before_AaveV3Base_GHOBaseLaunch_20241223_after.md b/diffs/AaveV3Base_GHOBaseLaunch_20241223_before_AaveV3Base_GHOBaseLaunch_20241223_after.md new file mode 100644 index 000000000..ce43cfac8 --- /dev/null +++ b/diffs/AaveV3Base_GHOBaseLaunch_20241223_before_AaveV3Base_GHOBaseLaunch_20241223_after.md @@ -0,0 +1,259 @@ +## Raw diff + +```json +{ + "raw": { + "from": null, + "to": { + "0x06179f7c1be40863405f374e7f5f8806c728660a": { + "label": null, + "balanceDiff": null, + "stateDiff": { + "0x0000000000000000000000000000000000000000000000000000000000000000": { + "previousValue": "0x000000000000000000009390b1735def18560c509e2d0bc090e9d6ba257a0001", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "0x0000000000000000000000000000000000000000000000000000000000000001": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000009390b1735def18560c509e2d0bc090e9d6ba257a" + }, + "0x0000000000000000000000000000000000000000000000000000000000000005": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000002" + }, + "0x0000000000000000000000000000000000000000000000000000000000000009": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x000000000000000000000000b94ab28c6869466a46a42aba834ca2b3cecca5eb" + }, + "0x036b6384b5eca791c62761152d0c79bb0604c104a5fb6f4eb0703f3154bb3db0": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x00000000000000000000000000000000000000000000000045849994fc9c7b15" + }, + "0x036b6384b5eca791c62761152d0c79bb0604c104a5fb6f4eb0703f3154bb3db1": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x00000000000000000000000000000000000000000000000044ae84d8e9a37444" + }, + "0x1562a759f38dadd687c76a66b280b167a6625aa5c9dc9ab890f26b5ebb953db7": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x4022ffcbcc105342b5616796d3f4b6a3b8d846ffa35e402b44080d7804f6f92a" + }, + "0x1b93a487e9618b94b9c2582403f62f37366b935fb16b5662f095ef8d4dc9f2f9": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000041" + }, + "0x2e1d28cbd79909ab3b781a3b6ed36308538ed5e921eff761e217374e33e3d5e1": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "0x570ba59651bc4978d5dd0af8080856f5a828ccc4c9701178b8e71a42644ae7c5": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000007dff72693f6a4149b17e7c6314655f6a9f7c8b33" + }, + "0x64813f543e9e6d0745c3a3af9a3c60e2498e805c8ed7cfabec0a723971f1cd37": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0xda04a948b3ded5b8c007440e09d0c5fbf733e665881cdda1263ce39a086d3c5c" + }, + "0x785a1cd22288cc8a5ce664c50ca5b8ac546d6bfb4ebed8d76a096d29831fa61b": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000016798fdcb0000000000003f870857a3e0e3800000" + }, + "0x785a1cd22288cc8a5ce664c50ca5b8ac546d6bfb4ebed8d76a096d29831fa61c": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x000000000000000340aad21b3b7000000000000000003f870857a3e0e3800000" + }, + "0x785a1cd22288cc8a5ce664c50ca5b8ac546d6bfb4ebed8d76a096d29831fa61d": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000016798fdcb0000000000003f870857a3e0e3800000" + }, + "0x785a1cd22288cc8a5ce664c50ca5b8ac546d6bfb4ebed8d76a096d29831fa61e": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x000000000000000340aad21b3b7000000000000000003f870857a3e0e3800000" + }, + "0x785a1cd22288cc8a5ce664c50ca5b8ac546d6bfb4ebed8d76a096d29831fa61f": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000041" + }, + "0x785a1cd22288cc8a5ce664c50ca5b8ac546d6bfb4ebed8d76a096d29831fa620": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "0x975100a3a7525219458f095aea06e943da1f3a875c175269c7e08b138b296af9": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "0x9e623807eecacc367291c7c35b49ec61a342551b4603ac4258a7598ad0a02bff": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000041" + }, + "0xc0cc08974de25f30bc8e557411378c89328786c54ed03dc7a3f682c7b56a6013": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x00000000000000000000000006179f7c1be40863405f374e7f5f8806c728660a" + }, + "0xc43d59e5cc2bf8f1b992795c27c2d5bb31adadefc93354955ac0e2ca9c5be0f7": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000016798fdcb0000000000003f870857a3e0e3800000" + }, + "0xc43d59e5cc2bf8f1b992795c27c2d5bb31adadefc93354955ac0e2ca9c5be0f8": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x000000000000000340aad21b3b7000000000000000003f870857a3e0e3800000" + }, + "0xc43d59e5cc2bf8f1b992795c27c2d5bb31adadefc93354955ac0e2ca9c5be0f9": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000016798fdcb0000000000003f870857a3e0e3800000" + }, + "0xc43d59e5cc2bf8f1b992795c27c2d5bb31adadefc93354955ac0e2ca9c5be0fa": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x000000000000000340aad21b3b7000000000000000003f870857a3e0e3800000" + }, + "0xc43d59e5cc2bf8f1b992795c27c2d5bb31adadefc93354955ac0e2ca9c5be0fb": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000041" + }, + "0xc43d59e5cc2bf8f1b992795c27c2d5bb31adadefc93354955ac0e2ca9c5be0fc": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "0xe08cacc565c769c614e476d4ccf4c6029add84095117a2b614dc884c00913837": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x00000000000000000000000040d16fc0246ad3160ccc09b8d0d3a2cd28ae6c2f" + }, + "0xebcbba45421961a613aa8525f049792af76b4a3b9c8cd725fff0451a22d8172d": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x000000000000000000000000b94ab28c6869466a46a42aba834ca2b3cecca5eb" + }, + "0xf293d3be32695610199eb8e41697284defa313df24a10638ab2a31b95a75822b": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "0xffac741cc353cc8e05c1f8c328ec18e682b88bec50d99165f3501c0057e802f2": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000002" + } + } + }, + "0x2dc219e716793fb4b21548c0f009ba3af753ab01": { + "label": null, + "balanceDiff": null, + "stateDiff": {} + }, + "0x319d156ea750b20d5370ef7b348b6ff1ab5d0256": { + "label": null, + "balanceDiff": null, + "stateDiff": { + "0x308b08755ec965f49e4d58d22ebbf80dc425791b553f8567a173e85e1abb76c3": { + "previousValue": "0x006798fdca000000000002000000000000000000000000000000000000000000", + "newValue": "0x006798fdca000000000003000000000000000000000000000000000000000000" + }, + "0x308b08755ec965f49e4d58d22ebbf80dc425791b553f8567a173e85e1abb76c4": { + "previousValue": "0x000000000000000000093a8000000000000067c7224b00000000000000000000", + "newValue": "0x000000000000000000093a8000000000000067c7224b0000000000006798fdcb" + } + } + }, + "0x3c47237479e7569653ef9bec4a7cd2ee3f78b396": { + "label": null, + "balanceDiff": null, + "stateDiff": { + "0x0000000000000000000000000000000000000000000000000000000000000003": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "0x0a36b21564a50b489c7b7dcca93215e2afd0702b503d1a8e3b50c108e8d7839f": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "0x6f85df03f4fcc3fc7d00f87af334c257c3705343ad81da16f37407378cb7e4ef": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "0xc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x00000000000000000000000098217a06721ebf727f2c8d9ad7718ec28b7aae34" + } + } + }, + "0x43955b0899ab7232e3a454cf84aedd22ad46fd33": { + "label": null, + "balanceDiff": null, + "stateDiff": { + "0x33d6f6460e5ce31939fee460cf887465d577f44ac26a089019dbc2ebf3b03ece": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + } + }, + "0x6bb7a212910682dcfdbd5bcbb3e28fb4e8da10ee": { + "label": null, + "balanceDiff": null, + "stateDiff": {} + }, + "0x6f6c373d09c07425baae72317863d7f6bb731e37": { + "label": null, + "balanceDiff": null, + "stateDiff": { + "0xaedb84c47d3cd05c3e68318f98649e7914b45a3b2f4b28a179e397c8798e721d": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000009390b1735def18560c509e2d0bc090e9d6ba257a" + }, + "0xaedb84c47d3cd05c3e68318f98649e7914b45a3b2f4b28a179e397c8798e721e": { + "previousValue": "0x0000000000000000000000009390b1735def18560c509e2d0bc090e9d6ba257a", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "0xaedb84c47d3cd05c3e68318f98649e7914b45a3b2f4b28a179e397c8798e721f": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x00000000000000000000000098217a06721ebf727f2c8d9ad7718ec28b7aae34" + } + } + }, + "0x9390b1735def18560c509e2d0bc090e9d6ba257a": { + "label": null, + "balanceDiff": null, + "stateDiff": {} + }, + "0x98217a06721ebf727f2c8d9ad7718ec28b7aae34": { + "label": null, + "balanceDiff": null, + "stateDiff": {} + }, + "0xb0e1c7830aa781362f79225559aa068e6bdaf1d1": { + "label": null, + "balanceDiff": null, + "stateDiff": { + "0x0000000000000000000000000000000000000000000000000000000000000009": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "0x1ebe9857f999ac76ade61e7ad82fa7307375fd90b4af169ab9379c4e6f15c5d7": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "0x3f0a097792ef532deba2417df261f5eb4dcafb88ed9bd5e0519348fa55de58f0": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "0x6e1540171b6c0c960b71a7020d9f60077f6af931a8bbf590da0223dacf75c7af": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x00000000000000000000000098217a06721ebf727f2c8d9ad7718ec28b7aae34" + }, + "0x83554d8c4502b3f6bc599262032c7cb8ccee1b1cd1e03f03eee240b5daede8f1": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "0x9979ee02abd3fbf4dbff89812e2c588f68544dc555d1f71b00135932d3287269": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x000000000000000000000000000000000000000000108b2a2c28029094000000" + }, + "0x9979ee02abd3fbf4dbff89812e2c588f68544dc555d1f71b00135932d328726a": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x4343495020546f6b656e506f6f6c2076312e352e31000000000000000000002a" + }, + "0xb5179edb10a59dba7cb7c41b3485b038dc584e4c944dba95c2dcafc04f33a252": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + } + } + } + } +} +``` \ No newline at end of file diff --git a/diffs/AaveV3Base_GHOBaseListing_20241223_before_AaveV3Base_GHOBaseListing_20241223_after.md b/diffs/AaveV3Base_GHOBaseListing_20241223_before_AaveV3Base_GHOBaseListing_20241223_after.md new file mode 100644 index 000000000..839c5ee1b --- /dev/null +++ b/diffs/AaveV3Base_GHOBaseListing_20241223_before_AaveV3Base_GHOBaseListing_20241223_after.md @@ -0,0 +1,405 @@ +## Reserve changes + +### Reserves added + +#### GHO ([0x6Bb7a212910682DCFdbd5BCBb3e28FB4E8da10Ee](https://basescan.org/address/0x6Bb7a212910682DCFdbd5BCBb3e28FB4E8da10Ee)) + +| description | value | +| --- | --- | +| decimals | 18 | +| isActive | true | +| isFrozen | false | +| supplyCap | 2,500,000 GHO | +| borrowCap | 2,250,000 GHO | +| debtCeiling | 0 $ [0] | +| isSiloed | false | +| isFlashloanable | true | +| oracle | [0xfc421aD3C883Bf9E7C4f42dE845C4e4405799e73](https://basescan.org/address/0xfc421aD3C883Bf9E7C4f42dE845C4e4405799e73) | +| oracleDecimals | 8 | +| oracleLatestAnswer | 1 | +| usageAsCollateralEnabled | false | +| ltv | 0 % [0] | +| liquidationThreshold | 0 % [0] | +| liquidationBonus | 0 % | +| liquidationProtocolFee | 0 % [0] | +| reserveFactor | 10 % [1000] | +| aToken | [0x067ae75628177FD257c2B1e500993e1a0baBcBd1](https://basescan.org/address/0x067ae75628177FD257c2B1e500993e1a0baBcBd1) | +| variableDebtToken | [0x38e59ADE183BbEb94583d44213c8f3297e9933e9](https://basescan.org/address/0x38e59ADE183BbEb94583d44213c8f3297e9933e9) | +| borrowingEnabled | true | +| isBorrowableInIsolation | false | +| interestRateStrategy | [0x86AB1C62A8bf868E1b3E1ab87d587Aba6fbCbDC5](https://basescan.org/address/0x86AB1C62A8bf868E1b3E1ab87d587Aba6fbCbDC5) | +| aTokenName | Aave Base GHO | +| aTokenSymbol | aBasGHO | +| aTokenUnderlyingBalance | 1 GHO [1000000000000000000] | +| id | 8 | +| isPaused | false | +| variableDebtTokenName | Aave Base Variable Debt GHO | +| variableDebtTokenSymbol | variableDebtBasGHO | +| virtualAccountingActive | true | +| virtualBalance | 1 GHO [1000000000000000000] | +| optimalUsageRatio | 90 % | +| maxVariableBorrowRate | 59.5 % | +| baseVariableBorrowRate | 0 % | +| variableRateSlope1 | 9.5 % | +| variableRateSlope2 | 50 % | +| interestRate | ![ir](https://dash.onaave.com/api/static?variableRateSlope1=95000000000000000000000000&variableRateSlope2=500000000000000000000000000&optimalUsageRatio=900000000000000000000000000&baseVariableBorrowRate=0&maxVariableBorrowRate=595000000000000000000000000) | + + +## Raw diff + +```json +{ + "reserves": { + "0x6Bb7a212910682DCFdbd5BCBb3e28FB4E8da10Ee": { + "from": null, + "to": { + "aToken": "0x067ae75628177FD257c2B1e500993e1a0baBcBd1", + "aTokenName": "Aave Base GHO", + "aTokenSymbol": "aBasGHO", + "aTokenUnderlyingBalance": "1000000000000000000", + "borrowCap": 2250000, + "borrowingEnabled": true, + "debtCeiling": 0, + "decimals": 18, + "id": 8, + "interestRateStrategy": "0x86AB1C62A8bf868E1b3E1ab87d587Aba6fbCbDC5", + "isActive": true, + "isBorrowableInIsolation": false, + "isFlashloanable": true, + "isFrozen": false, + "isPaused": false, + "isSiloed": false, + "liquidationBonus": 0, + "liquidationProtocolFee": 0, + "liquidationThreshold": 0, + "ltv": 0, + "oracle": "0xfc421aD3C883Bf9E7C4f42dE845C4e4405799e73", + "oracleDecimals": 8, + "oracleLatestAnswer": "100000000", + "reserveFactor": 1000, + "supplyCap": 2500000, + "symbol": "GHO", + "underlying": "0x6Bb7a212910682DCFdbd5BCBb3e28FB4E8da10Ee", + "usageAsCollateralEnabled": false, + "variableDebtToken": "0x38e59ADE183BbEb94583d44213c8f3297e9933e9", + "variableDebtTokenName": "Aave Base Variable Debt GHO", + "variableDebtTokenSymbol": "variableDebtBasGHO", + "virtualAccountingActive": true, + "virtualBalance": "1000000000000000000" + } + } + }, + "strategies": { + "0x6Bb7a212910682DCFdbd5BCBb3e28FB4E8da10Ee": { + "from": null, + "to": { + "address": "0x86AB1C62A8bf868E1b3E1ab87d587Aba6fbCbDC5", + "baseVariableBorrowRate": "0", + "maxVariableBorrowRate": "595000000000000000000000000", + "optimalUsageRatio": "900000000000000000000000000", + "variableRateSlope1": "95000000000000000000000000", + "variableRateSlope2": "500000000000000000000000000" + } + } + }, + "raw": { + "from": null, + "to": { + "0x067ae75628177fd257c2b1e500993e1a0babcbd1": { + "label": null, + "balanceDiff": null, + "stateDiff": { + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x00000000000000000000000098f409fc4a42f34ae3c326c7f48ed01ae8caec69" + } + } + }, + "0x2425a746911128c2eaa7bebdc9bc452ee52208a1": { + "label": null, + "balanceDiff": null, + "stateDiff": { + "0x0000000000000000000000000000000000000000000000000000000000000000": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "0x0000000000000000000000000000000000000000000000000000000000000001": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "0x0000000000000000000000000000000000000000000000000000000000000035": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0xafadae5f174ce8792f064102d9c7825f700733e1922a8ccc1351982f24441141" + }, + "0x0000000000000000000000000000000000000000000000000000000000000037": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000006bb7a212910682dcfdbd5bcbb3e28fb4e8da10ee" + }, + "0x000000000000000000000000000000000000000000000000000000000000003b": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x416176652042617365205661726961626c6520446562742047484f0000000036" + }, + "0x000000000000000000000000000000000000000000000000000000000000003c": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x7661726961626c654465627442617347484f0000000000000000000000000024" + }, + "0x000000000000000000000000000000000000000000000000000000000000003d": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000f9cc4f0d883f1a1eb2c253bdb46c254ca51e1f4412" + } + } + }, + "0x2b22e425c1322fba0dbf17bb1da25d71811ee7ba": { + "label": null, + "balanceDiff": null, + "stateDiff": { + "0x3fc949ca89c9af8f3bf977ace52c826b7b79c9f52b6c2a1a960c0b4f1387f40e": { + "previousValue": "0x0000000000000000000000000000000000000000033b2e3c9fd0803ce8000000", + "newValue": "0x0000000000000000000000000000000000000000033b2e3c9fd0803ce8000000" + }, + "0x3fc949ca89c9af8f3bf977ace52c826b7b79c9f52b6c2a1a960c0b4f1387f40f": { + "previousValue": "0x0000000000000000000000000000000000000000033b2e3c9fd0803ce8000000", + "newValue": "0x0000000000000000000000000000000000000000033b2e3c9fd0803ce8000000" + }, + "0x3fc949ca89c9af8f3bf977ace52c826b7b79c9f52b6c2a1a960c0b4f1387f416": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x00000000000000000de0b6b3a764000000000000000000000000000000000000" + } + } + }, + "0x2cc0fc26ed4563a5ce5e8bdcfe1a2878676ae156": { + "label": null, + "balanceDiff": null, + "stateDiff": { + "0xc982d0ced1f8c746631cfe3f49173ca3ba25fe902be9cce56024300ebb89fd7a": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x000000000000000000000000fc421ad3c883bf9e7c4f42de845c4e4405799e73" + } + } + }, + "0x2dc219e716793fb4b21548c0f009ba3af753ab01": { + "label": null, + "balanceDiff": null, + "stateDiff": {} + }, + "0x319d156ea750b20d5370ef7b348b6ff1ab5d0256": { + "label": null, + "balanceDiff": null, + "stateDiff": { + "0xda4c88cb8422456e6dbc87bdc0d70fdf69c0f9f7d6833899744759615d2d4cc5": { + "previousValue": "0x006798fdca000000000002000000000000000000000000000000000000000000", + "newValue": "0x006798fdca000000000003000000000000000000000000000000000000000000" + }, + "0xda4c88cb8422456e6dbc87bdc0d70fdf69c0f9f7d6833899744759615d2d4cc6": { + "previousValue": "0x000000000000000000093a8000000000000067c7224b00000000000000000000", + "newValue": "0x000000000000000000093a8000000000000067c7224b0000000000006798fdcb" + } + } + }, + "0x38e59ade183bbeb94583d44213c8f3297e9933e9": { + "label": null, + "balanceDiff": null, + "stateDiff": { + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000002425a746911128c2eaa7bebdc9bc452ee52208a1" + } + } + }, + "0x3a9c471f13c9ca1ebdf440cf713c8404e498f9c3": { + "label": null, + "balanceDiff": null, + "stateDiff": { + "0x3fc949ca89c9af8f3bf977ace52c826b7b79c9f52b6c2a1a960c0b4f1387f40d": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x100000000000000000000000000002625a000022551003e88512000000000000" + }, + "0x3fc949ca89c9af8f3bf977ace52c826b7b79c9f52b6c2a1a960c0b4f1387f40e": { + "previousValue": "0x0000000000000000000000000000000000000000033b2e3c9fd0803ce8000000", + "newValue": "0x0000000000000000000000000000000000000000033b2e3c9fd0803ce8000000" + }, + "0x3fc949ca89c9af8f3bf977ace52c826b7b79c9f52b6c2a1a960c0b4f1387f40f": { + "previousValue": "0x0000000000000000000000000000000000000000033b2e3c9fd0803ce8000000", + "newValue": "0x0000000000000000000000000000000000000000033b2e3c9fd0803ce8000000" + }, + "0x3fc949ca89c9af8f3bf977ace52c826b7b79c9f52b6c2a1a960c0b4f1387f410": { + "previousValue": "0x0000000000000000000008000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000008006798fdcb00000000000000000000000000000000" + } + } + }, + "0x43955b0899ab7232e3a454cf84aedd22ad46fd33": { + "label": null, + "balanceDiff": null, + "stateDiff": {} + }, + "0x4816b2c2895f97fb918f1ae7da403750a0ee372e": { + "label": null, + "balanceDiff": null, + "stateDiff": {} + }, + "0x4d0109d509e66df298257ffdd55f94a3814343aa": { + "label": null, + "balanceDiff": null, + "stateDiff": {} + }, + "0x5731a04b1e775f0fdd454bf70f3335886e9a96be": { + "label": null, + "balanceDiff": null, + "stateDiff": {} + }, + "0x6533a273f3ac84df91dcd654d6ebaba73687e246": { + "label": null, + "balanceDiff": null, + "stateDiff": { + "0x34912faa3b94f6260811dc90a5fcd80ba8bbd27a331cfb3ec427d39ba71b94ab": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x000000000000000000000000ac140648435d03f784879cd789130f22ef588fcd" + }, + "0xf944e233e6a975ddc399fd33c6934da7dc0e17700dea63dc462b634655557be1": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x000000000000000000000000ac140648435d03f784879cd789130f22ef588fcd" + } + } + }, + "0x6bb7a212910682dcfdbd5bcbb3e28fb4e8da10ee": { + "label": null, + "balanceDiff": null, + "stateDiff": {} + }, + "0x86ab1c62a8bf868e1b3e1ab87d587aba6fbcbdc5": { + "label": null, + "balanceDiff": null, + "stateDiff": { + "0xc982d0ced1f8c746631cfe3f49173ca3ba25fe902be9cce56024300ebb89fd7a": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x00000000000000000000000000000000000000001388000003b6000000002328" + } + } + }, + "0x9390b1735def18560c509e2d0bc090e9d6ba257a": { + "label": null, + "balanceDiff": null, + "stateDiff": {} + }, + "0x98f409fc4a42f34ae3c326c7f48ed01ae8caec69": { + "label": null, + "balanceDiff": null, + "stateDiff": { + "0x0000000000000000000000000000000000000000000000000000000000000000": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "0x0000000000000000000000000000000000000000000000000000000000000001": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "0x0000000000000000000000000000000000000000000000000000000000000036": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" + }, + "0x0000000000000000000000000000000000000000000000000000000000000037": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x4161766520426173652047484f0000000000000000000000000000000000001a" + }, + "0x0000000000000000000000000000000000000000000000000000000000000038": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x6142617347484f0000000000000000000000000000000000000000000000000e" + }, + "0x0000000000000000000000000000000000000000000000000000000000000039": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000f9cc4f0d883f1a1eb2c253bdb46c254ca51e1f4412" + }, + "0x000000000000000000000000000000000000000000000000000000000000003b": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0xd95ff480dfea0a7d204013fdf8d80bacff144fe289b39887d7b8fffc54fa187e" + }, + "0x000000000000000000000000000000000000000000000000000000000000003c": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x000000000000000000000000ba9424d650a4f5c80a0da641254d1acce2a37057" + }, + "0x000000000000000000000000000000000000000000000000000000000000003d": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000006bb7a212910682dcfdbd5bcbb3e28fb4e8da10ee" + }, + "0x2dc2afdad33a5feea586a9545052327b65d28efb10d11fa69e77da986a1031cd": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x00000000033b2e3c9fd0803ce800000000000000000000000de0b6b3a7640000" + } + } + }, + "0xa238dd80c259a72e81d7e4664a9801593f98d1c5": { + "label": null, + "balanceDiff": null, + "stateDiff": {} + }, + "0xa58fb47be9074828215a173564c0cd10f6f249bf": { + "label": null, + "balanceDiff": null, + "stateDiff": { + "0x000000000000000000000000000000000000000000000000000000000000003b": { + "previousValue": "0x00000000000000000000000000000000000000000000000800000000000009c4", + "newValue": "0x00000000000000000000000000000000000000000000000900000000000009c4" + }, + "0x05725f7419f52ac606bc65a60e5ab85095522694ed898882d2777964ee382600": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000006bb7a212910682dcfdbd5bcbb3e28fb4e8da10ee" + }, + "0x3fc949ca89c9af8f3bf977ace52c826b7b79c9f52b6c2a1a960c0b4f1387f40e": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000033b2e3c9fd0803ce8000000" + }, + "0x3fc949ca89c9af8f3bf977ace52c826b7b79c9f52b6c2a1a960c0b4f1387f40f": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000033b2e3c9fd0803ce8000000" + }, + "0x3fc949ca89c9af8f3bf977ace52c826b7b79c9f52b6c2a1a960c0b4f1387f410": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000008000000000000000000000000000000000000000000" + }, + "0x3fc949ca89c9af8f3bf977ace52c826b7b79c9f52b6c2a1a960c0b4f1387f411": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x000000000000000000000000067ae75628177fd257c2b1e500993e1a0babcbd1" + }, + "0x3fc949ca89c9af8f3bf977ace52c826b7b79c9f52b6c2a1a960c0b4f1387f413": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x00000000000000000000000038e59ade183bbeb94583d44213c8f3297e9933e9" + }, + "0x3fc949ca89c9af8f3bf977ace52c826b7b79c9f52b6c2a1a960c0b4f1387f414": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x00000000000000000000000086ab1c62a8bf868e1b3e1ab87d587aba6fbcbdc5" + } + } + }, + "0xb0e1c7830aa781362f79225559aa068e6bdaf1d1": { + "label": null, + "balanceDiff": null, + "stateDiff": { + "0x153355011b7d7d042d97174072318f29dbc50882f47ebb07d64ddb67538aea5d": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" + }, + "0x618b6524a63db141c6ec50e3099a7bb2a245c9f2f08057a6ca507f1a6edc4b3a": { + "previousValue": "0x0000000000000000000000000000000000000000000000000000000000000000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "0xb80f08d50aa0366243778050783b5dd4d04222561c98abe78842dedaf9ce8580": { + "previousValue": "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000", + "newValue": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + } + }, + "0xe20fcbdbffc4dd138ce8b2e6fbb6cb49777ad64d": { + "label": null, + "balanceDiff": null, + "stateDiff": {} + }, + "0xf9cc4f0d883f1a1eb2c253bdb46c254ca51e1f44": { + "label": null, + "balanceDiff": null, + "stateDiff": {} + } + } + } +} +``` \ No newline at end of file diff --git a/diffs/AaveV3Ethereum_GhoIncidentReport_20231113_before_AaveV3Ethereum_GhoIncidentReport_20231113_after.md b/diffs/AaveV3Ethereum_GhoIncidentReport_20231113_before_AaveV3Ethereum_GhoIncidentReport_20231113_after.md new file mode 100644 index 000000000..1088d0e5d --- /dev/null +++ b/diffs/AaveV3Ethereum_GhoIncidentReport_20231113_before_AaveV3Ethereum_GhoIncidentReport_20231113_after.md @@ -0,0 +1,25 @@ +## Reserve changes + +### Reserves altered + +#### GHO ([0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f](https://etherscan.io/address/0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f)) + +| description | value before | value after | +| --- | --- | --- | +| variableDebtTokenImpl | [0x7aa606b1B341fFEeAfAdbbE4A2992EFB35972775](https://etherscan.io/address/0x7aa606b1B341fFEeAfAdbbE4A2992EFB35972775) | [0x20Cb2f303EDe313e2Cc44549Ad8653a5E8c0050e](https://etherscan.io/address/0x20Cb2f303EDe313e2Cc44549Ad8653a5E8c0050e) | + + +## Raw diff + +```json +{ + "reserves": { + "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f": { + "variableDebtTokenImpl": { + "from": "0x7aa606b1B341fFEeAfAdbbE4A2992EFB35972775", + "to": "0x20Cb2f303EDe313e2Cc44549Ad8653a5E8c0050e" + } + } + } +} +``` \ No newline at end of file diff --git a/src/20241223_Multi_GHOBaseLaunch/AaveV3Arbitrum_GHOBaseLaunch_20241223.sol b/src/20241223_Multi_GHOBaseLaunch/AaveV3Arbitrum_GHOBaseLaunch_20241223.sol new file mode 100644 index 000000000..109c804c1 --- /dev/null +++ b/src/20241223_Multi_GHOBaseLaunch/AaveV3Arbitrum_GHOBaseLaunch_20241223.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IProposalGenericExecutor} from 'aave-helpers/src/interfaces/IProposalGenericExecutor.sol'; +import {IUpgradeableBurnMintTokenPool_1_5_1} from 'src/interfaces/ccip/tokenPool/IUpgradeableBurnMintTokenPool.sol'; +import {IRateLimiter} from 'src/interfaces/ccip/IRateLimiter.sol'; +import {ILegacyProxyAdmin, ITransparentUpgradeableProxy} from 'src/interfaces/ILegacyProxyAdmin.sol'; + +import {AaveV3ArbitrumAssets} from 'aave-address-book/AaveV3Arbitrum.sol'; +import {GhoArbitrum} from 'aave-address-book/GhoArbitrum.sol'; +import {MiscArbitrum} from 'aave-address-book/MiscArbitrum.sol'; + +/** + * @title GHO Base Launch + * @author Aave Labs + * - Discussion: https://governance.aave.com/t/arfc-launch-gho-on-base-set-aci-as-emissions-manager-for-rewards/19338 + * - Snapshot: https://snapshot.box/#/s:aave.eth/proposal/0x03dc21e0423c60082dc23317af6ebaf997610cbc2cbb0f5a385653bd99a524fe + */ +contract AaveV3Arbitrum_GHOBaseLaunch_20241223 is IProposalGenericExecutor { + uint64 public constant BASE_CHAIN_SELECTOR = 15971525489660198786; + + // https://arbiscan.io/address/0xB94Ab28c6869466a46a42abA834ca2B3cECCA5eB + IUpgradeableBurnMintTokenPool_1_5_1 public constant TOKEN_POOL = + IUpgradeableBurnMintTokenPool_1_5_1(GhoArbitrum.GHO_CCIP_TOKEN_POOL); + + // https://arbiscan.io/address/0x9f0e4F4c5664888442E459f40f635765BB6265Ec + address public constant NEW_GHO_TOKEN_PROXY_ADMIN = 0x9f0e4F4c5664888442E459f40f635765BB6265Ec; + + // https://basescan.org/address/0x98217A06721Ebf727f2C8d9aD7718ec28b7aAe34 + address public constant REMOTE_TOKEN_POOL_BASE = 0x98217A06721Ebf727f2C8d9aD7718ec28b7aAe34; + // https://basescan.org/address/0x6Bb7a212910682DCFdbd5BCBb3e28FB4E8da10Ee + address public constant REMOTE_GHO_TOKEN_BASE = 0x6Bb7a212910682DCFdbd5BCBb3e28FB4E8da10Ee; + + // Token Rate Limit Capacity: 300_000 GHO + uint128 public constant CCIP_RATE_LIMIT_CAPACITY = 300_000e18; + // 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 { + // Update ProxyAdmin on GHO + ILegacyProxyAdmin(MiscArbitrum.PROXY_ADMIN).changeProxyAdmin( + ITransparentUpgradeableProxy(AaveV3ArbitrumAssets.GHO_UNDERLYING), + NEW_GHO_TOKEN_PROXY_ADMIN + ); + + IRateLimiter.Config memory rateLimiterConfig = IRateLimiter.Config({ + isEnabled: true, + capacity: CCIP_RATE_LIMIT_CAPACITY, + rate: CCIP_RATE_LIMIT_REFILL_RATE + }); + + IUpgradeableBurnMintTokenPool_1_5_1.ChainUpdate[] + memory chainsToAdd = new IUpgradeableBurnMintTokenPool_1_5_1.ChainUpdate[](1); + + bytes[] memory remotePoolAddresses = new bytes[](1); + remotePoolAddresses[0] = abi.encode(REMOTE_TOKEN_POOL_BASE); + + chainsToAdd[0] = IUpgradeableBurnMintTokenPool_1_5_1.ChainUpdate({ + remoteChainSelector: BASE_CHAIN_SELECTOR, + remotePoolAddresses: remotePoolAddresses, + remoteTokenAddress: abi.encode(REMOTE_GHO_TOKEN_BASE), + outboundRateLimiterConfig: rateLimiterConfig, + inboundRateLimiterConfig: rateLimiterConfig + }); + + TOKEN_POOL.applyChainUpdates({ + remoteChainSelectorsToRemove: new uint64[](0), + chainsToAdd: chainsToAdd + }); + } +} diff --git a/src/20241223_Multi_GHOBaseLaunch/AaveV3Arbitrum_GHOBaseLaunch_20241223.t.sol b/src/20241223_Multi_GHOBaseLaunch/AaveV3Arbitrum_GHOBaseLaunch_20241223.t.sol new file mode 100644 index 000000000..fea8b4736 --- /dev/null +++ b/src/20241223_Multi_GHOBaseLaunch/AaveV3Arbitrum_GHOBaseLaunch_20241223.t.sol @@ -0,0 +1,498 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; + +import {IUpgradeableBurnMintTokenPool_1_5_1} from 'src/interfaces/ccip/tokenPool/IUpgradeableBurnMintTokenPool.sol'; +import {IPool as IPool_CCIP} from 'src/interfaces/ccip/tokenPool/IPool.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 {IRateLimiter} from 'src/interfaces/ccip/IRateLimiter.sol'; +import {IEVM2EVMOnRamp} from 'src/interfaces/ccip/IEVM2EVMOnRamp.sol'; +import {IEVM2EVMOffRamp_1_5} from 'src/interfaces/ccip/IEVM2EVMOffRamp.sol'; +import {ITokenAdminRegistry} from 'src/interfaces/ccip/ITokenAdminRegistry.sol'; +import {IGhoToken} from 'src/interfaces/IGhoToken.sol'; +import {IGhoCcipSteward} from 'src/interfaces/IGhoCcipSteward.sol'; + +import {ProtocolV3TestBase} from 'aave-helpers/src/ProtocolV3TestBase.sol'; +import {AaveV3Arbitrum} from 'aave-address-book/AaveV3Arbitrum.sol'; +import {AaveV3ArbitrumAssets} from 'aave-address-book/AaveV3Arbitrum.sol'; +import {AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; +import {GovernanceV3Arbitrum} from 'aave-address-book/GovernanceV3Arbitrum.sol'; +import {MiscArbitrum} from 'aave-address-book/MiscArbitrum.sol'; + +import {ProxyAdmin, ITransparentUpgradeableProxy} from 'openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol'; + +import {CCIPUtils} from './utils/CCIPUtils.sol'; + +import {AaveV3Arbitrum_GHOBaseLaunch_20241223} from './AaveV3Arbitrum_GHOBaseLaunch_20241223.sol'; + +/** + * @dev Test for AaveV3Arbitrum_GHOBaseLaunch_20241223 + * command: FOUNDRY_PROFILE=arbitrum forge test --match-path=src/20241223_Multi_GHOBaseLaunch/AaveV3Arbitrum_GHOBaseLaunch_20241223.t.sol -vv + */ +contract AaveV3Arbitrum_GHOBaseLaunch_20241223_Base is ProtocolV3TestBase { + struct CCIPSendParams { + address sender; + uint256 amount; + uint64 destChainSelector; + } + + uint64 internal constant ARB_CHAIN_SELECTOR = CCIPUtils.ARB_CHAIN_SELECTOR; + uint64 internal constant BASE_CHAIN_SELECTOR = CCIPUtils.BASE_CHAIN_SELECTOR; + uint64 internal constant ETH_CHAIN_SELECTOR = CCIPUtils.ETH_CHAIN_SELECTOR; + uint256 internal constant CCIP_RATE_LIMIT_CAPACITY = 300_000e18; + uint256 internal constant CCIP_RATE_LIMIT_REFILL_RATE = 60e18; + + IGhoToken internal constant GHO = IGhoToken(AaveV3ArbitrumAssets.GHO_UNDERLYING); + ITokenAdminRegistry internal constant TOKEN_ADMIN_REGISTRY = + ITokenAdminRegistry(0x39AE1032cF4B334a1Ed41cdD0833bdD7c7E7751E); + IEVM2EVMOnRamp internal constant ETH_ON_RAMP = + IEVM2EVMOnRamp(0x67761742ac8A21Ec4D76CA18cbd701e5A6F3Bef3); + IEVM2EVMOnRamp internal constant BASE_ON_RAMP = + IEVM2EVMOnRamp(0xc1b6287A3292d6469F2D8545877E40A2f75CA9a6); + IEVM2EVMOffRamp_1_5 internal constant ETH_OFF_RAMP = + IEVM2EVMOffRamp_1_5(0x91e46cc5590A4B9182e47f40006140A7077Dec31); + IEVM2EVMOffRamp_1_5 internal constant BASE_OFF_RAMP = + IEVM2EVMOffRamp_1_5(0xb62178f8198905D0Fa6d640Bdb188E4E8143Ac4b); + + address internal constant RISK_COUNCIL = 0x8513e6F37dBc52De87b166980Fa3F50639694B60; + address public constant NEW_REMOTE_TOKEN_BASE = 0x6Bb7a212910682DCFdbd5BCBb3e28FB4E8da10Ee; + IRouter internal constant ROUTER = IRouter(0x141fa059441E0ca23ce184B6A78bafD2A517DdE8); + IGhoCcipSteward internal constant NEW_GHO_CCIP_STEWARD = + IGhoCcipSteward(0xCd5ab470AaC5c13e1063ee700503f3346b7C90Db); + IUpgradeableBurnMintTokenPool_1_5_1 internal constant NEW_TOKEN_POOL = + IUpgradeableBurnMintTokenPool_1_5_1(0xB94Ab28c6869466a46a42abA834ca2B3cECCA5eB); + address internal constant NEW_REMOTE_POOL_ETH = 0x06179f7C1be40863405f374E7f5F8806c728660A; + address internal constant NEW_REMOTE_POOL_BASE = 0x98217A06721Ebf727f2C8d9aD7718ec28b7aAe34; + + AaveV3Arbitrum_GHOBaseLaunch_20241223 internal proposal; + + address internal alice = makeAddr('alice'); + address internal bob = makeAddr('bob'); + address internal carol = makeAddr('carol'); + + 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); + error InvalidSourcePoolAddress(bytes); + + function setUp() public virtual { + vm.createSelectFork(vm.rpcUrl('arbitrum'), 300142041); + proposal = new AaveV3Arbitrum_GHOBaseLaunch_20241223(); + _validateConstants(); + } + + function _validateConstants() private view { + assertEq(proposal.BASE_CHAIN_SELECTOR(), BASE_CHAIN_SELECTOR); + assertEq(address(proposal.TOKEN_POOL()), address(NEW_TOKEN_POOL)); + assertEq(proposal.REMOTE_TOKEN_POOL_BASE(), NEW_REMOTE_POOL_BASE); + assertEq(proposal.REMOTE_GHO_TOKEN_BASE(), NEW_REMOTE_TOKEN_BASE); + assertEq(proposal.CCIP_RATE_LIMIT_CAPACITY(), CCIP_RATE_LIMIT_CAPACITY); + assertEq(proposal.CCIP_RATE_LIMIT_REFILL_RATE(), CCIP_RATE_LIMIT_REFILL_RATE); + + assertEq(TOKEN_ADMIN_REGISTRY.typeAndVersion(), 'TokenAdminRegistry 1.5.0'); + assertEq(NEW_TOKEN_POOL.typeAndVersion(), 'BurnMintTokenPool 1.5.1'); + assertEq(ROUTER.typeAndVersion(), 'Router 1.2.0'); + + _assertOnRamp(ETH_ON_RAMP, ARB_CHAIN_SELECTOR, ETH_CHAIN_SELECTOR, ROUTER); + _assertOnRamp(BASE_ON_RAMP, ARB_CHAIN_SELECTOR, BASE_CHAIN_SELECTOR, ROUTER); + _assertOffRamp(ETH_OFF_RAMP, ETH_CHAIN_SELECTOR, ARB_CHAIN_SELECTOR, ROUTER); + _assertOffRamp(BASE_OFF_RAMP, BASE_CHAIN_SELECTOR, ARB_CHAIN_SELECTOR, ROUTER); + + assertEq(NEW_GHO_CCIP_STEWARD.RISK_COUNCIL(), RISK_COUNCIL); + assertEq(NEW_GHO_CCIP_STEWARD.GHO_TOKEN(), AaveV3ArbitrumAssets.GHO_UNDERLYING); + assertEq(NEW_GHO_CCIP_STEWARD.GHO_TOKEN_POOL(), address(NEW_TOKEN_POOL)); + assertFalse(NEW_GHO_CCIP_STEWARD.BRIDGE_LIMIT_ENABLED()); + + ProxyAdmin newProxyAdmin = ProxyAdmin(proposal.NEW_GHO_TOKEN_PROXY_ADMIN()); + assertEq(newProxyAdmin.owner(), GovernanceV3Arbitrum.EXECUTOR_LVL_1); + assertEq(newProxyAdmin.UPGRADE_INTERFACE_VERSION(), '5.0.0'); + } + + function _assertOnRamp( + IEVM2EVMOnRamp onRamp, + uint64 srcSelector, + uint64 dstSelector, + IRouter router + ) internal view { + assertEq(onRamp.typeAndVersion(), 'EVM2EVMOnRamp 1.5.0'); + assertEq(onRamp.getStaticConfig().chainSelector, srcSelector); + assertEq(onRamp.getStaticConfig().destChainSelector, dstSelector); + assertEq(onRamp.getDynamicConfig().router, address(router)); + assertEq(router.getOnRamp(dstSelector), address(onRamp)); + } + + function _assertOffRamp( + IEVM2EVMOffRamp_1_5 offRamp, + uint64 srcSelector, + uint64 dstSelector, + IRouter router + ) internal view { + assertEq(offRamp.typeAndVersion(), 'EVM2EVMOffRamp 1.5.0'); + assertEq(offRamp.getStaticConfig().sourceChainSelector, srcSelector); + assertEq(offRamp.getStaticConfig().chainSelector, dstSelector); + assertEq(offRamp.getDynamicConfig().router, address(router)); + assertTrue(router.isOffRamp(srcSelector, address(offRamp))); + } + + 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(GHO), amount: params.amount}); + + uint256 feeAmount = ROUTER.getFee(params.destChainSelector, message); + deal(params.sender, feeAmount); + + IInternal.EVM2EVMMessage memory eventArg = CCIPUtils.messageToEvent( + CCIPUtils.MessageToEventParams({ + message: message, + router: ROUTER, + sourceChainSelector: ARB_CHAIN_SELECTOR, + destChainSelector: params.destChainSelector, + feeTokenAmount: feeAmount, + originalSender: params.sender, + sourceToken: address(GHO), + destinationToken: address( + params.destChainSelector == BASE_CHAIN_SELECTOR + ? NEW_REMOTE_TOKEN_BASE + : AaveV3EthereumAssets.GHO_UNDERLYING + ) + }) + ); + + return (message, eventArg); + } + + function _tokenBucketToConfig( + IRateLimiter.TokenBucket memory bucket + ) internal pure returns (IRateLimiter.Config memory) { + return + IRateLimiter.Config({ + isEnabled: bucket.isEnabled, + capacity: bucket.capacity, + rate: bucket.rate + }); + } + + function _getDisabledConfig() internal pure returns (IRateLimiter.Config memory) { + return IRateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}); + } + + function _getImplementation(address proxy) internal view returns (address) { + bytes32 slot = bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1); + return address(uint160(uint256(vm.load(proxy, slot)))); + } + + function _getProxyAdmin(address proxy) internal view returns (address) { + bytes32 slot = bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1); + return address(uint160(uint256(vm.load(proxy, slot)))); + } + + function _readInitialized(address proxy) internal view returns (uint8) { + return uint8(uint256(vm.load(proxy, bytes32(0)))); + } + + 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() + }); + } + + function _getOutboundRefillTime(uint256 amount) internal pure returns (uint256) { + return (amount / CCIP_RATE_LIMIT_REFILL_RATE) + 1; // account for rounding + } + + function _getInboundRefillTime(uint256 amount) internal pure returns (uint256) { + return amount / CCIP_RATE_LIMIT_REFILL_RATE + 1; // account for rounding + } + + function _min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + + function assertEq( + IRateLimiter.TokenBucket memory bucket, + IRateLimiter.Config memory config + ) internal pure { + assertEq(bucket.isEnabled, config.isEnabled); + assertEq(bucket.capacity, config.capacity); + assertEq(bucket.rate, config.rate); + } +} + +contract AaveV3Arbitrum_GHOBaseLaunch_20241223_PreExecution is + AaveV3Arbitrum_GHOBaseLaunch_20241223_Base +{ + function test_ghoTokenProxyAdminUpgrade() public { + assertEq(_getProxyAdmin(address(GHO)), MiscArbitrum.PROXY_ADMIN); + executePayload(vm, address(proposal)); + assertEq(_getProxyAdmin(address(GHO)), proposal.NEW_GHO_TOKEN_PROXY_ADMIN()); + } + + function test_ghoProxyAdminCanUpgradeImplementation() public { + executePayload(vm, address(proposal)); + address miscImpl = makeAddr('miscImpl'); + vm.etch(miscImpl, hex'00'); // stop opcode + vm.startPrank(GovernanceV3Arbitrum.EXECUTOR_LVL_1); + ProxyAdmin(proposal.NEW_GHO_TOKEN_PROXY_ADMIN()).upgradeAndCall( + ITransparentUpgradeableProxy(address(GHO)), + miscImpl, + '' + ); + assertEq(_getImplementation(address(GHO)), miscImpl); + } +} + +contract AaveV3Arbitrum_GHOBaseLaunch_20241223_PostExecution is + AaveV3Arbitrum_GHOBaseLaunch_20241223_Base +{ + function setUp() public override { + super.setUp(); + executePayload(vm, address(proposal)); + } + + function test_basePoolConfig() public view { + assertEq(NEW_TOKEN_POOL.getSupportedChains().length, 2); + assertEq(NEW_TOKEN_POOL.getSupportedChains()[0], ETH_CHAIN_SELECTOR); + assertEq(NEW_TOKEN_POOL.getSupportedChains()[1], BASE_CHAIN_SELECTOR); + assertEq( + NEW_TOKEN_POOL.getRemoteToken(ETH_CHAIN_SELECTOR), + abi.encode(address(AaveV3EthereumAssets.GHO_UNDERLYING)) + ); + assertEq( + NEW_TOKEN_POOL.getRemoteToken(BASE_CHAIN_SELECTOR), + abi.encode(address(NEW_REMOTE_TOKEN_BASE)) + ); + assertEq(NEW_TOKEN_POOL.getRemotePools(BASE_CHAIN_SELECTOR).length, 1); + assertEq( + NEW_TOKEN_POOL.getRemotePools(BASE_CHAIN_SELECTOR)[0], + abi.encode(address(NEW_REMOTE_POOL_BASE)) + ); + assertEq(NEW_TOKEN_POOL.getRemotePools(ETH_CHAIN_SELECTOR).length, 2); + assertEq( + NEW_TOKEN_POOL.getRemotePools(ETH_CHAIN_SELECTOR)[1], // 0th is the 1.4 token pool + abi.encode(address(NEW_REMOTE_POOL_ETH)) + ); + assertEq( + NEW_TOKEN_POOL.getCurrentInboundRateLimiterState(ETH_CHAIN_SELECTOR), + _getRateLimiterConfig() + ); + assertEq( + NEW_TOKEN_POOL.getCurrentOutboundRateLimiterState(ETH_CHAIN_SELECTOR), + _getRateLimiterConfig() + ); + assertEq( + NEW_TOKEN_POOL.getCurrentInboundRateLimiterState(BASE_CHAIN_SELECTOR), + _getRateLimiterConfig() + ); + assertEq( + NEW_TOKEN_POOL.getCurrentOutboundRateLimiterState(BASE_CHAIN_SELECTOR), + _getRateLimiterConfig() + ); + } + + function test_sendMessageToBaseSucceeds(uint256 amount) public { + uint256 bridgeableAmount = _min( + GHO.getFacilitator(address(NEW_TOKEN_POOL)).bucketLevel, + CCIP_RATE_LIMIT_CAPACITY + ); + amount = bound(amount, 1, bridgeableAmount); + skip(_getOutboundRefillTime(amount)); // wait for the rate limiter to refill + + deal(address(GHO), alice, amount); + vm.prank(alice); + GHO.approve(address(ROUTER), amount); + + uint256 aliceBalance = GHO.balanceOf(alice); + uint256 bucketLevel = GHO.getFacilitator(address(NEW_TOKEN_POOL)).bucketLevel; + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({amount: amount, sender: alice, destChainSelector: BASE_CHAIN_SELECTOR}) + ); + + vm.expectEmit(address(NEW_TOKEN_POOL)); + emit Burned(address(BASE_ON_RAMP), amount); + vm.expectEmit(address(BASE_ON_RAMP)); + emit CCIPSendRequested(eventArg); + + vm.prank(alice); + ROUTER.ccipSend{value: eventArg.feeTokenAmount}(BASE_CHAIN_SELECTOR, message); + + assertEq(GHO.balanceOf(alice), aliceBalance - amount); + assertEq(GHO.getFacilitator(address(NEW_TOKEN_POOL)).bucketLevel, bucketLevel - amount); + } + + function test_sendMessageToEthSucceeds(uint256 amount) public { + uint256 bridgeableAmount = _min( + GHO.getFacilitator(address(NEW_TOKEN_POOL)).bucketLevel, + CCIP_RATE_LIMIT_CAPACITY + ); + amount = bound(amount, 1, bridgeableAmount); + skip(_getOutboundRefillTime(amount)); // wait for the rate limiter to refill + + deal(address(GHO), alice, amount); + vm.prank(alice); + GHO.approve(address(ROUTER), amount); + + uint256 aliceBalance = GHO.balanceOf(alice); + uint256 bucketLevel = GHO.getFacilitator(address(NEW_TOKEN_POOL)).bucketLevel; + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({amount: amount, sender: alice, destChainSelector: ETH_CHAIN_SELECTOR}) + ); + + vm.expectEmit(address(NEW_TOKEN_POOL)); + emit Burned(address(ETH_ON_RAMP), amount); + vm.expectEmit(address(ETH_ON_RAMP)); + emit CCIPSendRequested(eventArg); + + vm.prank(alice); + ROUTER.ccipSend{value: eventArg.feeTokenAmount}(ETH_CHAIN_SELECTOR, message); + + assertEq(GHO.balanceOf(alice), aliceBalance - amount); + assertEq(GHO.getFacilitator(address(NEW_TOKEN_POOL)).bucketLevel, bucketLevel - amount); + } + + function test_offRampViaBaseSucceeds(uint256 amount) public { + (uint256 bucketCapacity, uint256 bucketLevel) = GHO.getFacilitatorBucket( + address(NEW_TOKEN_POOL) + ); + uint256 mintAbleAmount = _min(bucketCapacity - bucketLevel, CCIP_RATE_LIMIT_CAPACITY); + amount = bound(amount, 1, mintAbleAmount); + skip(_getInboundRefillTime(amount)); + + uint256 aliceBalance = GHO.balanceOf(alice); + + vm.expectEmit(address(NEW_TOKEN_POOL)); + emit Minted(address(BASE_OFF_RAMP), alice, amount); + + vm.prank(address(BASE_OFF_RAMP)); + NEW_TOKEN_POOL.releaseOrMint( + IPool_CCIP.ReleaseOrMintInV1({ + originalSender: abi.encode(alice), + remoteChainSelector: BASE_CHAIN_SELECTOR, + receiver: alice, + amount: amount, + localToken: address(GHO), + sourcePoolAddress: abi.encode(address(NEW_REMOTE_POOL_BASE)), + sourcePoolData: new bytes(0), + offchainTokenData: new bytes(0) + }) + ); + + assertEq(GHO.getFacilitator(address(NEW_TOKEN_POOL)).bucketLevel, bucketLevel + amount); + assertEq(GHO.balanceOf(alice), aliceBalance + amount); + } + + function test_offRampViaEthSucceeds(uint256 amount) public { + (uint256 bucketCapacity, uint256 bucketLevel) = GHO.getFacilitatorBucket( + address(NEW_TOKEN_POOL) + ); + uint256 mintAbleAmount = _min(bucketCapacity - bucketLevel, CCIP_RATE_LIMIT_CAPACITY); + amount = bound(amount, 1, mintAbleAmount); + skip(_getInboundRefillTime(amount)); + + uint256 aliceBalance = GHO.balanceOf(alice); + + vm.expectEmit(address(NEW_TOKEN_POOL)); + emit Minted(address(ETH_OFF_RAMP), alice, amount); + + vm.prank(address(ETH_OFF_RAMP)); + NEW_TOKEN_POOL.releaseOrMint( + IPool_CCIP.ReleaseOrMintInV1({ + originalSender: abi.encode(alice), + remoteChainSelector: ETH_CHAIN_SELECTOR, + receiver: alice, + amount: amount, + localToken: address(GHO), + sourcePoolAddress: abi.encode(address(NEW_REMOTE_POOL_ETH)), + sourcePoolData: new bytes(0), + offchainTokenData: new bytes(0) + }) + ); + + assertEq(GHO.getFacilitator(address(NEW_TOKEN_POOL)).bucketLevel, bucketLevel + amount); + assertEq(GHO.balanceOf(alice), aliceBalance + amount); + } + + function test_cannotUseBaseOffRampForEthMessages() public { + uint256 amount = 100e18; + skip(_getInboundRefillTime(amount)); + + vm.expectRevert( + abi.encodeWithSelector(CallerIsNotARampOnRouter.selector, address(BASE_OFF_RAMP)) + ); + vm.prank(address(BASE_OFF_RAMP)); + NEW_TOKEN_POOL.releaseOrMint( + IPool_CCIP.ReleaseOrMintInV1({ + originalSender: abi.encode(alice), + remoteChainSelector: ETH_CHAIN_SELECTOR, + receiver: alice, + amount: amount, + localToken: address(GHO), + sourcePoolAddress: abi.encode(address(NEW_REMOTE_POOL_ETH)), + sourcePoolData: new bytes(0), + offchainTokenData: new bytes(0) + }) + ); + } + + function test_cannotOffRampOtherChainMessages() public { + uint256 amount = 100e18; + skip(_getInboundRefillTime(amount)); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidSourcePoolAddress.selector, + abi.encode(address(NEW_REMOTE_POOL_ETH)) + ) + ); + vm.prank(address(BASE_OFF_RAMP)); + NEW_TOKEN_POOL.releaseOrMint( + IPool_CCIP.ReleaseOrMintInV1({ + originalSender: abi.encode(alice), + remoteChainSelector: BASE_CHAIN_SELECTOR, + receiver: alice, + amount: amount, + localToken: address(GHO), + sourcePoolAddress: abi.encode(address(NEW_REMOTE_POOL_ETH)), + sourcePoolData: new bytes(0), + offchainTokenData: new bytes(0) + }) + ); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidSourcePoolAddress.selector, + abi.encode(address(NEW_REMOTE_POOL_BASE)) + ) + ); + vm.prank(address(ETH_OFF_RAMP)); + NEW_TOKEN_POOL.releaseOrMint( + IPool_CCIP.ReleaseOrMintInV1({ + originalSender: abi.encode(alice), + remoteChainSelector: ETH_CHAIN_SELECTOR, + receiver: alice, + amount: amount, + localToken: address(GHO), + sourcePoolAddress: abi.encode(address(NEW_REMOTE_POOL_BASE)), + sourcePoolData: new bytes(0), + offchainTokenData: new bytes(0) + }) + ); + } +} diff --git a/src/20241223_Multi_GHOBaseLaunch/AaveV3Base_GHOBaseLaunch_20241223.sol b/src/20241223_Multi_GHOBaseLaunch/AaveV3Base_GHOBaseLaunch_20241223.sol new file mode 100644 index 000000000..020dd68be --- /dev/null +++ b/src/20241223_Multi_GHOBaseLaunch/AaveV3Base_GHOBaseLaunch_20241223.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IProposalGenericExecutor} from 'aave-helpers/src/interfaces/IProposalGenericExecutor.sol'; +import {IUpgradeableBurnMintTokenPool_1_5_1} from 'src/interfaces/ccip/tokenPool/IUpgradeableBurnMintTokenPool.sol'; +import {ITokenAdminRegistry} from 'src/interfaces/ccip/ITokenAdminRegistry.sol'; +import {IRateLimiter} from 'src/interfaces/ccip/IRateLimiter.sol'; +import {IGhoBucketSteward} from 'src/interfaces/IGhoBucketSteward.sol'; +import {IGhoToken} from 'src/interfaces/IGhoToken.sol'; + +import {AaveV3Base} from 'aave-address-book/AaveV3Base.sol'; +import {GovernanceV3Base} from 'aave-address-book/GovernanceV3Base.sol'; +import {AaveV3ArbitrumAssets} from 'aave-address-book/AaveV3Arbitrum.sol'; +import {GhoArbitrum} from 'aave-address-book/GhoArbitrum.sol'; +import {GhoEthereum} from 'aave-address-book/GhoEthereum.sol'; +import {AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; + +/** + * @title GHO Base Launch + * @author Aave Labs + * - Discussion: https://governance.aave.com/t/arfc-launch-gho-on-base-set-aci-as-emissions-manager-for-rewards/19338 + * - Snapshot: https://snapshot.box/#/s:aave.eth/proposal/0x03dc21e0423c60082dc23317af6ebaf997610cbc2cbb0f5a385653bd99a524fe + */ +contract AaveV3Base_GHOBaseLaunch_20241223 is IProposalGenericExecutor { + uint64 public constant ETH_CHAIN_SELECTOR = 5009297550715157269; + uint64 public constant ARB_CHAIN_SELECTOR = 4949039107694359620; + + uint128 public constant CCIP_BUCKET_CAPACITY = 20_000_000e18; // 20M GHO + + // https://basescan.org/address/0x6f6C373d09C07425BaAE72317863d7F6bb731e37 + ITokenAdminRegistry public constant TOKEN_ADMIN_REGISTRY = + ITokenAdminRegistry(0x6f6C373d09C07425BaAE72317863d7F6bb731e37); + // https://basescan.org/address/0x98217A06721Ebf727f2C8d9aD7718ec28b7aAe34 + IUpgradeableBurnMintTokenPool_1_5_1 public constant TOKEN_POOL = + IUpgradeableBurnMintTokenPool_1_5_1(0x98217A06721Ebf727f2C8d9aD7718ec28b7aAe34); + + // https://basescan.org/address/0x6Bb7a212910682DCFdbd5BCBb3e28FB4E8da10Ee + IGhoToken public constant GHO_TOKEN = IGhoToken(0x6Bb7a212910682DCFdbd5BCBb3e28FB4E8da10Ee); + + // https://basescan.org/address/0xC5BcC58BE6172769ca1a78B8A45752E3C5059c39 + address public constant GHO_AAVE_STEWARD = 0xC5BcC58BE6172769ca1a78B8A45752E3C5059c39; + // https://basescan.org/address/0x3c47237479e7569653eF9beC4a7Cd2ee3F78b396 + address public constant GHO_BUCKET_STEWARD = 0x3c47237479e7569653eF9beC4a7Cd2ee3F78b396; + // https://basescan.org/address/0xB94Ab28c6869466a46a42abA834ca2B3cECCA5eB + address public constant GHO_CCIP_STEWARD = 0xB94Ab28c6869466a46a42abA834ca2B3cECCA5eB; + + // https://etherscan.io/address/0x06179f7C1be40863405f374E7f5F8806c728660A + address public constant REMOTE_TOKEN_POOL_ETH = GhoEthereum.GHO_CCIP_TOKEN_POOL; + // https://arbiscan.io/address/0xB94Ab28c6869466a46a42abA834ca2B3cECCA5eB + address public constant REMOTE_TOKEN_POOL_ARB = GhoArbitrum.GHO_CCIP_TOKEN_POOL; + + // Token Rate Limit Capacity: 300_000 GHO + uint128 public constant CCIP_RATE_LIMIT_CAPACITY = 300_000e18; + // 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 { + _acceptOwnership(); + _setupStewardsAndTokenPoolOnGho(); + _setupRemoteAndRegisterTokenPool(); + } + + function _acceptOwnership() internal { + TOKEN_ADMIN_REGISTRY.acceptAdminRole(address(GHO_TOKEN)); + TOKEN_POOL.acceptOwnership(); + } + + function _setupStewardsAndTokenPoolOnGho() internal { + GHO_TOKEN.grantRole(GHO_TOKEN.FACILITATOR_MANAGER_ROLE(), GovernanceV3Base.EXECUTOR_LVL_1); + GHO_TOKEN.grantRole(GHO_TOKEN.BUCKET_MANAGER_ROLE(), GovernanceV3Base.EXECUTOR_LVL_1); + + // Token Pool as facilitator with 20M GHO capacity + GHO_TOKEN.addFacilitator(address(TOKEN_POOL), 'CCIP TokenPool v1.5.1', CCIP_BUCKET_CAPACITY); + + // Gho Aave Steward + AaveV3Base.ACL_MANAGER.grantRole(AaveV3Base.ACL_MANAGER.RISK_ADMIN_ROLE(), GHO_AAVE_STEWARD); + + // Gho Bucket Steward + GHO_TOKEN.grantRole(GHO_TOKEN.BUCKET_MANAGER_ROLE(), GHO_BUCKET_STEWARD); + address[] memory facilitatorList = new address[](1); + facilitatorList[0] = address(TOKEN_POOL); + IGhoBucketSteward(GHO_BUCKET_STEWARD).setControlledFacilitator({ + facilitatorList: facilitatorList, + approve: true + }); + + // Gho CCIP Steward + TOKEN_POOL.setRateLimitAdmin(GHO_CCIP_STEWARD); + } + + function _setupRemoteAndRegisterTokenPool() internal { + IRateLimiter.Config memory rateLimiterConfig = IRateLimiter.Config({ + isEnabled: true, + capacity: CCIP_RATE_LIMIT_CAPACITY, + rate: CCIP_RATE_LIMIT_REFILL_RATE + }); + + IUpgradeableBurnMintTokenPool_1_5_1.ChainUpdate[] + memory chainsToAdd = new IUpgradeableBurnMintTokenPool_1_5_1.ChainUpdate[](2); + + { + bytes[] memory remotePoolAddresses = new bytes[](1); + remotePoolAddresses[0] = abi.encode(REMOTE_TOKEN_POOL_ETH); + chainsToAdd[0] = IUpgradeableBurnMintTokenPool_1_5_1.ChainUpdate({ + remoteChainSelector: ETH_CHAIN_SELECTOR, + remotePoolAddresses: remotePoolAddresses, + remoteTokenAddress: abi.encode(AaveV3EthereumAssets.GHO_UNDERLYING), + outboundRateLimiterConfig: rateLimiterConfig, + inboundRateLimiterConfig: rateLimiterConfig + }); + } + + { + bytes[] memory remotePoolAddresses = new bytes[](1); + remotePoolAddresses[0] = abi.encode(REMOTE_TOKEN_POOL_ARB); + chainsToAdd[1] = IUpgradeableBurnMintTokenPool_1_5_1.ChainUpdate({ + remoteChainSelector: ARB_CHAIN_SELECTOR, + remotePoolAddresses: remotePoolAddresses, + remoteTokenAddress: abi.encode(AaveV3ArbitrumAssets.GHO_UNDERLYING), + outboundRateLimiterConfig: rateLimiterConfig, + inboundRateLimiterConfig: rateLimiterConfig + }); + } + + // setup remote token pools + TOKEN_POOL.applyChainUpdates({ + remoteChainSelectorsToRemove: new uint64[](0), + chainsToAdd: chainsToAdd + }); + + // register token pool + TOKEN_ADMIN_REGISTRY.setPool(address(GHO_TOKEN), address(TOKEN_POOL)); + } +} diff --git a/src/20241223_Multi_GHOBaseLaunch/AaveV3Base_GHOBaseLaunch_20241223.t.sol b/src/20241223_Multi_GHOBaseLaunch/AaveV3Base_GHOBaseLaunch_20241223.t.sol new file mode 100644 index 000000000..eccd79d37 --- /dev/null +++ b/src/20241223_Multi_GHOBaseLaunch/AaveV3Base_GHOBaseLaunch_20241223.t.sol @@ -0,0 +1,570 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; + +import {IUpgradeableBurnMintTokenPool_1_5_1} from 'src/interfaces/ccip/tokenPool/IUpgradeableBurnMintTokenPool.sol'; +import {IPool as IPool_CCIP} from 'src/interfaces/ccip/tokenPool/IPool.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 {IRateLimiter} from 'src/interfaces/ccip/IRateLimiter.sol'; +import {IEVM2EVMOnRamp} from 'src/interfaces/ccip/IEVM2EVMOnRamp.sol'; +import {IEVM2EVMOffRamp_1_5} from 'src/interfaces/ccip/IEVM2EVMOffRamp.sol'; +import {ITokenAdminRegistry} from 'src/interfaces/ccip/ITokenAdminRegistry.sol'; +import {IGhoToken} from 'src/interfaces/IGhoToken.sol'; +import {IGhoAaveSteward} from 'src/interfaces/IGhoAaveSteward.sol'; +import {IGhoBucketSteward} from 'src/interfaces/IGhoBucketSteward.sol'; +import {IGhoCcipSteward} from 'src/interfaces/IGhoCcipSteward.sol'; +import {IOwnable} from 'aave-address-book/common/IOwnable.sol'; + +import {ProtocolV3TestBase} from 'aave-helpers/src/ProtocolV3TestBase.sol'; +import {AaveV3Arbitrum} from 'aave-address-book/AaveV3Arbitrum.sol'; +import {AaveV3Base} from 'aave-address-book/AaveV3Base.sol'; +import {AaveV3ArbitrumAssets} from 'aave-address-book/AaveV3Arbitrum.sol'; +import {AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; +import {GovernanceV3Base} from 'aave-address-book/GovernanceV3Base.sol'; + +import {ProxyAdmin} from 'openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol'; + +import {CCIPUtils} from './utils/CCIPUtils.sol'; + +import {AaveV3Base_GHOBaseLaunch_20241223} from './AaveV3Base_GHOBaseLaunch_20241223.sol'; + +/** + * @dev Test for AaveV3Base_GHOBaseLaunch_20241223 + * command: FOUNDRY_PROFILE=base forge test --match-path=src/20241223_Multi_GHOBaseLaunch/AaveV3Base_GHOBaseLaunch_20241223.t.sol -vv + */ +contract AaveV3Base_GHOBaseLaunch_20241223_Base is ProtocolV3TestBase { + struct CCIPSendParams { + address sender; + uint256 amount; + uint64 destChainSelector; + } + + uint64 internal constant ARB_CHAIN_SELECTOR = CCIPUtils.ARB_CHAIN_SELECTOR; + uint64 internal constant BASE_CHAIN_SELECTOR = CCIPUtils.BASE_CHAIN_SELECTOR; + uint64 internal constant ETH_CHAIN_SELECTOR = CCIPUtils.ETH_CHAIN_SELECTOR; + uint256 internal constant CCIP_RATE_LIMIT_CAPACITY = 300_000e18; + uint256 internal constant CCIP_RATE_LIMIT_REFILL_RATE = 60e18; + + ITokenAdminRegistry internal constant TOKEN_ADMIN_REGISTRY = + ITokenAdminRegistry(0x6f6C373d09C07425BaAE72317863d7F6bb731e37); + IEVM2EVMOnRamp internal constant ARB_ON_RAMP = + IEVM2EVMOnRamp(0x9D0ffA76C7F82C34Be313b5bFc6d42A72dA8CA69); + IEVM2EVMOnRamp internal constant ETH_ON_RAMP = + IEVM2EVMOnRamp(0x56b30A0Dcd8dc87Ec08b80FA09502bAB801fa78e); + + IEVM2EVMOffRamp_1_5 internal constant ARB_OFF_RAMP = + IEVM2EVMOffRamp_1_5(0x7D38c6363d5E4DFD500a691Bc34878b383F58d93); + IEVM2EVMOffRamp_1_5 internal constant ETH_OFF_RAMP = + IEVM2EVMOffRamp_1_5(0xCA04169671A81E4fB8768cfaD46c347ae65371F1); + + IRouter internal constant ROUTER = IRouter(0x881e3A65B4d4a04dD529061dd0071cf975F58bCD); + address internal constant RMN_PROXY = 0xC842c69d54F83170C42C4d556B4F6B2ca53Dd3E8; + + IGhoToken internal constant GHO = IGhoToken(0x6Bb7a212910682DCFdbd5BCBb3e28FB4E8da10Ee); + address internal constant RISK_COUNCIL = 0x8513e6F37dBc52De87b166980Fa3F50639694B60; + + IGhoAaveSteward internal constant NEW_GHO_AAVE_STEWARD = + IGhoAaveSteward(0xC5BcC58BE6172769ca1a78B8A45752E3C5059c39); + IGhoBucketSteward internal constant NEW_GHO_BUCKET_STEWARD = + IGhoBucketSteward(0x3c47237479e7569653eF9beC4a7Cd2ee3F78b396); + IGhoCcipSteward internal constant NEW_GHO_CCIP_STEWARD = + IGhoCcipSteward(0xB94Ab28c6869466a46a42abA834ca2B3cECCA5eB); + IUpgradeableBurnMintTokenPool_1_5_1 internal constant NEW_TOKEN_POOL = + IUpgradeableBurnMintTokenPool_1_5_1(0x98217A06721Ebf727f2C8d9aD7718ec28b7aAe34); + address internal constant NEW_REMOTE_POOL_ARB = 0xB94Ab28c6869466a46a42abA834ca2B3cECCA5eB; + address internal constant NEW_REMOTE_POOL_ETH = 0x06179f7C1be40863405f374E7f5F8806c728660A; + + AaveV3Base_GHOBaseLaunch_20241223 internal proposal; + + address internal alice = makeAddr('alice'); + address internal bob = makeAddr('bob'); + address internal carol = makeAddr('carol'); + + 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); + error InvalidSourcePoolAddress(bytes); + + function setUp() public virtual { + vm.createSelectFork(vm.rpcUrl('base'), 25645172); + proposal = new AaveV3Base_GHOBaseLaunch_20241223(); + _validateConstants(); + } + + function _validateConstants() private view { + assertEq(proposal.ETH_CHAIN_SELECTOR(), ETH_CHAIN_SELECTOR); + assertEq(proposal.ARB_CHAIN_SELECTOR(), ARB_CHAIN_SELECTOR); + assertEq(proposal.CCIP_BUCKET_CAPACITY(), 20_000_000e18); + assertEq(address(proposal.TOKEN_ADMIN_REGISTRY()), address(TOKEN_ADMIN_REGISTRY)); + assertEq(address(proposal.TOKEN_POOL()), address(NEW_TOKEN_POOL)); + assertEq(address(proposal.GHO_TOKEN()), address(GHO)); + assertEq(proposal.GHO_AAVE_STEWARD(), address(NEW_GHO_AAVE_STEWARD)); + assertEq(proposal.GHO_BUCKET_STEWARD(), address(NEW_GHO_BUCKET_STEWARD)); + assertEq(proposal.GHO_CCIP_STEWARD(), address(NEW_GHO_CCIP_STEWARD)); + assertEq(proposal.REMOTE_TOKEN_POOL_ARB(), NEW_REMOTE_POOL_ARB); + assertEq(proposal.REMOTE_TOKEN_POOL_ETH(), NEW_REMOTE_POOL_ETH); + assertEq(proposal.CCIP_RATE_LIMIT_CAPACITY(), CCIP_RATE_LIMIT_CAPACITY); + assertEq(proposal.CCIP_RATE_LIMIT_REFILL_RATE(), CCIP_RATE_LIMIT_REFILL_RATE); + + assertEq(TOKEN_ADMIN_REGISTRY.typeAndVersion(), 'TokenAdminRegistry 1.5.0'); + assertEq(NEW_TOKEN_POOL.typeAndVersion(), 'BurnMintTokenPool 1.5.1'); + assertEq(ROUTER.typeAndVersion(), 'Router 1.2.0'); + + _assertOnRamp(ARB_ON_RAMP, BASE_CHAIN_SELECTOR, ARB_CHAIN_SELECTOR, ROUTER); + _assertOnRamp(ETH_ON_RAMP, BASE_CHAIN_SELECTOR, ETH_CHAIN_SELECTOR, ROUTER); + _assertOffRamp(ARB_OFF_RAMP, ARB_CHAIN_SELECTOR, BASE_CHAIN_SELECTOR, ROUTER); + _assertOffRamp(ETH_OFF_RAMP, ETH_CHAIN_SELECTOR, BASE_CHAIN_SELECTOR, ROUTER); + + assertEq(_getProxyAdmin(address(GHO)).UPGRADE_INTERFACE_VERSION(), '5.0.0'); + assertEq(_getProxyAdmin(address(NEW_TOKEN_POOL)).UPGRADE_INTERFACE_VERSION(), '5.0.0'); + } + + function _assertOnRamp( + IEVM2EVMOnRamp onRamp, + uint64 srcSelector, + uint64 dstSelector, + IRouter router + ) internal view { + assertEq(onRamp.typeAndVersion(), 'EVM2EVMOnRamp 1.5.0'); + assertEq(onRamp.getStaticConfig().chainSelector, srcSelector); + assertEq(onRamp.getStaticConfig().destChainSelector, dstSelector); + assertEq(onRamp.getDynamicConfig().router, address(router)); + assertEq(router.getOnRamp(dstSelector), address(onRamp)); + } + + function _assertOffRamp( + IEVM2EVMOffRamp_1_5 offRamp, + uint64 srcSelector, + uint64 dstSelector, + IRouter router + ) internal view { + assertEq(offRamp.typeAndVersion(), 'EVM2EVMOffRamp 1.5.0'); + assertEq(offRamp.getStaticConfig().sourceChainSelector, srcSelector); + assertEq(offRamp.getStaticConfig().chainSelector, dstSelector); + assertEq(offRamp.getDynamicConfig().router, address(router)); + assertTrue(router.isOffRamp(srcSelector, address(offRamp))); + } + + 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(GHO), amount: params.amount}); + + uint256 feeAmount = ROUTER.getFee(params.destChainSelector, message); + deal(params.sender, feeAmount); + + IInternal.EVM2EVMMessage memory eventArg = CCIPUtils.messageToEvent( + CCIPUtils.MessageToEventParams({ + message: message, + router: ROUTER, + sourceChainSelector: BASE_CHAIN_SELECTOR, + destChainSelector: params.destChainSelector, + feeTokenAmount: feeAmount, + originalSender: params.sender, + sourceToken: address(GHO), + destinationToken: address( + params.destChainSelector == ARB_CHAIN_SELECTOR + ? AaveV3ArbitrumAssets.GHO_UNDERLYING + : AaveV3EthereumAssets.GHO_UNDERLYING + ) + }) + ); + + return (message, eventArg); + } + + function _tokenBucketToConfig( + IRateLimiter.TokenBucket memory bucket + ) internal pure returns (IRateLimiter.Config memory) { + return + IRateLimiter.Config({ + isEnabled: bucket.isEnabled, + capacity: bucket.capacity, + rate: bucket.rate + }); + } + + function _getDisabledConfig() internal pure returns (IRateLimiter.Config memory) { + return IRateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}); + } + + function _getImplementation(address proxy) internal view returns (address) { + bytes32 slot = bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1); + return address(uint160(uint256(vm.load(proxy, slot)))); + } + + function _getProxyAdmin(address proxy) internal view returns (ProxyAdmin) { + bytes32 slot = bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1); + return ProxyAdmin(address(uint160(uint256(vm.load(proxy, slot))))); + } + + function _readInitialized(address proxy) internal view returns (uint8) { + return uint8(uint256(vm.load(proxy, bytes32(0)))); + } + + 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() + }); + } + + function _getOutboundRefillTime(uint256 amount) internal pure returns (uint256) { + return (amount / CCIP_RATE_LIMIT_REFILL_RATE) + 1; // account for rounding + } + + function _getInboundRefillTime(uint256 amount) internal pure returns (uint256) { + return amount / CCIP_RATE_LIMIT_REFILL_RATE + 1; // account for rounding + } + + function _min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + + function assertEq( + IRateLimiter.TokenBucket memory bucket, + IRateLimiter.Config memory config + ) internal pure { + assertEq(bucket.isEnabled, config.isEnabled); + assertEq(bucket.capacity, config.capacity); + assertEq(bucket.rate, config.rate); + } +} + +contract AaveV3Base_GHOBaseLaunch_20241223_PreExecution is AaveV3Base_GHOBaseLaunch_20241223_Base { + /** + * @dev executes the generic test suite including e2e and config snapshots + */ + function test_defaultProposalExecution() public { + defaultTest('AaveV3Base_GHOBaseLaunch_20241223', AaveV3Base.POOL, address(proposal)); + } + + function test_stewardRoles() public { + // gho token is deployed in the AIP, does not existing before + + assertFalse( + AaveV3Base.ACL_MANAGER.hasRole( + AaveV3Base.ACL_MANAGER.RISK_ADMIN_ROLE(), + address(NEW_GHO_AAVE_STEWARD) + ) + ); + assertEq(NEW_GHO_BUCKET_STEWARD.getControlledFacilitators().length, 0); + assertEq(NEW_TOKEN_POOL.getRateLimitAdmin(), address(0)); + + executePayload(vm, address(proposal)); + + assertTrue(GHO.hasRole(GHO.FACILITATOR_MANAGER_ROLE(), GovernanceV3Base.EXECUTOR_LVL_1)); + assertTrue(GHO.hasRole(GHO.BUCKET_MANAGER_ROLE(), GovernanceV3Base.EXECUTOR_LVL_1)); + + IGhoToken.Facilitator memory facilitator = GHO.getFacilitator(address(NEW_TOKEN_POOL)); + assertEq(facilitator.label, 'CCIP TokenPool v1.5.1'); + assertEq(facilitator.bucketLevel, 0); + assertEq(facilitator.bucketCapacity, proposal.CCIP_BUCKET_CAPACITY()); + + assertTrue( + AaveV3Base.ACL_MANAGER.hasRole( + AaveV3Base.ACL_MANAGER.RISK_ADMIN_ROLE(), + address(NEW_GHO_AAVE_STEWARD) + ) + ); + + assertTrue(GHO.hasRole(GHO.BUCKET_MANAGER_ROLE(), address(NEW_GHO_BUCKET_STEWARD))); + + address[] memory facilitatorList = NEW_GHO_BUCKET_STEWARD.getControlledFacilitators(); + assertEq(facilitatorList.length, 1); + assertEq(facilitatorList[0], address(NEW_TOKEN_POOL)); + assertTrue(NEW_GHO_BUCKET_STEWARD.isControlledFacilitator(address(NEW_TOKEN_POOL))); + + assertEq(NEW_TOKEN_POOL.getRateLimitAdmin(), address(NEW_GHO_CCIP_STEWARD)); + } + + function test_stewardsConfig() public view { + assertEq(IOwnable(address(NEW_GHO_AAVE_STEWARD)).owner(), GovernanceV3Base.EXECUTOR_LVL_1); + assertEq( + NEW_GHO_AAVE_STEWARD.POOL_ADDRESSES_PROVIDER(), + address(AaveV3Base.POOL_ADDRESSES_PROVIDER) + ); + assertEq( + NEW_GHO_AAVE_STEWARD.POOL_DATA_PROVIDER(), + address(AaveV3Base.AAVE_PROTOCOL_DATA_PROVIDER) + ); + assertEq(NEW_GHO_AAVE_STEWARD.RISK_COUNCIL(), RISK_COUNCIL); + IGhoAaveSteward.BorrowRateConfig memory config = NEW_GHO_AAVE_STEWARD.getBorrowRateConfig(); + assertEq(config.optimalUsageRatioMaxChange, 500); + assertEq(config.baseVariableBorrowRateMaxChange, 500); + assertEq(config.variableRateSlope1MaxChange, 500); + assertEq(config.variableRateSlope2MaxChange, 500); + + assertEq(IOwnable(address(NEW_GHO_BUCKET_STEWARD)).owner(), GovernanceV3Base.EXECUTOR_LVL_1); + assertEq(NEW_GHO_BUCKET_STEWARD.GHO_TOKEN(), address(GHO)); + assertEq(NEW_GHO_BUCKET_STEWARD.RISK_COUNCIL(), RISK_COUNCIL); + assertEq(NEW_GHO_BUCKET_STEWARD.getControlledFacilitators().length, 0); // before AIP, no controlled facilitators are set + + assertEq(NEW_GHO_CCIP_STEWARD.GHO_TOKEN(), address(GHO)); + assertEq(NEW_GHO_CCIP_STEWARD.GHO_TOKEN_POOL(), address(NEW_TOKEN_POOL)); + assertEq(NEW_GHO_CCIP_STEWARD.RISK_COUNCIL(), RISK_COUNCIL); + assertFalse(NEW_GHO_CCIP_STEWARD.BRIDGE_LIMIT_ENABLED()); + assertEq( + abi.encode(NEW_GHO_CCIP_STEWARD.getCcipTimelocks()), + abi.encode(IGhoCcipSteward.CcipDebounce(0, 0)) + ); + } + + function test_newTokenPoolInitialization() public { + vm.expectRevert('Initializable: contract is already initialized'); + NEW_TOKEN_POOL.initialize(makeAddr('owner'), new address[](0), makeAddr('router')); + assertEq(_readInitialized(address(NEW_TOKEN_POOL)), 1); + + address IMPL = _getImplementation(address(NEW_TOKEN_POOL)); + vm.expectRevert('Initializable: contract is already initialized'); + IUpgradeableBurnMintTokenPool_1_5_1(IMPL).initialize( + makeAddr('owner'), + new address[](0), + makeAddr('router') + ); + assertEq(_readInitialized(IMPL), 255); + } + + function test_tokenPoolConfig() public { + executePayload(vm, address(proposal)); + + assertEq(NEW_TOKEN_POOL.owner(), GovernanceV3Base.EXECUTOR_LVL_1); + assertEq( + address(uint160(uint256(vm.load(address(NEW_TOKEN_POOL), bytes32(0))) >> 2)), // pending owner + address(0) + ); + assertEq(NEW_TOKEN_POOL.getToken(), address(GHO)); + assertEq(NEW_TOKEN_POOL.getTokenDecimals(), GHO.decimals()); + assertEq(NEW_TOKEN_POOL.getRmnProxy(), RMN_PROXY); + assertFalse(NEW_TOKEN_POOL.getAllowListEnabled()); + assertEq(abi.encode(NEW_TOKEN_POOL.getAllowList()), abi.encode(new address[](0))); + assertEq(NEW_TOKEN_POOL.getRouter(), address(ROUTER)); + + assertEq(NEW_TOKEN_POOL.getSupportedChains().length, 2); + assertEq(NEW_TOKEN_POOL.getSupportedChains()[0], ETH_CHAIN_SELECTOR); + assertEq(NEW_TOKEN_POOL.getSupportedChains()[1], ARB_CHAIN_SELECTOR); + assertEq( + NEW_TOKEN_POOL.getRemoteToken(ETH_CHAIN_SELECTOR), + abi.encode(AaveV3EthereumAssets.GHO_UNDERLYING) + ); + assertEq( + NEW_TOKEN_POOL.getRemoteToken(ARB_CHAIN_SELECTOR), + abi.encode(AaveV3ArbitrumAssets.GHO_UNDERLYING) + ); + assertEq(NEW_TOKEN_POOL.getRemotePools(ETH_CHAIN_SELECTOR).length, 1); + assertEq(NEW_TOKEN_POOL.getRemotePools(ETH_CHAIN_SELECTOR)[0], abi.encode(NEW_REMOTE_POOL_ETH)); + assertEq(NEW_TOKEN_POOL.getRemotePools(ARB_CHAIN_SELECTOR).length, 1); + assertEq(NEW_TOKEN_POOL.getRemotePools(ARB_CHAIN_SELECTOR)[0], abi.encode(NEW_REMOTE_POOL_ARB)); + + assertEq( + NEW_TOKEN_POOL.getCurrentInboundRateLimiterState(ETH_CHAIN_SELECTOR), + _getRateLimiterConfig() + ); + assertEq( + NEW_TOKEN_POOL.getCurrentOutboundRateLimiterState(ETH_CHAIN_SELECTOR), + _getRateLimiterConfig() + ); + assertEq( + NEW_TOKEN_POOL.getCurrentInboundRateLimiterState(ARB_CHAIN_SELECTOR), + _getRateLimiterConfig() + ); + assertEq( + NEW_TOKEN_POOL.getCurrentOutboundRateLimiterState(ARB_CHAIN_SELECTOR), + _getRateLimiterConfig() + ); + } +} + +contract AaveV3Base_GHOBaseLaunch_20241223_PostExecution is AaveV3Base_GHOBaseLaunch_20241223_Base { + function setUp() public override { + super.setUp(); + + executePayload(vm, address(proposal)); + } + + function test_sendMessageToArbSucceeds(uint256 amount) public { + amount = bound(amount, 1, CCIP_RATE_LIMIT_CAPACITY); + skip(_getOutboundRefillTime(amount)); // wait for the rate limiter to refill + // mock previously bridged amount + vm.prank(address(NEW_TOKEN_POOL)); + GHO.mint(alice, amount); // increase bucket level + + vm.prank(alice); + GHO.approve(address(ROUTER), amount); + + uint256 aliceBalance = GHO.balanceOf(alice); + uint256 bucketLevel = GHO.getFacilitator(address(NEW_TOKEN_POOL)).bucketLevel; + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({amount: amount, sender: alice, destChainSelector: ARB_CHAIN_SELECTOR}) + ); + + vm.expectEmit(address(NEW_TOKEN_POOL)); + emit Burned(address(ARB_ON_RAMP), amount); + vm.expectEmit(address(ARB_ON_RAMP)); + emit CCIPSendRequested(eventArg); + + vm.prank(alice); + ROUTER.ccipSend{value: eventArg.feeTokenAmount}(ARB_CHAIN_SELECTOR, message); + + assertEq(GHO.balanceOf(alice), aliceBalance - amount); + assertEq(GHO.getFacilitator(address(NEW_TOKEN_POOL)).bucketLevel, bucketLevel - amount); + } + + function test_sendMessageToEthSucceeds(uint256 amount) public { + amount = bound(amount, 1, CCIP_RATE_LIMIT_CAPACITY); + skip(_getOutboundRefillTime(amount)); // wait for the rate limiter to refill + // mock previously bridged amount + vm.prank(address(NEW_TOKEN_POOL)); + GHO.mint(alice, amount); // increase bucket level + + deal(address(GHO), alice, amount); + vm.prank(alice); + GHO.approve(address(ROUTER), amount); + + uint256 aliceBalance = GHO.balanceOf(alice); + uint256 bucketLevel = GHO.getFacilitator(address(NEW_TOKEN_POOL)).bucketLevel; + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({amount: amount, sender: alice, destChainSelector: ETH_CHAIN_SELECTOR}) + ); + + vm.expectEmit(address(NEW_TOKEN_POOL)); + emit Burned(address(ETH_ON_RAMP), amount); + vm.expectEmit(address(ETH_ON_RAMP)); + emit CCIPSendRequested(eventArg); + + vm.prank(alice); + ROUTER.ccipSend{value: eventArg.feeTokenAmount}(ETH_CHAIN_SELECTOR, message); + + assertEq(GHO.balanceOf(alice), aliceBalance - amount); + assertEq(GHO.getFacilitator(address(NEW_TOKEN_POOL)).bucketLevel, bucketLevel - amount); + } + + function test_offRampViaArbSucceeds(uint256 amount) public { + amount = bound( + amount, + 1, + _min( + GHO.getFacilitator(address(NEW_TOKEN_POOL)).bucketCapacity, // initially, bucketLevel == 0 + CCIP_RATE_LIMIT_CAPACITY + ) + ); + skip(_getInboundRefillTime(amount)); // wait for the rate limiter to refill + + uint256 aliceBalance = GHO.balanceOf(alice); + + vm.expectEmit(address(NEW_TOKEN_POOL)); + emit Minted(address(ARB_OFF_RAMP), alice, amount); + + vm.prank(address(ARB_OFF_RAMP)); + NEW_TOKEN_POOL.releaseOrMint( + IPool_CCIP.ReleaseOrMintInV1({ + originalSender: abi.encode(alice), + remoteChainSelector: ARB_CHAIN_SELECTOR, + receiver: alice, + amount: amount, + localToken: address(GHO), + sourcePoolAddress: abi.encode(address(NEW_REMOTE_POOL_ARB)), + sourcePoolData: new bytes(0), + offchainTokenData: new bytes(0) + }) + ); + + assertEq(GHO.getFacilitator(address(NEW_TOKEN_POOL)).bucketLevel, amount); + assertEq(GHO.balanceOf(alice), aliceBalance + amount); + } + + function test_offRampViaEthSucceeds(uint256 amount) public { + amount = bound( + amount, + 1, + _min( + GHO.getFacilitator(address(NEW_TOKEN_POOL)).bucketCapacity, // initially, bucketLevel == 0 + CCIP_RATE_LIMIT_CAPACITY + ) + ); + skip(_getInboundRefillTime(amount)); // wait for the rate limiter to refill + + uint256 aliceBalance = GHO.balanceOf(alice); + + vm.expectEmit(address(NEW_TOKEN_POOL)); + emit Minted(address(ETH_OFF_RAMP), alice, amount); + + vm.prank(address(ETH_OFF_RAMP)); + NEW_TOKEN_POOL.releaseOrMint( + IPool_CCIP.ReleaseOrMintInV1({ + originalSender: abi.encode(alice), + remoteChainSelector: ETH_CHAIN_SELECTOR, + receiver: alice, + amount: amount, + localToken: address(GHO), + sourcePoolAddress: abi.encode(address(NEW_REMOTE_POOL_ETH)), + sourcePoolData: new bytes(0), + offchainTokenData: new bytes(0) + }) + ); + + assertEq(GHO.getFacilitator(address(NEW_TOKEN_POOL)).bucketLevel, amount); + assertEq(GHO.balanceOf(alice), aliceBalance + amount); + } + + function test_cannotOffRampOtherChainMessages() public { + uint256 amount = 100e18; + skip(_getInboundRefillTime(amount)); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidSourcePoolAddress.selector, + abi.encode(address(NEW_REMOTE_POOL_ETH)) + ) + ); + vm.prank(address(ARB_OFF_RAMP)); + NEW_TOKEN_POOL.releaseOrMint( + IPool_CCIP.ReleaseOrMintInV1({ + originalSender: abi.encode(alice), + remoteChainSelector: ARB_CHAIN_SELECTOR, + receiver: alice, + amount: amount, + localToken: address(GHO), + sourcePoolAddress: abi.encode(address(NEW_REMOTE_POOL_ETH)), + sourcePoolData: new bytes(0), + offchainTokenData: new bytes(0) + }) + ); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidSourcePoolAddress.selector, + abi.encode(address(NEW_REMOTE_POOL_ARB)) + ) + ); + vm.prank(address(ETH_OFF_RAMP)); + NEW_TOKEN_POOL.releaseOrMint( + IPool_CCIP.ReleaseOrMintInV1({ + originalSender: abi.encode(alice), + remoteChainSelector: ETH_CHAIN_SELECTOR, + receiver: alice, + amount: amount, + localToken: address(GHO), + sourcePoolAddress: abi.encode(address(NEW_REMOTE_POOL_ARB)), + sourcePoolData: new bytes(0), + offchainTokenData: new bytes(0) + }) + ); + } +} diff --git a/src/20241223_Multi_GHOBaseLaunch/AaveV3Base_GHOBaseListing_20241223.sol b/src/20241223_Multi_GHOBaseLaunch/AaveV3Base_GHOBaseListing_20241223.sol new file mode 100644 index 000000000..273413a37 --- /dev/null +++ b/src/20241223_Multi_GHOBaseLaunch/AaveV3Base_GHOBaseListing_20241223.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {SafeERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol'; +import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; +import {IAaveV3ConfigEngine} from 'aave-v3-origin/contracts/extensions/v3-config-engine/IAaveV3ConfigEngine.sol'; +import {IEmissionManager} from 'aave-v3-origin/contracts/rewards/interfaces/IEmissionManager.sol'; +import {EngineFlags} from 'aave-v3-origin/contracts/extensions/v3-config-engine/EngineFlags.sol'; +import {AaveV3PayloadBase} from 'aave-helpers/src/v3-config-engine/AaveV3PayloadBase.sol'; +import {AaveV3Base} from 'aave-address-book/AaveV3Base.sol'; + +/** + * @title GHO Base Listing + * @author Aave Labs + * - Discussion: https://governance.aave.com/t/arfc-launch-gho-on-base-set-aci-as-emissions-manager-for-rewards/19338 + * - Snapshot: https://snapshot.box/#/s:aave.eth/proposal/0x03dc21e0423c60082dc23317af6ebaf997610cbc2cbb0f5a385653bd99a524fe + */ +contract AaveV3Base_GHOBaseListing_20241223 is AaveV3PayloadBase { + using SafeERC20 for IERC20; + + // https://basescan.org/address/0xac140648435d03f784879cd789130F22Ef588Fcd + address public constant EMISSION_ADMIN = 0xac140648435d03f784879cd789130F22Ef588Fcd; + // https://basescan.org/address/0xfc421aD3C883Bf9E7C4f42dE845C4e4405799e73 + address public constant GHO_PRICE_FEED = 0xfc421aD3C883Bf9E7C4f42dE845C4e4405799e73; + // https://basescan.org/address/0x6Bb7a212910682DCFdbd5BCBb3e28FB4E8da10Ee + address public constant GHO_TOKEN = 0x6Bb7a212910682DCFdbd5BCBb3e28FB4E8da10Ee; + uint256 public constant GHO_SEED_AMOUNT = 1e18; + + function _preExecute() internal view override { + // robot should simulate and only execute if seed amount has been bridged, redundant check + assert(IERC20(GHO_TOKEN).balanceOf(address(this)) >= GHO_SEED_AMOUNT); + } + + function _postExecute() internal override { + IERC20(GHO_TOKEN).forceApprove(address(AaveV3Base.POOL), GHO_SEED_AMOUNT); + AaveV3Base.POOL.supply(GHO_TOKEN, GHO_SEED_AMOUNT, address(0), 0); + + (address aGhoToken, , ) = AaveV3Base.AAVE_PROTOCOL_DATA_PROVIDER.getReserveTokensAddresses( + GHO_TOKEN + ); + IEmissionManager(AaveV3Base.EMISSION_MANAGER).setEmissionAdmin(GHO_TOKEN, EMISSION_ADMIN); + IEmissionManager(AaveV3Base.EMISSION_MANAGER).setEmissionAdmin(aGhoToken, EMISSION_ADMIN); + } + + function newListings() public pure override returns (IAaveV3ConfigEngine.Listing[] memory) { + IAaveV3ConfigEngine.Listing[] memory listings = new IAaveV3ConfigEngine.Listing[](1); + + listings[0] = IAaveV3ConfigEngine.Listing({ + asset: GHO_TOKEN, + assetSymbol: 'GHO', + priceFeed: GHO_PRICE_FEED, + enabledToBorrow: EngineFlags.ENABLED, + borrowableInIsolation: EngineFlags.DISABLED, + withSiloedBorrowing: EngineFlags.DISABLED, + flashloanable: EngineFlags.ENABLED, + ltv: 0, + liqThreshold: 0, + liqBonus: 0, + reserveFactor: 10_00, + supplyCap: 2_500_000, + borrowCap: 2_250_000, + debtCeiling: 0, + liqProtocolFee: 0, + rateStrategyParams: IAaveV3ConfigEngine.InterestRateInputData({ + optimalUsageRatio: 90_00, + baseVariableBorrowRate: 0, + variableRateSlope1: 9_50, + variableRateSlope2: 50_00 + }) + }); + + return listings; + } +} diff --git a/src/20241223_Multi_GHOBaseLaunch/AaveV3Base_GHOBaseListing_20241223.t.sol b/src/20241223_Multi_GHOBaseLaunch/AaveV3Base_GHOBaseListing_20241223.t.sol new file mode 100644 index 000000000..be1795766 --- /dev/null +++ b/src/20241223_Multi_GHOBaseLaunch/AaveV3Base_GHOBaseListing_20241223.t.sol @@ -0,0 +1,310 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; + +import {IEmissionManager} from 'aave-v3-origin/contracts/rewards/interfaces/IEmissionManager.sol'; +import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; +import {DataTypes, IDefaultInterestRateStrategyV2} from 'aave-address-book/AaveV3.sol'; +import {IUpgradeableBurnMintTokenPool_1_5_1} from 'src/interfaces/ccip/tokenPool/IUpgradeableBurnMintTokenPool.sol'; +import {ITokenAdminRegistry} from 'src/interfaces/ccip/ITokenAdminRegistry.sol'; +import {IRateLimiter} from 'src/interfaces/ccip/IRateLimiter.sol'; +import {IGhoAaveSteward} from 'src/interfaces/IGhoAaveSteward.sol'; +import {IGhoBucketSteward} from 'src/interfaces/IGhoBucketSteward.sol'; +import {IGhoCcipSteward} from 'src/interfaces/IGhoCcipSteward.sol'; +import {IGhoToken} from 'src/interfaces/IGhoToken.sol'; +import {IGhoOracle} from 'src/interfaces/IGhoOracle.sol'; + +import {ReserveConfiguration} from 'aave-v3-origin/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; +import {Errors} from 'aave-address-book/governance-v3/Errors.sol'; +import {ProtocolV3TestBase} from 'aave-helpers/src/ProtocolV3TestBase.sol'; +import {GovV3Helpers} from 'aave-helpers/src/GovV3Helpers.sol'; +import {AaveV3Base} from 'aave-address-book/AaveV3Base.sol'; +import {GovernanceV3Base} from 'aave-address-book/GovernanceV3Base.sol'; + +import {CCIPUtils} from './utils/CCIPUtils.sol'; +import {AaveV3Base_GHOBaseLaunch_20241223} from './AaveV3Base_GHOBaseLaunch_20241223.sol'; +import {AaveV3Base_GHOBaseListing_20241223} from './AaveV3Base_GHOBaseListing_20241223.sol'; + +/** + * @dev Test for AaveV3Base_Ads_20241231 + * command: FOUNDRY_PROFILE=base forge test --match-path=src/20241223_Multi_GHOBaseLaunch/AaveV3Base_GHOBaseListing_20241223.t.sol -vv + */ +contract AaveV3Base_GHOBaseListing_20241223_Base is ProtocolV3TestBase { + AaveV3Base_GHOBaseListing_20241223 internal proposal; + + ITokenAdminRegistry internal constant TOKEN_ADMIN_REGISTRY = + ITokenAdminRegistry(0x6f6C373d09C07425BaAE72317863d7F6bb731e37); + address internal constant ROUTER = 0x881e3A65B4d4a04dD529061dd0071cf975F58bCD; + address internal constant RMN_PROXY = 0xC842c69d54F83170C42C4d556B4F6B2ca53Dd3E8; + address internal constant RISK_COUNCIL = 0x8513e6F37dBc52De87b166980Fa3F50639694B60; + IGhoToken internal constant GHO_TOKEN = IGhoToken(0x6Bb7a212910682DCFdbd5BCBb3e28FB4E8da10Ee); + address internal constant NEW_REMOTE_POOL_ARB = 0xB94Ab28c6869466a46a42abA834ca2B3cECCA5eB; + address internal constant NEW_REMOTE_POOL_ETH = 0x06179f7C1be40863405f374E7f5F8806c728660A; + IGhoAaveSteward internal constant NEW_GHO_AAVE_STEWARD = + IGhoAaveSteward(0xC5BcC58BE6172769ca1a78B8A45752E3C5059c39); + IGhoBucketSteward internal constant NEW_GHO_BUCKET_STEWARD = + IGhoBucketSteward(0x3c47237479e7569653eF9beC4a7Cd2ee3F78b396); + IGhoCcipSteward internal constant NEW_GHO_CCIP_STEWARD = + IGhoCcipSteward(0xB94Ab28c6869466a46a42abA834ca2B3cECCA5eB); + IUpgradeableBurnMintTokenPool_1_5_1 internal constant NEW_TOKEN_POOL = + IUpgradeableBurnMintTokenPool_1_5_1(0x98217A06721Ebf727f2C8d9aD7718ec28b7aAe34); + + function setUp() public virtual { + vm.createSelectFork(vm.rpcUrl('base'), 25645172); + proposal = new AaveV3Base_GHOBaseListing_20241223(); + } + + function _executeLaunchAIP() internal { + executePayload(vm, address(new AaveV3Base_GHOBaseLaunch_20241223())); + } + + function _mockBridgeSeedAmount() internal { + uint256 seedAmount = proposal.GHO_SEED_AMOUNT(); + vm.prank(address(NEW_TOKEN_POOL)); + GHO_TOKEN.mint(GovernanceV3Base.EXECUTOR_LVL_1, seedAmount); + } + + function _isDifferenceLowerThanMax( + uint256 from, + uint256 to, + uint256 max + ) internal pure returns (bool) { + return from < to ? to - from <= max : from - to <= max; + } + + function assertEq( + IRateLimiter.TokenBucket memory bucket, + IRateLimiter.Config memory config + ) internal pure { + assertEq(bucket.capacity, config.capacity); + assertEq(bucket.rate, config.rate); + assertEq(bucket.isEnabled, config.isEnabled); + } + + function assertEq( + IDefaultInterestRateStrategyV2.InterestRateData memory a, + IDefaultInterestRateStrategyV2.InterestRateData memory b + ) internal pure { + assertEq(a.optimalUsageRatio, b.optimalUsageRatio); + assertEq(a.baseVariableBorrowRate, b.baseVariableBorrowRate); + assertEq(a.variableRateSlope1, b.variableRateSlope1); + assertEq(a.variableRateSlope2, b.variableRateSlope2); + } +} + +contract AaveV3Base_GHOBaseListing_20241223_ListingPreRequisites is + AaveV3Base_GHOBaseListing_20241223_Base +{ + uint40 internal payloadId; + + function setUp() public override { + super.setUp(); + payloadId = GovV3Helpers.readyPayload(vm, address(proposal)); + } + + function test_listingFailsPreLaunch() public { + vm.expectRevert(bytes(Errors.FAILED_ACTION_EXECUTION)); + GovernanceV3Base.PAYLOADS_CONTROLLER.executePayload(payloadId); + } + + function test_listingFailsWithoutSeedAmount() public { + test_listingFailsPreLaunch(); + _executeLaunchAIP(); // activates CCIP lane + + vm.expectRevert(bytes(Errors.FAILED_ACTION_EXECUTION)); // assertion failed on _preExecute() + GovernanceV3Base.PAYLOADS_CONTROLLER.executePayload(payloadId); + } + + function test_listingSucceedsOnlyAfterLaunchAndSeedAmount() public { + test_listingFailsWithoutSeedAmount(); + _mockBridgeSeedAmount(); + + GovernanceV3Base.PAYLOADS_CONTROLLER.executePayload(payloadId); + + (, , , , , , , , bool isActive, ) = AaveV3Base + .AAVE_PROTOCOL_DATA_PROVIDER + .getReserveConfigurationData(proposal.GHO_TOKEN()); + assertTrue(isActive); + } +} + +contract AaveV3Base_GHOBaseListing_20241223_Listing is AaveV3Base_GHOBaseListing_20241223_Base { + function setUp() public override { + super.setUp(); + _executeLaunchAIP(); // deploys gho token, token pool & stewards + _mockBridgeSeedAmount(); + } + + /** + * @dev executes the generic test suite including e2e and config snapshots + */ + function test_defaultProposalExecution() public { + defaultTest('AaveV3Base_GHOBaseListing_20241223', AaveV3Base.POOL, address(proposal)); + + (address aGhoToken, , ) = AaveV3Base.AAVE_PROTOCOL_DATA_PROVIDER.getReserveTokensAddresses( + proposal.GHO_TOKEN() + ); + assertGe(IERC20(aGhoToken).balanceOf(address(0)), proposal.GHO_SEED_AMOUNT()); + } + + function test_ghoPriceFeed() public view { + IGhoOracle priceOracle = IGhoOracle(proposal.GHO_PRICE_FEED()); + assertEq(priceOracle.latestAnswer(), 1e8); + assertEq(priceOracle.decimals(), 8); + } + + function test_ghoAdmin() public { + GovV3Helpers.executePayload(vm, address(proposal)); + (address aGhoToken, , ) = AaveV3Base.AAVE_PROTOCOL_DATA_PROVIDER.getReserveTokensAddresses( + proposal.GHO_TOKEN() + ); + assertEq( + IEmissionManager(AaveV3Base.EMISSION_MANAGER).getEmissionAdmin(proposal.GHO_TOKEN()), + proposal.EMISSION_ADMIN() + ); + assertEq( + IEmissionManager(AaveV3Base.EMISSION_MANAGER).getEmissionAdmin(aGhoToken), + proposal.EMISSION_ADMIN() + ); + } +} + +contract AaveV3Base_GHOBaseListing_20241223_Stewards is AaveV3Base_GHOBaseListing_20241223_Base { + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + + function setUp() public override { + super.setUp(); + _executeLaunchAIP(); // deploys gho token, token pool & stewards + _mockBridgeSeedAmount(); + + executePayload(vm, address(proposal)); + } + + function test_aaveStewardCanUpdateBorrowRate() public { + IDefaultInterestRateStrategyV2 irStrategy = IDefaultInterestRateStrategyV2( + AaveV3Base.AAVE_PROTOCOL_DATA_PROVIDER.getInterestRateStrategyAddress(address(GHO_TOKEN)) + ); + + IDefaultInterestRateStrategyV2.InterestRateData + memory currentRateData = IDefaultInterestRateStrategyV2.InterestRateData({ + optimalUsageRatio: 90_00, + baseVariableBorrowRate: 0, + variableRateSlope1: 9_50, + variableRateSlope2: 50_00 + }); + + assertEq(irStrategy.getInterestRateDataBps(address(GHO_TOKEN)), currentRateData); + + currentRateData.variableRateSlope1 -= 4_00; + currentRateData.variableRateSlope2 -= 3_00; + + vm.prank(RISK_COUNCIL); + NEW_GHO_AAVE_STEWARD.updateGhoBorrowRate( + currentRateData.optimalUsageRatio, + currentRateData.baseVariableBorrowRate, + currentRateData.variableRateSlope1, + currentRateData.variableRateSlope2 + ); + + assertEq(irStrategy.getInterestRateDataBps(address(GHO_TOKEN)), currentRateData); + } + + function test_aaveStewardCanUpdateBorrowCap(uint256 newBorrowCap) public { + uint256 currentBorrowCap = AaveV3Base.POOL.getConfiguration(address(GHO_TOKEN)).getBorrowCap(); + assertEq(currentBorrowCap, 2_250_000); + vm.assume( + newBorrowCap != currentBorrowCap && + _isDifferenceLowerThanMax(currentBorrowCap, newBorrowCap, currentBorrowCap) + ); + + vm.prank(RISK_COUNCIL); + NEW_GHO_AAVE_STEWARD.updateGhoBorrowCap(newBorrowCap); + + assertEq(AaveV3Base.POOL.getConfiguration(address(GHO_TOKEN)).getBorrowCap(), newBorrowCap); + assertEq(NEW_GHO_AAVE_STEWARD.getGhoTimelocks().ghoBorrowCapLastUpdate, vm.getBlockTimestamp()); + } + + function test_aaveStewardCanUpdateSupplyCap(uint256 newSupplyCap) public { + uint256 currentSupplyCap = AaveV3Base.POOL.getConfiguration(address(GHO_TOKEN)).getSupplyCap(); + assertEq(currentSupplyCap, 2_500_000); + + vm.assume( + currentSupplyCap != newSupplyCap && + _isDifferenceLowerThanMax(currentSupplyCap, newSupplyCap, currentSupplyCap) + ); + + vm.prank(RISK_COUNCIL); + NEW_GHO_AAVE_STEWARD.updateGhoSupplyCap(newSupplyCap); + + assertEq(AaveV3Base.POOL.getConfiguration(address(GHO_TOKEN)).getSupplyCap(), newSupplyCap); + assertEq(NEW_GHO_AAVE_STEWARD.getGhoTimelocks().ghoSupplyCapLastUpdate, vm.getBlockTimestamp()); + } + + function test_bucketStewardCanUpdateBucketCapacity(uint256 newBucketCapacity) public { + (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(NEW_TOKEN_POOL)); + assertEq(currentBucketCapacity, 20_000_000e18); + newBucketCapacity = bound( + newBucketCapacity, + currentBucketCapacity + 1, + 2 * currentBucketCapacity + ); + + vm.startPrank(RISK_COUNCIL); + NEW_GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity( + address(NEW_TOKEN_POOL), + uint128(newBucketCapacity) + ); + + assertEq( + GHO_TOKEN.getFacilitator(address(NEW_TOKEN_POOL)).bucketCapacity, + uint128(newBucketCapacity) + ); + } + + function test_ccipStewardCanChangeAndDisableRateLimit() public { + _runCcipStewardCanChangeAndDisableRateLimit(CCIPUtils.ETH_CHAIN_SELECTOR); + skip(NEW_GHO_CCIP_STEWARD.MINIMUM_DELAY() + 1); + _runCcipStewardCanChangeAndDisableRateLimit(CCIPUtils.ARB_CHAIN_SELECTOR); + } + + function _runCcipStewardCanChangeAndDisableRateLimit(uint64 remoteChain) internal { + IRateLimiter.Config memory outboundConfig = IRateLimiter.Config({ + isEnabled: true, + capacity: 500_000e18, + rate: 100e18 + }); + IRateLimiter.Config memory inboundConfig = IRateLimiter.Config({ + isEnabled: true, + capacity: 100_000e18, + rate: 50e18 + }); + + // we assert the steward can change the rate limit + vm.prank(NEW_GHO_CCIP_STEWARD.RISK_COUNCIL()); + NEW_GHO_CCIP_STEWARD.updateRateLimit( + remoteChain, + outboundConfig.isEnabled, + outboundConfig.capacity, + outboundConfig.rate, + inboundConfig.isEnabled, + inboundConfig.capacity, + inboundConfig.rate + ); + + assertEq(NEW_TOKEN_POOL.getCurrentOutboundRateLimiterState(remoteChain), outboundConfig); + assertEq(NEW_TOKEN_POOL.getCurrentInboundRateLimiterState(remoteChain), inboundConfig); + assertEq(NEW_GHO_CCIP_STEWARD.getCcipTimelocks().rateLimitLastUpdate, vm.getBlockTimestamp()); + + skip(NEW_GHO_CCIP_STEWARD.MINIMUM_DELAY() + 1); + + // now we assert the steward can disable the rate limit + vm.prank(NEW_GHO_CCIP_STEWARD.RISK_COUNCIL()); + NEW_GHO_CCIP_STEWARD.updateRateLimit(remoteChain, false, 0, 0, false, 0, 0); + + IRateLimiter.Config memory disabledConfig = IRateLimiter.Config(false, 0, 0); + assertEq(NEW_TOKEN_POOL.getCurrentOutboundRateLimiterState(remoteChain), disabledConfig); + assertEq(NEW_TOKEN_POOL.getCurrentInboundRateLimiterState(remoteChain), disabledConfig); + assertEq(NEW_GHO_CCIP_STEWARD.getCcipTimelocks().rateLimitLastUpdate, vm.getBlockTimestamp()); + } +} diff --git a/src/20241223_Multi_GHOBaseLaunch/AaveV3E2E_GHOBaseLaunch_20241223.t.sol b/src/20241223_Multi_GHOBaseLaunch/AaveV3E2E_GHOBaseLaunch_20241223.t.sol new file mode 100644 index 000000000..e7ad1ee5f --- /dev/null +++ b/src/20241223_Multi_GHOBaseLaunch/AaveV3E2E_GHOBaseLaunch_20241223.t.sol @@ -0,0 +1,797 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; + +import {IUpgradeableLockReleaseTokenPool_1_5_1} from 'src/interfaces/ccip/tokenPool/IUpgradeableLockReleaseTokenPool.sol'; +import {IUpgradeableBurnMintTokenPool_1_5_1} from 'src/interfaces/ccip/tokenPool/IUpgradeableBurnMintTokenPool.sol'; +import {IRateLimiter} from 'src/interfaces/ccip/IRateLimiter.sol'; +import {IInternal} from 'src/interfaces/ccip/IInternal.sol'; +import {IClient} from 'src/interfaces/ccip/IClient.sol'; +import {IRouter} from 'src/interfaces/ccip/IRouter.sol'; +import {IEVM2EVMOnRamp} from 'src/interfaces/ccip/IEVM2EVMOnRamp.sol'; +import {IEVM2EVMOffRamp_1_5} from 'src/interfaces/ccip/IEVM2EVMOffRamp.sol'; +import {ITokenAdminRegistry} from 'src/interfaces/ccip/ITokenAdminRegistry.sol'; +import {IPriceRegistry} from 'src/interfaces/ccip/IPriceRegistry.sol'; +import {IGhoToken} from 'src/interfaces/IGhoToken.sol'; +import {IGhoAaveSteward} from 'src/interfaces/IGhoAaveSteward.sol'; +import {IGhoBucketSteward} from 'src/interfaces/IGhoBucketSteward.sol'; +import {IGhoCcipSteward} from 'src/interfaces/IGhoCcipSteward.sol'; + +import {ProtocolV3TestBase} from 'aave-helpers/src/ProtocolV3TestBase.sol'; +import {AaveV3ArbitrumAssets} from 'aave-address-book/AaveV3Arbitrum.sol'; +import {AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; + +import {CCIPUtils} from './utils/CCIPUtils.sol'; +import {AaveV3Arbitrum_GHOBaseLaunch_20241223} from './AaveV3Arbitrum_GHOBaseLaunch_20241223.sol'; +import {AaveV3Base_GHOBaseLaunch_20241223} from './AaveV3Base_GHOBaseLaunch_20241223.sol'; +import {AaveV3Ethereum_GHOBaseLaunch_20241223} from './AaveV3Ethereum_GHOBaseLaunch_20241223.sol'; + +/** + * @dev Test for AaveV3Base_GHOBaseLaunch_20241223 + * command: FOUNDRY_PROFILE=base forge test --match-path=src/20241223_Multi_GHOBaseLaunch/AaveV3E2E_GHOBaseLaunch_20241223.t.sol -vv + */ +contract AaveV3Base_GHOBaseLaunch_20241223_Base is ProtocolV3TestBase { + struct Common { + IRouter router; + IGhoToken token; + IEVM2EVMOnRamp arbOnRamp; + IEVM2EVMOnRamp baseOnRamp; + IEVM2EVMOnRamp ethOnRamp; + IEVM2EVMOffRamp_1_5 arbOffRamp; + IEVM2EVMOffRamp_1_5 baseOffRamp; + IEVM2EVMOffRamp_1_5 ethOffRamp; + ITokenAdminRegistry tokenAdminRegistry; + uint64 chainSelector; + uint256 forkId; + } + + struct CCIPSendParams { + Common src; + Common dst; + uint256 amount; + address sender; + } + + struct ARB { + AaveV3Arbitrum_GHOBaseLaunch_20241223 proposal; + IUpgradeableBurnMintTokenPool_1_5_1 tokenPool; + Common c; + } + struct BASE { + AaveV3Base_GHOBaseLaunch_20241223 proposal; + IUpgradeableBurnMintTokenPool_1_5_1 tokenPool; + Common c; + } + struct ETH { + AaveV3Ethereum_GHOBaseLaunch_20241223 proposal; + IUpgradeableLockReleaseTokenPool_1_5_1 tokenPool; + Common c; + } + + address internal constant RISK_COUNCIL = 0x8513e6F37dBc52De87b166980Fa3F50639694B60; // common across all chains + address internal constant RMN_PROXY_BASE = 0xC842c69d54F83170C42C4d556B4F6B2ca53Dd3E8; + address internal constant ROUTER_BASE = 0x881e3A65B4d4a04dD529061dd0071cf975F58bCD; + address internal constant GHO_TOKEN_IMPL_BASE = 0xb0e1c7830aA781362f79225559Aa068E6bDaF1d1; + IGhoToken internal constant GHO_TOKEN_BASE = + IGhoToken(0x6Bb7a212910682DCFdbd5BCBb3e28FB4E8da10Ee); + uint256 internal constant CCIP_RATE_LIMIT_CAPACITY = 300_000e18; + uint256 internal constant CCIP_RATE_LIMIT_REFILL_RATE = 60e18; + + ARB internal arb; + BASE internal base; + ETH internal eth; + + address internal alice = makeAddr('alice'); + address internal bob = makeAddr('bob'); + address internal carol = makeAddr('carol'); + + IGhoAaveSteward internal GHO_AAVE_STEWARD_BASE; + IGhoBucketSteward internal GHO_BUCKET_STEWARD_BASE; + IGhoCcipSteward internal GHO_CCIP_STEWARD_BASE; + + 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); + + function setUp() public virtual { + arb.c.forkId = vm.createFork(vm.rpcUrl('arbitrum'), 300142041); + base.c.forkId = vm.createFork(vm.rpcUrl('base'), 25645172); + eth.c.forkId = vm.createFork(vm.rpcUrl('mainnet'), 21722753); + + arb.c.chainSelector = 4949039107694359620; + base.c.chainSelector = 15971525489660198786; + eth.c.chainSelector = 5009297550715157269; + + vm.selectFork(arb.c.forkId); + arb.proposal = new AaveV3Arbitrum_GHOBaseLaunch_20241223(); + arb.c.token = IGhoToken(AaveV3ArbitrumAssets.GHO_UNDERLYING); + arb.tokenPool = IUpgradeableBurnMintTokenPool_1_5_1(0xB94Ab28c6869466a46a42abA834ca2B3cECCA5eB); + arb.c.tokenAdminRegistry = ITokenAdminRegistry(0x39AE1032cF4B334a1Ed41cdD0833bdD7c7E7751E); + arb.c.router = IRouter(arb.tokenPool.getRouter()); + arb.c.baseOnRamp = IEVM2EVMOnRamp(arb.c.router.getOnRamp(base.c.chainSelector)); + arb.c.ethOnRamp = IEVM2EVMOnRamp(arb.c.router.getOnRamp(eth.c.chainSelector)); + arb.c.baseOffRamp = IEVM2EVMOffRamp_1_5(0xb62178f8198905D0Fa6d640Bdb188E4E8143Ac4b); + arb.c.ethOffRamp = IEVM2EVMOffRamp_1_5(0x91e46cc5590A4B9182e47f40006140A7077Dec31); + + vm.selectFork(base.c.forkId); + base.proposal = new AaveV3Base_GHOBaseLaunch_20241223(); + base.tokenPool = IUpgradeableBurnMintTokenPool_1_5_1( + 0x98217A06721Ebf727f2C8d9aD7718ec28b7aAe34 + ); + base.c.tokenAdminRegistry = ITokenAdminRegistry(0x6f6C373d09C07425BaAE72317863d7F6bb731e37); + base.c.token = GHO_TOKEN_BASE; + base.c.router = IRouter(base.tokenPool.getRouter()); + base.c.arbOnRamp = IEVM2EVMOnRamp(base.c.router.getOnRamp(arb.c.chainSelector)); + base.c.ethOnRamp = IEVM2EVMOnRamp(base.c.router.getOnRamp(eth.c.chainSelector)); + base.c.arbOffRamp = IEVM2EVMOffRamp_1_5(0x7D38c6363d5E4DFD500a691Bc34878b383F58d93); + base.c.ethOffRamp = IEVM2EVMOffRamp_1_5(0xCA04169671A81E4fB8768cfaD46c347ae65371F1); + + vm.selectFork(eth.c.forkId); + eth.proposal = new AaveV3Ethereum_GHOBaseLaunch_20241223(); + eth.c.token = IGhoToken(AaveV3EthereumAssets.GHO_UNDERLYING); + eth.tokenPool = IUpgradeableLockReleaseTokenPool_1_5_1( + 0x06179f7C1be40863405f374E7f5F8806c728660A + ); + eth.c.tokenAdminRegistry = ITokenAdminRegistry(0xb22764f98dD05c789929716D677382Df22C05Cb6); + eth.c.router = IRouter(eth.tokenPool.getRouter()); + eth.c.arbOnRamp = IEVM2EVMOnRamp(eth.c.router.getOnRamp(arb.c.chainSelector)); + eth.c.baseOnRamp = IEVM2EVMOnRamp(eth.c.router.getOnRamp(base.c.chainSelector)); + eth.c.arbOffRamp = IEVM2EVMOffRamp_1_5(0xdf615eF8D4C64d0ED8Fd7824BBEd2f6a10245aC9); + eth.c.baseOffRamp = IEVM2EVMOffRamp_1_5(0x6B4B6359Dd5B47Cdb030E5921456D2a0625a9EbD); + + _validateConfig({executed: 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.src.token), + amount: params.amount + }); + + uint256 feeAmount = params.src.router.getFee(params.dst.chainSelector, message); + deal(params.sender, feeAmount); + + IInternal.EVM2EVMMessage memory eventArg = CCIPUtils.messageToEvent( + CCIPUtils.MessageToEventParams({ + message: message, + router: params.src.router, + sourceChainSelector: params.src.chainSelector, + destChainSelector: params.dst.chainSelector, + feeTokenAmount: feeAmount, + originalSender: params.sender, + sourceToken: address(params.src.token), + destinationToken: address(params.dst.token) + }) + ); + + return (message, eventArg); + } + + function _validateConfig(bool executed) internal { + vm.selectFork(arb.c.forkId); + assertEq(arb.c.chainSelector, 4949039107694359620); + assertEq(address(arb.c.token), AaveV3ArbitrumAssets.GHO_UNDERLYING); + assertEq(arb.c.router.typeAndVersion(), 'Router 1.2.0'); + _assertOnRamp(arb.c.baseOnRamp, arb.c.chainSelector, base.c.chainSelector, arb.c.router); + _assertOnRamp(arb.c.ethOnRamp, arb.c.chainSelector, eth.c.chainSelector, arb.c.router); + _assertOffRamp(arb.c.baseOffRamp, base.c.chainSelector, arb.c.chainSelector, arb.c.router); + _assertOffRamp(arb.c.ethOffRamp, eth.c.chainSelector, arb.c.chainSelector, arb.c.router); + + // proposal constants + assertEq(arb.proposal.BASE_CHAIN_SELECTOR(), base.c.chainSelector); + assertEq(address(arb.proposal.TOKEN_POOL()), address(arb.tokenPool)); + assertEq(arb.proposal.REMOTE_TOKEN_POOL_BASE(), address(base.tokenPool)); + assertEq(arb.proposal.REMOTE_GHO_TOKEN_BASE(), address(base.c.token)); + + vm.selectFork(base.c.forkId); + assertEq(base.c.chainSelector, 15971525489660198786); + assertEq(base.c.router.typeAndVersion(), 'Router 1.2.0'); + _assertOnRamp(base.c.arbOnRamp, base.c.chainSelector, arb.c.chainSelector, base.c.router); + _assertOnRamp(base.c.ethOnRamp, base.c.chainSelector, eth.c.chainSelector, base.c.router); + _assertOffRamp(base.c.arbOffRamp, arb.c.chainSelector, base.c.chainSelector, base.c.router); + _assertOffRamp(base.c.ethOffRamp, eth.c.chainSelector, base.c.chainSelector, base.c.router); + + // proposal constants + assertEq(base.proposal.ETH_CHAIN_SELECTOR(), eth.c.chainSelector); + assertEq(base.proposal.ARB_CHAIN_SELECTOR(), arb.c.chainSelector); + assertEq(base.proposal.CCIP_BUCKET_CAPACITY(), 20_000_000e18); + assertEq(address(base.proposal.TOKEN_ADMIN_REGISTRY()), address(base.c.tokenAdminRegistry)); + assertEq(address(base.proposal.TOKEN_POOL()), address(base.tokenPool)); + IGhoCcipSteward ghoCcipSteward = IGhoCcipSteward(base.proposal.GHO_CCIP_STEWARD()); + assertEq(ghoCcipSteward.GHO_TOKEN_POOL(), address(base.tokenPool)); + assertEq(ghoCcipSteward.GHO_TOKEN(), address(base.c.token)); + assertEq(base.proposal.REMOTE_TOKEN_POOL_ETH(), address(eth.tokenPool)); + assertEq(base.proposal.REMOTE_TOKEN_POOL_ARB(), address(arb.tokenPool)); + + vm.selectFork(eth.c.forkId); + assertEq(eth.c.chainSelector, 5009297550715157269); + assertEq(address(eth.c.token), AaveV3EthereumAssets.GHO_UNDERLYING); + assertEq(eth.c.router.typeAndVersion(), 'Router 1.2.0'); + _assertOnRamp(eth.c.arbOnRamp, eth.c.chainSelector, arb.c.chainSelector, eth.c.router); + _assertOnRamp(eth.c.baseOnRamp, eth.c.chainSelector, base.c.chainSelector, eth.c.router); + _assertOffRamp(eth.c.arbOffRamp, arb.c.chainSelector, eth.c.chainSelector, eth.c.router); + _assertOffRamp(eth.c.baseOffRamp, base.c.chainSelector, eth.c.chainSelector, eth.c.router); + + // proposal constants + assertEq(eth.proposal.BASE_CHAIN_SELECTOR(), base.c.chainSelector); + assertEq(address(eth.proposal.TOKEN_POOL()), address(eth.tokenPool)); + assertEq(eth.proposal.REMOTE_TOKEN_POOL_BASE(), address(base.tokenPool)); + assertEq(eth.proposal.REMOTE_GHO_TOKEN_BASE(), address(base.c.token)); + + if (executed) { + vm.selectFork(arb.c.forkId); + assertEq(arb.c.tokenAdminRegistry.getPool(address(arb.c.token)), address(arb.tokenPool)); + assertEq(arb.tokenPool.getSupportedChains()[0], eth.c.chainSelector); + assertEq(arb.tokenPool.getSupportedChains()[1], base.c.chainSelector); + assertEq(arb.tokenPool.getRemoteToken(eth.c.chainSelector), abi.encode(address(eth.c.token))); + assertEq( + arb.tokenPool.getRemoteToken(base.c.chainSelector), + abi.encode(address(base.c.token)) + ); + assertEq(arb.tokenPool.getRemotePools(base.c.chainSelector).length, 1); + assertEq( + arb.tokenPool.getRemotePools(base.c.chainSelector)[0], + abi.encode(address(base.tokenPool)) + ); + assertEq(arb.tokenPool.getRemotePools(eth.c.chainSelector).length, 2); + assertEq( + arb.tokenPool.getRemotePools(eth.c.chainSelector)[1], // 0th is the 1.4 token pool + abi.encode(address(eth.tokenPool)) + ); + _assertSetRateLimit(arb.c, address(arb.tokenPool)); + + vm.selectFork(base.c.forkId); + assertEq(address(base.proposal.GHO_TOKEN()), address(base.c.token)); + assertEq(base.c.tokenAdminRegistry.getPool(address(base.c.token)), address(base.tokenPool)); + assertEq(base.tokenPool.getSupportedChains()[0], eth.c.chainSelector); + assertEq(base.tokenPool.getSupportedChains()[1], arb.c.chainSelector); + assertEq( + base.tokenPool.getRemoteToken(arb.c.chainSelector), + abi.encode(address(arb.c.token)) + ); + assertEq( + base.tokenPool.getRemoteToken(eth.c.chainSelector), + abi.encode(address(eth.c.token)) + ); + assertEq(base.tokenPool.getRemotePools(arb.c.chainSelector).length, 1); + assertEq( + base.tokenPool.getRemotePools(arb.c.chainSelector)[0], + abi.encode(address(arb.tokenPool)) + ); + assertEq(base.tokenPool.getRemotePools(eth.c.chainSelector).length, 1); + assertEq( + base.tokenPool.getRemotePools(eth.c.chainSelector)[0], + abi.encode(address(eth.tokenPool)) + ); + _assertSetRateLimit(base.c, address(base.tokenPool)); + + vm.selectFork(eth.c.forkId); + assertEq(eth.c.tokenAdminRegistry.getPool(address(eth.c.token)), address(eth.tokenPool)); + assertEq(eth.tokenPool.getSupportedChains()[0], arb.c.chainSelector); + assertEq(eth.tokenPool.getSupportedChains()[1], base.c.chainSelector); + assertEq(eth.tokenPool.getRemoteToken(arb.c.chainSelector), abi.encode(address(arb.c.token))); + assertEq( + eth.tokenPool.getRemoteToken(base.c.chainSelector), + abi.encode(address(base.c.token)) + ); + assertEq(eth.tokenPool.getRemotePools(arb.c.chainSelector).length, 2); + assertEq( + eth.tokenPool.getRemotePools(arb.c.chainSelector)[1], // 0th is the 1.4 token pool + abi.encode(address(arb.tokenPool)) + ); + assertEq(eth.tokenPool.getRemotePools(base.c.chainSelector).length, 1); + assertEq( + eth.tokenPool.getRemotePools(base.c.chainSelector)[0], + abi.encode(address(base.tokenPool)) + ); + _assertSetRateLimit(eth.c, address(eth.tokenPool)); + } + } + + function _assertOnRamp( + IEVM2EVMOnRamp onRamp, + uint64 srcSelector, + uint64 dstSelector, + IRouter router + ) internal view { + assertEq(onRamp.typeAndVersion(), 'EVM2EVMOnRamp 1.5.0'); + assertEq(onRamp.getStaticConfig().chainSelector, srcSelector); + assertEq(onRamp.getStaticConfig().destChainSelector, dstSelector); + assertEq(onRamp.getDynamicConfig().router, address(router)); + assertEq(router.getOnRamp(dstSelector), address(onRamp)); + } + + function _assertOffRamp( + IEVM2EVMOffRamp_1_5 offRamp, + uint64 srcSelector, + uint64 dstSelector, + IRouter router + ) internal view { + assertEq(offRamp.typeAndVersion(), 'EVM2EVMOffRamp 1.5.0'); + assertEq(offRamp.getStaticConfig().sourceChainSelector, srcSelector); + assertEq(offRamp.getStaticConfig().chainSelector, dstSelector); + assertEq(offRamp.getDynamicConfig().router, address(router)); + assertTrue(router.isOffRamp(srcSelector, address(offRamp))); + } + + function _assertSetRateLimit(Common memory src, address tokenPool) internal view { + (Common memory dst1, Common memory dst2) = _getDestination(src); + IUpgradeableLockReleaseTokenPool_1_5_1 _tokenPool = IUpgradeableLockReleaseTokenPool_1_5_1( + tokenPool + ); + assertEq( + _tokenPool.getCurrentInboundRateLimiterState(dst1.chainSelector), + _getRateLimiterConfig() + ); + assertEq( + _tokenPool.getCurrentOutboundRateLimiterState(dst1.chainSelector), + _getRateLimiterConfig() + ); + + assertEq( + _tokenPool.getCurrentInboundRateLimiterState(dst2.chainSelector), + _getRateLimiterConfig() + ); + assertEq( + _tokenPool.getCurrentOutboundRateLimiterState(dst2.chainSelector), + _getRateLimiterConfig() + ); + } + + function _getDestination(Common memory src) internal view returns (Common memory, Common memory) { + if (src.forkId == arb.c.forkId) return (base.c, eth.c); + else if (src.forkId == base.c.forkId) return (arb.c, eth.c); + else return (arb.c, base.c); + } + + function _tokenBucketToConfig( + IRateLimiter.TokenBucket memory bucket + ) internal pure returns (IRateLimiter.Config memory) { + return + IRateLimiter.Config({ + isEnabled: bucket.isEnabled, + capacity: bucket.capacity, + rate: bucket.rate + }); + } + + function _getDisabledConfig() internal pure returns (IRateLimiter.Config memory) { + return IRateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}); + } + + function _getImplementation(address proxy) internal view returns (address) { + bytes32 slot = bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1); + return address(uint160(uint256(vm.load(proxy, slot)))); + } + + function _readInitialized(address proxy) internal view returns (uint8) { + return uint8(uint256(vm.load(proxy, bytes32(0)))); + } + + function _getRateLimiterConfig() internal pure returns (IRateLimiter.Config memory) { + return + IRateLimiter.Config({ + isEnabled: true, + capacity: uint128(CCIP_RATE_LIMIT_CAPACITY), + rate: uint128(CCIP_RATE_LIMIT_REFILL_RATE) + }); + } + + function _getOutboundRefillTime(uint256 amount) internal pure returns (uint256) { + return (amount / CCIP_RATE_LIMIT_REFILL_RATE) + 1; // account for rounding + } + + function _getInboundRefillTime(uint256 amount) internal pure returns (uint256) { + return (amount / CCIP_RATE_LIMIT_REFILL_RATE) + 1; // account for rounding + } + + function _min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + + function assertEq( + IRateLimiter.TokenBucket memory bucket, + IRateLimiter.Config memory config + ) internal pure { + assertEq(abi.encode(_tokenBucketToConfig(bucket)), abi.encode(config)); + } + + // @dev refresh token prices to the last stored such that price is not stale + // @dev assumed src.forkId is already active + function _refreshGasAndTokenPrices(Common memory src, Common memory dst) internal { + uint64 destChainSelector = dst.chainSelector; + IEVM2EVMOnRamp srcOnRamp = IEVM2EVMOnRamp(src.router.getOnRamp(destChainSelector)); + address bridgeToken = address(src.token); + address feeToken = src.router.getWrappedNative(); // needed as we do tests with wrapped native as fee token + address linkToken = srcOnRamp.getStaticConfig().linkToken; // needed as feeTokenAmount is converted to linkTokenAmount + IInternal.TokenPriceUpdate[] memory tokenPriceUpdates = new IInternal.TokenPriceUpdate[](3); + IInternal.GasPriceUpdate[] memory gasPriceUpdates = new IInternal.GasPriceUpdate[](1); + IPriceRegistry priceRegistry = IPriceRegistry(srcOnRamp.getDynamicConfig().priceRegistry); // both ramps have the same price registry + + tokenPriceUpdates[0] = IInternal.TokenPriceUpdate({ + sourceToken: bridgeToken, + usdPerToken: priceRegistry.getTokenPrice(bridgeToken).value + }); + tokenPriceUpdates[1] = IInternal.TokenPriceUpdate({ + sourceToken: feeToken, + usdPerToken: priceRegistry.getTokenPrice(feeToken).value + }); + tokenPriceUpdates[2] = IInternal.TokenPriceUpdate({ + sourceToken: linkToken, + usdPerToken: priceRegistry.getTokenPrice(linkToken).value + }); + + gasPriceUpdates[0] = IInternal.GasPriceUpdate({ + destChainSelector: destChainSelector, + usdPerUnitGas: priceRegistry.getDestinationChainGasPrice(destChainSelector).value + }); + + vm.prank(priceRegistry.owner()); + priceRegistry.updatePrices( + IInternal.PriceUpdates({ + tokenPriceUpdates: tokenPriceUpdates, + gasPriceUpdates: gasPriceUpdates + }) + ); + } +} + +contract AaveV3Base_GHOBaseLaunch_20241223_PostExecution is AaveV3Base_GHOBaseLaunch_20241223_Base { + function setUp() public override { + super.setUp(); + + vm.selectFork(arb.c.forkId); + executePayload(vm, address(arb.proposal)); + + vm.selectFork(eth.c.forkId); + executePayload(vm, address(eth.proposal)); + + vm.selectFork(base.c.forkId); + executePayload(vm, address(base.proposal)); + + _validateConfig({executed: true}); + } + + function test_E2E_Eth_Base(uint256 amount) public { + { + vm.selectFork(eth.c.forkId); + uint256 bridgeableAmount = _min( + eth.tokenPool.getBridgeLimit() - eth.tokenPool.getCurrentBridgedAmount(), + CCIP_RATE_LIMIT_CAPACITY + ); + amount = bound(amount, 1, bridgeableAmount); + skip(_getOutboundRefillTime(amount)); + _refreshGasAndTokenPrices(eth.c, base.c); + + vm.prank(alice); + eth.c.token.approve(address(eth.c.router), amount); + deal(address(eth.c.token), alice, amount); + + uint256 tokenPoolBalance = eth.c.token.balanceOf(address(eth.tokenPool)); + uint256 aliceBalance = eth.c.token.balanceOf(alice); + uint256 bridgedAmount = eth.tokenPool.getCurrentBridgedAmount(); + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({src: eth.c, dst: base.c, sender: alice, amount: amount}) + ); + + vm.expectEmit(address(eth.tokenPool)); + emit Locked(address(eth.c.baseOnRamp), amount); + vm.expectEmit(address(eth.c.baseOnRamp)); + emit CCIPSendRequested(eventArg); + + vm.prank(alice); + eth.c.router.ccipSend{value: eventArg.feeTokenAmount}(base.c.chainSelector, message); + + assertEq(eth.c.token.balanceOf(address(eth.tokenPool)), tokenPoolBalance + amount); + assertEq(eth.c.token.balanceOf(alice), aliceBalance - amount); + assertEq(eth.tokenPool.getCurrentBridgedAmount(), bridgedAmount + amount); + + // base execute message + vm.selectFork(base.c.forkId); + + skip(_getInboundRefillTime(amount)); + _refreshGasAndTokenPrices(base.c, eth.c); + assertEq(base.c.token.balanceOf(alice), 0); + assertEq(base.c.token.totalSupply(), 0); // first bridge + assertEq(base.c.token.getFacilitator(address(base.tokenPool)).bucketLevel, 0); // first bridge + + vm.expectEmit(address(base.tokenPool)); + emit Minted(address(base.c.ethOffRamp), alice, amount); + + vm.prank(address(base.c.ethOffRamp)); + base.c.ethOffRamp.executeSingleMessage({ + message: eventArg, + offchainTokenData: new bytes[](message.tokenAmounts.length), + tokenGasOverrides: new uint32[](0) + }); + + assertEq(base.c.token.balanceOf(alice), amount); + assertEq(base.c.token.getFacilitator(address(base.tokenPool)).bucketLevel, amount); + } + + // send amount back to eth + { + // send base from base + vm.selectFork(base.c.forkId); + + skip(_getOutboundRefillTime(amount)); + _refreshGasAndTokenPrices(base.c, eth.c); + vm.prank(alice); + base.c.token.approve(address(base.c.router), amount); + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({src: base.c, dst: eth.c, sender: alice, amount: amount}) + ); + + vm.expectEmit(address(base.tokenPool)); + emit Burned(address(base.c.ethOnRamp), amount); + vm.expectEmit(address(base.c.ethOnRamp)); + emit CCIPSendRequested(eventArg); + + vm.prank(alice); + base.c.router.ccipSend{value: eventArg.feeTokenAmount}(eth.c.chainSelector, message); + + assertEq(base.c.token.balanceOf(alice), 0); + assertEq(base.c.token.getFacilitator(address(base.tokenPool)).bucketLevel, 0); + + // eth execute message + vm.selectFork(eth.c.forkId); + + skip(_getInboundRefillTime(amount)); + _refreshGasAndTokenPrices(eth.c, base.c); + uint256 bridgedAmount = eth.tokenPool.getCurrentBridgedAmount(); + + vm.expectEmit(address(eth.tokenPool)); + emit Released(address(eth.c.baseOffRamp), alice, amount); + vm.prank(address(eth.c.baseOffRamp)); + eth.c.baseOffRamp.executeSingleMessage({ + message: eventArg, + offchainTokenData: new bytes[](message.tokenAmounts.length), + tokenGasOverrides: new uint32[](0) + }); + + assertEq(eth.c.token.balanceOf(alice), amount); + assertEq(eth.tokenPool.getCurrentBridgedAmount(), bridgedAmount - amount); + } + } + + function test_E2E_Arb_Base(uint256 amount) public { + { + vm.selectFork(arb.c.forkId); + uint256 bridgeableAmount = _min( + arb.c.token.getFacilitator(address(arb.tokenPool)).bucketLevel, + CCIP_RATE_LIMIT_CAPACITY + ); + amount = bound(amount, 1, bridgeableAmount); + skip(_getOutboundRefillTime(amount)); + _refreshGasAndTokenPrices(arb.c, base.c); + + vm.prank(alice); + arb.c.token.approve(address(arb.c.router), amount); + deal(address(arb.c.token), alice, amount); + + uint256 aliceBalance = arb.c.token.balanceOf(alice); + uint256 facilitatorLevel = arb.c.token.getFacilitator(address(arb.tokenPool)).bucketLevel; + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({src: arb.c, dst: base.c, sender: alice, amount: amount}) + ); + + vm.expectEmit(address(arb.tokenPool)); + emit Burned(address(arb.c.baseOnRamp), amount); + vm.expectEmit(address(arb.c.baseOnRamp)); + emit CCIPSendRequested(eventArg); + + vm.prank(alice); + arb.c.router.ccipSend{value: eventArg.feeTokenAmount}(base.c.chainSelector, message); + + assertEq(arb.c.token.balanceOf(alice), aliceBalance - amount); + assertEq( + arb.c.token.getFacilitator(address(arb.tokenPool)).bucketLevel, + facilitatorLevel - amount + ); + + // base execute message + vm.selectFork(base.c.forkId); + + skip(_getInboundRefillTime(amount)); + _refreshGasAndTokenPrices(base.c, arb.c); + assertEq(base.c.token.balanceOf(alice), 0); + assertEq(base.c.token.totalSupply(), 0); // first bridge + assertEq(base.c.token.getFacilitator(address(base.tokenPool)).bucketLevel, 0); // first bridge + + vm.expectEmit(address(base.tokenPool)); + emit Minted(address(base.c.arbOffRamp), alice, amount); + + vm.prank(address(base.c.arbOffRamp)); + base.c.arbOffRamp.executeSingleMessage({ + message: eventArg, + offchainTokenData: new bytes[](message.tokenAmounts.length), + tokenGasOverrides: new uint32[](0) + }); + + assertEq(base.c.token.balanceOf(alice), amount); + assertEq(base.c.token.getFacilitator(address(base.tokenPool)).bucketLevel, amount); + } + + // send amount back to arb + { + // send base from base + vm.selectFork(base.c.forkId); + + skip(_getOutboundRefillTime(amount)); + _refreshGasAndTokenPrices(base.c, arb.c); + vm.prank(alice); + base.c.token.approve(address(base.c.router), amount); + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({src: base.c, dst: arb.c, sender: alice, amount: amount}) + ); + + vm.expectEmit(address(base.tokenPool)); + emit Burned(address(base.c.arbOnRamp), amount); + vm.expectEmit(address(base.c.arbOnRamp)); + emit CCIPSendRequested(eventArg); + + vm.prank(alice); + base.c.router.ccipSend{value: eventArg.feeTokenAmount}(arb.c.chainSelector, message); + + assertEq(base.c.token.balanceOf(alice), 0); + assertEq(base.c.token.getFacilitator(address(base.tokenPool)).bucketLevel, 0); + + // arb execute message + vm.selectFork(arb.c.forkId); + + skip(_getInboundRefillTime(amount)); + _refreshGasAndTokenPrices(arb.c, base.c); + uint256 facilitatorLevel = arb.c.token.getFacilitator(address(arb.tokenPool)).bucketLevel; + + vm.expectEmit(address(arb.tokenPool)); + emit Minted(address(arb.c.baseOffRamp), alice, amount); + vm.prank(address(arb.c.baseOffRamp)); + arb.c.baseOffRamp.executeSingleMessage({ + message: eventArg, + offchainTokenData: new bytes[](message.tokenAmounts.length), + tokenGasOverrides: new uint32[](0) + }); + + assertEq(arb.c.token.balanceOf(alice), amount); + assertEq( + arb.c.token.getFacilitator(address(arb.tokenPool)).bucketLevel, + facilitatorLevel + amount + ); + } + } + + function test_E2E_Eth_Arb(uint256 amount) public { + { + vm.selectFork(eth.c.forkId); + uint256 bridgeableAmount = _min( + eth.tokenPool.getBridgeLimit() - eth.tokenPool.getCurrentBridgedAmount(), + CCIP_RATE_LIMIT_CAPACITY + ); + amount = bound(amount, 1, bridgeableAmount); + skip(_getOutboundRefillTime(amount)); + _refreshGasAndTokenPrices(eth.c, arb.c); + + vm.prank(alice); + eth.c.token.approve(address(eth.c.router), amount); + deal(address(eth.c.token), alice, amount); + + uint256 tokenPoolBalance = eth.c.token.balanceOf(address(eth.tokenPool)); + uint256 aliceBalance = eth.c.token.balanceOf(alice); + uint256 bridgedAmount = eth.tokenPool.getCurrentBridgedAmount(); + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage(CCIPSendParams({src: eth.c, dst: arb.c, sender: alice, amount: amount})); + + vm.expectEmit(address(eth.tokenPool)); + emit Locked(address(eth.c.arbOnRamp), amount); + vm.expectEmit(address(eth.c.arbOnRamp)); + emit CCIPSendRequested(eventArg); + + vm.prank(alice); + eth.c.router.ccipSend{value: eventArg.feeTokenAmount}(arb.c.chainSelector, message); + + assertEq(eth.c.token.balanceOf(address(eth.tokenPool)), tokenPoolBalance + amount); + assertEq(eth.c.token.balanceOf(alice), aliceBalance - amount); + assertEq(eth.tokenPool.getCurrentBridgedAmount(), bridgedAmount + amount); + + // arb execute message + vm.selectFork(arb.c.forkId); + + skip(_getInboundRefillTime(amount)); + _refreshGasAndTokenPrices(arb.c, eth.c); + aliceBalance = arb.c.token.balanceOf(alice); + uint256 bucketLevel = arb.c.token.getFacilitator(address(arb.tokenPool)).bucketLevel; + + vm.expectEmit(address(arb.tokenPool)); + emit Minted(address(arb.c.ethOffRamp), alice, amount); + + vm.prank(address(arb.c.ethOffRamp)); + arb.c.ethOffRamp.executeSingleMessage({ + message: eventArg, + offchainTokenData: new bytes[](message.tokenAmounts.length), + tokenGasOverrides: new uint32[](0) + }); + + assertEq(arb.c.token.balanceOf(alice), aliceBalance + amount); + assertEq( + arb.c.token.getFacilitator(address(arb.tokenPool)).bucketLevel, + bucketLevel + amount + ); + } + + // send amount back to eth + { + // send back from arb + vm.selectFork(arb.c.forkId); + vm.prank(alice); + arb.c.token.approve(address(arb.c.router), amount); + skip(_getOutboundRefillTime(amount)); + _refreshGasAndTokenPrices(arb.c, eth.c); + + uint256 aliceBalance = arb.c.token.balanceOf(alice); + uint256 bucketLevel = arb.c.token.getFacilitator(address(arb.tokenPool)).bucketLevel; + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage(CCIPSendParams({src: arb.c, dst: eth.c, sender: alice, amount: amount})); + + vm.expectEmit(address(arb.tokenPool)); + emit Burned(address(arb.c.ethOnRamp), amount); + vm.expectEmit(address(arb.c.ethOnRamp)); + emit CCIPSendRequested(eventArg); + + vm.prank(alice); + arb.c.router.ccipSend{value: eventArg.feeTokenAmount}(eth.c.chainSelector, message); + + assertEq(arb.c.token.balanceOf(alice), aliceBalance - amount); + assertEq( + arb.c.token.getFacilitator(address(arb.tokenPool)).bucketLevel, + bucketLevel - amount + ); + + // eth execute message + vm.selectFork(eth.c.forkId); + + skip(_getInboundRefillTime(amount)); + _refreshGasAndTokenPrices(eth.c, arb.c); + uint256 bridgedAmount = eth.tokenPool.getCurrentBridgedAmount(); + + vm.expectEmit(address(eth.tokenPool)); + emit Released(address(eth.c.arbOffRamp), alice, amount); + vm.prank(address(eth.c.arbOffRamp)); + eth.c.arbOffRamp.executeSingleMessage({ + message: eventArg, + offchainTokenData: new bytes[](message.tokenAmounts.length), + tokenGasOverrides: new uint32[](0) + }); + + assertEq(eth.c.token.balanceOf(alice), amount); + assertEq(eth.tokenPool.getCurrentBridgedAmount(), bridgedAmount - amount); + } + } +} diff --git a/src/20241223_Multi_GHOBaseLaunch/AaveV3Ethereum_GHOBaseLaunch_20241223.sol b/src/20241223_Multi_GHOBaseLaunch/AaveV3Ethereum_GHOBaseLaunch_20241223.sol new file mode 100644 index 000000000..635869922 --- /dev/null +++ b/src/20241223_Multi_GHOBaseLaunch/AaveV3Ethereum_GHOBaseLaunch_20241223.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IProposalGenericExecutor} from 'aave-helpers/src/interfaces/IProposalGenericExecutor.sol'; +import {IUpgradeableLockReleaseTokenPool_1_5_1} from 'src/interfaces/ccip/tokenPool/IUpgradeableLockReleaseTokenPool.sol'; +import {IRateLimiter} from 'src/interfaces/ccip/IRateLimiter.sol'; +import {GhoEthereum} from 'aave-address-book/GhoEthereum.sol'; + +/** + * @title GHO Base Launch + * @author Aave Labs + * - Discussion: https://governance.aave.com/t/arfc-launch-gho-on-base-set-aci-as-emissions-manager-for-rewards/19338 + * - Snapshot: https://snapshot.box/#/s:aave.eth/proposal/0x03dc21e0423c60082dc23317af6ebaf997610cbc2cbb0f5a385653bd99a524fe + */ +contract AaveV3Ethereum_GHOBaseLaunch_20241223 is IProposalGenericExecutor { + uint64 public constant BASE_CHAIN_SELECTOR = 15971525489660198786; + + // https://etherscan.io/address/0x06179f7C1be40863405f374E7f5F8806c728660A + IUpgradeableLockReleaseTokenPool_1_5_1 public constant TOKEN_POOL = + IUpgradeableLockReleaseTokenPool_1_5_1(GhoEthereum.GHO_CCIP_TOKEN_POOL); + + // https://basescan.org/address/0x98217A06721Ebf727f2C8d9aD7718ec28b7aAe34 + address public constant REMOTE_TOKEN_POOL_BASE = 0x98217A06721Ebf727f2C8d9aD7718ec28b7aAe34; + // https://basescan.org/address/0x6Bb7a212910682DCFdbd5BCBb3e28FB4E8da10Ee + address public constant REMOTE_GHO_TOKEN_BASE = 0x6Bb7a212910682DCFdbd5BCBb3e28FB4E8da10Ee; + + // Token Rate Limit Capacity: 300_000 GHO + uint128 public constant CCIP_RATE_LIMIT_CAPACITY = 300_000e18; + // 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 { + IRateLimiter.Config memory rateLimiterConfig = IRateLimiter.Config({ + isEnabled: true, + capacity: CCIP_RATE_LIMIT_CAPACITY, + rate: CCIP_RATE_LIMIT_REFILL_RATE + }); + + IUpgradeableLockReleaseTokenPool_1_5_1.ChainUpdate[] + memory chainsToAdd = new IUpgradeableLockReleaseTokenPool_1_5_1.ChainUpdate[](1); + + bytes[] memory remotePoolAddresses = new bytes[](1); + remotePoolAddresses[0] = abi.encode(REMOTE_TOKEN_POOL_BASE); + + chainsToAdd[0] = IUpgradeableLockReleaseTokenPool_1_5_1.ChainUpdate({ + remoteChainSelector: BASE_CHAIN_SELECTOR, + remotePoolAddresses: remotePoolAddresses, + remoteTokenAddress: abi.encode(REMOTE_GHO_TOKEN_BASE), + outboundRateLimiterConfig: rateLimiterConfig, + inboundRateLimiterConfig: rateLimiterConfig + }); + + TOKEN_POOL.applyChainUpdates({ + remoteChainSelectorsToRemove: new uint64[](0), + chainsToAdd: chainsToAdd + }); + } +} diff --git a/src/20241223_Multi_GHOBaseLaunch/AaveV3Ethereum_GHOBaseLaunch_20241223.t.sol b/src/20241223_Multi_GHOBaseLaunch/AaveV3Ethereum_GHOBaseLaunch_20241223.t.sol new file mode 100644 index 000000000..3ea443774 --- /dev/null +++ b/src/20241223_Multi_GHOBaseLaunch/AaveV3Ethereum_GHOBaseLaunch_20241223.t.sol @@ -0,0 +1,462 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; + +import {IUpgradeableLockReleaseTokenPool_1_5_1} from 'src/interfaces/ccip/tokenPool/IUpgradeableLockReleaseTokenPool.sol'; +import {IPool as IPool_CCIP} from 'src/interfaces/ccip/tokenPool/IPool.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 {IRateLimiter} from 'src/interfaces/ccip/IRateLimiter.sol'; +import {IEVM2EVMOnRamp} from 'src/interfaces/ccip/IEVM2EVMOnRamp.sol'; +import {IEVM2EVMOffRamp_1_5} from 'src/interfaces/ccip/IEVM2EVMOffRamp.sol'; +import {ITokenAdminRegistry} from 'src/interfaces/ccip/ITokenAdminRegistry.sol'; +import {IGhoToken} from 'src/interfaces/IGhoToken.sol'; +import {IGhoCcipSteward} from 'src/interfaces/IGhoCcipSteward.sol'; + +import {ProtocolV3TestBase} from 'aave-helpers/src/ProtocolV3TestBase.sol'; +import {AaveV3Arbitrum} from 'aave-address-book/AaveV3Arbitrum.sol'; +import {AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; +import {AaveV3ArbitrumAssets} from 'aave-address-book/AaveV3Arbitrum.sol'; + +import {CCIPUtils} from './utils/CCIPUtils.sol'; + +import {AaveV3Ethereum_GHOBaseLaunch_20241223} from './AaveV3Ethereum_GHOBaseLaunch_20241223.sol'; + +/** + * @dev Test for AaveV3Ethereum_GHOBaseLaunch_20241223 + * command: FOUNDRY_PROFILE=mainnet forge test --match-path=src/20241223_Multi_GHOBaseLaunch/AaveV3Ethereum_GHOBaseLaunch_20241223.t.sol -vv + */ +contract AaveV3Ethereum_GHOBaseLaunch_20241223_Test is ProtocolV3TestBase { + struct CCIPSendParams { + address sender; + uint256 amount; + uint64 destChainSelector; + } + + uint64 internal constant ARB_CHAIN_SELECTOR = CCIPUtils.ARB_CHAIN_SELECTOR; + uint64 internal constant BASE_CHAIN_SELECTOR = CCIPUtils.BASE_CHAIN_SELECTOR; + uint64 internal constant ETH_CHAIN_SELECTOR = CCIPUtils.ETH_CHAIN_SELECTOR; + uint256 internal constant CCIP_RATE_LIMIT_CAPACITY = 300_000e18; + uint256 internal constant CCIP_RATE_LIMIT_REFILL_RATE = 60e18; + + IGhoToken internal constant GHO = IGhoToken(AaveV3EthereumAssets.GHO_UNDERLYING); + ITokenAdminRegistry internal constant TOKEN_ADMIN_REGISTRY = + ITokenAdminRegistry(0xb22764f98dD05c789929716D677382Df22C05Cb6); + IEVM2EVMOnRamp internal constant ARB_ON_RAMP = + IEVM2EVMOnRamp(0x69eCC4E2D8ea56E2d0a05bF57f4Fd6aEE7f2c284); + IEVM2EVMOnRamp internal constant BASE_ON_RAMP = + IEVM2EVMOnRamp(0xb8a882f3B88bd52D1Ff56A873bfDB84b70431937); + IEVM2EVMOffRamp_1_5 internal constant ARB_OFF_RAMP = + IEVM2EVMOffRamp_1_5(0xdf615eF8D4C64d0ED8Fd7824BBEd2f6a10245aC9); + IEVM2EVMOffRamp_1_5 internal constant BASE_OFF_RAMP = + IEVM2EVMOffRamp_1_5(0x6B4B6359Dd5B47Cdb030E5921456D2a0625a9EbD); + + address public constant NEW_REMOTE_TOKEN_BASE = 0x6Bb7a212910682DCFdbd5BCBb3e28FB4E8da10Ee; + address internal constant NEW_REMOTE_POOL_ARB = 0xB94Ab28c6869466a46a42abA834ca2B3cECCA5eB; + address internal constant NEW_REMOTE_POOL_BASE = 0x98217A06721Ebf727f2C8d9aD7718ec28b7aAe34; + address internal constant RISK_COUNCIL = 0x8513e6F37dBc52De87b166980Fa3F50639694B60; // common across all chains + IRouter internal constant ROUTER = IRouter(0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D); + IGhoCcipSteward internal constant NEW_GHO_CCIP_STEWARD = + IGhoCcipSteward(0xC5BcC58BE6172769ca1a78B8A45752E3C5059c39); + IUpgradeableLockReleaseTokenPool_1_5_1 internal constant NEW_TOKEN_POOL = + IUpgradeableLockReleaseTokenPool_1_5_1(0x06179f7C1be40863405f374E7f5F8806c728660A); + + AaveV3Ethereum_GHOBaseLaunch_20241223 internal proposal; + + address internal alice = makeAddr('alice'); + address internal bob = makeAddr('bob'); + address internal carol = makeAddr('carol'); + + 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); + error InvalidSourcePoolAddress(bytes); + + function setUp() public { + vm.createSelectFork(vm.rpcUrl('mainnet'), 21722753); + proposal = new AaveV3Ethereum_GHOBaseLaunch_20241223(); + _validateConstants(); + executePayload(vm, address(proposal)); + } + + function _validateConstants() private view { + assertEq(proposal.BASE_CHAIN_SELECTOR(), BASE_CHAIN_SELECTOR); + assertEq(address(proposal.TOKEN_POOL()), address(NEW_TOKEN_POOL)); + assertEq(proposal.REMOTE_TOKEN_POOL_BASE(), NEW_REMOTE_POOL_BASE); + assertEq(proposal.REMOTE_GHO_TOKEN_BASE(), NEW_REMOTE_TOKEN_BASE); + assertEq(proposal.CCIP_RATE_LIMIT_CAPACITY(), CCIP_RATE_LIMIT_CAPACITY); + assertEq(proposal.CCIP_RATE_LIMIT_REFILL_RATE(), CCIP_RATE_LIMIT_REFILL_RATE); + + assertEq(TOKEN_ADMIN_REGISTRY.typeAndVersion(), 'TokenAdminRegistry 1.5.0'); + assertEq(NEW_TOKEN_POOL.typeAndVersion(), 'LockReleaseTokenPool 1.5.1'); + assertEq(ROUTER.typeAndVersion(), 'Router 1.2.0'); + + _assertOnRamp(ARB_ON_RAMP, ETH_CHAIN_SELECTOR, ARB_CHAIN_SELECTOR, ROUTER); + _assertOnRamp(BASE_ON_RAMP, ETH_CHAIN_SELECTOR, BASE_CHAIN_SELECTOR, ROUTER); + _assertOffRamp(ARB_OFF_RAMP, ARB_CHAIN_SELECTOR, ETH_CHAIN_SELECTOR, ROUTER); + _assertOffRamp(BASE_OFF_RAMP, BASE_CHAIN_SELECTOR, ETH_CHAIN_SELECTOR, ROUTER); + + assertEq(NEW_GHO_CCIP_STEWARD.RISK_COUNCIL(), RISK_COUNCIL); + assertEq(NEW_GHO_CCIP_STEWARD.GHO_TOKEN(), AaveV3EthereumAssets.GHO_UNDERLYING); + assertEq(NEW_GHO_CCIP_STEWARD.GHO_TOKEN_POOL(), address(NEW_TOKEN_POOL)); + assertTrue(NEW_GHO_CCIP_STEWARD.BRIDGE_LIMIT_ENABLED()); + } + + function _assertOnRamp( + IEVM2EVMOnRamp onRamp, + uint64 srcSelector, + uint64 dstSelector, + IRouter router + ) internal view { + assertEq(onRamp.typeAndVersion(), 'EVM2EVMOnRamp 1.5.0'); + assertEq(onRamp.getStaticConfig().chainSelector, srcSelector); + assertEq(onRamp.getStaticConfig().destChainSelector, dstSelector); + assertEq(onRamp.getDynamicConfig().router, address(router)); + assertEq(router.getOnRamp(dstSelector), address(onRamp)); + } + + function _assertOffRamp( + IEVM2EVMOffRamp_1_5 offRamp, + uint64 srcSelector, + uint64 dstSelector, + IRouter router + ) internal view { + assertEq(offRamp.typeAndVersion(), 'EVM2EVMOffRamp 1.5.0'); + assertEq(offRamp.getStaticConfig().sourceChainSelector, srcSelector); + assertEq(offRamp.getStaticConfig().chainSelector, dstSelector); + assertEq(offRamp.getDynamicConfig().router, address(router)); + assertTrue(router.isOffRamp(srcSelector, address(offRamp))); + } + + 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(GHO), amount: params.amount}); + + uint256 feeAmount = ROUTER.getFee(params.destChainSelector, message); + deal(params.sender, feeAmount); + + IInternal.EVM2EVMMessage memory eventArg = CCIPUtils.messageToEvent( + CCIPUtils.MessageToEventParams({ + message: message, + router: ROUTER, + sourceChainSelector: ETH_CHAIN_SELECTOR, + destChainSelector: params.destChainSelector, + feeTokenAmount: feeAmount, + originalSender: params.sender, + sourceToken: address(GHO), + destinationToken: address( + params.destChainSelector == BASE_CHAIN_SELECTOR + ? NEW_REMOTE_TOKEN_BASE + : AaveV3ArbitrumAssets.GHO_UNDERLYING + ) + }) + ); + + return (message, eventArg); + } + + function _tokenBucketToConfig( + IRateLimiter.TokenBucket memory bucket + ) internal pure returns (IRateLimiter.Config memory) { + return + IRateLimiter.Config({ + isEnabled: bucket.isEnabled, + capacity: bucket.capacity, + rate: bucket.rate + }); + } + + function _getDisabledConfig() internal pure returns (IRateLimiter.Config memory) { + return IRateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}); + } + + function _getImplementation(address proxy) internal view returns (address) { + bytes32 slot = bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1); + return address(uint160(uint256(vm.load(proxy, slot)))); + } + + function _readInitialized(address proxy) internal view returns (uint8) { + return uint8(uint256(vm.load(proxy, bytes32(0)))); + } + + 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() + }); + } + + function _getOutboundRefillTime(uint256 amount) internal pure returns (uint256) { + return (amount / CCIP_RATE_LIMIT_REFILL_RATE) + 1; // account for rounding + } + + function _getInboundRefillTime(uint256 amount) internal pure returns (uint256) { + return amount / CCIP_RATE_LIMIT_REFILL_RATE + 1; // account for rounding + } + + function _min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + + function assertEq( + IRateLimiter.TokenBucket memory bucket, + IRateLimiter.Config memory config + ) internal pure { + assertEq(bucket.isEnabled, config.isEnabled); + assertEq(bucket.capacity, config.capacity); + assertEq(bucket.rate, config.rate); + } + + function test_basePoolConfig() public view { + assertEq(NEW_TOKEN_POOL.getSupportedChains().length, 2); + assertEq(NEW_TOKEN_POOL.getSupportedChains()[0], ARB_CHAIN_SELECTOR); + assertEq(NEW_TOKEN_POOL.getSupportedChains()[1], BASE_CHAIN_SELECTOR); + assertEq( + NEW_TOKEN_POOL.getRemoteToken(ARB_CHAIN_SELECTOR), + abi.encode(address(AaveV3ArbitrumAssets.GHO_UNDERLYING)) + ); + assertEq( + NEW_TOKEN_POOL.getRemoteToken(BASE_CHAIN_SELECTOR), + abi.encode(address(NEW_REMOTE_TOKEN_BASE)) + ); + assertEq(NEW_TOKEN_POOL.getRemotePools(BASE_CHAIN_SELECTOR).length, 1); + assertEq( + NEW_TOKEN_POOL.getRemotePools(BASE_CHAIN_SELECTOR)[0], + abi.encode(address(NEW_REMOTE_POOL_BASE)) + ); + assertEq(NEW_TOKEN_POOL.getRemotePools(ARB_CHAIN_SELECTOR).length, 2); + assertEq( + NEW_TOKEN_POOL.getRemotePools(ARB_CHAIN_SELECTOR)[1], // 0th is the 1.4 token pool + abi.encode(address(NEW_REMOTE_POOL_ARB)) + ); + assertEq( + NEW_TOKEN_POOL.getCurrentInboundRateLimiterState(ARB_CHAIN_SELECTOR), + _getRateLimiterConfig() + ); + assertEq( + NEW_TOKEN_POOL.getCurrentOutboundRateLimiterState(ARB_CHAIN_SELECTOR), + _getRateLimiterConfig() + ); + assertEq( + NEW_TOKEN_POOL.getCurrentInboundRateLimiterState(BASE_CHAIN_SELECTOR), + _getRateLimiterConfig() + ); + assertEq( + NEW_TOKEN_POOL.getCurrentOutboundRateLimiterState(BASE_CHAIN_SELECTOR), + _getRateLimiterConfig() + ); + } + + function test_sendMessageToBaseSucceeds(uint256 amount) public { + uint256 bridgeableAmount = _min( + NEW_TOKEN_POOL.getBridgeLimit() - NEW_TOKEN_POOL.getCurrentBridgedAmount(), + CCIP_RATE_LIMIT_CAPACITY + ); + amount = bound(amount, 1, bridgeableAmount); + skip(_getOutboundRefillTime(amount)); // wait for the rate limiter to refill + + deal(address(GHO), alice, amount); + vm.prank(alice); + GHO.approve(address(ROUTER), amount); + + uint256 aliceBalance = GHO.balanceOf(alice); + uint256 currentBridgedAmount = NEW_TOKEN_POOL.getCurrentBridgedAmount(); + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({amount: amount, sender: alice, destChainSelector: BASE_CHAIN_SELECTOR}) + ); + + vm.expectEmit(address(NEW_TOKEN_POOL)); + emit Locked(address(BASE_ON_RAMP), amount); + vm.expectEmit(address(BASE_ON_RAMP)); + emit CCIPSendRequested(eventArg); + + vm.prank(alice); + ROUTER.ccipSend{value: eventArg.feeTokenAmount}(BASE_CHAIN_SELECTOR, message); + + assertEq(GHO.balanceOf(alice), aliceBalance - amount); + assertEq(NEW_TOKEN_POOL.getCurrentBridgedAmount(), currentBridgedAmount + amount); + } + + function test_sendMessageToArbSucceeds(uint256 amount) public { + uint256 bridgeableAmount = _min( + NEW_TOKEN_POOL.getBridgeLimit() - NEW_TOKEN_POOL.getCurrentBridgedAmount(), + CCIP_RATE_LIMIT_CAPACITY + ); + amount = bound(amount, 1, bridgeableAmount); + skip(_getOutboundRefillTime(amount)); // wait for the rate limiter to refill + + deal(address(GHO), alice, amount); + vm.prank(alice); + GHO.approve(address(ROUTER), amount); + + uint256 aliceBalance = GHO.balanceOf(alice); + uint256 currentBridgedAmount = NEW_TOKEN_POOL.getCurrentBridgedAmount(); + + ( + IClient.EVM2AnyMessage memory message, + IInternal.EVM2EVMMessage memory eventArg + ) = _getTokenMessage( + CCIPSendParams({amount: amount, sender: alice, destChainSelector: ARB_CHAIN_SELECTOR}) + ); + + vm.expectEmit(address(NEW_TOKEN_POOL)); + emit Locked(address(ARB_ON_RAMP), amount); + vm.expectEmit(address(ARB_ON_RAMP)); + emit CCIPSendRequested(eventArg); + + vm.prank(alice); + ROUTER.ccipSend{value: eventArg.feeTokenAmount}(ARB_CHAIN_SELECTOR, message); + + assertEq(GHO.balanceOf(alice), aliceBalance - amount); + assertEq(NEW_TOKEN_POOL.getCurrentBridgedAmount(), currentBridgedAmount + amount); + } + + function test_offRampViaBaseSucceeds(uint256 amount) public { + uint256 bridgeableAmount = _min( + NEW_TOKEN_POOL.getCurrentBridgedAmount(), + CCIP_RATE_LIMIT_CAPACITY + ); + amount = bound(amount, 1, bridgeableAmount); + skip(_getInboundRefillTime(amount)); + + uint256 aliceBalance = GHO.balanceOf(alice); + uint256 poolBalance = GHO.balanceOf(address(NEW_TOKEN_POOL)); + uint256 currentBridgedAmount = NEW_TOKEN_POOL.getCurrentBridgedAmount(); + + vm.expectEmit(address(NEW_TOKEN_POOL)); + emit Released(address(BASE_OFF_RAMP), alice, amount); + + vm.prank(address(BASE_OFF_RAMP)); + NEW_TOKEN_POOL.releaseOrMint( + IPool_CCIP.ReleaseOrMintInV1({ + originalSender: abi.encode(alice), + remoteChainSelector: BASE_CHAIN_SELECTOR, + receiver: alice, + amount: amount, + localToken: address(GHO), + sourcePoolAddress: abi.encode(address(NEW_REMOTE_POOL_BASE)), + sourcePoolData: new bytes(0), + offchainTokenData: new bytes(0) + }) + ); + + assertEq(GHO.balanceOf(address(NEW_TOKEN_POOL)), poolBalance - amount); + assertEq(GHO.balanceOf(alice), aliceBalance + amount); + assertEq(NEW_TOKEN_POOL.getCurrentBridgedAmount(), currentBridgedAmount - amount); + assertEq(NEW_TOKEN_POOL.getCurrentBridgedAmount(), GHO.balanceOf(address(NEW_TOKEN_POOL))); + } + + function test_offRampViaArbSucceeds(uint256 amount) public { + uint256 bridgeableAmount = _min( + NEW_TOKEN_POOL.getCurrentBridgedAmount(), + CCIP_RATE_LIMIT_CAPACITY + ); + amount = bound(amount, 1, bridgeableAmount); + skip(_getInboundRefillTime(amount)); + + uint256 aliceBalance = GHO.balanceOf(alice); + uint256 poolBalance = GHO.balanceOf(address(NEW_TOKEN_POOL)); + uint256 currentBridgedAmount = NEW_TOKEN_POOL.getCurrentBridgedAmount(); + + vm.expectEmit(address(NEW_TOKEN_POOL)); + emit Released(address(ARB_OFF_RAMP), alice, amount); + + vm.prank(address(ARB_OFF_RAMP)); + NEW_TOKEN_POOL.releaseOrMint( + IPool_CCIP.ReleaseOrMintInV1({ + originalSender: abi.encode(alice), + remoteChainSelector: ARB_CHAIN_SELECTOR, + receiver: alice, + amount: amount, + localToken: address(GHO), + sourcePoolAddress: abi.encode(address(NEW_REMOTE_POOL_ARB)), + sourcePoolData: new bytes(0), + offchainTokenData: new bytes(0) + }) + ); + + assertEq(GHO.balanceOf(address(NEW_TOKEN_POOL)), poolBalance - amount); + assertEq(GHO.balanceOf(alice), aliceBalance + amount); + assertEq(NEW_TOKEN_POOL.getCurrentBridgedAmount(), currentBridgedAmount - amount); + assertEq(NEW_TOKEN_POOL.getCurrentBridgedAmount(), GHO.balanceOf(address(NEW_TOKEN_POOL))); + } + + function test_cannotUseBaseOffRampForArbMessages() public { + uint256 amount = 100e18; + skip(_getInboundRefillTime(amount)); + + vm.expectRevert( + abi.encodeWithSelector(CallerIsNotARampOnRouter.selector, address(BASE_OFF_RAMP)) + ); + vm.prank(address(BASE_OFF_RAMP)); + NEW_TOKEN_POOL.releaseOrMint( + IPool_CCIP.ReleaseOrMintInV1({ + originalSender: abi.encode(alice), + remoteChainSelector: ARB_CHAIN_SELECTOR, + receiver: alice, + amount: amount, + localToken: address(GHO), + sourcePoolAddress: abi.encode(address(NEW_REMOTE_POOL_ARB)), + sourcePoolData: new bytes(0), + offchainTokenData: new bytes(0) + }) + ); + } + + function test_cannotOffRampOtherChainMessages() public { + uint256 amount = 100e18; + skip(_getInboundRefillTime(amount)); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidSourcePoolAddress.selector, + abi.encode(address(NEW_REMOTE_POOL_ARB)) + ) + ); + vm.prank(address(BASE_OFF_RAMP)); + NEW_TOKEN_POOL.releaseOrMint( + IPool_CCIP.ReleaseOrMintInV1({ + originalSender: abi.encode(alice), + remoteChainSelector: BASE_CHAIN_SELECTOR, + receiver: alice, + amount: amount, + localToken: address(GHO), + sourcePoolAddress: abi.encode(address(NEW_REMOTE_POOL_ARB)), + sourcePoolData: new bytes(0), + offchainTokenData: new bytes(0) + }) + ); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidSourcePoolAddress.selector, + abi.encode(address(NEW_REMOTE_POOL_BASE)) + ) + ); + vm.prank(address(ARB_OFF_RAMP)); + NEW_TOKEN_POOL.releaseOrMint( + IPool_CCIP.ReleaseOrMintInV1({ + originalSender: abi.encode(alice), + remoteChainSelector: ARB_CHAIN_SELECTOR, + receiver: alice, + amount: amount, + localToken: address(GHO), + sourcePoolAddress: abi.encode(address(NEW_REMOTE_POOL_BASE)), + sourcePoolData: new bytes(0), + offchainTokenData: new bytes(0) + }) + ); + } +} diff --git a/src/20241223_Multi_GHOBaseLaunch/GHOBaseLaunch.md b/src/20241223_Multi_GHOBaseLaunch/GHOBaseLaunch.md new file mode 100644 index 000000000..a92bbe8d7 --- /dev/null +++ b/src/20241223_Multi_GHOBaseLaunch/GHOBaseLaunch.md @@ -0,0 +1,60 @@ +--- +title: "GHO Base Launch" +author: "Aave Labs" +discussions: "https://governance.aave.com/t/arfc-launch-gho-on-base-set-aci-as-emissions-manager-for-rewards/19338" +--- + +## Simple Summary + +This AIP proposes the expansion of GHO, the native asset of the Aave Protocol, to the Base network utilizing the Chainlink Cross-Chain Interoperability Protocol (CCIP) v1.5.1. + +The smart contracts have been refined through multiple stages of design, development, testing, and implementation. Likewise, Certora, the DAO service provider, was engaged to conduct code reviews of the implementation. + +## Motivation + +Building on the successful expansion of GHO into Arbitrum, it is now time to expand GHO to other networks. The Base ecosystem will bring a new set of opportunities, allowing access to a wide array of integrations with other protocols and tools and ultimately enriching GHO's utility potential. + +## Specification + +This AIP includes a series of actions required to launch GHO on Base: + +1. Configure new Chainlink CCIP lanes between Base and Ethereum/Arbitrum (while retaining existing ones) with a rate limit of 300,000 GHO capacity and 60 GHO per second rate. +2. Update the proxy admin of GHO token on Arbitrum to OpenZeppelin v5.1 Proxy Contract, enabling GHO on Arbitrum to be aligned with Base deployment. +3. Configure and activate GhoAaveSteward and GhoCcipSteward to control GHO listing and CCIP lane. +4. List GHO as a borrowable asset on the Aave Pool, with the risk configuration specified in the ARFC. Then, initial liquidity will be provided to the pool as a security measure to mitigate potential vulnerabilities and facilitate a stable launch. +5. Set ACI multisig ([0xac140648435d03f784879cd789130F22Ef588Fcd](https://basescan.org/address/0xac140648435d03f784879cd789130F22Ef588Fcd)) as Emission Admin for GHO and aGHO rewards, as specified in the ARFC. + +The table below illustrates the configured risk parameters for **GHO** + +| Parameter | Value | +| ------------------------- | -----------------------------------------: | +| Isolation Mode | false | +| Borrowable | ENABLED | +| Collateral Enabled | false | +| Supply Cap (BLT) | 2,500,000 | +| Borrow Cap (BLT) | 2,250,000 | +| Debt Ceiling | USD 0 | +| LTV | 0 % | +| LT | 0 % | +| Liquidation Bonus | 0 % | +| Liquidation Protocol Fee | 0 % | +| Reserve Factor | 10 % | +| Base Variable Borrow Rate | 0 % | +| Variable Slope 1 | 9.5 % | +| Variable Slope 2 | 50 % | +| Uoptimal | 90 % | +| Flashloanable | ENABLED | +| Siloed Borrowing | DISABLED | +| Borrowable in Isolation | DISABLED | +| Oracle | 0xfc421aD3C883Bf9E7C4f42dE845C4e4405799e73 | + +## References + +- Implementation: [AaveV3Ethereum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20241223_Multi_GHOBaseLaunch/AaveV3Ethereum_GHOBaseLaunch_20241223.sol), [AaveV3Arbitrum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20241223_Multi_GHOBaseLaunch/AaveV3Arbitrum_GHOBaseLaunch_20241223.sol), [AaveV3BaseLaunch](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20241223_Multi_GHOBaseLaunch/AaveV3Base_GHOBaseLaunch_20241223.sol), [AaveV3BaseListing](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20241223_Multi_GHOBaseLaunch/AaveV3Base_GHOBaseListing_20241223.sol) +- Tests: [AaveV3Ethereum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20241223_Multi_GHOBaseLaunch/AaveV3Ethereum_GHOBaseLaunch_20241223.t.sol), [AaveV3Arbitrum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20241223_Multi_GHOBaseLaunch/AaveV3Arbitrum_GHOBaseLaunch_20241223.t.sol), [AaveV3BaseLaunch](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20241223_Multi_GHOBaseLaunch/AaveV3Base_GHOBaseLaunch_20241223.t.sol), [AaveV3BaseListing](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20241223_Multi_GHOBaseLaunch/AaveV3Base_GHOBaseListing_20241223.t.sol), [E2EFlow](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20241223_Multi_GHOBaseLaunch/AaveV3E2E_GHOBaseLaunch_20241223.t.sol) +- [Snapshot](https://snapshot.box/#/s:aave.eth/proposal/0x03dc21e0423c60082dc23317af6ebaf997610cbc2cbb0f5a385653bd99a524fe) +- [Discussion](https://governance.aave.com/t/arfc-launch-gho-on-base-set-aci-as-emissions-manager-for-rewards/19338) + +## Copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). diff --git a/src/20241223_Multi_GHOBaseLaunch/GHOBaseLaunch_20241223.s.sol b/src/20241223_Multi_GHOBaseLaunch/GHOBaseLaunch_20241223.s.sol new file mode 100644 index 000000000..b954d900e --- /dev/null +++ b/src/20241223_Multi_GHOBaseLaunch/GHOBaseLaunch_20241223.s.sol @@ -0,0 +1,128 @@ +// 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, BaseScript} from 'solidity-utils/contracts/utils/ScriptUtils.sol'; +import {AaveV3Ethereum_GHOBaseLaunch_20241223} from './AaveV3Ethereum_GHOBaseLaunch_20241223.sol'; +import {AaveV3Arbitrum_GHOBaseLaunch_20241223} from './AaveV3Arbitrum_GHOBaseLaunch_20241223.sol'; +import {AaveV3Base_GHOBaseLaunch_20241223} from './AaveV3Base_GHOBaseLaunch_20241223.sol'; +import {AaveV3Base_GHOBaseListing_20241223} from './AaveV3Base_GHOBaseListing_20241223.sol'; +/** + * @dev Deploy Ethereum + * deploy-command: make deploy-ledger contract=src/20241223_Multi_GHOBaseLaunch/GHOBaseLaunch_20241223.s.sol:DeployEthereum chain=mainnet + * verify-command: FOUNDRY_PROFILE=mainnet npx catapulta-verify -b broadcast/GHOBaseLaunch_20241223.s.sol/1/run-latest.json + */ +contract DeployEthereum is EthereumScript { + function run() external broadcast { + // deploy payloads + address payload0 = GovV3Helpers.deployDeterministic( + type(AaveV3Ethereum_GHOBaseLaunch_20241223).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/20241223_Multi_GHOBaseLaunch/GHOBaseLaunch_20241223.s.sol:DeployArbitrum chain=arbitrum + * verify-command: FOUNDRY_PROFILE=arbitrum npx catapulta-verify -b broadcast/GHOBaseLaunch_20241223.s.sol/42161/run-latest.json + */ +contract DeployArbitrum is ArbitrumScript { + function run() external broadcast { + // deploy payloads + address payload0 = GovV3Helpers.deployDeterministic( + type(AaveV3Arbitrum_GHOBaseLaunch_20241223).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 Base + * deploy-command: make deploy-ledger contract=src/20241223_Multi_GHOBaseLaunch/GHOBaseLaunch_20241223.s.sol:DeployBase chain=base + * verify-command: FOUNDRY_PROFILE=base npx catapulta-verify -b broadcast/GHOBaseLaunch_20241223.s.sol/8453/run-latest.json + */ +contract DeployBase is BaseScript { + function run() external broadcast { + // compose action + IPayloadsControllerCore.ExecutionAction[] + memory launchActions = new IPayloadsControllerCore.ExecutionAction[](1); + launchActions[0] = GovV3Helpers.buildAction( + GovV3Helpers.deployDeterministic(type(AaveV3Base_GHOBaseLaunch_20241223).creationCode) + ); + + IPayloadsControllerCore.ExecutionAction[] + memory listingActions = new IPayloadsControllerCore.ExecutionAction[](1); + listingActions[0] = GovV3Helpers.buildAction( + GovV3Helpers.deployDeterministic(type(AaveV3Base_GHOBaseListing_20241223).creationCode) + ); + + // register both actions separately at payloadsController + GovV3Helpers.createPayload(launchActions); + GovV3Helpers.createPayload(listingActions); + } +} + +/** + * @dev Create Proposal + * command: make deploy-ledger contract=src/20241223_Multi_GHOBaseLaunch/GHOBaseLaunch_20241223.s.sol:CreateProposal chain=mainnet + */ +contract CreateProposal is EthereumScript { + function run() external { + // create payloads + PayloadsControllerUtils.Payload[] memory payloads = new PayloadsControllerUtils.Payload[](4); + + // compose actions for validation + IPayloadsControllerCore.ExecutionAction[] + memory actionsEthereum = new IPayloadsControllerCore.ExecutionAction[](1); + actionsEthereum[0] = GovV3Helpers.buildAction( + type(AaveV3Ethereum_GHOBaseLaunch_20241223).creationCode + ); + payloads[0] = GovV3Helpers.buildMainnetPayload(vm, actionsEthereum); + + IPayloadsControllerCore.ExecutionAction[] + memory actionsArbitrum = new IPayloadsControllerCore.ExecutionAction[](1); + actionsArbitrum[0] = GovV3Helpers.buildAction( + type(AaveV3Arbitrum_GHOBaseLaunch_20241223).creationCode + ); + payloads[1] = GovV3Helpers.buildArbitrumPayload(vm, actionsArbitrum); + + IPayloadsControllerCore.ExecutionAction[] + memory actionsBaseLaunch = new IPayloadsControllerCore.ExecutionAction[](1); + actionsBaseLaunch[0] = GovV3Helpers.buildAction( + type(AaveV3Base_GHOBaseLaunch_20241223).creationCode + ); + payloads[2] = GovV3Helpers.buildBasePayload(vm, actionsBaseLaunch); + + IPayloadsControllerCore.ExecutionAction[] + memory actionsBaseListing = new IPayloadsControllerCore.ExecutionAction[](1); + actionsBaseListing[0] = GovV3Helpers.buildAction( + type(AaveV3Base_GHOBaseListing_20241223).creationCode + ); + payloads[3] = GovV3Helpers.buildBasePayload(vm, actionsBaseListing); + + // create proposal + vm.startBroadcast(); + GovV3Helpers.createProposal( + vm, + payloads, + GovernanceV3Ethereum.VOTING_PORTAL_ETH_POL, + GovV3Helpers.ipfsHashFile(vm, 'src/20241223_Multi_GHOBaseLaunch/GHOBaseLaunch.md') + ); + } +} diff --git a/src/20241223_Multi_GHOBaseLaunch/config.ts b/src/20241223_Multi_GHOBaseLaunch/config.ts new file mode 100644 index 000000000..1a6c4208a --- /dev/null +++ b/src/20241223_Multi_GHOBaseLaunch/config.ts @@ -0,0 +1,51 @@ +import {ConfigFile} from '../../generator/types'; +export const config: ConfigFile = { + rootOptions: { + pools: ['AaveV3Ethereum', 'AaveV3Arbitrum', 'AaveV3Base'], + title: 'Launch GHO on Base', + shortName: 'GHOBaseLaunch', + date: '20241223', + author: 'Aave Labs', + discussion: + 'https://governance.aave.com/t/arfc-launch-gho-on-base-set-aci-as-emissions-manager-for-rewards/19338', + snapshot: + 'https://snapshot.box/#/s:aave.eth/proposal/0x03dc21e0423c60082dc23317af6ebaf997610cbc2cbb0f5a385653bd99a524fe', + votingNetwork: 'POLYGON', + }, + poolOptions: { + AaveV3Ethereum: {configs: {OTHERS: {}}, cache: {blockNumber: 21722753}}, + AaveV3Arbitrum: {configs: {OTHERS: {}}, cache: {blockNumber: 300142041}}, + AaveV3Base: { + configs: { + ASSET_LISTING: [ + { + assetSymbol: 'GHO', + decimals: 18, + priceFeed: '0xfc421aD3C883Bf9E7C4f42dE845C4e4405799e73', + ltv: '0', + liqThreshold: '0', + liqBonus: '0', + debtCeiling: '0', + liqProtocolFee: '0', + enabledToBorrow: 'ENABLED', + flashloanable: 'ENABLED', + borrowableInIsolation: 'DISABLED', + withSiloedBorrowing: 'DISABLED', + reserveFactor: '10', + supplyCap: '2500000', + borrowCap: '2250000', + rateStrategyParams: { + optimalUtilizationRate: '90', + baseVariableBorrowRate: '0', + variableRateSlope1: '9.5', + variableRateSlope2: '50', + }, + asset: '0x6Bb7a212910682DCFdbd5BCBb3e28FB4E8da10Ee', + admin: '0xac140648435d03f784879cd789130F22Ef588Fcd', + }, + ], + }, + cache: {blockNumber: 25645172}, + }, + }, +}; diff --git a/src/20241223_Multi_GHOBaseLaunch/utils/CCIPUtils.sol b/src/20241223_Multi_GHOBaseLaunch/utils/CCIPUtils.sol new file mode 100644 index 000000000..c3b489477 --- /dev/null +++ b/src/20241223_Multi_GHOBaseLaunch/utils/CCIPUtils.sol @@ -0,0 +1,166 @@ +// 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 ARB_CHAIN_SELECTOR = 4949039107694359620; + uint64 internal constant BASE_CHAIN_SELECTOR = 15971525489660198786; + uint64 internal constant ETH_CHAIN_SELECTOR = 5009297550715157269; + + 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; + uint64 destChainSelector; + uint256 feeTokenAmount; + address originalSender; + address sourceToken; + address destinationToken; + } + + 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) { + IEVM2EVMOnRamp onRamp = IEVM2EVMOnRamp(params.router.getOnRamp(params.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: '' + }); + + for (uint256 i; i < params.message.tokenAmounts.length; ++i) { + messageEvent.sourceTokenData[i] = abi.encode( + SourceTokenData({ + sourcePoolAddress: abi.encode( + onRamp.getPoolBySourceToken( + params.destChainSelector, + params.message.tokenAmounts[i].token + ) + ), + destTokenAddress: abi.encode(params.destinationToken), + extraData: abi.encode(getTokenDecimals(params.sourceToken)), + destGasAmount: getDestGasAmount(onRamp, params.message.tokenAmounts[i].token) + }) + ); + } + + messageEvent.messageId = hash( + messageEvent, + generateMetadataHash(params.sourceChainSelector, params.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; + } + + function getTokenDecimals(address token) internal view returns (uint8) { + (bool success, bytes memory data) = token.staticcall(abi.encodeWithSignature('decimals()')); + require(success, 'CCIPUtils: failed to get token decimals'); + return abi.decode(data, (uint8)); + } +} diff --git a/src/interfaces/IGhoOracle.sol b/src/interfaces/IGhoOracle.sol new file mode 100644 index 000000000..65d895d9b --- /dev/null +++ b/src/interfaces/IGhoOracle.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +/** + * @title IGhoOracle + * @notice Price feed for GHO (USD denominated) + * @dev Price fixed at 1 USD, Chainlink format with 8 decimals + * @author Aave + */ +interface IGhoOracle { + /** + * @notice Returns the price of a unit of GHO (USD denominated) + * @dev GHO price is fixed at 1 USD + * @return The price of a unit of GHO (with 8 decimals) + */ + function latestAnswer() external view returns (int256); + + /** + * @notice Returns the number of decimals the price is formatted with + * @return The number of decimals + */ + function decimals() external view returns (uint8); +} diff --git a/src/interfaces/IGhoToken.sol b/src/interfaces/IGhoToken.sol index d37fb1643..db432b820 100644 --- a/src/interfaces/IGhoToken.sol +++ b/src/interfaces/IGhoToken.sol @@ -10,6 +10,8 @@ interface IGhoToken is IERC20 { string label; } + function initialize(address admin) external; + /** * @notice Mints the requested amount of tokens to the account address. * @dev Only facilitators with enough bucket capacity available can mint. @@ -76,6 +78,12 @@ interface IGhoToken is IERC20 { */ function getFacilitatorBucket(address facilitator) external view returns (uint256, uint256); + /** + * @notice Returns the identifier of the Facilitator Manager Role + * @return The bytes32 id hash of the FacilitatorManager role + */ + function FACILITATOR_MANAGER_ROLE() external pure returns (bytes32); + /** * @notice Returns the identifier of the Bucket Manager Role * @return The bytes32 id hash of the BucketManager role diff --git a/src/interfaces/ccip/IEVM2EVMOffRamp.sol b/src/interfaces/ccip/IEVM2EVMOffRamp.sol index 3c79cb4ef..34f565594 100644 --- a/src/interfaces/ccip/IEVM2EVMOffRamp.sol +++ b/src/interfaces/ccip/IEVM2EVMOffRamp.sol @@ -20,18 +20,18 @@ interface IEVM2EVMOffRamp_1_2 is ITypeAndVersion { } interface IEVM2EVMOffRamp_1_5 is ITypeAndVersion { - /// @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; + /// @notice Static offRamp config + /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. + //solhint-disable gas-struct-packing + struct StaticConfig { + address commitStore; // ────────╮ CommitStore address on the destination chain + uint64 chainSelector; // ───────╯ Destination chainSelector + uint64 sourceChainSelector; // ─╮ Source chainSelector + address onRamp; // ─────────────╯ OnRamp address on the source chain + address prevOffRamp; // Address of previous-version OffRamp + address rmnProxy; // RMN proxy address + address tokenAdminRegistry; // Token admin registry address + } /// @notice Dynamic offRamp config /// @dev since OffRampConfig is part of OffRampConfigChanged event, if changing it, we should update the ABI on Atlas @@ -43,7 +43,25 @@ interface IEVM2EVMOffRamp_1_5 is ITypeAndVersion { address priceRegistry; // Price registry address } + /// @notice Returns the static config. + /// @dev This function will always return the same struct as the contents is static and can never change. + /// RMN depends on this function, if changing, please notify the RMN maintainers. + function getStaticConfig() external view returns (StaticConfig memory); + /// @notice Returns the current dynamic config. /// @return The current config. function getDynamicConfig() external view returns (DynamicConfig memory); + + /// @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 index 306f1ebf0..f4521ff76 100644 --- a/src/interfaces/ccip/IEVM2EVMOnRamp.sol +++ b/src/interfaces/ccip/IEVM2EVMOnRamp.sol @@ -17,6 +17,17 @@ interface IEVM2EVMOnRamp is ITypeAndVersion { bool isEnabled; // ─────────────────╯ Whether this token has custom transfer fees } + struct StaticConfig { + address linkToken; // ────────╮ Link token address + uint64 chainSelector; // ─────╯ Source chainSelector + uint64 destChainSelector; // ─╮ Destination chainSelector + uint64 defaultTxGasLimit; // │ Default gas limit for a tx + uint96 maxNopFeesJuels; // ───╯ Max nop fee balance onramp can have + address prevOnRamp; // Address of previous-version OnRamp + address rmnProxy; // Address of RMN proxy + address tokenAdminRegistry; // Address of the token admin registry + } + struct DynamicConfig { address router; // ──────────────────────────╮ Router address uint16 maxNumberOfTokensPerMsg; // │ Maximum number of distinct ERC20 token transferred per message @@ -35,17 +46,6 @@ interface IEVM2EVMOnRamp is ITypeAndVersion { bool enforceOutOfOrder; // ──────────────────╯ Whether to enforce the allowOutOfOrderExecution extraArg value to be true. } - struct StaticConfig { - address linkToken; // ────────╮ Link token address - uint64 chainSelector; // ─────╯ Source chainSelector - uint64 destChainSelector; // ─╮ Destination chainSelector - uint64 defaultTxGasLimit; // │ Default gas limit for a tx - uint96 maxNopFeesJuels; // ───╯ Max nop fee balance onramp can have - address prevOnRamp; // Address of previous-version OnRamp - address rmnProxy; // Address of RMN proxy - address tokenAdminRegistry; // Address of the token admin registry - } - /// @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);