diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 4fa8eee9..60736851 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -8,6 +8,7 @@ jobs: ALCHEMY_KEY: '${{secrets.ALCHEMY_KEY}}' ETH_RPC_URL: 'https://eth-mainnet.g.alchemy.com/v2/${{secrets.ALCHEMY_KEY}}' RPC_MAINNET: 'https://eth-mainnet.g.alchemy.com/v2/${{secrets.ALCHEMY_KEY}}' + RPC_ARBITRUM: 'https://arb-mainnet.g.alchemy.com/v2/${{secrets.ALCHEMY_KEY}}' strategy: matrix: node-version: diff --git a/foundry.toml b/foundry.toml index 2b01aa8f..82d209b8 100644 --- a/foundry.toml +++ b/foundry.toml @@ -14,5 +14,6 @@ optimizer_runs = 200 [rpc_endpoints] mainnet = "${RPC_MAINNET}" +arbitrum = "${RPC_ARBITRUM}" # See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file diff --git a/lib/aave-address-book b/lib/aave-address-book index e65e63ce..4d208edf 160000 --- a/lib/aave-address-book +++ b/lib/aave-address-book @@ -1 +1 @@ -Subproject commit e65e63cec1dd61e7a21ed0db34795a708577a503 +Subproject commit 4d208edf7271e0fff0eceed55de535e32dc055d4 diff --git a/src/contracts/misc/dependencies/Ccip.sol b/src/contracts/misc/dependencies/Ccip.sol index 381ad6d6..6b2c238f 100644 --- a/src/contracts/misc/dependencies/Ccip.sol +++ b/src/contracts/misc/dependencies/Ccip.sol @@ -196,6 +196,14 @@ interface IUpgradeableLockReleaseTokenPool { RateLimiter.Config memory inboundConfig ) external; + function setRateLimitAdmin(address rateLimitAdmin) external; + + function setBridgeLimitAdmin(address bridgeLimitAdmin) external; + + function getRateLimitAdmin() external view returns (address); + + function getBridgeLimitAdmin() external view returns (address); + function getBridgeLimit() external view returns (uint256); function getCurrentOutboundRateLimiterState( diff --git a/src/test/TestGhoBase.t.sol b/src/test/TestGhoBase.t.sol index 8f6f7175..eb4ad824 100644 --- a/src/test/TestGhoBase.t.sol +++ b/src/test/TestGhoBase.t.sol @@ -43,7 +43,6 @@ import {IGhoVariableDebtTokenTransferHook} from 'aave-stk-v1-5/src/interfaces/IG import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; import {IPoolAddressesProvider} from '@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol'; import {IStakedAaveV3} from 'aave-stk-v1-5/src/interfaces/IStakedAaveV3.sol'; -import {IFixedRateStrategyFactory} from '../contracts/facilitators/aave/interestStrategy/interfaces/IFixedRateStrategyFactory.sol'; // non-GHO contracts import {AdminUpgradeabilityProxy} from '@aave/core-v3/contracts/dependencies/openzeppelin/upgradeability/AdminUpgradeabilityProxy.sol'; @@ -80,7 +79,7 @@ import {SampleSwapFreezer} from '../contracts/facilitators/gsm/misc/SampleSwapFr import {GsmRegistry} from '../contracts/facilitators/gsm/misc/GsmRegistry.sol'; import {IGhoGsmSteward} from '../contracts/misc/interfaces/IGhoGsmSteward.sol'; import {GhoGsmSteward} from '../contracts/misc/GhoGsmSteward.sol'; -import {FixedFeeStrategyFactory} from 'src/contracts/facilitators/gsm/feeStrategy/FixedFeeStrategyFactory.sol'; +import {FixedFeeStrategyFactory} from '../contracts/facilitators/gsm/feeStrategy/FixedFeeStrategyFactory.sol'; // CCIP contracts import {MockUpgradeableLockReleaseTokenPool} from './mocks/MockUpgradeableLockReleaseTokenPool.sol'; diff --git a/src/test/TestGhoCcipSteward.t.sol b/src/test/TestGhoCcipSteward.t.sol index 9cbf5bf5..2295ed4a 100644 --- a/src/test/TestGhoCcipSteward.t.sol +++ b/src/test/TestGhoCcipSteward.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; -import {RateLimiter} from 'src/contracts/misc/dependencies/Ccip.sol'; +import {RateLimiter} from '../contracts/misc/dependencies/Ccip.sol'; contract TestGhoCcipSteward is TestGhoBase { RateLimiter.Config rateLimitConfig = diff --git a/src/test/TestGhoStewards.t.sol b/src/test/TestGhoStewards.t.sol deleted file mode 100644 index 98adbbf5..00000000 --- a/src/test/TestGhoStewards.t.sol +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import 'forge-std/Test.sol'; -import {IAccessControl} from '@openzeppelin/contracts/access/IAccessControl.sol'; -import {IACLManager} from '@aave/core-v3/contracts/interfaces/IACLManager.sol'; -import {IPoolAddressesProvider} from '@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol'; -import {FixedFeeStrategyFactory} from 'src/contracts/facilitators/gsm/feeStrategy/FixedFeeStrategyFactory.sol'; -import {IGhoAaveSteward} from 'src/contracts/misc/interfaces/IGhoAaveSteward.sol'; -import {GhoAaveSteward} from 'src/contracts/misc/GhoAaveSteward.sol'; -import {GhoBucketSteward} from 'src/contracts/misc/GhoBucketSteward.sol'; -import {GhoCcipSteward} from 'src/contracts/misc/GhoCcipSteward.sol'; -import {GhoGsmSteward} from 'src/contracts/misc/GhoGsmSteward.sol'; - -contract TestGhoStewards is Test { - address public OWNER = makeAddr('OWNER'); - address public RISK_COUNCIL = makeAddr('RISK_COUNCIL'); - address public POOL_ADDRESSES_PROVIDER = 0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e; - address public POOL_DATA_PROVIDER = 0x5c5228aC8BC1528482514aF3e27E692495148717; - address public GHO_TOKEN = 0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f; - address public GHO_TOKEN_POOL = 0x5756880B6a1EAba0175227bf02a7E87c1e02B28C; - address public AAVE_V3_ETHEREUM_POOL = 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2; - address public ACL_ADMIN = 0x5300A1a15135EA4dc7aD5a167152C01EFc9b192A; - address public ACL_MANAGER; - - GhoAaveSteward public ghoAaveSteward; - GhoBucketSteward public ghoBucketSteward; - GhoCcipSteward public ghoCcipSteward; - GhoGsmSteward public ghoGsmSteward; - - function setUp() public { - vm.createSelectFork(vm.rpcUrl('mainnet'), 20580302); - ACL_MANAGER = IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER).getACLManager(); - - IGhoAaveSteward.BorrowRateConfig memory defaultBorrowRateConfig = IGhoAaveSteward - .BorrowRateConfig({ - optimalUsageRatioMaxChange: 10_00, - baseVariableBorrowRateMaxChange: 5_00, - variableRateSlope1MaxChange: 10_00, - variableRateSlope2MaxChange: 10_00 - }); - - // Deploy Gho Aave Steward - ghoAaveSteward = new GhoAaveSteward( - OWNER, - POOL_ADDRESSES_PROVIDER, - POOL_DATA_PROVIDER, - GHO_TOKEN, - RISK_COUNCIL, - defaultBorrowRateConfig - ); - // Grant roles - vm.startPrank(ACL_ADMIN); - IAccessControl(ACL_MANAGER).grantRole( - IACLManager(ACL_MANAGER).RISK_ADMIN_ROLE(), - address(ghoAaveSteward) - ); - vm.stopPrank(); - - // Deploy Gho Bucket Steward - ghoBucketSteward = new GhoBucketSteward(OWNER, GHO_TOKEN, RISK_COUNCIL); - - // Deploy Gho Ccip Steward - ghoCcipSteward = new GhoCcipSteward(GHO_TOKEN, GHO_TOKEN_POOL, RISK_COUNCIL, true); - - // Deploy Gho Gsm Steward - FixedFeeStrategyFactory strategyFactory = new FixedFeeStrategyFactory(); - ghoGsmSteward = new GhoGsmSteward(address(strategyFactory), RISK_COUNCIL); - - // TODO: Find which contracts are already deployed that we need - // TODO: Deploy stewards, using corresponding contracts - // TODO: Ensure we grant all appropriate roles - } - - function testSetup() public { - assertEq( - IAccessControl(ACL_MANAGER).hasRole( - IACLManager(ACL_MANAGER).RISK_ADMIN_ROLE(), - address(ghoAaveSteward) - ), - true - ); - } -} diff --git a/src/test/TestGhoStewardsForkEthereum.t.sol b/src/test/TestGhoStewardsForkEthereum.t.sol new file mode 100644 index 00000000..98579ad1 --- /dev/null +++ b/src/test/TestGhoStewardsForkEthereum.t.sol @@ -0,0 +1,311 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {IAccessControl} from '@openzeppelin/contracts/access/IAccessControl.sol'; +import {IACLManager} from '@aave/core-v3/contracts/interfaces/IACLManager.sol'; +import {AaveV3Ethereum, AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; +import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; +import {IPoolAddressesProvider, IPoolDataProvider, IPool} from 'aave-address-book/AaveV3.sol'; +import {DataTypes} from 'aave-v3-core/contracts/protocol/libraries/types/DataTypes.sol'; +import {ReserveConfiguration} from 'aave-v3-core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; +import {FixedFeeStrategyFactory} from '../contracts/facilitators/gsm/feeStrategy/FixedFeeStrategyFactory.sol'; +import {IGsmFeeStrategy} from '../contracts/facilitators/gsm/feeStrategy/interfaces/IGsmFeeStrategy.sol'; +import {Gsm} from '../contracts/facilitators/gsm/Gsm.sol'; +import {GhoToken} from '../contracts/gho/GhoToken.sol'; +import {IGhoAaveSteward} from '../contracts/misc/interfaces/IGhoAaveSteward.sol'; +import {GhoAaveSteward} from '../contracts/misc/GhoAaveSteward.sol'; +import {GhoBucketSteward} from '../contracts/misc/GhoBucketSteward.sol'; +import {GhoCcipSteward} from '../contracts/misc/GhoCcipSteward.sol'; +import {GhoGsmSteward} from '../contracts/misc/GhoGsmSteward.sol'; +import {RateLimiter, IUpgradeableLockReleaseTokenPool} from '../contracts/misc/dependencies/Ccip.sol'; +import {IDefaultInterestRateStrategyV2} from '../contracts/misc/dependencies/AaveV3-1.sol'; +import {MockPool} from './mocks/MockPool.sol'; +import {MockUpgradeableLockReleaseTokenPool} from './mocks/MockUpgradeableLockReleaseTokenPool.sol'; + +contract TestGhoStewardsForkEthereum is Test { + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + + address public OWNER = makeAddr('OWNER'); + address public RISK_COUNCIL = makeAddr('RISK_COUNCIL'); + IPoolDataProvider public POOL_DATA_PROVIDER = AaveV3Ethereum.AAVE_PROTOCOL_DATA_PROVIDER; + IPoolAddressesProvider public POOL_ADDRESSES_PROVIDER = AaveV3Ethereum.POOL_ADDRESSES_PROVIDER; + address public GHO_TOKEN = AaveV3EthereumAssets.GHO_UNDERLYING; + address public GHO_ATOKEN = AaveV3EthereumAssets.GHO_A_TOKEN; + IPool public POOL = AaveV3Ethereum.POOL; + address public ACL_ADMIN = AaveV3Ethereum.ACL_ADMIN; + address public GHO_TOKEN_POOL = MiscEthereum.GHO_CCIP_TOKEN_POOL; + address public GHO_GSM_USDC = MiscEthereum.GSM_USDC; + address public GHO_GSM_USDT = MiscEthereum.GSM_USDT; + address public ACL_MANAGER; + + GhoAaveSteward public GHO_AAVE_STEWARD; + GhoBucketSteward public GHO_BUCKET_STEWARD; + GhoCcipSteward public GHO_CCIP_STEWARD; + GhoGsmSteward public GHO_GSM_STEWARD; + + uint64 public remoteChainSelector = 4949039107694359620; + + event ChainConfigured( + uint64 remoteChainSelector, + RateLimiter.Config outboundRateLimiterConfig, + RateLimiter.Config inboundRateLimiterConfig + ); + + function setUp() public { + vm.createSelectFork(vm.rpcUrl('mainnet'), 20580302); + vm.startPrank(ACL_ADMIN); + ACL_MANAGER = POOL_ADDRESSES_PROVIDER.getACLManager(); + + IGhoAaveSteward.BorrowRateConfig memory defaultBorrowRateConfig = IGhoAaveSteward + .BorrowRateConfig({ + optimalUsageRatioMaxChange: 10_00, + baseVariableBorrowRateMaxChange: 5_00, + variableRateSlope1MaxChange: 10_00, + variableRateSlope2MaxChange: 10_00 + }); + + GHO_AAVE_STEWARD = new GhoAaveSteward( + OWNER, + address(POOL_ADDRESSES_PROVIDER), + address(POOL_DATA_PROVIDER), + GHO_TOKEN, + RISK_COUNCIL, + defaultBorrowRateConfig + ); + IAccessControl(ACL_MANAGER).grantRole( + IACLManager(ACL_MANAGER).RISK_ADMIN_ROLE(), + address(GHO_AAVE_STEWARD) + ); + + GHO_BUCKET_STEWARD = new GhoBucketSteward(OWNER, GHO_TOKEN, RISK_COUNCIL); + GhoToken(GHO_TOKEN).grantRole( + GhoToken(GHO_TOKEN).BUCKET_MANAGER_ROLE(), + address(GHO_BUCKET_STEWARD) + ); + + GHO_CCIP_STEWARD = new GhoCcipSteward(GHO_TOKEN, GHO_TOKEN_POOL, RISK_COUNCIL, true); + IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL).setRateLimitAdmin(address(GHO_CCIP_STEWARD)); + IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL).setBridgeLimitAdmin(address(GHO_CCIP_STEWARD)); + + FixedFeeStrategyFactory strategyFactory = new FixedFeeStrategyFactory(); + GHO_GSM_STEWARD = new GhoGsmSteward(address(strategyFactory), RISK_COUNCIL); + Gsm(GHO_GSM_USDC).grantRole(Gsm(GHO_GSM_USDC).CONFIGURATOR_ROLE(), address(GHO_GSM_STEWARD)); + Gsm(GHO_GSM_USDT).grantRole(Gsm(GHO_GSM_USDT).CONFIGURATOR_ROLE(), address(GHO_GSM_STEWARD)); + + address[] memory controlledFacilitators = new address[](3); + controlledFacilitators[0] = address(GHO_ATOKEN); + controlledFacilitators[1] = address(GHO_GSM_USDC); + controlledFacilitators[2] = address(GHO_GSM_USDT); + changePrank(OWNER); + GHO_BUCKET_STEWARD.setControlledFacilitator(controlledFacilitators, true); + + vm.stopPrank(); + } + + function testStewardsPermissions() public { + assertEq( + IAccessControl(ACL_MANAGER).hasRole( + IACLManager(ACL_MANAGER).RISK_ADMIN_ROLE(), + address(GHO_AAVE_STEWARD) + ), + true + ); + + assertEq( + IAccessControl(GHO_TOKEN).hasRole( + GhoToken(GHO_TOKEN).BUCKET_MANAGER_ROLE(), + address(GHO_BUCKET_STEWARD) + ), + true + ); + + assertEq( + IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL).getRateLimitAdmin(), + address(GHO_CCIP_STEWARD) + ); + assertEq( + IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL).getBridgeLimitAdmin(), + address(GHO_CCIP_STEWARD) + ); + + assertEq( + Gsm(GHO_GSM_USDC).hasRole(Gsm(GHO_GSM_USDC).CONFIGURATOR_ROLE(), address(GHO_GSM_STEWARD)), + true + ); + assertEq( + Gsm(GHO_GSM_USDT).hasRole(Gsm(GHO_GSM_USDT).CONFIGURATOR_ROLE(), address(GHO_GSM_STEWARD)), + true + ); + } + + function testGhoAaveStewardUpdateGhoBorrowCap() public { + uint256 currentBorrowCap = _getGhoBorrowCap(); + uint256 newBorrowCap = currentBorrowCap + 1; + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowCap(newBorrowCap); + assertEq(_getGhoBorrowCap(), newBorrowCap); + } + + function testGhoAaveStewardUpdateGhoSupplyCap() public { + uint256 currentSupplyCap = _getGhoSupplyCap(); + assertEq(currentSupplyCap, 0); + uint256 newSupplyCap = currentSupplyCap + 1; + // Can't update supply cap even by 1 since it's 0, and 100% of 0 is 0 + vm.expectRevert('INVALID_SUPPLY_CAP_UPDATE'); + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoSupplyCap(newSupplyCap); + } + + function testGhoAaveStewardUpdateGhoBorrowRate() public { + IDefaultInterestRateStrategyV2.InterestRateData memory currentRates = _getGhoBorrowRates(); + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + currentRates.optimalUsageRatio - 1, + currentRates.baseVariableBorrowRate + 1, + currentRates.variableRateSlope1 + 1, + currentRates.variableRateSlope2 + 1 + ); + assertEq(_getOptimalUsageRatio(), currentRates.optimalUsageRatio - 1); + assertEq(_getBaseVariableBorrowRate(), currentRates.baseVariableBorrowRate + 1); + assertEq(_getVariableRateSlope1(), currentRates.variableRateSlope1 + 1); + assertEq(_getVariableRateSlope2(), currentRates.variableRateSlope2 + 1); + } + + function testGhoBucketStewardUpdateFacilitatorBucketCapacity() public { + (uint256 currentBucketCapacity, ) = GhoToken(GHO_TOKEN).getFacilitatorBucket( + address(GHO_ATOKEN) + ); + vm.prank(RISK_COUNCIL); + uint128 newBucketCapacity = uint128(currentBucketCapacity) + 1; + GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity(address(GHO_ATOKEN), newBucketCapacity); + (uint256 capacity, ) = GhoToken(GHO_TOKEN).getFacilitatorBucket(address(GHO_ATOKEN)); + assertEq(newBucketCapacity, capacity); + } + + function testGhoBucketStewardSetControlledFacilitator() public { + address[] memory newGsmList = new address[](1); + address gho_gsm_4626 = makeAddr('gho_gsm_4626'); + newGsmList[0] = gho_gsm_4626; + vm.prank(OWNER); + GHO_BUCKET_STEWARD.setControlledFacilitator(newGsmList, true); + assertTrue(_isControlledFacilitator(gho_gsm_4626)); + vm.prank(OWNER); + GHO_BUCKET_STEWARD.setControlledFacilitator(newGsmList, false); + assertFalse(_isControlledFacilitator(gho_gsm_4626)); + } + + function testGhoCcipStewardUpdateBridgeLimit() public { + uint256 oldBridgeLimit = IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL).getBridgeLimit(); + uint256 newBridgeLimit = oldBridgeLimit + 1; + vm.prank(RISK_COUNCIL); + GHO_CCIP_STEWARD.updateBridgeLimit(newBridgeLimit); + uint256 currentBridgeLimit = IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL).getBridgeLimit(); + assertEq(currentBridgeLimit, newBridgeLimit); + } + + function testGhoCcipStewardUpdateRateLimit() public { + RateLimiter.TokenBucket memory outboundConfig = MockUpgradeableLockReleaseTokenPool( + GHO_TOKEN_POOL + ).getCurrentOutboundRateLimiterState(remoteChainSelector); + RateLimiter.TokenBucket memory inboundConfig = MockUpgradeableLockReleaseTokenPool( + GHO_TOKEN_POOL + ).getCurrentInboundRateLimiterState(remoteChainSelector); + + RateLimiter.Config memory newOutboundConfig = RateLimiter.Config({ + isEnabled: outboundConfig.isEnabled, + capacity: outboundConfig.capacity + 1, + rate: outboundConfig.rate + }); + + RateLimiter.Config memory newInboundConfig = RateLimiter.Config({ + isEnabled: outboundConfig.isEnabled, + capacity: inboundConfig.capacity, + rate: inboundConfig.rate + }); + + // Currently rate limit set to 0, so can't even change by 1 because 100% of 0 is 0 + vm.expectRevert('INVALID_RATE_LIMIT_UPDATE'); + vm.prank(RISK_COUNCIL); + GHO_CCIP_STEWARD.updateRateLimit( + remoteChainSelector, + newOutboundConfig.isEnabled, + newOutboundConfig.capacity, + newOutboundConfig.rate, + newInboundConfig.isEnabled, + newInboundConfig.capacity, + newInboundConfig.rate + ); + } + + function testGhoGsmStewardUpdateExposureCap() public { + uint128 oldExposureCap = Gsm(GHO_GSM_USDC).getExposureCap(); + uint128 newExposureCap = oldExposureCap + 1; + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmExposureCap(GHO_GSM_USDC, newExposureCap); + uint128 currentExposureCap = Gsm(GHO_GSM_USDC).getExposureCap(); + assertEq(currentExposureCap, newExposureCap); + } + + function testGhoGsmStewardUpdateGsmBuySellFees() public { + address feeStrategy = Gsm(GHO_GSM_USDC).getFeeStrategy(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmBuySellFees(GHO_GSM_USDC, buyFee + 1, sellFee); + address newStrategy = Gsm(GHO_GSM_USDC).getFeeStrategy(); + uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); + assertEq(newBuyFee, buyFee + 1); + } + + function _getGhoBorrowCap() internal view returns (uint256) { + DataTypes.ReserveConfigurationMap memory configuration = POOL.getConfiguration(GHO_TOKEN); + return configuration.getBorrowCap(); + } + + function _getGhoSupplyCap() internal view returns (uint256) { + DataTypes.ReserveConfigurationMap memory configuration = POOL.getConfiguration( + address(GHO_TOKEN) + ); + return configuration.getSupplyCap(); + } + + function _getOptimalUsageRatio() internal view returns (uint16) { + IDefaultInterestRateStrategyV2.InterestRateData memory currentRates = _getGhoBorrowRates(); + return currentRates.optimalUsageRatio; + } + + function _getBaseVariableBorrowRate() internal view returns (uint32) { + IDefaultInterestRateStrategyV2.InterestRateData memory currentRates = _getGhoBorrowRates(); + return currentRates.baseVariableBorrowRate; + } + + function _getVariableRateSlope1() internal view returns (uint32) { + IDefaultInterestRateStrategyV2.InterestRateData memory currentRates = _getGhoBorrowRates(); + return currentRates.variableRateSlope1; + } + + function _getVariableRateSlope2() internal view returns (uint32) { + IDefaultInterestRateStrategyV2.InterestRateData memory currentRates = _getGhoBorrowRates(); + return currentRates.variableRateSlope2; + } + + function _getGhoBorrowRates() + internal + view + returns (IDefaultInterestRateStrategyV2.InterestRateData memory) + { + address rateStrategyAddress = POOL_DATA_PROVIDER.getInterestRateStrategyAddress(GHO_TOKEN); + return IDefaultInterestRateStrategyV2(rateStrategyAddress).getInterestRateDataBps(GHO_TOKEN); + } + + function _isControlledFacilitator(address target) internal view returns (bool) { + address[] memory controlledFacilitators = GHO_BUCKET_STEWARD.getControlledFacilitators(); + for (uint256 i = 0; i < controlledFacilitators.length; i++) { + if (controlledFacilitators[i] == target) { + return true; + } + } + return false; + } +} diff --git a/src/test/TestGhoStewardsForkRemote.t.sol b/src/test/TestGhoStewardsForkRemote.t.sol new file mode 100644 index 00000000..70de1600 --- /dev/null +++ b/src/test/TestGhoStewardsForkRemote.t.sol @@ -0,0 +1,335 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {IAccessControl} from '@openzeppelin/contracts/access/IAccessControl.sol'; +import {IACLManager} from '@aave/core-v3/contracts/interfaces/IACLManager.sol'; +import {TransparentUpgradeableProxy} from 'solidity-utils/contracts/transparent-proxy/TransparentUpgradeableProxy.sol'; +import {AaveV3Arbitrum} from 'aave-address-book/AaveV3Arbitrum.sol'; +import {MiscArbitrum} from 'aave-address-book/MiscArbitrum.sol'; +import {IPoolAddressesProvider, IPoolDataProvider} from 'aave-address-book/AaveV3.sol'; +import {GhoToken} from '../contracts/gho/GhoToken.sol'; +import {IGhoAaveSteward} from '../contracts/misc/interfaces/IGhoAaveSteward.sol'; +import {GhoAaveSteward} from '../contracts/misc/GhoAaveSteward.sol'; +import {GhoBucketSteward} from '../contracts/misc/GhoBucketSteward.sol'; +import {GhoCcipSteward} from '../contracts/misc/GhoCcipSteward.sol'; +import {RateLimiter, IUpgradeableLockReleaseTokenPool} from '../contracts/misc/dependencies/Ccip.sol'; +import {IDefaultInterestRateStrategyV2} from '../contracts/misc/dependencies/AaveV3-1.sol'; +import {MockUpgradeableBurnMintTokenPool} from './mocks/MockUpgradeableBurnMintTokenPool.sol'; + +contract TestGhoStewardsForkRemote is Test { + address public OWNER = makeAddr('OWNER'); + address public RISK_COUNCIL = makeAddr('RISK_COUNCIL'); + IPoolDataProvider public POOL_DATA_PROVIDER = AaveV3Arbitrum.AAVE_PROTOCOL_DATA_PROVIDER; + IPoolAddressesProvider public POOL_ADDRESSES_PROVIDER = AaveV3Arbitrum.POOL_ADDRESSES_PROVIDER; + address public GHO_TOKEN = 0x7dfF72693f6A4149b17e7C6314655f6A9F7c8B33; + address public ARM_PROXY = 0xC311a21e6fEf769344EB1515588B9d535662a145; + address public ACL_ADMIN = AaveV3Arbitrum.ACL_ADMIN; + address public GHO_TOKEN_POOL = MiscArbitrum.GHO_CCIP_TOKEN_POOL; + address public PROXY_ADMIN = MiscArbitrum.PROXY_ADMIN; + address public ACL_MANAGER; + + GhoAaveSteward public GHO_AAVE_STEWARD; + GhoBucketSteward public GHO_BUCKET_STEWARD; + GhoCcipSteward public GHO_CCIP_STEWARD; + + uint64 public remoteChainSelector = 5009297550715157269; + + event ChainConfigured( + uint64 remoteChainSelector, + RateLimiter.Config outboundRateLimiterConfig, + RateLimiter.Config inboundRateLimiterConfig + ); + + function setUp() public { + vm.createSelectFork(vm.rpcUrl('arbitrum'), 247477524); + vm.startPrank(ACL_ADMIN); + ACL_MANAGER = POOL_ADDRESSES_PROVIDER.getACLManager(); + + IGhoAaveSteward.BorrowRateConfig memory defaultBorrowRateConfig = IGhoAaveSteward + .BorrowRateConfig({ + optimalUsageRatioMaxChange: 10_00, + baseVariableBorrowRateMaxChange: 5_00, + variableRateSlope1MaxChange: 10_00, + variableRateSlope2MaxChange: 60_00 + }); + + GHO_AAVE_STEWARD = new GhoAaveSteward( + OWNER, + address(POOL_ADDRESSES_PROVIDER), + address(POOL_DATA_PROVIDER), + GHO_TOKEN, + RISK_COUNCIL, + defaultBorrowRateConfig + ); + IAccessControl(ACL_MANAGER).grantRole( + IACLManager(ACL_MANAGER).RISK_ADMIN_ROLE(), + address(GHO_AAVE_STEWARD) + ); + + GHO_BUCKET_STEWARD = new GhoBucketSteward(OWNER, GHO_TOKEN, RISK_COUNCIL); + GhoToken(GHO_TOKEN).grantRole( + GhoToken(GHO_TOKEN).BUCKET_MANAGER_ROLE(), + address(GHO_BUCKET_STEWARD) + ); + + GHO_CCIP_STEWARD = new GhoCcipSteward(GHO_TOKEN, GHO_TOKEN_POOL, RISK_COUNCIL, true); + + address[] memory controlledFacilitators = new address[](1); + controlledFacilitators[0] = address(GHO_TOKEN_POOL); + changePrank(OWNER); + GHO_BUCKET_STEWARD.setControlledFacilitator(controlledFacilitators, true); + + vm.stopPrank(); + } + + function testStewardsPermissions() public { + assertEq( + IAccessControl(ACL_MANAGER).hasRole( + IACLManager(ACL_MANAGER).RISK_ADMIN_ROLE(), + address(GHO_AAVE_STEWARD) + ), + true + ); + + assertEq( + IAccessControl(GHO_TOKEN).hasRole( + GhoToken(GHO_TOKEN).BUCKET_MANAGER_ROLE(), + address(GHO_BUCKET_STEWARD) + ), + true + ); + } + + function testGhoAaveStewardUpdateGhoBorrowRate() public { + IDefaultInterestRateStrategyV2.InterestRateData memory currentRates = _getGhoBorrowRates(); + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + currentRates.optimalUsageRatio - 1, + currentRates.baseVariableBorrowRate + 1, + currentRates.variableRateSlope1 - 700, + currentRates.variableRateSlope2 - 6000 + ); + assertEq(_getOptimalUsageRatio(), currentRates.optimalUsageRatio - 1); + assertEq(_getBaseVariableBorrowRate(), currentRates.baseVariableBorrowRate + 1); + assertEq(_getVariableRateSlope1(), currentRates.variableRateSlope1 - 700); + assertEq(_getVariableRateSlope2(), currentRates.variableRateSlope2 - 6000); + } + + function testGhoBucketStewardUpdateFacilitatorBucketCapacity() public { + (uint256 currentBucketCapacity, ) = GhoToken(GHO_TOKEN).getFacilitatorBucket( + address(GHO_TOKEN_POOL) + ); + vm.prank(RISK_COUNCIL); + uint128 newBucketCapacity = uint128(currentBucketCapacity) + 1; + GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity(address(GHO_TOKEN_POOL), newBucketCapacity); + (uint256 bucketCapacity, ) = GhoToken(GHO_TOKEN).getFacilitatorBucket(address(GHO_TOKEN_POOL)); + assertEq(bucketCapacity, newBucketCapacity); + } + + function testGhoBucketStewardSetControlledFacilitator() public { + address[] memory newGsmList = new address[](1); + address gho_gsm_4626 = makeAddr('gho_gsm_4626'); + newGsmList[0] = gho_gsm_4626; + vm.prank(OWNER); + GHO_BUCKET_STEWARD.setControlledFacilitator(newGsmList, true); + assertTrue(_isControlledFacilitator(gho_gsm_4626)); + vm.prank(OWNER); + GHO_BUCKET_STEWARD.setControlledFacilitator(newGsmList, false); + assertFalse(_isControlledFacilitator(gho_gsm_4626)); + } + + function testGhoCcipStewardUpdateRateLimit() public { + RateLimiter.TokenBucket memory outboundConfig = IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL) + .getCurrentOutboundRateLimiterState(remoteChainSelector); + RateLimiter.TokenBucket memory inboundConfig = IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL) + .getCurrentInboundRateLimiterState(remoteChainSelector); + + RateLimiter.Config memory newOutboundConfig = RateLimiter.Config({ + isEnabled: outboundConfig.isEnabled, + capacity: outboundConfig.capacity + 1, + rate: outboundConfig.rate + }); + + RateLimiter.Config memory newInboundConfig = RateLimiter.Config({ + isEnabled: outboundConfig.isEnabled, + capacity: inboundConfig.capacity, + rate: inboundConfig.rate + }); + + // Currently rate limit set to 0, so can't even change by 1 because 100% of 0 is 0 + vm.expectRevert('INVALID_RATE_LIMIT_UPDATE'); + vm.prank(RISK_COUNCIL); + GHO_CCIP_STEWARD.updateRateLimit( + remoteChainSelector, + newOutboundConfig.isEnabled, + newOutboundConfig.capacity, + newOutboundConfig.rate, + newInboundConfig.isEnabled, + newInboundConfig.capacity, + newInboundConfig.rate + ); + } + + function testGhoCcipStewardRevertUpdateRateLimitUnauthorizedBeforeUpgrade() public { + RateLimiter.TokenBucket memory mockConfig = RateLimiter.TokenBucket({ + rate: 50, + capacity: 50, + tokens: 1, + lastUpdated: 1, + isEnabled: true + }); + // Mocking response due to rate limit currently being 0 + vm.mockCall( + GHO_TOKEN_POOL, + abi.encodeWithSelector( + IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL) + .getCurrentOutboundRateLimiterState + .selector, + remoteChainSelector + ), + abi.encode(mockConfig) + ); + + RateLimiter.TokenBucket memory outboundConfig = IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL) + .getCurrentOutboundRateLimiterState(remoteChainSelector); + RateLimiter.TokenBucket memory inboundConfig = IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL) + .getCurrentInboundRateLimiterState(remoteChainSelector); + + RateLimiter.Config memory newOutboundConfig = RateLimiter.Config({ + isEnabled: outboundConfig.isEnabled, + capacity: outboundConfig.capacity, + rate: outboundConfig.rate + 1 + }); + + RateLimiter.Config memory newInboundConfig = RateLimiter.Config({ + isEnabled: outboundConfig.isEnabled, + capacity: inboundConfig.capacity, + rate: inboundConfig.rate + }); + + vm.expectRevert('Only callable by owner'); + vm.prank(RISK_COUNCIL); + GHO_CCIP_STEWARD.updateRateLimit( + remoteChainSelector, + newOutboundConfig.isEnabled, + newOutboundConfig.capacity, + newOutboundConfig.rate, + newInboundConfig.isEnabled, + newInboundConfig.capacity, + newInboundConfig.rate + ); + } + + function testGhoCcipStewardUpdateRateLimitAfterPoolUpgrade() public { + MockUpgradeableBurnMintTokenPool tokenPoolImpl = new MockUpgradeableBurnMintTokenPool( + address(GHO_TOKEN), + address(ARM_PROXY), + false, + false + ); + + vm.prank(PROXY_ADMIN); + TransparentUpgradeableProxy(payable(address(GHO_TOKEN_POOL))).upgradeTo(address(tokenPoolImpl)); + + vm.prank(ACL_ADMIN); + IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL).setRateLimitAdmin(address(GHO_CCIP_STEWARD)); + + RateLimiter.TokenBucket memory mockConfig = RateLimiter.TokenBucket({ + rate: 50, + capacity: 50, + tokens: 1, + lastUpdated: 1, + isEnabled: true + }); + + // Mocking response due to rate limit currently being 0 + vm.mockCall( + GHO_TOKEN_POOL, + abi.encodeWithSelector( + IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL) + .getCurrentOutboundRateLimiterState + .selector, + remoteChainSelector + ), + abi.encode(mockConfig) + ); + vm.mockCall( + GHO_TOKEN_POOL, + abi.encodeWithSelector( + IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL).getCurrentInboundRateLimiterState.selector, + remoteChainSelector + ), + abi.encode(mockConfig) + ); + + RateLimiter.TokenBucket memory outboundConfig = IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL) + .getCurrentOutboundRateLimiterState(remoteChainSelector); + RateLimiter.TokenBucket memory inboundConfig = IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL) + .getCurrentInboundRateLimiterState(remoteChainSelector); + + RateLimiter.Config memory newOutboundConfig = RateLimiter.Config({ + isEnabled: outboundConfig.isEnabled, + capacity: outboundConfig.capacity + 1, + rate: outboundConfig.rate + }); + + RateLimiter.Config memory newInboundConfig = RateLimiter.Config({ + isEnabled: outboundConfig.isEnabled, + capacity: inboundConfig.capacity + 1, + rate: inboundConfig.rate + }); + + vm.expectEmit(false, false, false, true); + emit ChainConfigured(remoteChainSelector, newOutboundConfig, newInboundConfig); + vm.prank(RISK_COUNCIL); + GHO_CCIP_STEWARD.updateRateLimit( + remoteChainSelector, + newOutboundConfig.isEnabled, + newOutboundConfig.capacity, + newOutboundConfig.rate, + newInboundConfig.isEnabled, + newInboundConfig.capacity, + newInboundConfig.rate + ); + } + + function _getOptimalUsageRatio() internal view returns (uint16) { + IDefaultInterestRateStrategyV2.InterestRateData memory currentRates = _getGhoBorrowRates(); + return currentRates.optimalUsageRatio; + } + + function _getBaseVariableBorrowRate() internal view returns (uint32) { + IDefaultInterestRateStrategyV2.InterestRateData memory currentRates = _getGhoBorrowRates(); + return currentRates.baseVariableBorrowRate; + } + + function _getVariableRateSlope1() internal view returns (uint32) { + IDefaultInterestRateStrategyV2.InterestRateData memory currentRates = _getGhoBorrowRates(); + return currentRates.variableRateSlope1; + } + + function _getVariableRateSlope2() internal view returns (uint32) { + IDefaultInterestRateStrategyV2.InterestRateData memory currentRates = _getGhoBorrowRates(); + return currentRates.variableRateSlope2; + } + + function _getGhoBorrowRates() + internal + view + returns (IDefaultInterestRateStrategyV2.InterestRateData memory) + { + address rateStrategyAddress = POOL_DATA_PROVIDER.getInterestRateStrategyAddress(GHO_TOKEN); + return IDefaultInterestRateStrategyV2(rateStrategyAddress).getInterestRateDataBps(GHO_TOKEN); + } + + function _isControlledFacilitator(address target) internal view returns (bool) { + address[] memory controlledFacilitators = GHO_BUCKET_STEWARD.getControlledFacilitators(); + for (uint256 i = 0; i < controlledFacilitators.length; i++) { + if (controlledFacilitators[i] == target) { + return true; + } + } + return false; + } +} diff --git a/src/test/mocks/MockUpgradeableBurnMintTokenPool.sol b/src/test/mocks/MockUpgradeableBurnMintTokenPool.sol new file mode 100644 index 00000000..45b78af8 --- /dev/null +++ b/src/test/mocks/MockUpgradeableBurnMintTokenPool.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; +import {Initializable} from 'solidity-utils/contracts/transparent-proxy/Initializable.sol'; +import {EnumerableSet} from '@openzeppelin/contracts/utils/structs/EnumerableSet.sol'; +import {RateLimiter} from 'src/contracts/misc/dependencies/Ccip.sol'; +import {IRouter} from 'src/contracts/misc/dependencies/Ccip.sol'; +import {IARM} from 'src/contracts/misc/dependencies/AaveV3-1.sol'; + +contract MockUpgradeableBurnMintTokenPool is Initializable { + using SafeERC20 for IERC20; + using RateLimiter for RateLimiter.TokenBucket; + + error Unauthorized(address caller); + error ZeroAddressNotAllowed(); + + event ChainConfigured( + uint64 remoteChainSelector, + RateLimiter.Config outboundRateLimiterConfig, + RateLimiter.Config inboundRateLimiterConfig + ); + + struct ChainUpdate { + uint64 remoteChainSelector; + bool allowed; + RateLimiter.Config outboundRateLimiterConfig; + RateLimiter.Config inboundRateLimiterConfig; + } + + address internal _owner; + bool internal immutable i_acceptLiquidity; + address internal s_rateLimitAdmin; + uint256 private s_bridgeLimit; + address internal s_bridgeLimitAdmin; + IERC20 internal immutable i_token; + address internal immutable i_armProxy; + bool internal immutable i_allowlistEnabled; + EnumerableSet.AddressSet internal s_allowList; + IRouter internal s_router; + EnumerableSet.UintSet internal s_remoteChainSelectors; + mapping(uint64 => RateLimiter.TokenBucket) internal s_outboundRateLimits; + mapping(uint64 => RateLimiter.TokenBucket) internal s_inboundRateLimits; + + constructor(address token, address armProxy, bool allowlistEnabled, bool acceptLiquidity) { + i_acceptLiquidity = acceptLiquidity; + if (address(token) == address(0)) revert ZeroAddressNotAllowed(); + i_token = IERC20(token); + i_armProxy = armProxy; + i_allowlistEnabled = allowlistEnabled; + } + + function initialize( + address owner, + address[] memory allowlist, + address router, + uint256 bridgeLimit + ) public virtual initializer { + if (owner == address(0)) revert ZeroAddressNotAllowed(); + if (router == address(0)) revert ZeroAddressNotAllowed(); + _transferOwnership(owner); + + s_router = IRouter(router); + s_bridgeLimit = bridgeLimit; + } + + function owner() public view returns (address) { + return _owner; + } + + function acceptOwnership() external {} + + function setRateLimitAdmin(address rateLimitAdmin) external { + s_rateLimitAdmin = rateLimitAdmin; + } + + function setBridgeLimit(uint256 newBridgeLimit) external { + if (msg.sender != s_bridgeLimitAdmin && msg.sender != owner()) revert Unauthorized(msg.sender); + uint256 oldBridgeLimit = s_bridgeLimit; + s_bridgeLimit = newBridgeLimit; + } + + function setBridgeLimitAdmin(address bridgeLimitAdmin) external { + address oldAdmin = s_bridgeLimitAdmin; + s_bridgeLimitAdmin = bridgeLimitAdmin; + } + + function getBridgeLimit() external view virtual returns (uint256) { + return s_bridgeLimit; + } + + function getRateLimitAdmin() external view returns (address) { + return s_rateLimitAdmin; + } + + function getBridgeLimitAdmin() external view returns (address) { + return s_bridgeLimitAdmin; + } + + function setChainRateLimiterConfig( + uint64 remoteChainSelector, + RateLimiter.Config memory outboundConfig, + RateLimiter.Config memory inboundConfig + ) external { + if (msg.sender != s_rateLimitAdmin && msg.sender != owner()) revert Unauthorized(msg.sender); + + _setRateLimitConfig(remoteChainSelector, outboundConfig, inboundConfig); + } + + function _setRateLimitConfig( + uint64 remoteChainSelector, + RateLimiter.Config memory outboundConfig, + RateLimiter.Config memory inboundConfig + ) internal { + RateLimiter._validateTokenBucketConfig(outboundConfig, false); + s_outboundRateLimits[remoteChainSelector]._setTokenBucketConfig(outboundConfig); + RateLimiter._validateTokenBucketConfig(inboundConfig, false); + s_inboundRateLimits[remoteChainSelector]._setTokenBucketConfig(inboundConfig); + emit ChainConfigured(remoteChainSelector, outboundConfig, inboundConfig); + } + + function getCurrentOutboundRateLimiterState( + uint64 remoteChainSelector + ) external view returns (RateLimiter.TokenBucket memory) { + return s_outboundRateLimits[remoteChainSelector]._currentTokenBucketState(); + } + + function getCurrentInboundRateLimiterState( + uint64 remoteChainSelector + ) external view returns (RateLimiter.TokenBucket memory) { + return s_inboundRateLimits[remoteChainSelector]._currentTokenBucketState(); + } + + function applyChainUpdates(ChainUpdate[] calldata chains) external virtual { + for (uint256 i = 0; i < chains.length; ++i) { + ChainUpdate memory update = chains[i]; + s_outboundRateLimits[update.remoteChainSelector] = RateLimiter.TokenBucket({ + rate: update.outboundRateLimiterConfig.rate, + capacity: update.outboundRateLimiterConfig.capacity, + tokens: update.outboundRateLimiterConfig.capacity, + lastUpdated: uint32(block.timestamp), + isEnabled: update.outboundRateLimiterConfig.isEnabled + }); + + s_inboundRateLimits[update.remoteChainSelector] = RateLimiter.TokenBucket({ + rate: update.inboundRateLimiterConfig.rate, + capacity: update.inboundRateLimiterConfig.capacity, + tokens: update.inboundRateLimiterConfig.capacity, + lastUpdated: uint32(block.timestamp), + isEnabled: update.inboundRateLimiterConfig.isEnabled + }); + } + } + + function _transferOwnership(address newOwner) internal { + _owner = newOwner; + } +}