From 3684128be4a1af9d7100b095cd3c6ef6fc0ff62a Mon Sep 17 00:00:00 2001 From: Cheyenne Atapour Date: Tue, 17 Sep 2024 03:03:59 -0700 Subject: [PATCH] feat: Modular Gho Stewards (#414) --------- Co-authored-by: miguelmtzinf Co-authored-by: Michael Morami <91594326+MichaelMorami@users.noreply.github.com> --- .github/workflows/certora-steward.yml | 9 +- .github/workflows/node.js.yml | 2 + ...15-09-2024_Modular_Gho_Steward_Certora.pdf | Bin 0 -> 277000 bytes certora/steward/Makefile | 28 + certora/steward/applyHarness.patch | 7 + certora/steward/conf/GhoAaveSteward.conf | 27 + .../{rules.conf => GhoBucketSteward.conf} | 11 +- certora/steward/conf/GhoCcipSteward.conf | 27 + .../conf/{sanity.conf => GhoGsmSteward.conf} | 18 +- .../harness/GhoAaveSteward_Harness.sol | 24 + .../harness/GhoCcipSteward_Harness.sol | 17 + .../steward/harness/GhoGsmSteward_Harness.sol | 11 + certora/steward/munged/.gitignore | 2 + certora/steward/specs/GhoAaveSteward.spec | 258 ++++++ certora/steward/specs/GhoBucketSteward.spec | 137 +++ certora/steward/specs/GhoCcipSteward.spec | 214 +++++ .../specs/{rules.spec => GhoGsmSteward.spec} | 156 +--- deploy/10_deploy_ghomanager.ts | 29 - docs/gho-stewards.md | 38 + foundry.toml | 4 + helpers/contract-getters.ts | 5 - lib/aave-address-book | 2 +- .../interfaces/IFixedRateStrategyFactory.sol | 5 + .../feeStrategy/FixedFeeStrategyFactory.sol | 85 ++ .../interfaces/IFixedFeeStrategyFactory.sol | 54 ++ src/contracts/misc/GhoAaveSteward.sol | 281 ++++++ src/contracts/misc/GhoBucketSteward.sol | 122 +++ src/contracts/misc/GhoCcipSteward.sol | 151 +++ src/contracts/misc/GhoGsmSteward.sol | 125 +++ src/contracts/misc/GhoSteward.sol | 193 ---- src/contracts/misc/GhoStewardV2.sol | 302 ------ src/contracts/misc/RiskCouncilControlled.sol | 28 + src/contracts/misc/dependencies/AaveV3-1.sol | 698 ++++++++++++++ src/contracts/misc/dependencies/Ccip.sol | 216 +++++ .../misc/interfaces/IGhoAaveSteward.sol | 127 +++ .../misc/interfaces/IGhoBucketSteward.sol | 59 ++ .../misc/interfaces/IGhoCcipSteward.sol | 73 ++ .../misc/interfaces/IGhoGsmSteward.sol | 67 ++ src/contracts/misc/interfaces/IGhoSteward.sol | 99 -- .../misc/interfaces/IGhoStewardV2.sol | 158 ---- src/test/TestGhoAaveSteward.t.sol | 813 +++++++++++++++++ src/test/TestGhoBase.t.sol | 115 ++- src/test/TestGhoBucketSteward.t.sol | 215 +++++ src/test/TestGhoCcipSteward.t.sol | 308 +++++++ src/test/TestGhoGsmSteward.t.sol | 466 ++++++++++ src/test/TestGhoSteward.t.sol | 415 --------- src/test/TestGhoStewardV2.t.sol | 858 ------------------ src/test/TestGhoStewardsForkEthereum.t.sol | 311 +++++++ src/test/TestGhoStewardsForkRemote.t.sol | 361 ++++++++ src/test/TestGsmRegistry.t.sol | 2 +- src/test/helpers/Constants.sol | 13 +- src/test/helpers/Events.sol | 3 - src/test/mocks/MockConfigurator.sol | 43 + src/test/mocks/MockPoolDataProvider.sol | 160 ++++ .../MockUpgradeableBurnMintTokenPool.sol | 158 ++++ .../MockUpgradeableLockReleaseTokenPool.sol | 158 ++++ tasks/main/gho-testnet-setup.ts | 8 - tasks/testnet-setup/07_add-gho-steward.ts | 36 - test/gho-steward.test.ts | 218 ----- test/helpers/make-suite.ts | 6 - 60 files changed, 5993 insertions(+), 2543 deletions(-) create mode 100644 audits/15-09-2024_Modular_Gho_Steward_Certora.pdf create mode 100644 certora/steward/Makefile create mode 100644 certora/steward/applyHarness.patch create mode 100644 certora/steward/conf/GhoAaveSteward.conf rename certora/steward/conf/{rules.conf => GhoBucketSteward.conf} (74%) create mode 100644 certora/steward/conf/GhoCcipSteward.conf rename certora/steward/conf/{sanity.conf => GhoGsmSteward.conf} (66%) create mode 100644 certora/steward/harness/GhoAaveSteward_Harness.sol create mode 100644 certora/steward/harness/GhoCcipSteward_Harness.sol create mode 100644 certora/steward/harness/GhoGsmSteward_Harness.sol create mode 100644 certora/steward/munged/.gitignore create mode 100644 certora/steward/specs/GhoAaveSteward.spec create mode 100644 certora/steward/specs/GhoBucketSteward.spec create mode 100644 certora/steward/specs/GhoCcipSteward.spec rename certora/steward/specs/{rules.spec => GhoGsmSteward.spec} (56%) delete mode 100644 deploy/10_deploy_ghomanager.ts create mode 100644 docs/gho-stewards.md create mode 100644 src/contracts/facilitators/gsm/feeStrategy/FixedFeeStrategyFactory.sol create mode 100644 src/contracts/facilitators/gsm/feeStrategy/interfaces/IFixedFeeStrategyFactory.sol create mode 100644 src/contracts/misc/GhoAaveSteward.sol create mode 100644 src/contracts/misc/GhoBucketSteward.sol create mode 100644 src/contracts/misc/GhoCcipSteward.sol create mode 100644 src/contracts/misc/GhoGsmSteward.sol delete mode 100644 src/contracts/misc/GhoSteward.sol delete mode 100644 src/contracts/misc/GhoStewardV2.sol create mode 100644 src/contracts/misc/RiskCouncilControlled.sol create mode 100644 src/contracts/misc/dependencies/AaveV3-1.sol create mode 100644 src/contracts/misc/dependencies/Ccip.sol create mode 100644 src/contracts/misc/interfaces/IGhoAaveSteward.sol create mode 100644 src/contracts/misc/interfaces/IGhoBucketSteward.sol create mode 100644 src/contracts/misc/interfaces/IGhoCcipSteward.sol create mode 100644 src/contracts/misc/interfaces/IGhoGsmSteward.sol delete mode 100644 src/contracts/misc/interfaces/IGhoSteward.sol delete mode 100644 src/contracts/misc/interfaces/IGhoStewardV2.sol create mode 100644 src/test/TestGhoAaveSteward.t.sol create mode 100644 src/test/TestGhoBucketSteward.t.sol create mode 100644 src/test/TestGhoCcipSteward.t.sol create mode 100644 src/test/TestGhoGsmSteward.t.sol delete mode 100644 src/test/TestGhoSteward.t.sol delete mode 100644 src/test/TestGhoStewardV2.t.sol create mode 100644 src/test/TestGhoStewardsForkEthereum.t.sol create mode 100644 src/test/TestGhoStewardsForkRemote.t.sol create mode 100644 src/test/mocks/MockPoolDataProvider.sol create mode 100644 src/test/mocks/MockUpgradeableBurnMintTokenPool.sol create mode 100644 src/test/mocks/MockUpgradeableLockReleaseTokenPool.sol delete mode 100644 tasks/testnet-setup/07_add-gho-steward.ts delete mode 100644 test/gho-steward.test.ts diff --git a/.github/workflows/certora-steward.yml b/.github/workflows/certora-steward.yml index adeac1f8..023144c7 100644 --- a/.github/workflows/certora-steward.yml +++ b/.github/workflows/certora-steward.yml @@ -32,6 +32,10 @@ jobs: - name: Install solc run: | + cd certora/steward/ + touch applyHarness.patch + make munged + cd ../.. wget https://github.com/ethereum/solidity/releases/download/v0.8.10/solc-static-linux chmod +x solc-static-linux sudo mv solc-static-linux /usr/local/bin/solc8.10 @@ -48,4 +52,7 @@ jobs: max-parallel: 16 matrix: rule: - - rules.conf + - GhoAaveSteward.conf + - GhoBucketSteward.conf + - GhoCcipSteward.conf + - GhoGsmSteward.conf diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index f28925b4..60736851 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -7,6 +7,8 @@ jobs: env: 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/audits/15-09-2024_Modular_Gho_Steward_Certora.pdf b/audits/15-09-2024_Modular_Gho_Steward_Certora.pdf new file mode 100644 index 0000000000000000000000000000000000000000..45e8dd2eb8e8e7dac471ff2b47c09d76217dd078 GIT binary patch literal 277000 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Ő&vW0+txEew%3r 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

)$!?@p_ldvz{;vyeK>8f+6abfA` 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�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𘈆K=K!G2=l-VXe3nLys*y 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 uNp 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 z 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 $(PATCH) + rm -rf tmp + +clean: + git clean -fdX + touch $(PATCH) + diff --git a/certora/steward/applyHarness.patch b/certora/steward/applyHarness.patch new file mode 100644 index 00000000..6fc8ba12 --- /dev/null +++ b/certora/steward/applyHarness.patch @@ -0,0 +1,7 @@ +diff -ruN .gitignore .gitignore +--- .gitignore 1970-01-01 02:00:00.000000000 +0200 ++++ .gitignore 2024-08-12 17:28:45.843705526 +0300 +@@ -0,0 +1,2 @@ ++* ++!.gitignore +\ No newline at end of file \ No newline at end of file diff --git a/certora/steward/conf/GhoAaveSteward.conf b/certora/steward/conf/GhoAaveSteward.conf new file mode 100644 index 00000000..2d5296ec --- /dev/null +++ b/certora/steward/conf/GhoAaveSteward.conf @@ -0,0 +1,27 @@ +{ + "files": ["certora/steward/harness/GhoAaveSteward_Harness.sol"], + "packages": [ + "@aave/core-v3/=lib/aave-v3-core", + "@aave/periphery-v3/=lib/aave-v3-periphery", + "@aave/=lib/aave-token", + "@openzeppelin/=lib/openzeppelin-contracts", + "aave-stk-v1-5/=lib/aave-stk-v1-5", + "ds-test/=lib/forge-std/lib/ds-test/src", + "forge-std/=lib/forge-std/src", + "aave-address-book/=lib/aave-address-book/src", + "aave-helpers/=lib/aave-stk-v1-5/lib/aave-helpers", + "aave-v3-core/=lib/aave-address-book/lib/aave-v3-core", + "erc4626-tests/=lib/aave-stk-v1-5/lib/openzeppelin-contracts/lib/erc4626-tests", + "openzeppelin-contracts/=lib/aave-stk-v1-5/lib/openzeppelin-contracts", + "solidity-utils/=lib/solidity-utils/src" + ], + "build_cache": true, + "optimistic_loop": true, + "process": "emv", + "prover_args": ["-depth 15","-mediumTimeout 1000"], + "smt_timeout": "2000", + "solc": "solc8.10", + "verify": "GhoAaveSteward_Harness:certora/steward/specs/GhoAaveSteward.spec", + "rule_sanity": "basic", + "msg": "GhoAaveSteward: all rules" +} \ No newline at end of file diff --git a/certora/steward/conf/rules.conf b/certora/steward/conf/GhoBucketSteward.conf similarity index 74% rename from certora/steward/conf/rules.conf rename to certora/steward/conf/GhoBucketSteward.conf index 9585a2a3..19c24a9b 100644 --- a/certora/steward/conf/rules.conf +++ b/certora/steward/conf/GhoBucketSteward.conf @@ -1,10 +1,6 @@ { "files": [ - "certora/steward/harness/GhoStewardV2_Harness.sol", - "src/contracts/facilitators/aave/interestStrategy/FixedRateStrategyFactory.sol" - ], - "link": [ - "GhoStewardV2_Harness:FIXED_RATE_STRATEGY_FACTORY=FixedRateStrategyFactory", + "src/contracts/misc/GhoBucketSteward.sol" ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", @@ -21,12 +17,13 @@ "openzeppelin-contracts/=lib/aave-stk-v1-5/lib/openzeppelin-contracts", "solidity-utils/=lib/solidity-utils/src" ], + "build_cache": true, "optimistic_loop": true, "process": "emv", "prover_args": ["-depth 15","-mediumTimeout 1000"], "smt_timeout": "2000", "solc": "solc8.10", - "verify": "GhoStewardV2_Harness:certora/steward/specs/rules.spec", + "verify": "GhoBucketSteward:certora/steward/specs/GhoBucketSteward.spec", "rule_sanity": "basic", - "msg": "STEWARD: all rules" + "msg": "GhoBucketSteward: all rules" } \ No newline at end of file diff --git a/certora/steward/conf/GhoCcipSteward.conf b/certora/steward/conf/GhoCcipSteward.conf new file mode 100644 index 00000000..556761d0 --- /dev/null +++ b/certora/steward/conf/GhoCcipSteward.conf @@ -0,0 +1,27 @@ +{ + "files": ["certora/steward/harness/GhoCcipSteward_Harness.sol"], + "packages": [ + "@aave/core-v3/=lib/aave-v3-core", + "@aave/periphery-v3/=lib/aave-v3-periphery", + "@aave/=lib/aave-token", + "@openzeppelin/=lib/openzeppelin-contracts", + "aave-stk-v1-5/=lib/aave-stk-v1-5", + "ds-test/=lib/forge-std/lib/ds-test/src", + "forge-std/=lib/forge-std/src", + "aave-address-book/=lib/aave-address-book/src", + "aave-helpers/=lib/aave-stk-v1-5/lib/aave-helpers", + "aave-v3-core/=lib/aave-address-book/lib/aave-v3-core", + "erc4626-tests/=lib/aave-stk-v1-5/lib/openzeppelin-contracts/lib/erc4626-tests", + "openzeppelin-contracts/=lib/aave-stk-v1-5/lib/openzeppelin-contracts", + "solidity-utils/=lib/solidity-utils/src" + ], + "build_cache": true, + "optimistic_loop": true, + "process": "emv", + "prover_args": ["-depth 15","-mediumTimeout 1000"], + "smt_timeout": "2000", + "solc": "solc8.10", + "verify": "GhoCcipSteward_Harness:certora/steward/specs/GhoCcipSteward.spec", + "rule_sanity": "basic", + "msg": "GhoCcipSteward: all rules" +} \ No newline at end of file diff --git a/certora/steward/conf/sanity.conf b/certora/steward/conf/GhoGsmSteward.conf similarity index 66% rename from certora/steward/conf/sanity.conf rename to certora/steward/conf/GhoGsmSteward.conf index b69e3c96..0929927c 100644 --- a/certora/steward/conf/sanity.conf +++ b/certora/steward/conf/GhoGsmSteward.conf @@ -1,10 +1,10 @@ { "files": [ - "certora/steward/harness/GhoStewardV2_Harness.sol", - "src/contracts/facilitators/aave/interestStrategy/FixedRateStrategyFactory.sol" + "certora/steward/harness/GhoGsmSteward_Harness.sol", + "src/contracts/facilitators/gsm/feeStrategy/FixedFeeStrategyFactory.sol" ], "link": [ - "GhoStewardV2_Harness:FIXED_RATE_STRATEGY_FACTORY=FixedRateStrategyFactory", + "GhoGsmSteward_Harness:FIXED_FEE_STRATEGY_FACTORY=FixedFeeStrategyFactory", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", @@ -21,13 +21,13 @@ "openzeppelin-contracts/=lib/aave-stk-v1-5/lib/openzeppelin-contracts", "solidity-utils/=lib/solidity-utils/src" ], + "build_cache": true, "optimistic_loop": true, - "prover_args": ["-depth 15","-mediumTimeout 1000","-cache none"], + "process": "emv", + "prover_args": ["-depth 15","-mediumTimeout 1000"], "smt_timeout": "2000", "solc": "solc8.10", - "verify": "GhoStewardV2_Harness:certora/steward/specs/rules.spec", - "rule": ["sanity"], - "disable_auto_cache_key_gen" :true, - "cache" :"none", - "msg": "STEWARD::sanity" + "verify": "GhoGsmSteward_Harness:certora/steward/specs/GhoGsmSteward.spec", + "rule_sanity": "basic", + "msg": "GhoGsmSteward: all rules" } \ No newline at end of file diff --git a/certora/steward/harness/GhoAaveSteward_Harness.sol b/certora/steward/harness/GhoAaveSteward_Harness.sol new file mode 100644 index 00000000..707a0476 --- /dev/null +++ b/certora/steward/harness/GhoAaveSteward_Harness.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {GhoAaveSteward} from '../munged/src/contracts/misc/GhoAaveSteward.sol'; + +contract GhoAaveSteward_Harness is GhoAaveSteward { + constructor( + address owner, + address addressesProvider, + address poolDataProvider, + address ghoToken, + address riskCouncil, + BorrowRateConfig memory borrowRateConfig + ) + GhoAaveSteward( + owner, + addressesProvider, + poolDataProvider, + ghoToken, + riskCouncil, + borrowRateConfig + ) + {} +} diff --git a/certora/steward/harness/GhoCcipSteward_Harness.sol b/certora/steward/harness/GhoCcipSteward_Harness.sol new file mode 100644 index 00000000..d23cc716 --- /dev/null +++ b/certora/steward/harness/GhoCcipSteward_Harness.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {GhoCcipSteward} from '../../../src/contracts/misc/GhoCcipSteward.sol'; + +contract GhoCcipSteward_Harness is GhoCcipSteward { + constructor( + address ghoToken, + address ghoTokenPool, + address riskCouncil, + bool bridgeLimitEnabled + ) GhoCcipSteward(ghoToken, ghoTokenPool, riskCouncil, bridgeLimitEnabled) {} + + function getCcipTimelocks() external view returns (CcipDebounce memory) { + return _ccipTimelocks; + } +} diff --git a/certora/steward/harness/GhoGsmSteward_Harness.sol b/certora/steward/harness/GhoGsmSteward_Harness.sol new file mode 100644 index 00000000..45c02b0c --- /dev/null +++ b/certora/steward/harness/GhoGsmSteward_Harness.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {GhoGsmSteward} from '../../../src/contracts/misc/GhoGsmSteward.sol'; + +contract GhoGsmSteward_Harness is GhoGsmSteward { + constructor( + address fixedRateStrategyFactory, + address riskCouncil + ) GhoGsmSteward(fixedRateStrategyFactory, riskCouncil) {} +} diff --git a/certora/steward/munged/.gitignore b/certora/steward/munged/.gitignore new file mode 100644 index 00000000..c96a04f0 --- /dev/null +++ b/certora/steward/munged/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/certora/steward/specs/GhoAaveSteward.spec b/certora/steward/specs/GhoAaveSteward.spec new file mode 100644 index 00000000..15514470 --- /dev/null +++ b/certora/steward/specs/GhoAaveSteward.spec @@ -0,0 +1,258 @@ + +/*=========================================================================== + This is a specification file for the contract GhoAaveSteward. + The rules were written base on the following: + https://github.com/aave/gho-core/pull/388 + + We check the following aspects: + - Limitations due to timelocks. + - For the relevant functions, only autorized sender can call them. + - When setting new paramethers they are in the correct range. + - The new paramethers are indeed set. + =============================================================================*/ + +methods { + function _.getPool() external => NONDET; + function _.getConfiguration(address) external => NONDET; + function _.getPoolConfigurator() external => NONDET; + + function _.getBorrowCap(DataTypes.ReserveConfigurationMap memory) internal => + get_BORROW_CAP_cvl() expect uint256 ; + function _.setBorrowCap(address token, uint256 newCap) external => + set_BORROW_CAP_cvl(token,newCap) expect void ALL; + + function _.getSupplyCap(DataTypes.ReserveConfigurationMap memory) internal => + get_SUPPLY_CAP_cvl() expect uint256 ; + function _.setSupplyCap(address token, uint256 newCap) external => + set_SUPPLY_CAP_cvl(token,newCap) expect void ALL; + + function _._getInterestRatesForAsset(address) internal => + get_INTEREST_RATE_cvl() expect (uint256,uint256,uint256,uint256); + + + + function getGhoTimelocks() external returns (IGhoAaveSteward.GhoDebounce) envfree; + function GHO_BORROW_RATE_MAX() external returns uint32 envfree; + function MINIMUM_DELAY() external returns uint256 envfree; + function RISK_COUNCIL() external returns address envfree; + + function owner() external returns address envfree; +} + + +ghost uint256 BORROW_CAP { + axiom 1==1; +} +function get_BORROW_CAP_cvl() returns uint256 { + return BORROW_CAP; +} +function set_BORROW_CAP_cvl(address token, uint256 newCap) { + BORROW_CAP = newCap; +} + +ghost uint256 SUPPLY_CAP { + axiom 1==1; +} +function get_SUPPLY_CAP_cvl() returns uint256 { + return SUPPLY_CAP; +} +function set_SUPPLY_CAP_cvl(address token, uint256 newCap) { + SUPPLY_CAP = newCap; +} + + +ghost uint16 OPTIMAL_USAGE_RATIO; +ghost uint32 BASE_VARIABLE_BORROW_RATE; +ghost uint32 VARIABLE_RATE_SLOPE1; +ghost uint32 VARIABLE_RATE_SLOPE2; + +function get_INTEREST_RATE_cvl() returns (uint16, uint32, uint32, uint32) { + return (OPTIMAL_USAGE_RATIO,BASE_VARIABLE_BORROW_RATE,VARIABLE_RATE_SLOPE1,VARIABLE_RATE_SLOPE2); +} + + + + + +/* ================================================================================= + ================================================================================ + Part 1: validity of the timelocks + ================================================================================= + ==============================================================================*/ + +// FUNCTION: updateGhoBorrowRate +rule ghoBorrowRateLastUpdate__updated_only_by_updateGhoBorrowRate(method f) { + env e; calldataarg args; + + uint40 ghoBorrowRateLastUpdate_before = getGhoTimelocks().ghoBorrowRateLastUpdate; + f(e,args); + uint40 ghoBorrowRateLastUpdate_after = getGhoTimelocks().ghoBorrowRateLastUpdate; + + assert (ghoBorrowRateLastUpdate_after != ghoBorrowRateLastUpdate_before) => + f.selector == sig:updateGhoBorrowRate(uint16,uint32,uint32,uint32).selector; +} +rule updateGhoBorrowRate_update_correctly__ghoBorrowRateLastUpdate() { + env e; uint16 optimalUsageRatio; uint32 baseVariableBorrowRate; + uint32 variableRateSlope1; uint32 variableRateSlope2; + updateGhoBorrowRate(e,optimalUsageRatio, baseVariableBorrowRate, + variableRateSlope1, variableRateSlope2); + assert getGhoTimelocks().ghoBorrowRateLastUpdate == require_uint40(e.block.timestamp); +} +rule updateGhoBorrowRate_timelock() { + uint40 ghoBorrowRateLastUpdate_before = getGhoTimelocks().ghoBorrowRateLastUpdate; + env e; uint16 optimalUsageRatio; uint32 baseVariableBorrowRate; + uint32 variableRateSlope1; uint32 variableRateSlope2; + + updateGhoBorrowRate(e,optimalUsageRatio, baseVariableBorrowRate, + variableRateSlope1, variableRateSlope2); + + assert to_mathint(e.block.timestamp) > ghoBorrowRateLastUpdate_before + MINIMUM_DELAY(); +} + + +// FUNCTION: updateGhoBorrowCap +rule ghoBorrowCapLastUpdate__updated_only_by_updateGhoBorrowCap(method f) { + env e; calldataarg args; + + uint40 ghoBorrowCapLastUpdate_before = getGhoTimelocks().ghoBorrowCapLastUpdate; + f(e,args); + uint40 ghoBorrowCapLastUpdate_after = getGhoTimelocks().ghoBorrowCapLastUpdate; + + assert (ghoBorrowCapLastUpdate_after != ghoBorrowCapLastUpdate_before) => + f.selector == sig:updateGhoBorrowCap(uint256).selector; +} +rule updateGhoBorrowCap_update_correctly__ghoBorrowCapLastUpdate() { + env e; uint256 newBorrowCap; + updateGhoBorrowCap(e,newBorrowCap); + assert getGhoTimelocks().ghoBorrowCapLastUpdate == require_uint40(e.block.timestamp); +} +rule updateGhoBorrowCap_timelock() { + uint40 ghoBorrowCapLastUpdate_before = getGhoTimelocks().ghoBorrowCapLastUpdate; + env e; uint256 newBorrowCap; + updateGhoBorrowCap(e,newBorrowCap); + + assert to_mathint(e.block.timestamp) > ghoBorrowCapLastUpdate_before + MINIMUM_DELAY(); +} + + +// FUNCTION: updateGhoSupplyCap +rule ghoSupplyCapLastUpdate__updated_only_by_updateGhoSupplyCap(method f) { + env e; calldataarg args; + + uint40 ghoSupplyCapLastUpdate_before = getGhoTimelocks().ghoSupplyCapLastUpdate; + f(e,args); + uint40 ghoSupplyCapLastUpdate_after = getGhoTimelocks().ghoSupplyCapLastUpdate; + + assert (ghoSupplyCapLastUpdate_after != ghoSupplyCapLastUpdate_before) => + f.selector == sig:updateGhoSupplyCap(uint256).selector; +} +rule updateGhoSupplyCap_update_correctly__ghoSupplyCapLastUpdate() { + env e; uint256 newSupplyCap; + updateGhoSupplyCap(e,newSupplyCap); + assert getGhoTimelocks().ghoSupplyCapLastUpdate == require_uint40(e.block.timestamp); +} +rule updateGhoSupplyCap_timelock() { + uint40 ghoSupplyCapLastUpdate_before = getGhoTimelocks().ghoSupplyCapLastUpdate; + env e; uint256 newSupplyCap; + updateGhoSupplyCap(e,newSupplyCap); + + assert to_mathint(e.block.timestamp) > ghoSupplyCapLastUpdate_before + MINIMUM_DELAY(); +} + + +/* ================================================================================= + ================================================================================ + Part 2: autorized message sender + ================================================================================= + ==============================================================================*/ +rule only_RISK_COUNCIL_can_call__updateGhoBorrowCap() { + env e; uint256 newBorrowCap; + + updateGhoBorrowCap(e,newBorrowCap); + assert (e.msg.sender==RISK_COUNCIL()); +} +rule only_RISK_COUNCIL_can_call__updateGhoBorrowRate() { + env e; uint16 optimalUsageRatio; uint32 baseVariableBorrowRate; + uint32 variableRateSlope1; uint32 variableRateSlope2; + + updateGhoBorrowRate(e,optimalUsageRatio, baseVariableBorrowRate, + variableRateSlope1, variableRateSlope2); + assert (e.msg.sender==RISK_COUNCIL()); +} +rule only_RISK_COUNCIL_can_call__updateGhoSupplyCap() { + env e; uint256 newSupplyCap; + + updateGhoSupplyCap(e,newSupplyCap); + assert (e.msg.sender==RISK_COUNCIL()); +} +rule only_owner_can_call__setBorrowRateConfig() { + env e; + uint16 optimalUsageRatioMaxChange; + uint32 baseVariableBorrowRateMaxChange; + uint32 variableRateSlope1MaxChange; + uint32 variableRateSlope2MaxChange; + + setBorrowRateConfig(e,optimalUsageRatioMaxChange, baseVariableBorrowRateMaxChange, variableRateSlope1MaxChange, variableRateSlope2MaxChange); + assert (e.msg.sender==owner()); +} + + + +/* ================================================================================= + ================================================================================ + Part 3: correctness of the main functions. + We check the validity of the new paramethers values, and that are indeed set. + ================================================================================= + ==============================================================================*/ +rule updateGhoBorrowCap__correctness() { + env e; uint256 newBorrowCap; + uint256 borrow_cap_before = BORROW_CAP; + updateGhoBorrowCap(e,newBorrowCap); + assert BORROW_CAP==newBorrowCap; + + uint256 borrow_cap_after = BORROW_CAP; + assert to_mathint(borrow_cap_after) <= 2*borrow_cap_before; +} + +rule updateGhoSupplyCap__correctness() { + env e; uint256 newSupplyCap; + uint256 supply_cap_before = SUPPLY_CAP; + updateGhoSupplyCap(e,newSupplyCap); + assert SUPPLY_CAP==newSupplyCap; + + uint256 supply_cap_after = SUPPLY_CAP; + assert to_mathint(supply_cap_after) <= 2*supply_cap_before; +} + + +rule updateGhoBorrowRate__correctness() { + env e; uint16 optimalUsageRatio; uint32 baseVariableBorrowRate; + uint32 variableRateSlope1; uint32 variableRateSlope2; + + uint16 optimalUsageRatio_before = OPTIMAL_USAGE_RATIO; + uint32 baseVariableBorrowRate_before = BASE_VARIABLE_BORROW_RATE; + uint32 variableRateSlope1_before = VARIABLE_RATE_SLOPE1; + uint32 variableRateSlope2_before = VARIABLE_RATE_SLOPE2; + + updateGhoBorrowRate(e,optimalUsageRatio, baseVariableBorrowRate, variableRateSlope1, variableRateSlope2); + + + assert baseVariableBorrowRate + variableRateSlope1 + variableRateSlope2 <= to_mathint(GHO_BORROW_RATE_MAX()); +} + + + + + + + +/* ================================================================================= + Rule: sanity. + Status: PASS. + ================================================================================*/ +rule sanity(method f) { + env e; + calldataarg args; + f(e,args); + satisfy true; +} diff --git a/certora/steward/specs/GhoBucketSteward.spec b/certora/steward/specs/GhoBucketSteward.spec new file mode 100644 index 00000000..996945dc --- /dev/null +++ b/certora/steward/specs/GhoBucketSteward.spec @@ -0,0 +1,137 @@ +//using FixedRateStrategyFactory as FAC; + + +/*=========================================================================== + This is a specification file for the contract GhoStewardV2. + The rules were written base on the following: + https://github.com/aave/gho-core/pull/388 + + We check the following aspects: + - Limitations due to timelocks. + - For the relevant functions, only autorized sender can call them. + - When setting new paramethers they are in the correct range. + - The new paramethers are indeed set. + =============================================================================*/ + +methods { + function _.getPool() external => NONDET; + function _.getConfiguration(address) external => NONDET; + function _.getPoolConfigurator() external => NONDET; + + function _.getFacilitatorBucket(address facilitator) external => + get_BUCKET_CAPACITY_cvl() expect (uint256,uint256); + function _.setFacilitatorBucketCapacity(address,uint128 newBucketCapacity) external => + set_BUCKET_CAPACITY_cvl(newBucketCapacity) expect void; + + function owner() external returns (address) envfree; + function getFacilitatorBucketCapacityTimelock(address) external returns (uint40) envfree; + function MINIMUM_DELAY() external returns uint256 envfree; + function RISK_COUNCIL() external returns address envfree; +} + + + +ghost uint128 BUCKET_CAPACITY; +function get_BUCKET_CAPACITY_cvl() returns (uint256,uint256) { + uint256 ret; + return (BUCKET_CAPACITY,ret); +} +function set_BUCKET_CAPACITY_cvl(uint128 newBucketCapacity) { + BUCKET_CAPACITY = newBucketCapacity; +} + + + + +/* ================================================================================= + ================================================================================ + Part 1: validity of the timelocks + ================================================================================= + ==============================================================================*/ + +// FUNCTION: updateFacilitatorBucketCapacity +rule timestamp__updated_only_by_updateFacilitatorBucketCapacity(method f) { + env e; calldataarg args; + address facilitator; + + uint40 timestamp_before = getFacilitatorBucketCapacityTimelock(facilitator); + f(e,args); + uint40 timestamp_after = getFacilitatorBucketCapacityTimelock(facilitator); + + assert (timestamp_before != timestamp_after) => + f.selector == sig:updateFacilitatorBucketCapacity(address,uint128).selector; +} + +rule updateFacilitatorBucketCapacity_update_correctly__timestamp() { + env e; address facilitator; uint128 newBucketCapacity; + updateFacilitatorBucketCapacity(e,facilitator,newBucketCapacity); + assert getFacilitatorBucketCapacityTimelock(facilitator) == require_uint40(e.block.timestamp); +} + +rule updateFacilitatorBucketCapacity_timelock() { + env e; address facilitator; uint128 newBucketCapacity; + uint40 timestamp_before = getFacilitatorBucketCapacityTimelock(facilitator); + updateFacilitatorBucketCapacity(e,facilitator, newBucketCapacity); + + assert to_mathint(e.block.timestamp) > timestamp_before + MINIMUM_DELAY(); +} + + + + + + +/* ================================================================================= + ================================================================================ + Part 2: autorized message sender + ================================================================================= + ==============================================================================*/ +rule only_RISK_COUNCIL_can_call__updateFacilitatorBucketCapacity() { + env e; address facilitator; uint128 newBucketCapacity; + + updateFacilitatorBucketCapacity(e,facilitator,newBucketCapacity); + assert (e.msg.sender==RISK_COUNCIL()); +} +rule only_owner_can_call__setControlledFacilitator() { + env e; + address[] facilitatorList; + bool approve; + + setControlledFacilitator(e,facilitatorList,approve); + assert (e.msg.sender==owner()); +} + + + +/* ================================================================================= + ================================================================================ + Part 3: correctness of the main functions. + We check the validity of the new paramethers values, and that are indeed set. + ================================================================================= + ==============================================================================*/ + +rule updateFacilitatorBucketCapacity__correctness() { + env e; address facilitator; uint128 newBucketCapacity; + + uint256 bucket_capacity_before = BUCKET_CAPACITY; + updateFacilitatorBucketCapacity(e,facilitator,newBucketCapacity); + assert BUCKET_CAPACITY==newBucketCapacity; + + assert to_mathint(BUCKET_CAPACITY) <= 2*bucket_capacity_before; +} + + + + + + +/* ================================================================================= + Rule: sanity. + Status: PASS. + ================================================================================*/ +rule sanity(method f) { + env e; + calldataarg args; + f(e,args); + satisfy true; +} diff --git a/certora/steward/specs/GhoCcipSteward.spec b/certora/steward/specs/GhoCcipSteward.spec new file mode 100644 index 00000000..ccfaf076 --- /dev/null +++ b/certora/steward/specs/GhoCcipSteward.spec @@ -0,0 +1,214 @@ +//using FixedFeeStrategyFactory as FAC; + + +/*=========================================================================== + This is a specification file for the contract GhoStewardV2. + The rules were written base on the following: + https://github.com/aave/gho-core/pull/388 + + We check the following aspects: + - Limitations due to timelocks. + - For the relevant functions, only autorized sender can call them. + - When setting new paramethers they are in the correct range. + - The new paramethers are indeed set. + =============================================================================*/ + +methods { + function _.getPool() external => NONDET; + function _.getConfiguration(address) external => NONDET; + function _.getPoolConfigurator() external => NONDET; + + + function _.getCurrentOutboundRateLimiterState(uint64 remoteCS) external + => OutboundRate(remoteCS) expect RateLimiter.TokenBucket; + + function _.getCurrentInboundRateLimiterState(uint64 remoteCS) external + => InboundRate(remoteCS) expect RateLimiter.TokenBucket; + + function _.setChainRateLimiterConfig(uint64,RateLimiter.Config,RateLimiter.Config) + external => NONDET; + + function getCcipTimelocks() external returns (IGhoCcipSteward.CcipDebounce) envfree; + function MINIMUM_DELAY() external returns uint256 envfree; + function RISK_COUNCIL() external returns address envfree; +} + + +ghost uint128 CAPACITY_OUT; +ghost uint128 RATE_OUT; +function OutboundRate(uint64 remoteCS) returns RateLimiter.TokenBucket { + RateLimiter.TokenBucket ret; + + require ret.capacity == CAPACITY_OUT; + require ret.rate == RATE_OUT; + + return ret; +} + +ghost uint128 CAPACITY_IN; +ghost uint128 RATE_IN; +function InboundRate(uint64 remoteCS) returns RateLimiter.TokenBucket { + RateLimiter.TokenBucket ret; + + require ret.capacity == CAPACITY_IN; + require ret.rate == RATE_IN; + + return ret; +} + + + + + +ghost uint128 BUY_FEE { + axiom 1==1; +} +function get_BUY_FEE_cvl() returns uint128 { + return BUY_FEE; +} +ghost uint128 SELL_FEE { + axiom 1==1; +} +function get_SELL_FEE_cvl() returns uint128 { + return SELL_FEE; +} +ghost address FEE_STRATEGY { + axiom 1==1; +} +function set_FEE_STRATEGY(address strategy) { + FEE_STRATEGY = strategy; +} + + + +/* ================================================================================= + ================================================================================ + Part 1: validity of the timelocks + ================================================================================= + ==============================================================================*/ + +// FUNCTION: updateBridgeLimit +rule bridgeLimitLastUpdate__updated_only_by_updateBridgeLimit(method f) { + env e; calldataarg args; + + uint40 bridgeLimitLastUpdate_before = getCcipTimelocks().bridgeLimitLastUpdate; + f(e,args); + uint40 bridgeLimitLastUpdate_after = getCcipTimelocks().bridgeLimitLastUpdate; + + assert (bridgeLimitLastUpdate_before != bridgeLimitLastUpdate_after) => + f.selector == sig:updateBridgeLimit(uint256).selector; +} + +rule updateBridgeLimit_update_correctly__bridgeLimitLastUpdate() { + env e; uint256 newBridgeLimit; + updateBridgeLimit(e,newBridgeLimit); + assert getCcipTimelocks().bridgeLimitLastUpdate == require_uint40(e.block.timestamp); +} + +rule updateBridgeLimit_timelock() { + env e; uint128 newBridgeLimit; + uint40 before = getCcipTimelocks().bridgeLimitLastUpdate; + updateBridgeLimit(e,newBridgeLimit); + + assert to_mathint(e.block.timestamp) > before + MINIMUM_DELAY(); +} + + + +// FUNCTION: updateRateLimit +rule rateLimitLastUpdate__updated_only_by_updateRateLimit(method f) { + env e; calldataarg args; + + uint40 before = getCcipTimelocks().rateLimitLastUpdate; + f(e,args); + uint40 after = getCcipTimelocks().rateLimitLastUpdate; + + assert (before != after) => + f.selector == sig:updateRateLimit(uint64,bool,uint128,uint128,bool,uint128,uint128).selector; +} + +rule updateRateLimit_update_correctly__rateLimitLastUpdate() { + env e; calldataarg args; + updateRateLimit(e,args); + assert getCcipTimelocks().rateLimitLastUpdate == require_uint40(e.block.timestamp); +} + +rule updateRateLimit_timelock() { + env e; calldataarg args; + uint40 before = getCcipTimelocks().rateLimitLastUpdate; + updateRateLimit(e,args); + + assert to_mathint(e.block.timestamp) > before + MINIMUM_DELAY(); +} + + + + + +/* ================================================================================= + ================================================================================ + Part 2: autorized message sender + ================================================================================= + ==============================================================================*/ + +rule only_RISK_COUNCIL_can_call__updateBridgeLimit() { + env e; calldataarg args; + + updateBridgeLimit(e,args); + assert (e.msg.sender==RISK_COUNCIL()); +} + +rule only_RISK_COUNCIL_can_call__updateRateLimit() { + env e; calldataarg args; + + updateRateLimit(e,args); + assert (e.msg.sender==RISK_COUNCIL()); +} + + + +/* ================================================================================= + ================================================================================ + Part 3: correctness of the main functions. + We check the validity of the new paramethers values. + ================================================================================= + ==============================================================================*/ + +rule updateBridgeLimit__correctness() { + env e; + + uint64 remoteChainSelector; + bool outboundEnabled; + uint128 outboundCapacity; + uint128 outboundRate; + bool inboundEnabled; + uint128 inboundCapacity; + uint128 inboundRate; + + updateRateLimit(e, remoteChainSelector, + outboundEnabled, outboundCapacity, outboundRate, + inboundEnabled, inboundCapacity, inboundRate); + + assert to_mathint(outboundCapacity) <= 2*CAPACITY_OUT; + assert to_mathint(outboundRate) <= 2*RATE_OUT; + + assert to_mathint(inboundCapacity) <= 2*CAPACITY_IN; + assert to_mathint(inboundRate) <= 2*RATE_IN; +} + + + + + + + +/* ================================================================================= + Rule: sanity. + Status: PASS. + ================================================================================*/ +rule sanity(method f) { + env e; + calldataarg args; + f(e,args); + satisfy true; +} diff --git a/certora/steward/specs/rules.spec b/certora/steward/specs/GhoGsmSteward.spec similarity index 56% rename from certora/steward/specs/rules.spec rename to certora/steward/specs/GhoGsmSteward.spec index 7a49ab70..caf84390 100644 --- a/certora/steward/specs/rules.spec +++ b/certora/steward/specs/GhoGsmSteward.spec @@ -1,4 +1,4 @@ -using FixedRateStrategyFactory as FAC; +using FixedFeeStrategyFactory as FAC; /*=========================================================================== @@ -18,16 +18,6 @@ methods { function _.getConfiguration(address) external => NONDET; function _.getPoolConfigurator() external => NONDET; - function _.getBorrowCap(DataTypes.ReserveConfigurationMap memory) internal => - get_BORROW_CAP_cvl() expect uint256 ; - function _.setBorrowCap(address token, uint256 newCap) external => - set_BORROW_CAP_cvl(token,newCap) expect void ALL; - - function _.getBaseVariableBorrowRate() external => - get_BORROW_RATE_cvl() expect uint256; - function _.setReserveInterestRateStrategyAddress(address,address strategy) external => - set_STRATEGY(strategy) expect void ALL; - function _.getExposureCap() external => get_EXPOSURE_CAP_cvl() expect uint256 ; function _.updateExposureCap(uint128 newCap) external => set_EXPOSURE_CAP_cvl(newCap) expect void ALL; @@ -35,44 +25,14 @@ methods { function _.getBuyFee(uint256) external => get_BUY_FEE_cvl() expect uint256; function _.getSellFee(uint256) external => get_SELL_FEE_cvl() expect uint256; function _.updateFeeStrategy(address strategy) external => - set_FEE_STRATEGY(strategy) expect void ALL; + set_FEE_STRATEGY(strategy) expect void ALL; - function owner() external returns (address) envfree; - function getGhoTimelocks() external returns (IGhoStewardV2.GhoDebounce) envfree; - function getGsmTimelocks(address) external returns (IGhoStewardV2.GsmDebounce) envfree; - function GHO_BORROW_RATE_CHANGE_MAX() external returns uint256 envfree; + function getGsmTimelocks(address) external returns (IGhoGsmSteward.GsmDebounce) envfree; function GSM_FEE_RATE_CHANGE_MAX() external returns uint256 envfree; - function GHO_BORROW_RATE_MAX() external returns uint256 envfree; function MINIMUM_DELAY() external returns uint256 envfree; function RISK_COUNCIL() external returns address envfree; - function FAC.getStrategyByRate(uint256) external returns (address) envfree; - function get_gsmFeeStrategiesByRates(uint256,uint256) external returns(address) envfree; -} - - -ghost uint256 BORROW_CAP { - axiom 1==1; -} -function get_BORROW_CAP_cvl() returns uint256 { - return BORROW_CAP; -} -function set_BORROW_CAP_cvl(address token, uint256 newCap) { - BORROW_CAP = newCap; -} - -ghost uint256 BORROW_RATE { - axiom BORROW_RATE <= 10^27; -} -function get_BORROW_RATE_cvl() returns uint256 { - return BORROW_RATE; -} - -ghost address STRATEGY { - axiom 1==1; -} -function set_STRATEGY(address strategy) { - STRATEGY = strategy; + function FAC.getFixedFeeStrategy(uint256 buyFee, uint256 sellFee) external returns (address) envfree; } @@ -115,61 +75,6 @@ function set_FEE_STRATEGY(address strategy) { ================================================================================= ==============================================================================*/ -// FUNCTION: updateGhoBorrowCap -rule ghoBorrowCapLastUpdate__updated_only_by_updateGhoBorrowCap(method f) { - env e; calldataarg args; - - uint40 ghoBorrowCapLastUpdate_before = getGhoTimelocks().ghoBorrowCapLastUpdate; - f(e,args); - uint40 ghoBorrowCapLastUpdate_after = getGhoTimelocks().ghoBorrowCapLastUpdate; - - assert (ghoBorrowCapLastUpdate_after != ghoBorrowCapLastUpdate_before) => - f.selector == sig:updateGhoBorrowCap(uint256).selector; -} - -rule updateGhoBorrowCap_update_correctly__ghoBorrowCapLastUpdate() { - env e; uint256 newBorrowCap; - updateGhoBorrowCap(e,newBorrowCap); - assert getGhoTimelocks().ghoBorrowCapLastUpdate == require_uint40(e.block.timestamp); -} - -rule updateGhoBorrowCap_timelock() { - uint40 ghoBorrowCapLastUpdate_before = getGhoTimelocks().ghoBorrowCapLastUpdate; - env e; uint256 newBorrowCap; - updateGhoBorrowCap(e,newBorrowCap); - - assert to_mathint(e.block.timestamp) > ghoBorrowCapLastUpdate_before + MINIMUM_DELAY(); -} - - -// FUNCTION: updateGhoBorrowRate -rule ghoBorrowRateLastUpdate__updated_only_by_updateGhoBorrowRate(method f) { - env e; calldataarg args; - - uint40 ghoBorrowRateLastUpdate_before = getGhoTimelocks().ghoBorrowRateLastUpdate; - f(e,args); - uint40 ghoBorrowRateLastUpdate_after = getGhoTimelocks().ghoBorrowRateLastUpdate; - - assert (ghoBorrowRateLastUpdate_after != ghoBorrowRateLastUpdate_before) => - f.selector == sig:updateGhoBorrowRate(uint256).selector; -} - -rule updateGhoBorrowRate_update_correctly__ghoBorrowRateLastUpdate() { - env e; uint256 newBorrowRate; - updateGhoBorrowRate(e,newBorrowRate); - assert getGhoTimelocks().ghoBorrowRateLastUpdate == require_uint40(e.block.timestamp); -} - -rule updateGhoBorrowRate_timelock() { - uint40 ghoBorrowRateLastUpdate_before = getGhoTimelocks().ghoBorrowRateLastUpdate; - env e; uint256 newBorrowRate; - updateGhoBorrowRate(e,newBorrowRate); - - assert to_mathint(e.block.timestamp) > ghoBorrowRateLastUpdate_before + MINIMUM_DELAY(); -} - - - // FUNCTION: updateGsmExposureCap rule gsmExposureCapLastUpdated__updated_only_by_updateGsmExposureCap(method f) { env e; calldataarg args; @@ -234,24 +139,6 @@ rule updateGsmBuySellFees_timelock() { Part 2: autorized message sender ================================================================================= ==============================================================================*/ -rule only_RISK_COUNCIL_can_call__updateFacilitatorBucketCapacity() { - env e; address facilitator; uint128 newBucketCapacity; - - updateFacilitatorBucketCapacity(e,facilitator,newBucketCapacity); - assert (e.msg.sender==RISK_COUNCIL()); -} -rule only_RISK_COUNCIL_can_call__updateGhoBorrowCap() { - env e; uint256 newBorrowCap; - - updateGhoBorrowCap(e,newBorrowCap); - assert (e.msg.sender==RISK_COUNCIL()); -} -rule only_RISK_COUNCIL_can_call__updateGhoBorrowRate() { - env e; uint256 newBorrowRate; - - updateGhoBorrowRate(e,newBorrowRate); - assert (e.msg.sender==RISK_COUNCIL()); -} rule only_RISK_COUNCIL_can_call__updateGsmExposureCap() { env e; address gsm; uint128 newExposureCap; @@ -261,18 +148,9 @@ rule only_RISK_COUNCIL_can_call__updateGsmExposureCap() { rule only_RISK_COUNCIL_can_call__updateGsmBuySellFees() { env e; address gsm; uint256 buyFee; uint256 sellFee; - updateGsmBuySellFees(e,gsm,buyFee,sellFee); assert (e.msg.sender==RISK_COUNCIL()); } -rule only_owner_can_call__setControlledFacilitator() { - env e; - address[] facilitatorList; - bool approve; - - setControlledFacilitator(e,facilitatorList,approve); - assert (e.msg.sender==owner()); -} @@ -282,30 +160,6 @@ rule only_owner_can_call__setControlledFacilitator() { We check the validity of the new paramethers values, and that are indeed set. ================================================================================= ==============================================================================*/ -rule updateGhoBorrowCap__correctness() { - env e; uint256 newBorrowCap; - uint256 borrow_cap_before = BORROW_CAP; - updateGhoBorrowCap(e,newBorrowCap); - assert BORROW_CAP==newBorrowCap; - - uint256 borrow_cap_after = BORROW_CAP; - assert to_mathint(borrow_cap_after) <= 2*borrow_cap_before; -} - - -rule updateGhoBorrowRate__correctness() { - env e; uint256 newBorrowRate; - uint256 borrow_rate_before = BORROW_RATE; - updateGhoBorrowRate(e,newBorrowRate); - assert FAC.getStrategyByRate(newBorrowRate) == STRATEGY; - - assert (borrow_rate_before-GHO_BORROW_RATE_CHANGE_MAX() <= to_mathint(newBorrowRate) - && - to_mathint(newBorrowRate) <= borrow_rate_before+GHO_BORROW_RATE_CHANGE_MAX()); - assert (newBorrowRate <= GHO_BORROW_RATE_MAX()); -} - - rule updateGsmExposureCap__correctness() { env e; address gsm; uint128 newExposureCap; uint128 exposure_cap_before = EXPOSURE_CAP; @@ -322,7 +176,7 @@ rule updateGsmBuySellFees__correctness() { uint256 buyFee_before = BUY_FEE; uint256 sellFee_before = SELL_FEE; updateGsmBuySellFees(e,gsm,buyFee,sellFee); - assert get_gsmFeeStrategiesByRates(buyFee,sellFee)==FEE_STRATEGY; + assert FAC.getFixedFeeStrategy(buyFee,sellFee)==FEE_STRATEGY; assert to_mathint(buyFee) <= buyFee_before + GSM_FEE_RATE_CHANGE_MAX(); assert to_mathint(sellFee) <= sellFee_before + GSM_FEE_RATE_CHANGE_MAX(); diff --git a/deploy/10_deploy_ghomanager.ts b/deploy/10_deploy_ghomanager.ts deleted file mode 100644 index b176f030..00000000 --- a/deploy/10_deploy_ghomanager.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { DeployFunction } from 'hardhat-deploy/types'; -import { getPoolAddressesProvider } from '@aave/deploy-v3'; -import { getGhoToken } from '../helpers/contract-getters'; - -const func: DeployFunction = async function ({ - getNamedAccounts, - deployments, -}: HardhatRuntimeEnvironment) { - const { deploy } = deployments; - const { deployer } = await getNamedAccounts(); - - const addressesProvider = await getPoolAddressesProvider(); - const ghoToken = await getGhoToken(); - - const ghoSteward = await deploy('GhoSteward', { - from: deployer, - args: [addressesProvider.address, ghoToken.address, deployer, deployer], - log: true, - }); - console.log(`GHO Steward: ${ghoSteward.address}`); - - return true; -}; - -func.id = 'GhoSteward'; -func.tags = ['GhoSteward', 'full_gho_deploy']; - -export default func; diff --git a/docs/gho-stewards.md b/docs/gho-stewards.md new file mode 100644 index 00000000..77f3eb66 --- /dev/null +++ b/docs/gho-stewards.md @@ -0,0 +1,38 @@ +## Overview + +These contracts each control different parameters related to GHO and its facilitators. They allow the Aave DAO and an approved Risk Council to change these parameters, according to set rules and configurations. + +Each Steward is designed to have a specific set of segregated responsibilities in an effort to avoid having to redeploy the entire original Steward. Instead, only the specific steward whose responsibilities are affected will have to be redeployed. + +### [GhoAaveSteward](src/contracts/misc/GhoAaveSteward.sol) + +This Steward manages parameters related to the GHO token. Specifically, it allows the Risk Council to change the following parameters: + +- Borrow Rate +- Borrow Cap +- Supply Cap + +In addition, the Aave DAO is allowed to change the configuration for the GHO Borrow Rate. This puts restrictions on how much the Risk Council is allowed to change parameters related to the borrow rate. There are 4 parameters that comprise the borrow rate: + +- `optimalUsageRatio` +- `baseVariableBorrowRate` +- `variableRateSlope1` +- `variableRateSlope2` + +For example, the Aave DAO can specify that the optimalUsageRatio variable may only be changed by 3% at a time. + +### [GhoBucketSteward](src/contracts/misc/GhoBucketSteward.sol) + +This Steward allows the Risk Council to set the bucket capacities of controlled facilitators. Additionally, it allows the Aave DAO to add or remove controlled facilitators. + +### [GhoCcipSteward](src/contracts/misc/GhoCcipSteward.sol) + +This Steward allows the management of parameters related to CCIP token pools. It allows the Risk Council to update the CCIP bridge limit, and to update the CCIP rate limit configuration. + +### [GhoGsmSteward](src/contracts/misc/GhoGsmSteward.sol) + +This Steward allows the Risk Council to update the exposure cap of the GSM, and to update the buy and sell fees of the GSM. + +### [RiskCouncilControlled](src/contracts/misc/RiskCouncilControlled.sol) + +This is a helper contract to define the approved Risk Council and enforce its authority to call permissioned functions. diff --git a/foundry.toml b/foundry.toml index 35991f55..82d209b8 100644 --- a/foundry.toml +++ b/foundry.toml @@ -12,4 +12,8 @@ extra_output_files = ["metadata"] optimizer = true 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/helpers/contract-getters.ts b/helpers/contract-getters.ts index 648d14ff..7f74d19e 100644 --- a/helpers/contract-getters.ts +++ b/helpers/contract-getters.ts @@ -23,8 +23,6 @@ import { VariableDebtToken, StakedAaveV3, GhoFlashMinter, - GhoSteward, - GhoStableDebtToken, } from '../types'; // Prevent error HH9 when importing this file inside tasks or helpers at Hardhat config load @@ -78,9 +76,6 @@ export const getGhoStableDebtToken = async ( address || (await hre.deployments.get('GhoStableDebtToken')).address ); -export const getGhoSteward = async (address?: tEthereumAddress): Promise => - getContract('GhoSteward', address || (await hre.deployments.get('GhoSteward')).address); - export const getBaseImmutableAdminUpgradeabilityProxy = async ( address: tEthereumAddress ): Promise => 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/facilitators/aave/interestStrategy/interfaces/IFixedRateStrategyFactory.sol b/src/contracts/facilitators/aave/interestStrategy/interfaces/IFixedRateStrategyFactory.sol index caa28544..190cd3dd 100644 --- a/src/contracts/facilitators/aave/interestStrategy/interfaces/IFixedRateStrategyFactory.sol +++ b/src/contracts/facilitators/aave/interestStrategy/interfaces/IFixedRateStrategyFactory.sol @@ -1,6 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +/** + * @title IFixedRateStrategyFactory + * @author Aave Labs + * @notice Defines the interface of the FixedRateStrategyFactory + */ interface IFixedRateStrategyFactory { /** * @dev Emitted when a new strategy is created diff --git a/src/contracts/facilitators/gsm/feeStrategy/FixedFeeStrategyFactory.sol b/src/contracts/facilitators/gsm/feeStrategy/FixedFeeStrategyFactory.sol new file mode 100644 index 00000000..78cc0caf --- /dev/null +++ b/src/contracts/facilitators/gsm/feeStrategy/FixedFeeStrategyFactory.sol @@ -0,0 +1,85 @@ +/// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {EnumerableSet} from '@openzeppelin/contracts/utils/structs/EnumerableSet.sol'; +import {VersionedInitializable} from '@aave/core-v3/contracts/protocol/libraries/aave-upgradeability/VersionedInitializable.sol'; +import {IFixedFeeStrategyFactory} from './interfaces/IFixedFeeStrategyFactory.sol'; +import {IGsmFeeStrategy} from './interfaces/IGsmFeeStrategy.sol'; +import {FixedFeeStrategy} from './FixedFeeStrategy.sol'; + +/** + * @title FixedFeeStrategyFactory + * @author Aave Labs + * @notice Factory contract to create and keep record of Gsm FixedFeeStrategy contracts + */ +contract FixedFeeStrategyFactory is VersionedInitializable, IFixedFeeStrategyFactory { + using EnumerableSet for EnumerableSet.AddressSet; + + // Mapping of fee strategy contracts by buy and sell fees (buyFee => sellFee => feeStrategy) + mapping(uint256 => mapping(uint256 => address)) internal _gsmFeeStrategiesByFees; + EnumerableSet.AddressSet internal _gsmFeeStrategies; + + /** + * @dev Initializer + * @param feeStrategiesList List of fee strategies + * @dev Assumes that the addresses provided are deployed FixedFeeStrategy contracts + */ + function initialize(address[] memory feeStrategiesList) external initializer { + for (uint256 i = 0; i < feeStrategiesList.length; i++) { + address feeStrategy = feeStrategiesList[i]; + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + + _gsmFeeStrategiesByFees[buyFee][sellFee] = feeStrategy; + _gsmFeeStrategies.add(feeStrategy); + + emit FeeStrategyCreated(feeStrategy, buyFee, sellFee); + } + } + + ///@inheritdoc IFixedFeeStrategyFactory + function createStrategies( + uint256[] memory buyFeeList, + uint256[] memory sellFeeList + ) external returns (address[] memory) { + require(buyFeeList.length == sellFeeList.length, 'INVALID_FEE_LIST'); + address[] memory strategies = new address[](buyFeeList.length); + for (uint256 i = 0; i < buyFeeList.length; i++) { + uint256 buyFee = buyFeeList[i]; + uint256 sellFee = sellFeeList[i]; + address cachedStrategy = _gsmFeeStrategiesByFees[buyFee][sellFee]; + + if (cachedStrategy == address(0)) { + cachedStrategy = address(new FixedFeeStrategy(buyFee, sellFee)); + _gsmFeeStrategiesByFees[buyFee][sellFee] = cachedStrategy; + _gsmFeeStrategies.add(cachedStrategy); + + emit FeeStrategyCreated(cachedStrategy, buyFee, sellFee); + } + + strategies[i] = cachedStrategy; + } + + return strategies; + } + + ///@inheritdoc IFixedFeeStrategyFactory + function getFixedFeeStrategies() external view returns (address[] memory) { + return _gsmFeeStrategies.values(); + } + + ///@inheritdoc IFixedFeeStrategyFactory + function getFixedFeeStrategy(uint256 buyFee, uint256 sellFee) external view returns (address) { + return _gsmFeeStrategiesByFees[buyFee][sellFee]; + } + + ///@inheritdoc IFixedFeeStrategyFactory + function REVISION() public pure virtual override returns (uint256) { + return 1; + } + + /// @inheritdoc VersionedInitializable + function getRevision() internal pure virtual override returns (uint256) { + return REVISION(); + } +} diff --git a/src/contracts/facilitators/gsm/feeStrategy/interfaces/IFixedFeeStrategyFactory.sol b/src/contracts/facilitators/gsm/feeStrategy/interfaces/IFixedFeeStrategyFactory.sol new file mode 100644 index 00000000..63e41ae2 --- /dev/null +++ b/src/contracts/facilitators/gsm/feeStrategy/interfaces/IFixedFeeStrategyFactory.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @title IFixedFeeStrategyFactory + * @author Aave Labs + * @notice Defines the interface of the FixedFeeStrategyFactory + */ +interface IFixedFeeStrategyFactory { + /** + * @dev Emitted when a new strategy is created + * @param strategy The address of the new Gsm fee strategy + * @param buyFee The buy fee of the new strategy + * @param sellFee The sell fee of the new strategy + */ + event FeeStrategyCreated( + address indexed strategy, + uint256 indexed buyFee, + uint256 indexed sellFee + ); + + /** + * @notice Creates new Gsm Fee strategy contracts from lists of buy and sell fees + * @dev Returns the address of a cached contract if a strategy with same fees already exists + * @param buyFeeList The list of buy fees for Gsm fee strategies + * @param sellFeeList The list of sell fees for Gsm fee strategies + * @return The list of Gsm fee strategy contracts + */ + function createStrategies( + uint256[] memory buyFeeList, + uint256[] memory sellFeeList + ) external returns (address[] memory); + + /** + * @notice Returns all the fee strategy contracts of the factory + * @return The list of fee strategy contracts + */ + function getFixedFeeStrategies() external view returns (address[] memory); + + /** + * @notice Returns the fee strategy contract which corresponds to the given fees. + * @dev Returns `address(0)` if there is no fee strategy for the given fees + * @param buyFee The buy fee of the fee strategy contract + * @param sellFee The sell fee of the fee strategy contract + * @return The address of the fee strategy contract + */ + function getFixedFeeStrategy(uint256 buyFee, uint256 sellFee) external view returns (address); + + /** + * @notice Returns the GsmFeeStrategyFactory revision number + * @return The revision number + */ + function REVISION() external pure returns (uint256); +} diff --git a/src/contracts/misc/GhoAaveSteward.sol b/src/contracts/misc/GhoAaveSteward.sol new file mode 100644 index 00000000..5481f064 --- /dev/null +++ b/src/contracts/misc/GhoAaveSteward.sol @@ -0,0 +1,281 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; +import {IPoolDataProvider} from '@aave/core-v3/contracts/interfaces/IPoolDataProvider.sol'; +import {IPoolAddressesProvider} from '@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol'; +import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; +import {DataTypes} from '@aave/core-v3/contracts/protocol/libraries/types/DataTypes.sol'; +import {ReserveConfiguration} from '@aave/core-v3/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; +import {IPoolConfigurator, IDefaultInterestRateStrategyV2} from './dependencies/AaveV3-1.sol'; +import {IGhoAaveSteward} from './interfaces/IGhoAaveSteward.sol'; +import {RiskCouncilControlled} from './RiskCouncilControlled.sol'; + +/** + * @title GhoAaveSteward + * @author Aave Labs + * @notice Helper contract for managing parameters of the GHO reserve + * @dev Only the Risk Council is able to action contract's functions, based on specific conditions that have been agreed upon with the community. + * @dev Requires role RiskAdmin on the Aave V3 Ethereum Pool + */ +contract GhoAaveSteward is Ownable, RiskCouncilControlled, IGhoAaveSteward { + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + + /// @inheritdoc IGhoAaveSteward + uint32 public constant GHO_BORROW_RATE_MAX = 0.25e4; // 25.00% + + uint256 internal constant BPS_MAX = 100_00; + + /// @inheritdoc IGhoAaveSteward + address public immutable POOL_DATA_PROVIDER; + + /// @inheritdoc IGhoAaveSteward + uint256 public constant MINIMUM_DELAY = 1 days; + + /// @inheritdoc IGhoAaveSteward + address public immutable POOL_ADDRESSES_PROVIDER; + + /// @inheritdoc IGhoAaveSteward + address public immutable GHO_TOKEN; + + BorrowRateConfig internal _borrowRateConfig; + + GhoDebounce internal _ghoTimelocks; + + /** + * @dev Only methods that are not timelocked can be called if marked by this modifier. + */ + modifier notTimelocked(uint40 timelock) { + require(block.timestamp - timelock > MINIMUM_DELAY, 'DEBOUNCE_NOT_RESPECTED'); + _; + } + + /** + * @dev Constructor + * @param owner The address of the contract's owner + * @param addressesProvider The address of the PoolAddressesProvider of Aave V3 Ethereum Pool + * @param poolDataProvider The pool data provider of the pool to be controlled by the steward + * @param ghoToken The address of the GhoToken + * @param riskCouncil The address of the risk council + * @param borrowRateConfig The configuration conditions for GHO borrow rate changes + */ + constructor( + address owner, + address addressesProvider, + address poolDataProvider, + address ghoToken, + address riskCouncil, + BorrowRateConfig memory borrowRateConfig + ) RiskCouncilControlled(riskCouncil) { + require(owner != address(0), 'INVALID_OWNER'); + require(addressesProvider != address(0), 'INVALID_ADDRESSES_PROVIDER'); + require(poolDataProvider != address(0), 'INVALID_DATA_PROVIDER'); + require(ghoToken != address(0), 'INVALID_GHO_TOKEN'); + + POOL_ADDRESSES_PROVIDER = addressesProvider; + POOL_DATA_PROVIDER = poolDataProvider; + GHO_TOKEN = ghoToken; + _borrowRateConfig = borrowRateConfig; + + _transferOwnership(owner); + } + + /// @inheritdoc IGhoAaveSteward + function updateGhoBorrowRate( + uint16 optimalUsageRatio, + uint32 baseVariableBorrowRate, + uint32 variableRateSlope1, + uint32 variableRateSlope2 + ) external onlyRiskCouncil notTimelocked(_ghoTimelocks.ghoBorrowRateLastUpdate) { + IDefaultInterestRateStrategyV2.InterestRateData + memory rateParams = IDefaultInterestRateStrategyV2.InterestRateData({ + optimalUsageRatio: optimalUsageRatio, + baseVariableBorrowRate: baseVariableBorrowRate, + variableRateSlope1: variableRateSlope1, + variableRateSlope2: variableRateSlope2 + }); + _validateRatesUpdate(rateParams); + + _ghoTimelocks.ghoBorrowRateLastUpdate = uint40(block.timestamp); + + IPoolConfigurator(IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER).getPoolConfigurator()) + .setReserveInterestRateData(GHO_TOKEN, abi.encode(rateParams)); + } + + /// @inheritdoc IGhoAaveSteward + function updateGhoBorrowCap( + uint256 newBorrowCap + ) external onlyRiskCouncil notTimelocked(_ghoTimelocks.ghoBorrowCapLastUpdate) { + DataTypes.ReserveConfigurationMap memory configuration = IPool( + IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER).getPool() + ).getConfiguration(GHO_TOKEN); + uint256 currentBorrowCap = configuration.getBorrowCap(); + require(newBorrowCap != currentBorrowCap, 'NO_CHANGE_IN_BORROW_CAP'); + require( + _isDifferenceLowerThanMax(currentBorrowCap, newBorrowCap, currentBorrowCap), + 'INVALID_BORROW_CAP_UPDATE' + ); + + _ghoTimelocks.ghoBorrowCapLastUpdate = uint40(block.timestamp); + + IPoolConfigurator(IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER).getPoolConfigurator()) + .setBorrowCap(GHO_TOKEN, newBorrowCap); + } + + /// @inheritdoc IGhoAaveSteward + function updateGhoSupplyCap( + uint256 newSupplyCap + ) external onlyRiskCouncil notTimelocked(_ghoTimelocks.ghoSupplyCapLastUpdate) { + DataTypes.ReserveConfigurationMap memory configuration = IPool( + IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER).getPool() + ).getConfiguration(GHO_TOKEN); + uint256 currentSupplyCap = configuration.getSupplyCap(); + require(newSupplyCap != currentSupplyCap, 'NO_CHANGE_IN_SUPPLY_CAP'); + require( + _isDifferenceLowerThanMax(currentSupplyCap, newSupplyCap, currentSupplyCap), + 'INVALID_SUPPLY_CAP_UPDATE' + ); + + _ghoTimelocks.ghoSupplyCapLastUpdate = uint40(block.timestamp); + + IPoolConfigurator(IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER).getPoolConfigurator()) + .setSupplyCap(GHO_TOKEN, newSupplyCap); + } + + /// @inheritdoc IGhoAaveSteward + function setBorrowRateConfig( + uint16 optimalUsageRatioMaxChange, + uint32 baseVariableBorrowRateMaxChange, + uint32 variableRateSlope1MaxChange, + uint32 variableRateSlope2MaxChange + ) external onlyOwner { + _borrowRateConfig.optimalUsageRatioMaxChange = optimalUsageRatioMaxChange; + _borrowRateConfig.baseVariableBorrowRateMaxChange = baseVariableBorrowRateMaxChange; + _borrowRateConfig.variableRateSlope1MaxChange = variableRateSlope1MaxChange; + _borrowRateConfig.variableRateSlope2MaxChange = variableRateSlope2MaxChange; + } + + /// @inheritdoc IGhoAaveSteward + function getBorrowRateConfig() external view returns (BorrowRateConfig memory) { + return _borrowRateConfig; + } + + /// @inheritdoc IGhoAaveSteward + function getGhoTimelocks() external view returns (GhoDebounce memory) { + return _ghoTimelocks; + } + + /// @inheritdoc IGhoAaveSteward + function RISK_COUNCIL() public view override returns (address) { + return _riskCouncil; + } + + /** + * @dev Validates the interest rates update + * @param newRates The new interest rate data + */ + function _validateRatesUpdate( + IDefaultInterestRateStrategyV2.InterestRateData memory newRates + ) internal view { + address rateStrategyAddress = IPoolDataProvider(POOL_DATA_PROVIDER) + .getInterestRateStrategyAddress(GHO_TOKEN); + IDefaultInterestRateStrategyV2.InterestRateData + memory currentRates = IDefaultInterestRateStrategyV2(rateStrategyAddress) + .getInterestRateDataBps(GHO_TOKEN); + + require( + newRates.optimalUsageRatio != currentRates.optimalUsageRatio || + newRates.baseVariableBorrowRate != currentRates.baseVariableBorrowRate || + newRates.variableRateSlope1 != currentRates.variableRateSlope1 || + newRates.variableRateSlope2 != currentRates.variableRateSlope2, + 'NO_CHANGE_IN_RATES' + ); + + require( + _updateWithinAllowedRange( + currentRates.optimalUsageRatio, + newRates.optimalUsageRatio, + _borrowRateConfig.optimalUsageRatioMaxChange, + false + ), + 'INVALID_OPTIMAL_USAGE_RATIO' + ); + require( + _updateWithinAllowedRange( + currentRates.baseVariableBorrowRate, + newRates.baseVariableBorrowRate, + _borrowRateConfig.baseVariableBorrowRateMaxChange, + false + ), + 'INVALID_BORROW_RATE_UPDATE' + ); + require( + _updateWithinAllowedRange( + currentRates.variableRateSlope1, + newRates.variableRateSlope1, + _borrowRateConfig.variableRateSlope1MaxChange, + false + ), + 'INVALID_VARIABLE_RATE_SLOPE1' + ); + require( + _updateWithinAllowedRange( + currentRates.variableRateSlope2, + newRates.variableRateSlope2, + _borrowRateConfig.variableRateSlope2MaxChange, + false + ), + 'INVALID_VARIABLE_RATE_SLOPE2' + ); + + require( + uint256(newRates.baseVariableBorrowRate) + + uint256(newRates.variableRateSlope1) + + uint256(newRates.variableRateSlope2) <= + GHO_BORROW_RATE_MAX, + 'BORROW_RATE_HIGHER_THAN_MAX' + ); + } + + /** + * @dev Ensures that the change difference is lower than max. + * @param from current value + * @param to new value + * @param max maximum difference between from and to + * @return bool true if difference between values lower than max, false otherwise + */ + function _isDifferenceLowerThanMax( + uint256 from, + uint256 to, + uint256 max + ) internal pure returns (bool) { + return from < to ? to - from <= max : from - to <= max; + } + + /** + * @notice Ensures the risk param update is within the allowed range + * @param from current risk param value + * @param to new updated risk param value + * @param maxPercentChange the max percent change allowed + * @param isChangeRelative true, if maxPercentChange is relative in value, false if maxPercentChange + * is absolute in value. + * @return bool true, if difference is within the maxPercentChange + */ + function _updateWithinAllowedRange( + uint256 from, + uint256 to, + uint256 maxPercentChange, + bool isChangeRelative + ) internal pure returns (bool) { + // diff denotes the difference between the from and to values, ensuring it is a positive value always + uint256 diff = from > to ? from - to : to - from; + + // maxDiff denotes the max permitted difference on both the upper and lower bounds, if the maxPercentChange is relative in value + // we calculate the max permitted difference using the maxPercentChange and the from value, otherwise if the maxPercentChange is absolute in value + // the max permitted difference is the maxPercentChange itself + uint256 maxDiff = isChangeRelative ? (maxPercentChange * from) / BPS_MAX : maxPercentChange; + + if (diff > maxDiff) return false; + return true; + } +} diff --git a/src/contracts/misc/GhoBucketSteward.sol b/src/contracts/misc/GhoBucketSteward.sol new file mode 100644 index 00000000..4c764bc6 --- /dev/null +++ b/src/contracts/misc/GhoBucketSteward.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; +import {EnumerableSet} from '@openzeppelin/contracts/utils/structs/EnumerableSet.sol'; +import {IGhoToken} from '../gho/interfaces/IGhoToken.sol'; +import {RiskCouncilControlled} from './RiskCouncilControlled.sol'; +import {IGhoBucketSteward} from './interfaces/IGhoBucketSteward.sol'; + +/** + * @title GhoBucketSteward + * @author Aave Labs + * @notice Helper contract for managing bucket capacities of controlled facilitators + * @dev Only the Risk Council is able to action contract's functions, based on specific conditions that have been agreed upon with the community. + * @dev Requires role GHO_TOKEN_BUCKET_MANAGER_ROLE on GhoToken + */ +contract GhoBucketSteward is Ownable, RiskCouncilControlled, IGhoBucketSteward { + using EnumerableSet for EnumerableSet.AddressSet; + + /// @inheritdoc IGhoBucketSteward + uint256 public constant MINIMUM_DELAY = 1 days; + + /// @inheritdoc IGhoBucketSteward + address public immutable GHO_TOKEN; + + mapping(address => uint40) internal _facilitatorsBucketCapacityTimelocks; + + mapping(address => bool) internal _controlledFacilitatorsByAddress; + EnumerableSet.AddressSet internal _controlledFacilitators; + + /** + * @dev Only methods that are not timelocked can be called if marked by this modifier. + */ + modifier notTimelocked(uint40 timelock) { + require(block.timestamp - timelock > MINIMUM_DELAY, 'DEBOUNCE_NOT_RESPECTED'); + _; + } + + /** + * @dev Constructor + * @param owner The address of the contract's owner + * @param ghoToken The address of the GhoToken + * @param riskCouncil The address of the risk council + */ + constructor( + address owner, + address ghoToken, + address riskCouncil + ) RiskCouncilControlled(riskCouncil) { + require(owner != address(0), 'INVALID_OWNER'); + require(ghoToken != address(0), 'INVALID_GHO_TOKEN'); + + GHO_TOKEN = ghoToken; + + _transferOwnership(owner); + } + + /// @inheritdoc IGhoBucketSteward + function updateFacilitatorBucketCapacity( + address facilitator, + uint128 newBucketCapacity + ) external onlyRiskCouncil notTimelocked(_facilitatorsBucketCapacityTimelocks[facilitator]) { + require(_controlledFacilitatorsByAddress[facilitator], 'FACILITATOR_NOT_CONTROLLED'); + (uint256 currentBucketCapacity, ) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(facilitator); + require(newBucketCapacity != currentBucketCapacity, 'NO_CHANGE_IN_BUCKET_CAPACITY'); + require( + _isIncreaseLowerThanMax(currentBucketCapacity, newBucketCapacity, currentBucketCapacity), + 'INVALID_BUCKET_CAPACITY_UPDATE' + ); + + _facilitatorsBucketCapacityTimelocks[facilitator] = uint40(block.timestamp); + + IGhoToken(GHO_TOKEN).setFacilitatorBucketCapacity(facilitator, newBucketCapacity); + } + + /// @inheritdoc IGhoBucketSteward + function setControlledFacilitator( + address[] memory facilitatorList, + bool approve + ) external onlyOwner { + for (uint256 i = 0; i < facilitatorList.length; i++) { + _controlledFacilitatorsByAddress[facilitatorList[i]] = approve; + if (approve) { + _controlledFacilitators.add(facilitatorList[i]); + } else { + _controlledFacilitators.remove(facilitatorList[i]); + } + } + } + + /// @inheritdoc IGhoBucketSteward + function getControlledFacilitators() external view returns (address[] memory) { + return _controlledFacilitators.values(); + } + + /// @inheritdoc IGhoBucketSteward + function getFacilitatorBucketCapacityTimelock( + address facilitator + ) external view returns (uint40) { + return _facilitatorsBucketCapacityTimelocks[facilitator]; + } + + /// @inheritdoc IGhoBucketSteward + function RISK_COUNCIL() public view override returns (address) { + return _riskCouncil; + } + + /** + * @dev Ensures that the change is positive and the difference is lower than max. + * @param from current value + * @param to new value + * @param max maximum difference between from and to + * @return bool true if difference between values is positive and lower than max, false otherwise + */ + function _isIncreaseLowerThanMax( + uint256 from, + uint256 to, + uint256 max + ) internal pure returns (bool) { + return to >= from && to - from <= max; + } +} diff --git a/src/contracts/misc/GhoCcipSteward.sol b/src/contracts/misc/GhoCcipSteward.sol new file mode 100644 index 00000000..26235676 --- /dev/null +++ b/src/contracts/misc/GhoCcipSteward.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {IUpgradeableLockReleaseTokenPool, RateLimiter} from './dependencies/Ccip.sol'; +import {IGhoCcipSteward} from './interfaces/IGhoCcipSteward.sol'; +import {RiskCouncilControlled} from './RiskCouncilControlled.sol'; + +/** + * @title GhoCcipSteward + * @author Aave Labs + * @notice Helper contract for managing parameters of the CCIP token pools + * @dev Only the Risk Council is able to action contract's functions, based on specific conditions that have been agreed upon with the community. + * @dev Requires roles RateLimitAdmin and BridgeLimitAdmin (if on Ethereum) on GhoTokenPool + */ +contract GhoCcipSteward is RiskCouncilControlled, IGhoCcipSteward { + /// @inheritdoc IGhoCcipSteward + uint256 public constant MINIMUM_DELAY = 1 days; + + /// @inheritdoc IGhoCcipSteward + address public immutable GHO_TOKEN; + + /// @inheritdoc IGhoCcipSteward + address public immutable GHO_TOKEN_POOL; + + /// @inheritdoc IGhoCcipSteward + bool public immutable BRIDGE_LIMIT_ENABLED; + + CcipDebounce internal _ccipTimelocks; + + /** + * @dev Only methods that are not timelocked can be called if marked by this modifier. + */ + modifier notTimelocked(uint40 timelock) { + require(block.timestamp - timelock > MINIMUM_DELAY, 'DEBOUNCE_NOT_RESPECTED'); + _; + } + + /** + * @dev Constructor + * @param ghoToken The address of the GhoToken + * @param ghoTokenPool The address of the Gho CCIP Token Pool + * @param riskCouncil The address of the risk council + * @param bridgeLimitEnabled Whether the bridge limit feature is supported in the GhoTokenPool + */ + constructor( + address ghoToken, + address ghoTokenPool, + address riskCouncil, + bool bridgeLimitEnabled + ) RiskCouncilControlled(riskCouncil) { + require(ghoToken != address(0), 'INVALID_GHO_TOKEN'); + require(ghoTokenPool != address(0), 'INVALID_GHO_TOKEN_POOL'); + + GHO_TOKEN = ghoToken; + GHO_TOKEN_POOL = ghoTokenPool; + BRIDGE_LIMIT_ENABLED = bridgeLimitEnabled; + } + + /// @inheritdoc IGhoCcipSteward + function updateBridgeLimit( + uint256 newBridgeLimit + ) external onlyRiskCouncil notTimelocked(_ccipTimelocks.bridgeLimitLastUpdate) { + require(BRIDGE_LIMIT_ENABLED, 'BRIDGE_LIMIT_DISABLED'); + + uint256 currentBridgeLimit = IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL).getBridgeLimit(); + require(newBridgeLimit != currentBridgeLimit, 'NO_CHANGE_IN_BRIDGE_LIMIT'); + require( + _isDifferenceLowerThanMax(currentBridgeLimit, newBridgeLimit, currentBridgeLimit), + 'INVALID_BRIDGE_LIMIT_UPDATE' + ); + + _ccipTimelocks.bridgeLimitLastUpdate = uint40(block.timestamp); + + IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL).setBridgeLimit(newBridgeLimit); + } + + /// @inheritdoc IGhoCcipSteward + function updateRateLimit( + uint64 remoteChainSelector, + bool outboundEnabled, + uint128 outboundCapacity, + uint128 outboundRate, + bool inboundEnabled, + uint128 inboundCapacity, + uint128 inboundRate + ) external onlyRiskCouncil notTimelocked(_ccipTimelocks.rateLimitLastUpdate) { + RateLimiter.TokenBucket memory outboundConfig = IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL) + .getCurrentOutboundRateLimiterState(remoteChainSelector); + RateLimiter.TokenBucket memory inboundConfig = IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL) + .getCurrentInboundRateLimiterState(remoteChainSelector); + + require( + outboundEnabled != outboundConfig.isEnabled || + outboundCapacity != outboundConfig.capacity || + outboundRate != outboundConfig.rate || + inboundEnabled != inboundConfig.isEnabled || + inboundCapacity != inboundConfig.capacity || + inboundRate != inboundConfig.rate, + 'NO_CHANGE_IN_RATE_LIMIT' + ); + + require( + _isDifferenceLowerThanMax(outboundConfig.capacity, outboundCapacity, outboundConfig.capacity), + 'INVALID_RATE_LIMIT_UPDATE' + ); + require( + _isDifferenceLowerThanMax(outboundConfig.rate, outboundRate, outboundConfig.rate), + 'INVALID_RATE_LIMIT_UPDATE' + ); + require( + _isDifferenceLowerThanMax(inboundConfig.capacity, inboundCapacity, inboundConfig.capacity), + 'INVALID_RATE_LIMIT_UPDATE' + ); + require( + _isDifferenceLowerThanMax(inboundConfig.rate, inboundRate, inboundConfig.rate), + 'INVALID_RATE_LIMIT_UPDATE' + ); + + _ccipTimelocks.rateLimitLastUpdate = uint40(block.timestamp); + + IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL).setChainRateLimiterConfig( + remoteChainSelector, + RateLimiter.Config({ + isEnabled: outboundEnabled, + capacity: outboundCapacity, + rate: outboundRate + }), + RateLimiter.Config({isEnabled: inboundEnabled, capacity: inboundCapacity, rate: inboundRate}) + ); + } + + /// @inheritdoc IGhoCcipSteward + function RISK_COUNCIL() public view override returns (address) { + return _riskCouncil; + } + + /** + * @dev Ensures that the change difference is lower than max. + * @param from current value + * @param to new value + * @param max maximum difference between from and to + * @return bool true if difference between values lower than max, false otherwise + */ + function _isDifferenceLowerThanMax( + uint256 from, + uint256 to, + uint256 max + ) internal pure returns (bool) { + return from < to ? to - from <= max : from - to <= max; + } +} diff --git a/src/contracts/misc/GhoGsmSteward.sol b/src/contracts/misc/GhoGsmSteward.sol new file mode 100644 index 00000000..adaf407f --- /dev/null +++ b/src/contracts/misc/GhoGsmSteward.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {IGsm} from '../facilitators/gsm/interfaces/IGsm.sol'; +import {IGsmFeeStrategy} from '../facilitators/gsm/feeStrategy/interfaces/IGsmFeeStrategy.sol'; +import {IFixedFeeStrategyFactory} from '../facilitators/gsm/feeStrategy/interfaces/IFixedFeeStrategyFactory.sol'; +import {IGhoGsmSteward} from './interfaces/IGhoGsmSteward.sol'; +import {RiskCouncilControlled} from './RiskCouncilControlled.sol'; + +/** + * @title GhoGsmSteward + * @author Aave Labs + * @notice Helper contract for managing parameters of the GSM + * @dev Only the Risk Council is able to action contract's functions, based on specific conditions that have been agreed upon with the community. + * @dev Requires role GSM_CONFIGURATOR_ROLE on every GSM contract to be managed + */ +contract GhoGsmSteward is RiskCouncilControlled, IGhoGsmSteward { + /// @inheritdoc IGhoGsmSteward + uint256 public constant GSM_FEE_RATE_CHANGE_MAX = 0.0050e4; // 0.50% + + /// @inheritdoc IGhoGsmSteward + uint256 public constant MINIMUM_DELAY = 1 days; + + /// @inheritdoc IGhoGsmSteward + address public immutable FIXED_FEE_STRATEGY_FACTORY; + + mapping(address => GsmDebounce) internal _gsmTimelocksByAddress; + + /** + * @dev Only methods that are not timelocked can be called if marked by this modifier. + */ + modifier notTimelocked(uint40 timelock) { + require(block.timestamp - timelock > MINIMUM_DELAY, 'DEBOUNCE_NOT_RESPECTED'); + _; + } + + /** + * @dev Constructor + * @param fixedFeeStrategyFactory The address of the Fixed Fee Strategy Factory + * @param riskCouncil The address of the risk council + */ + constructor( + address fixedFeeStrategyFactory, + address riskCouncil + ) RiskCouncilControlled(riskCouncil) { + require(fixedFeeStrategyFactory != address(0), 'INVALID_FIXED_FEE_STRATEGY_FACTORY'); + + FIXED_FEE_STRATEGY_FACTORY = fixedFeeStrategyFactory; + } + + /// @inheritdoc IGhoGsmSteward + function updateGsmExposureCap( + address gsm, + uint128 newExposureCap + ) external onlyRiskCouncil notTimelocked(_gsmTimelocksByAddress[gsm].gsmExposureCapLastUpdated) { + uint128 currentExposureCap = IGsm(gsm).getExposureCap(); + require(newExposureCap != currentExposureCap, 'NO_CHANGE_IN_EXPOSURE_CAP'); + require( + _isDifferenceLowerThanMax(currentExposureCap, newExposureCap, currentExposureCap), + 'INVALID_EXPOSURE_CAP_UPDATE' + ); + + _gsmTimelocksByAddress[gsm].gsmExposureCapLastUpdated = uint40(block.timestamp); + + IGsm(gsm).updateExposureCap(newExposureCap); + } + + /// @inheritdoc IGhoGsmSteward + function updateGsmBuySellFees( + address gsm, + uint256 buyFee, + uint256 sellFee + ) external onlyRiskCouncil notTimelocked(_gsmTimelocksByAddress[gsm].gsmFeeStrategyLastUpdated) { + address currentFeeStrategy = IGsm(gsm).getFeeStrategy(); + require(currentFeeStrategy != address(0), 'FIXED_FEE_STRATEGY_NOT_FOUND'); + + uint256 currentBuyFee = IGsmFeeStrategy(currentFeeStrategy).getBuyFee(1e4); + uint256 currentSellFee = IGsmFeeStrategy(currentFeeStrategy).getSellFee(1e4); + require(buyFee != currentBuyFee || sellFee != currentSellFee, 'NO_CHANGE_IN_FEES'); + require( + _isDifferenceLowerThanMax(currentBuyFee, buyFee, GSM_FEE_RATE_CHANGE_MAX), + 'INVALID_BUY_FEE_UPDATE' + ); + require( + _isDifferenceLowerThanMax(currentSellFee, sellFee, GSM_FEE_RATE_CHANGE_MAX), + 'INVALID_SELL_FEE_UPDATE' + ); + + IFixedFeeStrategyFactory strategyFactory = IFixedFeeStrategyFactory(FIXED_FEE_STRATEGY_FACTORY); + uint256[] memory buyFeeList = new uint256[](1); + uint256[] memory sellFeeList = new uint256[](1); + buyFeeList[0] = buyFee; + sellFeeList[0] = sellFee; + address strategy = strategyFactory.createStrategies(buyFeeList, sellFeeList)[0]; + + _gsmTimelocksByAddress[gsm].gsmFeeStrategyLastUpdated = uint40(block.timestamp); + + IGsm(gsm).updateFeeStrategy(strategy); + } + + /// @inheritdoc IGhoGsmSteward + function getGsmTimelocks(address gsm) external view returns (GsmDebounce memory) { + return _gsmTimelocksByAddress[gsm]; + } + + /// @inheritdoc IGhoGsmSteward + function RISK_COUNCIL() public view override returns (address) { + return _riskCouncil; + } + + /** + * @dev Ensures that the change difference is lower than max. + * @param from current value + * @param to new value + * @param max maximum difference between from and to + * @return bool true if difference between values lower than max, false otherwise + */ + function _isDifferenceLowerThanMax( + uint256 from, + uint256 to, + uint256 max + ) internal pure returns (bool) { + return from < to ? to - from <= max : from - to <= max; + } +} diff --git a/src/contracts/misc/GhoSteward.sol b/src/contracts/misc/GhoSteward.sol deleted file mode 100644 index 7db4a400..00000000 --- a/src/contracts/misc/GhoSteward.sol +++ /dev/null @@ -1,193 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.10; - -import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; -import {IPoolAddressesProvider} from '@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol'; -import {IPoolConfigurator} from '@aave/core-v3/contracts/interfaces/IPoolConfigurator.sol'; -import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; -import {DataTypes} from '@aave/core-v3/contracts/protocol/libraries/types/DataTypes.sol'; -import {PercentageMath} from '@aave/core-v3/contracts/protocol/libraries/math/PercentageMath.sol'; -import {GhoInterestRateStrategy} from '../facilitators/aave/interestStrategy/GhoInterestRateStrategy.sol'; -import {IGhoToken} from '../gho/interfaces/IGhoToken.sol'; -import {IGhoSteward} from './interfaces/IGhoSteward.sol'; - -/** - * @title GhoSteward - * @author Aave - * @notice Helper contract for managing risk parameters of the GHO reserve within the Aave Facilitator - * @dev This contract must be granted `PoolAdmin` in the Aave V3 Ethereum Pool and `BucketManager` in GHO Token - * @dev Only the Risk Council is able to action contract's functions. - * @dev Only the Aave DAO is able to extend the steward's lifespan. - */ -contract GhoSteward is Ownable, IGhoSteward { - using PercentageMath for uint256; - - /// @inheritdoc IGhoSteward - uint256 public constant MINIMUM_DELAY = 5 days; - - /// @inheritdoc IGhoSteward - uint256 public constant BORROW_RATE_CHANGE_MAX = 0.01e4; - - /// @inheritdoc IGhoSteward - uint40 public constant STEWARD_LIFESPAN = 90 days; - - /// @inheritdoc IGhoSteward - address public immutable POOL_ADDRESSES_PROVIDER; - - /// @inheritdoc IGhoSteward - address public immutable GHO_TOKEN; - - /// @inheritdoc IGhoSteward - address public immutable RISK_COUNCIL; - - Debounce internal _timelocks; - uint40 internal _stewardExpiration; - mapping(uint256 => address) internal _strategiesByRate; - address[] internal _strategies; - - /** - * @dev Only Risk Council can call functions marked by this modifier. - */ - modifier onlyRiskCouncil() { - require(RISK_COUNCIL == msg.sender, 'INVALID_CALLER'); - _; - } - - /** - * @dev Constructor - * @param addressesProvider The address of the PoolAddressesProvider of Aave V3 Ethereum Pool - * @param ghoToken The address of the GhoToken - * @param riskCouncil The address of the RiskCouncil - * @param shortExecutor The address of the Aave Short Executor - */ - constructor( - address addressesProvider, - address ghoToken, - address riskCouncil, - address shortExecutor - ) { - require(addressesProvider != address(0), 'INVALID_ADDRESSES_PROVIDER'); - require(ghoToken != address(0), 'INVALID_GHO_TOKEN'); - require(riskCouncil != address(0), 'INVALID_RISK_COUNCIL'); - require(shortExecutor != address(0), 'INVALID_SHORT_EXECUTOR'); - POOL_ADDRESSES_PROVIDER = addressesProvider; - GHO_TOKEN = ghoToken; - RISK_COUNCIL = riskCouncil; - _stewardExpiration = uint40(block.timestamp + STEWARD_LIFESPAN); - - _transferOwnership(shortExecutor); - } - - /// @inheritdoc IGhoSteward - function updateBorrowRate(uint256 newBorrowRate) external onlyRiskCouncil { - require(block.timestamp < _stewardExpiration, 'STEWARD_EXPIRED'); - require( - block.timestamp - _timelocks.borrowRateLastUpdated > MINIMUM_DELAY, - 'DEBOUNCE_NOT_RESPECTED' - ); - - DataTypes.ReserveData memory ghoReserveData = IPool( - IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER).getPool() - ).getReserveData(GHO_TOKEN); - require( - ghoReserveData.interestRateStrategyAddress != address(0), - 'GHO_INTEREST_RATE_STRATEGY_NOT_FOUND' - ); - - uint256 oldBorrowRate = GhoInterestRateStrategy(ghoReserveData.interestRateStrategyAddress) - .getBaseVariableBorrowRate(); - require(_borrowRateChangeAllowed(oldBorrowRate, newBorrowRate), 'INVALID_BORROW_RATE_UPDATE'); - - _timelocks.borrowRateLastUpdated = uint40(block.timestamp); - - address cachedStrategyAddress = _strategiesByRate[newBorrowRate]; - // Deploy a new one if does not exist - if (cachedStrategyAddress == address(0)) { - GhoInterestRateStrategy newRateStrategy = new GhoInterestRateStrategy( - POOL_ADDRESSES_PROVIDER, - newBorrowRate - ); - cachedStrategyAddress = address(newRateStrategy); - - _strategiesByRate[newBorrowRate] = address(newRateStrategy); - _strategies.push(address(newRateStrategy)); - } - - IPoolConfigurator(IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER).getPoolConfigurator()) - .setReserveInterestRateStrategyAddress(GHO_TOKEN, cachedStrategyAddress); - } - - /// @inheritdoc IGhoSteward - function updateBucketCapacity(uint128 newBucketCapacity) external onlyRiskCouncil { - require(block.timestamp < _stewardExpiration, 'STEWARD_EXPIRED'); - require( - block.timestamp - _timelocks.bucketCapacityLastUpdated > MINIMUM_DELAY, - 'DEBOUNCE_NOT_RESPECTED' - ); - - DataTypes.ReserveData memory ghoReserveData = IPool( - IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER).getPool() - ).getReserveData(GHO_TOKEN); - require(ghoReserveData.aTokenAddress != address(0), 'GHO_ATOKEN_NOT_FOUND'); - - (uint256 oldBucketCapacity, ) = IGhoToken(GHO_TOKEN).getFacilitatorBucket( - ghoReserveData.aTokenAddress - ); - require( - _bucketCapacityIncreaseAllowed(oldBucketCapacity, newBucketCapacity), - 'INVALID_BUCKET_CAPACITY_UPDATE' - ); - - _timelocks.bucketCapacityLastUpdated = uint40(block.timestamp); - - IGhoToken(GHO_TOKEN).setFacilitatorBucketCapacity( - ghoReserveData.aTokenAddress, - newBucketCapacity - ); - } - - /// @inheritdoc IGhoSteward - function extendStewardExpiration() external onlyOwner { - uint40 oldStewardExpiration = _stewardExpiration; - _stewardExpiration += uint40(STEWARD_LIFESPAN); - emit StewardExpirationUpdated(oldStewardExpiration, _stewardExpiration); - } - - /// @inheritdoc IGhoSteward - function getTimelock() external view returns (Debounce memory) { - return _timelocks; - } - - /// @inheritdoc IGhoSteward - function getStewardExpiration() external view returns (uint40) { - return _stewardExpiration; - } - - /// @inheritdoc IGhoSteward - function getAllStrategies() external view returns (address[] memory) { - return _strategies; - } - - /** - * @notice Ensures the borrow rate change is within the allowed range. - * @param from current borrow rate (in ray) - * @param to new borrow rate (in ray) - * @return bool true, if difference is within the max 0.5% change window - */ - function _borrowRateChangeAllowed(uint256 from, uint256 to) internal pure returns (bool) { - return - from < to - ? to - from <= from.percentMul(BORROW_RATE_CHANGE_MAX) - : from - to <= from.percentMul(BORROW_RATE_CHANGE_MAX); - } - - /** - * @notice Ensures the bucket capacity increase is within the allowed range. - * @param from current bucket capacity - * @param to new bucket capacity - * @return bool true, if difference is within the max 100% increase window - */ - function _bucketCapacityIncreaseAllowed(uint256 from, uint256 to) internal pure returns (bool) { - return to >= from && to - from <= from; - } -} diff --git a/src/contracts/misc/GhoStewardV2.sol b/src/contracts/misc/GhoStewardV2.sol deleted file mode 100644 index 40311ed4..00000000 --- a/src/contracts/misc/GhoStewardV2.sol +++ /dev/null @@ -1,302 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.10; - -import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; -import {EnumerableSet} from '@openzeppelin/contracts/utils/structs/EnumerableSet.sol'; -import {IPoolAddressesProvider} from '@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol'; -import {IPoolConfigurator} from '@aave/core-v3/contracts/interfaces/IPoolConfigurator.sol'; -import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; -import {DataTypes} from '@aave/core-v3/contracts/protocol/libraries/types/DataTypes.sol'; -import {ReserveConfiguration} from '@aave/core-v3/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; -import {GhoInterestRateStrategy} from '../facilitators/aave/interestStrategy/GhoInterestRateStrategy.sol'; -import {IFixedRateStrategyFactory} from '../facilitators/aave/interestStrategy/interfaces/IFixedRateStrategyFactory.sol'; -import {FixedFeeStrategy} from '../facilitators/gsm/feeStrategy/FixedFeeStrategy.sol'; -import {IGsm} from '../facilitators/gsm/interfaces/IGsm.sol'; -import {IGsmFeeStrategy} from '../facilitators/gsm/feeStrategy/interfaces/IGsmFeeStrategy.sol'; -import {IGhoToken} from '../gho/interfaces/IGhoToken.sol'; -import {IGhoStewardV2} from './interfaces/IGhoStewardV2.sol'; - -/** - * @title GhoStewardV2 - * @author Aave Labs - * @notice Helper contract for managing parameters of the GHO reserve and GSM - * @dev This contract must be granted `RiskAdmin` in the Aave V3 Ethereum Pool, `BucketManager` in GHO Token and `Configurator` in every GSM asset to be managed. - * @dev Only the Risk Council is able to action contract's functions, based on specific conditions that have been agreed upon with the community. - * @dev Only the Aave DAO is able add or remove approved GSMs. - * @dev When updating GSM fee strategy the method assumes that the current strategy is FixedFeeStrategy for enforcing parameters - * @dev FixedFeeStrategy is used when creating a new strategy for GSM - * @dev FixedRateStrategyFactory is used when creating a new borrow rate strategy for GHO - */ -contract GhoStewardV2 is Ownable, IGhoStewardV2 { - using EnumerableSet for EnumerableSet.AddressSet; - using ReserveConfiguration for DataTypes.ReserveConfigurationMap; - - /// @inheritdoc IGhoStewardV2 - uint256 public constant GHO_BORROW_RATE_MAX = 0.2500e27; // 25.00% - - /// @inheritdoc IGhoStewardV2 - uint256 public constant GHO_BORROW_RATE_CHANGE_MAX = 0.0500e27; // 5.00% - - /// @inheritdoc IGhoStewardV2 - uint256 public constant GSM_FEE_RATE_CHANGE_MAX = 0.0050e4; // 0.50% - - /// @inheritdoc IGhoStewardV2 - uint256 public constant MINIMUM_DELAY = 2 days; - - /// @inheritdoc IGhoStewardV2 - address public immutable POOL_ADDRESSES_PROVIDER; - - /// @inheritdoc IGhoStewardV2 - address public immutable GHO_TOKEN; - - /// @inheritdoc IGhoStewardV2 - address public immutable FIXED_RATE_STRATEGY_FACTORY; - - /// @inheritdoc IGhoStewardV2 - address public immutable RISK_COUNCIL; - - GhoDebounce internal _ghoTimelocks; - mapping(address => uint40) _facilitatorsBucketCapacityTimelocks; - mapping(address => GsmDebounce) internal _gsmTimelocksByAddress; - - mapping(address => bool) internal _controlledFacilitatorsByAddress; - EnumerableSet.AddressSet internal _controlledFacilitators; - - mapping(uint256 => mapping(uint256 => address)) internal _gsmFeeStrategiesByRates; - EnumerableSet.AddressSet internal _gsmFeeStrategies; - - /** - * @dev Only Risk Council can call functions marked by this modifier. - */ - modifier onlyRiskCouncil() { - require(RISK_COUNCIL == msg.sender, 'INVALID_CALLER'); - _; - } - - /** - * @dev Only methods that are not timelocked can be called if marked by this modifier. - */ - modifier notTimelocked(uint40 timelock) { - require(block.timestamp - timelock > MINIMUM_DELAY, 'DEBOUNCE_NOT_RESPECTED'); - _; - } - - /** - * @dev Constructor - * @param owner The address of the owner of the contract - * @param addressesProvider The address of the PoolAddressesProvider of Aave V3 Ethereum Pool - * @param ghoToken The address of the GhoToken - * @param fixedRateStrategyFactory The address of the FixedRateStrategyFactory - * @param riskCouncil The address of the risk council - */ - constructor( - address owner, - address addressesProvider, - address ghoToken, - address fixedRateStrategyFactory, - address riskCouncil - ) { - require(owner != address(0), 'INVALID_OWNER'); - require(addressesProvider != address(0), 'INVALID_ADDRESSES_PROVIDER'); - require(ghoToken != address(0), 'INVALID_GHO_TOKEN'); - require(fixedRateStrategyFactory != address(0), 'INVALID_FIXED_RATE_STRATEGY_FACTORY'); - require(riskCouncil != address(0), 'INVALID_RISK_COUNCIL'); - - POOL_ADDRESSES_PROVIDER = addressesProvider; - GHO_TOKEN = ghoToken; - FIXED_RATE_STRATEGY_FACTORY = fixedRateStrategyFactory; - RISK_COUNCIL = riskCouncil; - - _transferOwnership(owner); - } - - /// @inheritdoc IGhoStewardV2 - function updateFacilitatorBucketCapacity( - address facilitator, - uint128 newBucketCapacity - ) external onlyRiskCouncil notTimelocked(_facilitatorsBucketCapacityTimelocks[facilitator]) { - require(_controlledFacilitatorsByAddress[facilitator], 'FACILITATOR_NOT_CONTROLLED'); - (uint256 currentBucketCapacity, ) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(facilitator); - require( - _isIncreaseLowerThanMax(currentBucketCapacity, newBucketCapacity, currentBucketCapacity), - 'INVALID_BUCKET_CAPACITY_UPDATE' - ); - - _facilitatorsBucketCapacityTimelocks[facilitator] = uint40(block.timestamp); - - IGhoToken(GHO_TOKEN).setFacilitatorBucketCapacity(facilitator, newBucketCapacity); - } - - /// @inheritdoc IGhoStewardV2 - function updateGhoBorrowCap( - uint256 newBorrowCap - ) external onlyRiskCouncil notTimelocked(_ghoTimelocks.ghoBorrowCapLastUpdate) { - DataTypes.ReserveConfigurationMap memory configuration = IPool( - IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER).getPool() - ).getConfiguration(GHO_TOKEN); - uint256 currentBorrowCap = configuration.getBorrowCap(); - require( - _isDifferenceLowerThanMax(currentBorrowCap, newBorrowCap, currentBorrowCap), - 'INVALID_BORROW_CAP_UPDATE' - ); - - _ghoTimelocks.ghoBorrowCapLastUpdate = uint40(block.timestamp); - - IPoolConfigurator(IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER).getPoolConfigurator()) - .setBorrowCap(GHO_TOKEN, newBorrowCap); - } - - /// @inheritdoc IGhoStewardV2 - function updateGhoBorrowRate( - uint256 newBorrowRate - ) external onlyRiskCouncil notTimelocked(_ghoTimelocks.ghoBorrowRateLastUpdate) { - DataTypes.ReserveData memory ghoReserveData = IPool( - IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER).getPool() - ).getReserveData(GHO_TOKEN); - require( - ghoReserveData.interestRateStrategyAddress != address(0), - 'GHO_INTEREST_RATE_STRATEGY_NOT_FOUND' - ); - - uint256 currentBorrowRate = GhoInterestRateStrategy(ghoReserveData.interestRateStrategyAddress) - .getBaseVariableBorrowRate(); - require(newBorrowRate <= GHO_BORROW_RATE_MAX, 'BORROW_RATE_HIGHER_THAN_MAX'); - require( - _isDifferenceLowerThanMax(currentBorrowRate, newBorrowRate, GHO_BORROW_RATE_CHANGE_MAX), - 'INVALID_BORROW_RATE_UPDATE' - ); - - IFixedRateStrategyFactory strategyFactory = IFixedRateStrategyFactory( - FIXED_RATE_STRATEGY_FACTORY - ); - uint256[] memory borrowRateList = new uint256[](1); - borrowRateList[0] = newBorrowRate; - address strategy = strategyFactory.createStrategies(borrowRateList)[0]; - - _ghoTimelocks.ghoBorrowRateLastUpdate = uint40(block.timestamp); - - IPoolConfigurator(IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER).getPoolConfigurator()) - .setReserveInterestRateStrategyAddress(GHO_TOKEN, strategy); - } - - /// @inheritdoc IGhoStewardV2 - function updateGsmExposureCap( - address gsm, - uint128 newExposureCap - ) external onlyRiskCouncil notTimelocked(_gsmTimelocksByAddress[gsm].gsmExposureCapLastUpdated) { - uint128 currentExposureCap = IGsm(gsm).getExposureCap(); - require( - _isDifferenceLowerThanMax(currentExposureCap, newExposureCap, currentExposureCap), - 'INVALID_EXPOSURE_CAP_UPDATE' - ); - - _gsmTimelocksByAddress[gsm].gsmExposureCapLastUpdated = uint40(block.timestamp); - - IGsm(gsm).updateExposureCap(newExposureCap); - } - - /// @inheritdoc IGhoStewardV2 - function updateGsmBuySellFees( - address gsm, - uint256 buyFee, - uint256 sellFee - ) external onlyRiskCouncil notTimelocked(_gsmTimelocksByAddress[gsm].gsmFeeStrategyLastUpdated) { - address currentFeeStrategy = IGsm(gsm).getFeeStrategy(); - require(currentFeeStrategy != address(0), 'GSM_FEE_STRATEGY_NOT_FOUND'); - - uint256 currentBuyFee = IGsmFeeStrategy(currentFeeStrategy).getBuyFee(1e4); - uint256 currentSellFee = IGsmFeeStrategy(currentFeeStrategy).getSellFee(1e4); - require( - _isDifferenceLowerThanMax(currentBuyFee, buyFee, GSM_FEE_RATE_CHANGE_MAX), - 'INVALID_BUY_FEE_UPDATE' - ); - require( - _isDifferenceLowerThanMax(currentSellFee, sellFee, GSM_FEE_RATE_CHANGE_MAX), - 'INVALID_SELL_FEE_UPDATE' - ); - - address cachedStrategyAddress = _gsmFeeStrategiesByRates[buyFee][sellFee]; - if (cachedStrategyAddress == address(0)) { - FixedFeeStrategy newRateStrategy = new FixedFeeStrategy(buyFee, sellFee); - cachedStrategyAddress = address(newRateStrategy); - _gsmFeeStrategiesByRates[buyFee][sellFee] = cachedStrategyAddress; - _gsmFeeStrategies.add(cachedStrategyAddress); - } - - _gsmTimelocksByAddress[gsm].gsmFeeStrategyLastUpdated = uint40(block.timestamp); - - IGsm(gsm).updateFeeStrategy(cachedStrategyAddress); - } - - /// @inheritdoc IGhoStewardV2 - function setControlledFacilitator( - address[] memory facilitatorList, - bool approve - ) external onlyOwner { - for (uint256 i = 0; i < facilitatorList.length; i++) { - _controlledFacilitatorsByAddress[facilitatorList[i]] = approve; - if (approve) { - _controlledFacilitators.add(facilitatorList[i]); - } else { - _controlledFacilitators.remove(facilitatorList[i]); - } - } - } - - /// @inheritdoc IGhoStewardV2 - function getControlledFacilitators() external view returns (address[] memory) { - return _controlledFacilitators.values(); - } - - /// @inheritdoc IGhoStewardV2 - function getGhoTimelocks() external view returns (GhoDebounce memory) { - return _ghoTimelocks; - } - - /// @inheritdoc IGhoStewardV2 - function getGsmTimelocks(address gsm) external view returns (GsmDebounce memory) { - return _gsmTimelocksByAddress[gsm]; - } - - /// @inheritdoc IGhoStewardV2 - function getFacilitatorBucketCapacityTimelock( - address facilitator - ) external view returns (uint40) { - return _facilitatorsBucketCapacityTimelocks[facilitator]; - } - - /// @inheritdoc IGhoStewardV2 - function getGsmFeeStrategies() external view returns (address[] memory) { - return _gsmFeeStrategies.values(); - } - - /** - * @dev Ensures that the change is positive and the difference is lower than max. - * @param from current value - * @param to new value - * @param max maximum difference between from and to - * @return bool true if difference between values is positive and lower than max, false otherwise - */ - function _isIncreaseLowerThanMax( - uint256 from, - uint256 to, - uint256 max - ) internal pure returns (bool) { - return to >= from && to - from <= max; - } - - /** - * @dev Ensures that the change difference is lower than max. - * @param from current value - * @param to new value - * @param max maximum difference between from and to - * @return bool true if difference between values lower than max, false otherwise - */ - function _isDifferenceLowerThanMax( - uint256 from, - uint256 to, - uint256 max - ) internal pure returns (bool) { - return from < to ? to - from <= max : from - to <= max; - } -} diff --git a/src/contracts/misc/RiskCouncilControlled.sol b/src/contracts/misc/RiskCouncilControlled.sol new file mode 100644 index 00000000..7f46de39 --- /dev/null +++ b/src/contracts/misc/RiskCouncilControlled.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +/** + * @title RiskCouncilControlled + * @author Aave Labs + * @notice Helper contract for controlling access to Steward and other functions restricted to Risk Council + */ +abstract contract RiskCouncilControlled { + address internal immutable _riskCouncil; + + /** + * @dev Constructor + * @param riskCouncil The address of the risk council + */ + constructor(address riskCouncil) { + require(riskCouncil != address(0), 'INVALID_RISK_COUNCIL'); + _riskCouncil = riskCouncil; + } + + /** + * @dev Only Risk Council can call functions marked by this modifier. + */ + modifier onlyRiskCouncil() { + require(_riskCouncil == msg.sender, 'INVALID_CALLER'); + _; + } +} diff --git a/src/contracts/misc/dependencies/AaveV3-1.sol b/src/contracts/misc/dependencies/AaveV3-1.sol new file mode 100644 index 00000000..01e8ea59 --- /dev/null +++ b/src/contracts/misc/dependencies/AaveV3-1.sol @@ -0,0 +1,698 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {Address} from 'solidity-utils/contracts/oz-common/Address.sol'; +import {SafeCast} from '@openzeppelin/contracts/utils/math/SafeCast.sol'; +import {IERC165} from '@openzeppelin/contracts/utils/introspection/IERC165.sol'; +import {IPoolAddressesProvider} from '@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol'; +import {ConfiguratorInputTypes} from '@aave/core-v3/contracts/protocol/libraries/types/ConfiguratorInputTypes.sol'; + +library DataTypes { + struct CalculateInterestRatesParams { + uint256 unbacked; + uint256 liquidityAdded; + uint256 liquidityTaken; + uint256 totalStableDebt; + uint256 totalVariableDebt; + uint256 averageStableBorrowRate; + uint256 reserveFactor; + address reserve; + bool usingVirtualBalance; + uint256 virtualUnderlyingBalance; + } +} + +/** + * @title Errors library + * @author Aave + * @notice Defines the error messages emitted by the different contracts of the Aave protocol + */ +library Errors { + string public constant CALLER_NOT_POOL_CONFIGURATOR = '10'; // 'The caller of the function is not the pool configurator' + string public constant INVALID_ADDRESSES_PROVIDER = '12'; // 'The address of the pool addresses provider is invalid' + string public constant ZERO_ADDRESS_NOT_VALID = '77'; // 'Zero address not valid' + string public constant INVALID_OPTIMAL_USAGE_RATIO = '83'; // 'Invalid optimal usage ratio' + string public constant INVALID_MAX_RATE = '92'; // The expect maximum borrow rate is invalid + string public constant SLOPE_2_MUST_BE_GTE_SLOPE_1 = '95'; // Variable interest rate slope 2 can not be lower than slope 1 +} + +/** + * @title PercentageMath library + * @author Aave + * @notice Provides functions to perform percentage calculations + * @dev Percentages are defined by default with 2 decimals of precision (100.00). The precision is indicated by PERCENTAGE_FACTOR + * @dev Operations are rounded. If a value is >=.5, will be rounded up, otherwise rounded down. + */ +library PercentageMath { + // Maximum percentage factor (100.00%) + uint256 internal constant PERCENTAGE_FACTOR = 1e4; + + // Half percentage factor (50.00%) + uint256 internal constant HALF_PERCENTAGE_FACTOR = 0.5e4; + + /** + * @notice Executes a percentage multiplication + * @dev assembly optimized for improved gas savings, see https://twitter.com/transmissions11/status/1451131036377571328 + * @param value The value of which the percentage needs to be calculated + * @param percentage The percentage of the value to be calculated + * @return result value percentmul percentage + */ + function percentMul(uint256 value, uint256 percentage) internal pure returns (uint256 result) { + // to avoid overflow, value <= (type(uint256).max - HALF_PERCENTAGE_FACTOR) / percentage + assembly { + if iszero( + or( + iszero(percentage), + iszero(gt(value, div(sub(not(0), HALF_PERCENTAGE_FACTOR), percentage))) + ) + ) { + revert(0, 0) + } + + result := div(add(mul(value, percentage), HALF_PERCENTAGE_FACTOR), PERCENTAGE_FACTOR) + } + } + + /** + * @notice Executes a percentage division + * @dev assembly optimized for improved gas savings, see https://twitter.com/transmissions11/status/1451131036377571328 + * @param value The value of which the percentage needs to be calculated + * @param percentage The percentage of the value to be calculated + * @return result value percentdiv percentage + */ + function percentDiv(uint256 value, uint256 percentage) internal pure returns (uint256 result) { + // to avoid overflow, value <= (type(uint256).max - halfPercentage) / PERCENTAGE_FACTOR + assembly { + if or( + iszero(percentage), + iszero(iszero(gt(value, div(sub(not(0), div(percentage, 2)), PERCENTAGE_FACTOR)))) + ) { + revert(0, 0) + } + + result := div(add(mul(value, PERCENTAGE_FACTOR), div(percentage, 2)), percentage) + } + } +} + +/** + * @title WadRayMath library + * @author Aave + * @notice Provides functions to perform calculations with Wad and Ray units + * @dev Provides mul and div function for wads (decimal numbers with 18 digits of precision) and rays (decimal numbers + * with 27 digits of precision) + * @dev Operations are rounded. If a value is >=.5, will be rounded up, otherwise rounded down. + */ +library WadRayMath { + // HALF_WAD and HALF_RAY expressed with extended notation as constant with operations are not supported in Yul assembly + uint256 internal constant WAD = 1e18; + uint256 internal constant HALF_WAD = 0.5e18; + + uint256 internal constant RAY = 1e27; + uint256 internal constant HALF_RAY = 0.5e27; + + uint256 internal constant WAD_RAY_RATIO = 1e9; + + /** + * @dev Multiplies two wad, rounding half up to the nearest wad + * @dev assembly optimized for improved gas savings, see https://twitter.com/transmissions11/status/1451131036377571328 + * @param a Wad + * @param b Wad + * @return c = a*b, in wad + */ + function wadMul(uint256 a, uint256 b) internal pure returns (uint256 c) { + // to avoid overflow, a <= (type(uint256).max - HALF_WAD) / b + assembly { + if iszero(or(iszero(b), iszero(gt(a, div(sub(not(0), HALF_WAD), b))))) { + revert(0, 0) + } + + c := div(add(mul(a, b), HALF_WAD), WAD) + } + } + + /** + * @dev Divides two wad, rounding half up to the nearest wad + * @dev assembly optimized for improved gas savings, see https://twitter.com/transmissions11/status/1451131036377571328 + * @param a Wad + * @param b Wad + * @return c = a/b, in wad + */ + function wadDiv(uint256 a, uint256 b) internal pure returns (uint256 c) { + // to avoid overflow, a <= (type(uint256).max - halfB) / WAD + assembly { + if or(iszero(b), iszero(iszero(gt(a, div(sub(not(0), div(b, 2)), WAD))))) { + revert(0, 0) + } + + c := div(add(mul(a, WAD), div(b, 2)), b) + } + } + + /** + * @notice Multiplies two ray, rounding half up to the nearest ray + * @dev assembly optimized for improved gas savings, see https://twitter.com/transmissions11/status/1451131036377571328 + * @param a Ray + * @param b Ray + * @return c = a raymul b + */ + function rayMul(uint256 a, uint256 b) internal pure returns (uint256 c) { + // to avoid overflow, a <= (type(uint256).max - HALF_RAY) / b + assembly { + if iszero(or(iszero(b), iszero(gt(a, div(sub(not(0), HALF_RAY), b))))) { + revert(0, 0) + } + + c := div(add(mul(a, b), HALF_RAY), RAY) + } + } + + /** + * @notice Divides two ray, rounding half up to the nearest ray + * @dev assembly optimized for improved gas savings, see https://twitter.com/transmissions11/status/1451131036377571328 + * @param a Ray + * @param b Ray + * @return c = a raydiv b + */ + function rayDiv(uint256 a, uint256 b) internal pure returns (uint256 c) { + // to avoid overflow, a <= (type(uint256).max - halfB) / RAY + assembly { + if or(iszero(b), iszero(iszero(gt(a, div(sub(not(0), div(b, 2)), RAY))))) { + revert(0, 0) + } + + c := div(add(mul(a, RAY), div(b, 2)), b) + } + } + + /** + * @dev Casts ray down to wad + * @dev assembly optimized for improved gas savings, see https://twitter.com/transmissions11/status/1451131036377571328 + * @param a Ray + * @return b = a converted to wad, rounded half up to the nearest wad + */ + function rayToWad(uint256 a) internal pure returns (uint256 b) { + assembly { + b := div(a, WAD_RAY_RATIO) + let remainder := mod(a, WAD_RAY_RATIO) + if iszero(lt(remainder, div(WAD_RAY_RATIO, 2))) { + b := add(b, 1) + } + } + } + + /** + * @dev Converts wad up to ray + * @dev assembly optimized for improved gas savings, see https://twitter.com/transmissions11/status/1451131036377571328 + * @param a Wad + * @return b = a converted in ray + */ + function wadToRay(uint256 a) internal pure returns (uint256 b) { + // to avoid overflow, b/WAD_RAY_RATIO == a + assembly { + b := mul(a, WAD_RAY_RATIO) + + if iszero(eq(div(b, WAD_RAY_RATIO), a)) { + revert(0, 0) + } + } + } +} + +/// @notice This interface contains the only ARM-related functions that might be used on-chain by other CCIP contracts. +interface IARM { + /// @notice A Merkle root tagged with the address of the commit store contract it is destined for. + struct TaggedRoot { + address commitStore; + bytes32 root; + } + + /// @notice Callers MUST NOT cache the return value as a blessed tagged root could become unblessed. + function isBlessed(TaggedRoot calldata taggedRoot) external view returns (bool); + + /// @notice When the ARM is "cursed", CCIP pauses until the curse is lifted. + function isCursed() external view returns (bool); +} + +/** + * @title IPoolConfigurator + * @author Aave + * @notice Defines the basic interface for a Pool configurator. + * @dev Reduced interface from https://github.com/aave-dao/aave-v3-origin/blob/main/src/core/contracts/interfaces/IPoolConfigurator.sol + */ +interface IPoolConfigurator { + /** + * @notice Sets interest rate data for a reserve + * @param asset The address of the underlying asset of the reserve + * @param rateData bytes-encoded rate data. In this format in order to allow the rate strategy contract + * to de-structure custom data + */ + function setReserveInterestRateData(address asset, bytes calldata rateData) external; + + /** + * @notice Updates the borrow cap of a reserve. + * @param asset The address of the underlying asset of the reserve + * @param newBorrowCap The new borrow cap of the reserve + */ + function setBorrowCap(address asset, uint256 newBorrowCap) external; + + /** + * @notice Updates the supply cap of a reserve. + * @param asset The address of the underlying asset of the reserve + * @param newSupplyCap The new supply cap of the reserve + */ + function setSupplyCap(address asset, uint256 newSupplyCap) external; +} + +/** + * @title IReserveInterestRateStrategy + * @author BGD Labs + * @notice Basic interface for any rate strategy used by the Aave protocol + */ +interface IReserveInterestRateStrategy { + /** + * @notice Sets interest rate data for an Aave rate strategy + * @param reserve The reserve to update + * @param rateData The abi encoded reserve interest rate data to apply to the given reserve + * Abstracted this way as rate strategies can be custom + */ + function setInterestRateParams(address reserve, bytes calldata rateData) external; + + /** + * @notice Calculates the interest rates depending on the reserve's state and configurations + * @param params The parameters needed to calculate interest rates + * @return liquidityRate The liquidity rate expressed in ray + * @return stableBorrowRate The stable borrow rate expressed in ray + * @return variableBorrowRate The variable borrow rate expressed in ray + */ + function calculateInterestRates( + DataTypes.CalculateInterestRatesParams memory params + ) external view returns (uint256, uint256, uint256); +} + +/** + * @title IDefaultInterestRateStrategyV2 + * @author BGD Labs + * @notice Interface of the default interest rate strategy used by the Aave protocol + */ +interface IDefaultInterestRateStrategyV2 is IReserveInterestRateStrategy { + struct CalcInterestRatesLocalVars { + uint256 availableLiquidity; + uint256 totalDebt; + uint256 currentVariableBorrowRate; + uint256 currentLiquidityRate; + uint256 borrowUsageRatio; + uint256 supplyUsageRatio; + uint256 availableLiquidityPlusDebt; + } + + /** + * @notice Holds the interest rate data for a given reserve + * + * @dev Since values are in bps, they are multiplied by 1e23 in order to become rays with 27 decimals. This + * in turn means that the maximum supported interest rate is 4294967295 (2**32-1) bps or 42949672.95%. + * + * @param optimalUsageRatio The optimal usage ratio, in bps + * @param baseVariableBorrowRate The base variable borrow rate, in bps + * @param variableRateSlope1 The slope of the variable interest curve, before hitting the optimal ratio, in bps + * @param variableRateSlope2 The slope of the variable interest curve, after hitting the optimal ratio, in bps + */ + struct InterestRateData { + uint16 optimalUsageRatio; + uint32 baseVariableBorrowRate; + uint32 variableRateSlope1; + uint32 variableRateSlope2; + } + + /** + * @notice The interest rate data, where all values are in ray (fixed-point 27 decimal numbers) for a given reserve, + * used in in-memory calculations. + * + * @param optimalUsageRatio The optimal usage ratio + * @param baseVariableBorrowRate The base variable borrow rate + * @param variableRateSlope1 The slope of the variable interest curve, before hitting the optimal ratio + * @param variableRateSlope2 The slope of the variable interest curve, after hitting the optimal ratio + */ + struct InterestRateDataRay { + uint256 optimalUsageRatio; + uint256 baseVariableBorrowRate; + uint256 variableRateSlope1; + uint256 variableRateSlope2; + } + + /** + * @notice emitted when new interest rate data is set in a reserve + * + * @param reserve address of the reserve that has new interest rate data set + * @param optimalUsageRatio The optimal usage ratio, in bps + * @param baseVariableBorrowRate The base variable borrow rate, in bps + * @param variableRateSlope1 The slope of the variable interest curve, before hitting the optimal ratio, in bps + * @param variableRateSlope2 The slope of the variable interest curve, after hitting the optimal ratio, in bps + */ + event RateDataUpdate( + address indexed reserve, + uint256 optimalUsageRatio, + uint256 baseVariableBorrowRate, + uint256 variableRateSlope1, + uint256 variableRateSlope2 + ); + + /** + * @notice Returns the address of the PoolAddressesProvider + * @return The address of the PoolAddressesProvider contract + */ + function ADDRESSES_PROVIDER() external view returns (IPoolAddressesProvider); + + /** + * @notice Returns the maximum value achievable for variable borrow rate, in bps + * @return The maximum rate + */ + function MAX_BORROW_RATE() external view returns (uint256); + + /** + * @notice Returns the minimum optimal point, in bps + * @return The optimal point + */ + function MIN_OPTIMAL_POINT() external view returns (uint256); + + /** + * @notice Returns the maximum optimal point, in bps + * @return The optimal point + */ + function MAX_OPTIMAL_POINT() external view returns (uint256); + + /** + * notice Returns the full InterestRateDataRay object for the given reserve, in bps + * + * @param reserve The reserve to get the data of + * + * @return The InterestRateData object for the given reserve + */ + function getInterestRateDataBps(address reserve) external view returns (InterestRateData memory); + + /** + * @notice Returns the optimal usage rate for the given reserve in ray + * + * @param reserve The reserve to get the optimal usage rate of + * + * @return The optimal usage rate is the level of borrow / collateral at which the borrow rate + */ + function getOptimalUsageRatio(address reserve) external view returns (uint256); + + /** + * @notice Returns the variable rate slope below optimal usage ratio in ray + * @dev It's the variable rate when usage ratio > 0 and <= OPTIMAL_USAGE_RATIO + * + * @param reserve The reserve to get the variable rate slope 1 of + * + * @return The variable rate slope + */ + function getVariableRateSlope1(address reserve) external view returns (uint256); + + /** + * @notice Returns the variable rate slope above optimal usage ratio in ray + * @dev It's the variable rate when usage ratio > OPTIMAL_USAGE_RATIO + * + * @param reserve The reserve to get the variable rate slope 2 of + * + * @return The variable rate slope + */ + function getVariableRateSlope2(address reserve) external view returns (uint256); + + /** + * @notice Returns the base variable borrow rate, in ray + * + * @param reserve The reserve to get the base variable borrow rate of + * + * @return The base variable borrow rate + */ + function getBaseVariableBorrowRate(address reserve) external view returns (uint256); + + /** + * @notice Returns the maximum variable borrow rate, in ray + * + * @param reserve The reserve to get the maximum variable borrow rate of + * + * @return The maximum variable borrow rate + */ + function getMaxVariableBorrowRate(address reserve) external view returns (uint256); + + /** + * @notice Sets interest rate data for an Aave rate strategy + * @param reserve The reserve to update + * @param rateData The reserve interest rate data to apply to the given reserve + * Being specific to this custom implementation, with custom struct type, + * overloading the function on the generic interface + */ + function setInterestRateParams(address reserve, InterestRateData calldata rateData) external; +} + +/** + * @title DefaultReserveInterestRateStrategyV2 contract + * @author BGD Labs + * @notice Default interest rate strategy used by the Aave protocol + * @dev Strategies are pool-specific: each contract CAN'T be used across different Aave pools + * due to the caching of the PoolAddressesProvider and the usage of underlying addresses as + * index of the _interestRateData + */ +contract DefaultReserveInterestRateStrategyV2 is IDefaultInterestRateStrategyV2 { + using WadRayMath for uint256; + using PercentageMath for uint256; + + /// @inheritdoc IDefaultInterestRateStrategyV2 + IPoolAddressesProvider public immutable ADDRESSES_PROVIDER; + + /// @inheritdoc IDefaultInterestRateStrategyV2 + uint256 public constant MAX_BORROW_RATE = 1000_00; + + /// @inheritdoc IDefaultInterestRateStrategyV2 + uint256 public constant MIN_OPTIMAL_POINT = 1_00; + + /// @inheritdoc IDefaultInterestRateStrategyV2 + uint256 public constant MAX_OPTIMAL_POINT = 99_00; + + /// @dev Map of reserves address and their interest rate data (reserveAddress => interestRateData) + mapping(address => InterestRateData) internal _interestRateData; + + modifier onlyPoolConfigurator() { + require( + msg.sender == ADDRESSES_PROVIDER.getPoolConfigurator(), + Errors.CALLER_NOT_POOL_CONFIGURATOR + ); + _; + } + + /** + * @dev Constructor. + * @param provider The address of the PoolAddressesProvider of the associated Aave pool + */ + constructor(address provider) { + require(provider != address(0), Errors.INVALID_ADDRESSES_PROVIDER); + ADDRESSES_PROVIDER = IPoolAddressesProvider(provider); + } + + /// @inheritdoc IReserveInterestRateStrategy + function setInterestRateParams( + address reserve, + bytes calldata rateData + ) external onlyPoolConfigurator { + _setInterestRateParams(reserve, abi.decode(rateData, (InterestRateData))); + } + + /// @inheritdoc IDefaultInterestRateStrategyV2 + function setInterestRateParams( + address reserve, + InterestRateData calldata rateData + ) external onlyPoolConfigurator { + _setInterestRateParams(reserve, rateData); + } + + /// @inheritdoc IDefaultInterestRateStrategyV2 + function getInterestRateDataBps(address reserve) external view returns (InterestRateData memory) { + return _interestRateData[reserve]; + } + + /// @inheritdoc IDefaultInterestRateStrategyV2 + function getOptimalUsageRatio(address reserve) external view returns (uint256) { + return _bpsToRay(uint256(_interestRateData[reserve].optimalUsageRatio)); + } + + /// @inheritdoc IDefaultInterestRateStrategyV2 + function getVariableRateSlope1(address reserve) external view returns (uint256) { + return _bpsToRay(uint256(_interestRateData[reserve].variableRateSlope1)); + } + + /// @inheritdoc IDefaultInterestRateStrategyV2 + function getVariableRateSlope2(address reserve) external view returns (uint256) { + return _bpsToRay(uint256(_interestRateData[reserve].variableRateSlope2)); + } + + /// @inheritdoc IDefaultInterestRateStrategyV2 + function getBaseVariableBorrowRate(address reserve) external view override returns (uint256) { + return _bpsToRay(uint256(_interestRateData[reserve].baseVariableBorrowRate)); + } + + /// @inheritdoc IDefaultInterestRateStrategyV2 + function getMaxVariableBorrowRate(address reserve) external view override returns (uint256) { + return + _bpsToRay( + uint256( + _interestRateData[reserve].baseVariableBorrowRate + + _interestRateData[reserve].variableRateSlope1 + + _interestRateData[reserve].variableRateSlope2 + ) + ); + } + + /// @inheritdoc IReserveInterestRateStrategy + function calculateInterestRates( + DataTypes.CalculateInterestRatesParams memory params + ) external view virtual override returns (uint256, uint256, uint256) { + InterestRateDataRay memory rateData = _rayifyRateData(_interestRateData[params.reserve]); + + // @note This is a short circuit to allow mintable assets (ex. GHO), which by definition cannot be supplied + // and thus do not use virtual underlying balances. + if (!params.usingVirtualBalance) { + return (0, 0, rateData.baseVariableBorrowRate); + } + + CalcInterestRatesLocalVars memory vars; + + vars.totalDebt = params.totalStableDebt + params.totalVariableDebt; + + vars.currentLiquidityRate = 0; + vars.currentVariableBorrowRate = rateData.baseVariableBorrowRate; + + if (vars.totalDebt != 0) { + vars.availableLiquidity = + params.virtualUnderlyingBalance + + params.liquidityAdded - + params.liquidityTaken; + + vars.availableLiquidityPlusDebt = vars.availableLiquidity + vars.totalDebt; + vars.borrowUsageRatio = vars.totalDebt.rayDiv(vars.availableLiquidityPlusDebt); + vars.supplyUsageRatio = vars.totalDebt.rayDiv( + vars.availableLiquidityPlusDebt + params.unbacked + ); + } else { + return (0, 0, vars.currentVariableBorrowRate); + } + + if (vars.borrowUsageRatio > rateData.optimalUsageRatio) { + uint256 excessBorrowUsageRatio = (vars.borrowUsageRatio - rateData.optimalUsageRatio).rayDiv( + WadRayMath.RAY - rateData.optimalUsageRatio + ); + + vars.currentVariableBorrowRate += + rateData.variableRateSlope1 + + rateData.variableRateSlope2.rayMul(excessBorrowUsageRatio); + } else { + vars.currentVariableBorrowRate += rateData + .variableRateSlope1 + .rayMul(vars.borrowUsageRatio) + .rayDiv(rateData.optimalUsageRatio); + } + + vars.currentLiquidityRate = _getOverallBorrowRate( + params.totalStableDebt, + params.totalVariableDebt, + vars.currentVariableBorrowRate, + params.averageStableBorrowRate + ).rayMul(vars.supplyUsageRatio).percentMul( + PercentageMath.PERCENTAGE_FACTOR - params.reserveFactor + ); + + return (vars.currentLiquidityRate, 0, vars.currentVariableBorrowRate); + } + + /** + * @dev Calculates the overall borrow rate as the weighted average between the total variable debt and total stable + * debt + * @param totalStableDebt The total borrowed from the reserve at a stable rate + * @param totalVariableDebt The total borrowed from the reserve at a variable rate + * @param currentVariableBorrowRate The current variable borrow rate of the reserve + * @param currentAverageStableBorrowRate The current weighted average of all the stable rate loans + * @return The weighted averaged borrow rate + */ + function _getOverallBorrowRate( + uint256 totalStableDebt, + uint256 totalVariableDebt, + uint256 currentVariableBorrowRate, + uint256 currentAverageStableBorrowRate + ) internal pure returns (uint256) { + uint256 totalDebt = totalStableDebt + totalVariableDebt; + + uint256 weightedVariableRate = totalVariableDebt.wadToRay().rayMul(currentVariableBorrowRate); + + uint256 weightedStableRate = totalStableDebt.wadToRay().rayMul(currentAverageStableBorrowRate); + + uint256 overallBorrowRate = (weightedVariableRate + weightedStableRate).rayDiv( + totalDebt.wadToRay() + ); + + return overallBorrowRate; + } + + /** + * @dev Doing validations and data update for an asset + * @param reserve address of the underlying asset of the reserve + * @param rateData Encoded reserve interest rate data to apply + */ + function _setInterestRateParams(address reserve, InterestRateData memory rateData) internal { + require(reserve != address(0), Errors.ZERO_ADDRESS_NOT_VALID); + + require( + rateData.optimalUsageRatio <= MAX_OPTIMAL_POINT && + rateData.optimalUsageRatio >= MIN_OPTIMAL_POINT, + Errors.INVALID_OPTIMAL_USAGE_RATIO + ); + + require( + rateData.variableRateSlope1 <= rateData.variableRateSlope2, + Errors.SLOPE_2_MUST_BE_GTE_SLOPE_1 + ); + + // The maximum rate should not be above certain threshold + require( + uint256(rateData.baseVariableBorrowRate) + + uint256(rateData.variableRateSlope1) + + uint256(rateData.variableRateSlope2) <= + MAX_BORROW_RATE, + Errors.INVALID_MAX_RATE + ); + + _interestRateData[reserve] = rateData; + emit RateDataUpdate( + reserve, + rateData.optimalUsageRatio, + rateData.baseVariableBorrowRate, + rateData.variableRateSlope1, + rateData.variableRateSlope2 + ); + } + + /** + * @dev Transforms an InterestRateData struct to an InterestRateDataRay struct by multiplying all values + * by 1e23, turning them into ray values + * + * @param data The InterestRateData struct to transform + * + * @return The resulting InterestRateDataRay struct + */ + function _rayifyRateData( + InterestRateData memory data + ) internal pure returns (InterestRateDataRay memory) { + return + InterestRateDataRay({ + optimalUsageRatio: _bpsToRay(uint256(data.optimalUsageRatio)), + baseVariableBorrowRate: _bpsToRay(uint256(data.baseVariableBorrowRate)), + variableRateSlope1: _bpsToRay(uint256(data.variableRateSlope1)), + variableRateSlope2: _bpsToRay(uint256(data.variableRateSlope2)) + }); + } + + // @dev helper function added here, as generally the protocol doesn't use bps + function _bpsToRay(uint256 n) internal pure returns (uint256) { + return n * 1e23; + } +} diff --git a/src/contracts/misc/dependencies/Ccip.sol b/src/contracts/misc/dependencies/Ccip.sol new file mode 100644 index 00000000..6b2c238f --- /dev/null +++ b/src/contracts/misc/dependencies/Ccip.sol @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// End consumer library. +library Client { + /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. + struct EVMTokenAmount { + address token; // token address on the local chain. + uint256 amount; // Amount of tokens. + } + + struct Any2EVMMessage { + bytes32 messageId; // MessageId corresponding to ccipSend on source. + uint64 sourceChainSelector; // Source chain selector. + bytes sender; // abi.decode(sender) if coming from an EVM chain. + bytes data; // payload sent in original message. + EVMTokenAmount[] destTokenAmounts; // Tokens and their amounts in their destination chain representation. + } + + // If extraArgs is empty bytes, the default is 200k gas limit. + struct EVM2AnyMessage { + bytes receiver; // abi.encode(receiver address) for dest EVM chains + bytes data; // Data payload + EVMTokenAmount[] tokenAmounts; // Token transfers + address feeToken; // Address of feeToken. address(0) means you will send msg.value. + bytes extraArgs; // Populate this with _argsToBytes(EVMExtraArgsV1) + } + + // bytes4(keccak256("CCIP EVMExtraArgsV1")); + bytes4 public constant EVM_EXTRA_ARGS_V1_TAG = 0x97a657c9; + struct EVMExtraArgsV1 { + uint256 gasLimit; + } + + function _argsToBytes(EVMExtraArgsV1 memory extraArgs) internal pure returns (bytes memory bts) { + return abi.encodeWithSelector(EVM_EXTRA_ARGS_V1_TAG, extraArgs); + } +} + +/// @notice Implements Token Bucket rate limiting. +/// @dev Reduced library from https://github.com/aave/ccip/blob/ccip-gho/contracts/src/v0.8/ccip/libraries/RateLimiter.sol +/// @dev uint128 is safe for rate limiter state. +/// For USD value rate limiting, it can adequately store USD value in 18 decimals. +/// For ERC20 token amount rate limiting, all tokens that will be listed will have at most +/// a supply of uint128.max tokens, and it will therefore not overflow the bucket. +/// In exceptional scenarios where tokens consumed may be larger than uint128, +/// e.g. compromised issuer, an enabled RateLimiter will check and revert. +library RateLimiter { + error InvalidRatelimitRate(Config rateLimiterConfig); + error DisabledNonZeroRateLimit(Config config); + error RateLimitMustBeDisabled(); + + event ConfigChanged(Config config); + + struct TokenBucket { + uint128 tokens; // ──────╮ Current number of tokens that are in the bucket. + uint32 lastUpdated; // │ Timestamp in seconds of the last token refill, good for 100+ years. + bool isEnabled; // ──────╯ Indication whether the rate limiting is enabled or not + uint128 capacity; // ────╮ Maximum number of tokens that can be in the bucket. + uint128 rate; // ────────╯ Number of tokens per second that the bucket is refilled. + } + + struct Config { + bool isEnabled; // Indication whether the rate limiting should be enabled + uint128 capacity; // ────╮ Specifies the capacity of the rate limiter + uint128 rate; // ───────╯ Specifies the rate of the rate limiter + } + + /// @notice Gets the token bucket with its values for the block it was requested at. + /// @return The token bucket. + function _currentTokenBucketState( + TokenBucket memory bucket + ) internal view returns (TokenBucket memory) { + // We update the bucket to reflect the status at the exact time of the + // call. This means we might need to refill a part of the bucket based + // on the time that has passed since the last update. + bucket.tokens = uint128( + _calculateRefill( + bucket.capacity, + bucket.tokens, + block.timestamp - bucket.lastUpdated, + bucket.rate + ) + ); + bucket.lastUpdated = uint32(block.timestamp); + return bucket; + } + + /// @notice Sets the rate limited config. + /// @param s_bucket The token bucket + /// @param config The new config + function _setTokenBucketConfig(TokenBucket storage s_bucket, Config memory config) internal { + // First update the bucket to make sure the proper rate is used for all the time + // up until the config change. + uint256 timeDiff = block.timestamp - s_bucket.lastUpdated; + if (timeDiff != 0) { + s_bucket.tokens = uint128( + _calculateRefill(s_bucket.capacity, s_bucket.tokens, timeDiff, s_bucket.rate) + ); + + s_bucket.lastUpdated = uint32(block.timestamp); + } + + s_bucket.tokens = uint128(_min(config.capacity, s_bucket.tokens)); + s_bucket.isEnabled = config.isEnabled; + s_bucket.capacity = config.capacity; + s_bucket.rate = config.rate; + + emit ConfigChanged(config); + } + + /// @notice Validates the token bucket config + function _validateTokenBucketConfig(Config memory config, bool mustBeDisabled) internal pure { + if (config.isEnabled) { + if (config.rate >= config.capacity || config.rate == 0) { + revert InvalidRatelimitRate(config); + } + if (mustBeDisabled) { + revert RateLimitMustBeDisabled(); + } + } else { + if (config.rate != 0 || config.capacity != 0) { + revert DisabledNonZeroRateLimit(config); + } + } + } + + /// @notice Calculate refilled tokens + /// @param capacity bucket capacity + /// @param tokens current bucket tokens + /// @param timeDiff block time difference since last refill + /// @param rate bucket refill rate + /// @return the value of tokens after refill + function _calculateRefill( + uint256 capacity, + uint256 tokens, + uint256 timeDiff, + uint256 rate + ) private pure returns (uint256) { + return _min(capacity, tokens + timeDiff * rate); + } + + /// @notice Return the smallest of two integers + /// @param a first int + /// @param b second int + /// @return smallest + function _min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } +} + +/// @dev Reduced interface of CCIP Router contract with needed functions only +/// @dev Adapted from https://github.com/aave/ccip/blob/ccip-gho/contracts/src/v0.8/ccip/interfaces/IRouter.sol +interface IRouter { + error OnlyOffRamp(); + + /// @notice Route the message to its intended receiver contract. + /// @param message Client.Any2EVMMessage struct. + /// @param gasForCallExactCheck of params for exec + /// @param gasLimit set of params for exec + /// @param receiver set of params for exec + /// @dev if the receiver is a contracts that signals support for CCIP execution through EIP-165. + /// the contract is called. If not, only tokens are transferred. + /// @return success A boolean value indicating whether the ccip message was received without errors. + /// @return retBytes A bytes array containing return data form CCIP receiver. + /// @return gasUsed the gas used by the external customer call. Does not include any overhead. + function routeMessage( + Client.Any2EVMMessage calldata message, + uint16 gasForCallExactCheck, + uint256 gasLimit, + address receiver + ) external returns (bool success, bytes memory retBytes, uint256 gasUsed); + + /// @notice Returns the configured onramp for a specific destination chain. + /// @param destChainSelector The destination chain Id to get the onRamp for. + /// @return onRampAddress The address of the onRamp. + function getOnRamp(uint64 destChainSelector) external view returns (address onRampAddress); + + /// @notice Return true if the given offRamp is a configured offRamp for the given source chain. + /// @param sourceChainSelector The source chain selector to check. + /// @param offRamp The address of the offRamp to check. + function isOffRamp( + uint64 sourceChainSelector, + address offRamp + ) external view returns (bool isOffRamp); +} + +/// @dev Reduced interface of CCIP UpgradeableLockReleaseTokenPool contract with needed functions only +/// @dev Adapted from https://github.com/aave/ccip/blob/ccip-gho/contracts/src/v0.8/ccip/pools/GHO/UpgradeableLockReleaseTokenPool.sol +interface IUpgradeableLockReleaseTokenPool { + function setBridgeLimit(uint256 newBridgeLimit) external; + + function setChainRateLimiterConfig( + uint64 remoteChainSelector, + RateLimiter.Config memory outboundConfig, + 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( + uint64 remoteChainSelector + ) external view returns (RateLimiter.TokenBucket memory); + + function getCurrentInboundRateLimiterState( + uint64 remoteChainSelector + ) external view returns (RateLimiter.TokenBucket memory); +} diff --git a/src/contracts/misc/interfaces/IGhoAaveSteward.sol b/src/contracts/misc/interfaces/IGhoAaveSteward.sol new file mode 100644 index 00000000..002ae3ef --- /dev/null +++ b/src/contracts/misc/interfaces/IGhoAaveSteward.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +/** + * @title IGhoAaveSteward + * @author Aave Labs + * @notice Defines the basic interface of the GhoAaveSteward + */ +interface IGhoAaveSteward { + /** + * @notice Struct storing the last update by the steward of each borrow rate param + */ + struct GhoDebounce { + uint40 ghoBorrowCapLastUpdate; + uint40 ghoSupplyCapLastUpdate; + uint40 ghoBorrowRateLastUpdate; + } + + /** + * @notice Struct storing the configuration for the borrow rate params + */ + struct BorrowRateConfig { + uint16 optimalUsageRatioMaxChange; + uint32 baseVariableBorrowRateMaxChange; + uint32 variableRateSlope1MaxChange; + uint32 variableRateSlope2MaxChange; + } + + /** + * @notice Updates the borrow rate of GHO, only if: + * - respects `MINIMUM_DELAY`, the minimum time delay between updates + * - the update changes parameters up to the maximum allowed change according to risk config + * - the update is lower than `GHO_BORROW_RATE_MAX` + * @dev Only callable by Risk Council + * @dev Values are all expressed in BPS + * @param optimalUsageRatio The new optimal usage ratio + * @param baseVariableBorrowRate The new base variable borrow rate + * @param variableRateSlope1 The new variable rate slope 1 + * @param variableRateSlope2 The new variable rate slope 2 + */ + function updateGhoBorrowRate( + uint16 optimalUsageRatio, + uint32 baseVariableBorrowRate, + uint32 variableRateSlope1, + uint32 variableRateSlope2 + ) external; + + /** + * @notice Updates the GHO borrow cap, only if: + * - respects `MINIMUM_DELAY`, the minimum time delay between updates + * - the update changes up to 100% upwards or downwards + * @dev Only callable by Risk Council + * @param newBorrowCap The new borrow cap (in whole tokens) + */ + function updateGhoBorrowCap(uint256 newBorrowCap) external; + + /** + * @notice Updates the GHO supply cap, only if: + * - respects `MINIMUM_DELAY`, the minimum time delay between updates + * - the update changes up to 100% upwards or downwards + * @dev Only callable by Risk Council + * @param newSupplyCap The new supply cap (in whole tokens) + */ + function updateGhoSupplyCap(uint256 newSupplyCap) external; + + /** + * @notice Updates the configuration conditions for borrow rate changes + * @dev Values are all expressed in BPS + * @param optimalUsageRatioMaxChange The new allowed max percentage change for optimal usage ratio + * @param baseVariableBorrowRateMaxChange The new allowed max percentage change for base variable borrow rate + * @param variableRateSlope1MaxChange The new allowed max percentage change for variable rate slope 1 + * @param variableRateSlope2MaxChange The new allowed max percentage change for variable rate slope 2 + */ + function setBorrowRateConfig( + uint16 optimalUsageRatioMaxChange, + uint32 baseVariableBorrowRateMaxChange, + uint32 variableRateSlope1MaxChange, + uint32 variableRateSlope2MaxChange + ) external; + + /** + * @notice Returns the configuration conditions for a GHO borrow rate change + * @return struct containing the borrow rate configuration + */ + function getBorrowRateConfig() external view returns (BorrowRateConfig memory); + + /** + * @notice Returns timestamp of the last update of GHO parameters + * @return The GhoDebounce struct describing the last update of GHO parameters + */ + function getGhoTimelocks() external view returns (GhoDebounce memory); + + /** + * @notice Returns maximum value that can be assigned to GHO borrow rate. + * @return The maximum value that can be assigned to GHO borrow rate in ray (e.g. 0.01e27 results in 1.0%) + */ + function GHO_BORROW_RATE_MAX() external view returns (uint32); + + /** + * @notice The address of pool data provider of the POOL the steward controls + */ + function POOL_DATA_PROVIDER() external view returns (address); + + /** + * @notice Returns the minimum delay that must be respected between parameters update. + * @return The minimum delay between parameter updates (in seconds) + */ + function MINIMUM_DELAY() external view returns (uint256); + + /** + * @notice Returns the address of the Pool Addresses Provider of the Aave V3 Ethereum Pool + * @return The address of the PoolAddressesProvider of Aave V3 Ethereum Pool + */ + function POOL_ADDRESSES_PROVIDER() external view returns (address); + + /** + * @notice Returns the address of the Gho Token + * @return The address of the GhoToken + */ + function GHO_TOKEN() external view returns (address); + + /** + * @notice Returns the address of the risk council + * @return The address of the RiskCouncil + */ + function RISK_COUNCIL() external view returns (address); +} diff --git a/src/contracts/misc/interfaces/IGhoBucketSteward.sol b/src/contracts/misc/interfaces/IGhoBucketSteward.sol new file mode 100644 index 00000000..1dba429b --- /dev/null +++ b/src/contracts/misc/interfaces/IGhoBucketSteward.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +/** + * @title IGhoBucketSteward + * @author Aave Labs + * @notice Defines the basic interface of the GhoBucketSteward + */ +interface IGhoBucketSteward { + /** + * @notice Updates the bucket capacity of facilitator, only if: + * - respects `MINIMUM_DELAY`, the minimum time delay between updates + * - the update changes up to 100% upwards + * - the facilitator is controlled + * @dev Only callable by Risk Council + * @param facilitator The facilitator address + * @param newBucketCapacity The new facilitator bucket capacity + */ + function updateFacilitatorBucketCapacity(address facilitator, uint128 newBucketCapacity) external; + + /** + * @notice Adds/Removes controlled facilitators + * @dev Only callable by owner + * @param facilitatorList A list of facilitators addresses to add to control + * @param approve True to add as controlled facilitators, false to remove + */ + function setControlledFacilitator(address[] memory facilitatorList, bool approve) external; + + /** + * @notice Returns the list of controlled facilitators by this steward. + * @return An array of facilitator addresses + */ + function getControlledFacilitators() external view returns (address[] memory); + + /** + * @notice Returns timestamp of the facilitators last bucket capacity update + * @param facilitator The facilitator address + * @return The unix time of the last bucket capacity (in seconds). + */ + function getFacilitatorBucketCapacityTimelock(address facilitator) external view returns (uint40); + + /** + * @notice Returns the minimum delay that must be respected between parameters update. + * @return The minimum delay between parameter updates (in seconds) + */ + function MINIMUM_DELAY() external view returns (uint256); + + /** + * @notice Returns the address of the Gho Token + * @return The address of the GhoToken + */ + function GHO_TOKEN() external view returns (address); + + /** + * @notice Returns the address of the risk council + * @return The address of the RiskCouncil + */ + function RISK_COUNCIL() external view returns (address); +} diff --git a/src/contracts/misc/interfaces/IGhoCcipSteward.sol b/src/contracts/misc/interfaces/IGhoCcipSteward.sol new file mode 100644 index 00000000..39bec929 --- /dev/null +++ b/src/contracts/misc/interfaces/IGhoCcipSteward.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +/** + * @title IGhoCcipSteward + * @author Aave Labs + * @notice Defines the basic interface of the GhoCcipSteward + */ +interface IGhoCcipSteward { + struct CcipDebounce { + uint40 bridgeLimitLastUpdate; + uint40 rateLimitLastUpdate; + } + + /** + * @notice Updates the CCIP bridge limit + * @dev Only callable by Risk Council + * @param newBridgeLimit The new desired bridge limit + */ + function updateBridgeLimit(uint256 newBridgeLimit) external; + + /** + * @notice Updates the CCIP rate limit config + * @dev Only callable by Risk Council + * @dev Rate limit update must be consistent with other pools' rate limit + * @param remoteChainSelector The remote chain selector for which the rate limits apply. + * @param outboundEnabled True if the outbound rate limiter is enabled. + * @param outboundCapacity The outbound rate limiter capacity. + * @param outboundRate The outbound rate limiter rate. + * @param inboundEnabled True if the inbound rate limiter is enabled. + * @param inboundCapacity The inbound rate limiter capacity. + * @param inboundRate The inbound rate limiter rate. + */ + function updateRateLimit( + uint64 remoteChainSelector, + bool outboundEnabled, + uint128 outboundCapacity, + uint128 outboundRate, + bool inboundEnabled, + uint128 inboundCapacity, + uint128 inboundRate + ) external; + + /** + * @notice Returns the minimum delay that must be respected between parameters update. + * @return The minimum delay between parameter updates (in seconds) + */ + function MINIMUM_DELAY() external view returns (uint256); + + /** + * @notice Returns the address of the Gho Token + * @return The address of the GhoToken + */ + function GHO_TOKEN() external view returns (address); + + /** + * @notice Returns the address of the Gho CCIP Token Pool + * @return The address of the Gho CCIP Token Pool + */ + function GHO_TOKEN_POOL() external view returns (address); + + /** + * @notice Returns whether the bridge limit feature is supported in the GhoTokenPool + * @return True if bridge limit is enabled in the CCIP GhoTokenPool, false otherwise + */ + function BRIDGE_LIMIT_ENABLED() external view returns (bool); + + /** + * @notice Returns the address of the risk council + * @return The address of the RiskCouncil + */ + function RISK_COUNCIL() external view returns (address); +} diff --git a/src/contracts/misc/interfaces/IGhoGsmSteward.sol b/src/contracts/misc/interfaces/IGhoGsmSteward.sol new file mode 100644 index 00000000..9df182c1 --- /dev/null +++ b/src/contracts/misc/interfaces/IGhoGsmSteward.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +/** + * @title IGhoGsmSteward + * @author Aave Labs + * @notice Defines the basic interface of the GhoGsmSteward + */ +interface IGhoGsmSteward { + struct GsmDebounce { + uint40 gsmExposureCapLastUpdated; + uint40 gsmFeeStrategyLastUpdated; + } + + /** + * @notice Updates the exposure cap of the GSM, only if: + * - respects `MINIMUM_DELAY`, the minimum time delay between updates + * - the update changes up to 100% upwards or downwards + * @dev Only callable by Risk Council + * @param gsm The gsm address to update + * @param newExposureCap The new exposure cap (in underlying asset terms) + */ + function updateGsmExposureCap(address gsm, uint128 newExposureCap) external; + + /** + * @notice Updates the fixed percent fees of the GSM, only if: + * - respects `MINIMUM_DELAY`, the minimum time delay between updates + * - the update changes up to `GSM_FEE_RATE_CHANGE_MAX` upwards or downwards (for both buy and sell individually) + * @dev Only callable by Risk Council + * @dev Reverts if fee strategy is not set, or zero fees. Must be updated via AIP in this case + * @param gsm The gsm address to update + * @param buyFee The new buy fee (expressed in bps) (e.g. 0.0150e4 results in 1.50%) + * @param sellFee The new sell fee (expressed in bps) (e.g. 0.0150e4 results in 1.50%) + */ + function updateGsmBuySellFees(address gsm, uint256 buyFee, uint256 sellFee) external; + + /** + * @notice Returns timestamp of the last update of Gsm parameters + * @param gsm The GSM address + * @return The GsmDebounce struct describing the last update of GSM parameters + */ + function getGsmTimelocks(address gsm) external view returns (GsmDebounce memory); + + /** + * @notice Returns the maximum increase for GSM fee rates (buy or sell). + * @return The maximum increase change for GSM fee rates updates in bps (e.g. 0.010e4 results in 1.00%) + */ + function GSM_FEE_RATE_CHANGE_MAX() external view returns (uint256); + + /** + * @notice Returns the minimum delay that must be respected between parameters update. + * @return The minimum delay between parameter updates (in seconds) + */ + function MINIMUM_DELAY() external view returns (uint256); + + /** + * @notice Returns the address of the GSM Fee Strategy Factory + * @return The address of the GSM Fee Strategy Factory + */ + function FIXED_FEE_STRATEGY_FACTORY() external view returns (address); + + /** + * @notice Returns the address of the risk council + * @return The address of the RiskCouncil + */ + function RISK_COUNCIL() external view returns (address); +} diff --git a/src/contracts/misc/interfaces/IGhoSteward.sol b/src/contracts/misc/interfaces/IGhoSteward.sol deleted file mode 100644 index 56e9b1e8..00000000 --- a/src/contracts/misc/interfaces/IGhoSteward.sol +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.10; - -/** - * @title IGhoSteward - * @author Aave - * @notice Defines the basic interface of the GhoSteward - */ -interface IGhoSteward { - struct Debounce { - uint40 borrowRateLastUpdated; - uint40 bucketCapacityLastUpdated; - } - - /** - * @dev Emitted when the steward expiration is updated - * @param oldStewardExpiration The old expiration unix time of the steward (in seconds) - * @param oldStewardExpiration The new expiration unix time of the steward (in seconds) - */ - event StewardExpirationUpdated(uint40 oldStewardExpiration, uint40 newStewardExpiration); - - /** - * @notice Returns the minimum delay that must be respected between updating a specific parameter twice - * @return The minimum delay between parameter updates (in seconds) - */ - function MINIMUM_DELAY() external view returns (uint256); - - /** - * @notice Returns the maximum percentage change for borrow rate updates. The new borrow rate can only differ up to this percentage. - * @return The maximum percentage change for borrow rate updates (e.g. 0.01e4 is 100, which results in 1.0%) - */ - function BORROW_RATE_CHANGE_MAX() external view returns (uint256); - - /** - * @notice Returns the lifespan of the steward - * @return The lifespan of the steward (in seconds) - */ - function STEWARD_LIFESPAN() external view returns (uint40); - - /** - * @notice Returns the address of the Pool Addresses Provider of the Aave V3 Ethereum Facilitator - * @return The address of the PoolAddressesProvider of Aave V3 Ethereum Facilitator - */ - function POOL_ADDRESSES_PROVIDER() external view returns (address); - - /** - * @notice Returns the address of the Gho Token - * @return The address of the GhoToken - */ - function GHO_TOKEN() external view returns (address); - - /** - * @notice Returns the address of the Risk Council - * @return The address of the RiskCouncil - */ - function RISK_COUNCIL() external view returns (address); - - /** - * @notice Updates the borrow rate of GHO, only if: - * - respects the debounce duration (5 day pause between updates must be respected) - * - the update changes up to 0.50% upwards or downwards - * @dev Only callable by Risk Council - * @param newBorrowRate The new variable borrow rate (expressed in ray) (e.g. 0.0150e27 results in 1.50%) - */ - function updateBorrowRate(uint256 newBorrowRate) external; - - /** - * @notice Updates the Bucket Capacity of the Aave V3 Ethereum Pool Facilitator, only if: - * - respects the debounce duration (5 day pause between updates must be respected) - * - the update changes up to 100% upwards - * @dev Only callable by Risk Council - * @param newBucketCapacity The new bucket capacity of the facilitator - */ - function updateBucketCapacity(uint128 newBucketCapacity) external; - - /** - * @notice Extends the steward expiration date by `STEWARD_LIFESPAN` - * @dev Only callable by Aave Short Executor - */ - function extendStewardExpiration() external; - - /** - * @notice Returns the timelock values for all parameters updates - * @return The Debounce struct with parameters' timelock - */ - function getTimelock() external view returns (Debounce memory); - - /** - * @notice Returns the expiration time of the steward - * @return The expiration unix time of the steward (in seconds) - */ - function getStewardExpiration() external view returns (uint40); - - /** - * @notice Returns the list of Interest Rate Strategies for GHO - * @return An array of GhoInterestRateStrategy addresses - */ - function getAllStrategies() external view returns (address[] memory); -} diff --git a/src/contracts/misc/interfaces/IGhoStewardV2.sol b/src/contracts/misc/interfaces/IGhoStewardV2.sol deleted file mode 100644 index 2b0138da..00000000 --- a/src/contracts/misc/interfaces/IGhoStewardV2.sol +++ /dev/null @@ -1,158 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.10; - -/** - * @title IGhoStewardV2 - * @author Aave Labs - * @notice Defines the basic interface of the GhoStewardV2 - */ -interface IGhoStewardV2 { - struct GhoDebounce { - uint40 ghoBorrowCapLastUpdate; - uint40 ghoBorrowRateLastUpdate; - } - - struct GsmDebounce { - uint40 gsmExposureCapLastUpdated; - uint40 gsmFeeStrategyLastUpdated; - } - - /** - * @notice Updates the bucket capacity of facilitator, only if: - * - respects `MINIMUM_DELAY`, the minimum time delay between updates - * - the update changes up to 100% upwards - * - the facilitator is controlled - * @dev Only callable by Risk Council - * @param facilitator The facilitator address - * @param newBucketCapacity The new facilitator bucket capacity - */ - function updateFacilitatorBucketCapacity(address facilitator, uint128 newBucketCapacity) external; - - /** - * @notice Updates the GHO borrow cap, only if: - * - respects `MINIMUM_DELAY`, the minimum time delay between updates - * - the update changes up to 100% upwards or downwards - * @dev Only callable by Risk Council - * @param newBorrowCap The new borrow cap (in whole tokens) - */ - function updateGhoBorrowCap(uint256 newBorrowCap) external; - - /** - * @notice Updates the borrow rate of GHO, only if: - * - respects `MINIMUM_DELAY`, the minimum time delay between updates - * - the update changes up to `GHO_BORROW_RATE_CHANGE_MAX` upwards or downwards - * - the update is lower than `GHO_BORROW_RATE_MAX` - * @dev Only callable by Risk Council - * @param newBorrowRate The new variable borrow rate (expressed in ray) (e.g. 0.0150e27 results in 1.50%) - */ - function updateGhoBorrowRate(uint256 newBorrowRate) external; - - /** - * @notice Updates the exposure cap of the GSM, only if: - * - respects `MINIMUM_DELAY`, the minimum time delay between updates - * - the update changes up to 100% upwards or downwards - * @dev Only callable by Risk Council - * @param gsm The gsm address to update - * @param newExposureCap The new exposure cap (in underlying asset terms) - */ - function updateGsmExposureCap(address gsm, uint128 newExposureCap) external; - - /** - * @notice Updates the fixed percent fees of the GSM, only if: - * - respects `MINIMUM_DELAY`, the minimum time delay between updates - * - the update changes up to `GSM_FEE_RATE_CHANGE_MAX` upwards or downwards (for both buy and sell individually) - * @dev Only callable by Risk Council - * @param gsm The gsm address to update - * @param buyFee The new buy fee (expressed in bps) (e.g. 0.0150e4 results in 1.50%) - * @param sellFee The new sell fee (expressed in bps) (e.g. 0.0150e4 results in 1.50%) - */ - function updateGsmBuySellFees(address gsm, uint256 buyFee, uint256 sellFee) external; - - /** - * @notice Adds/Removes controlled facilitators - * @dev Only callable by owner - * @param facilitatorList A list of facilitators addresses to add to control - * @param approve True to add as controlled facilitators, false to remove - */ - function setControlledFacilitator(address[] memory facilitatorList, bool approve) external; - - /** - * @notice Returns the maximum increase/decrease for GHO borrow rate updates. - * @return The maximum increase change for borrow rate updates in ray (e.g. 0.010e27 results in 1.00%) - */ - function GHO_BORROW_RATE_CHANGE_MAX() external view returns (uint256); - - /** - * @notice Returns the maximum increase for GSM fee rates (buy or sell). - * @return The maximum increase change for GSM fee rates updates in bps (e.g. 0.010e4 results in 1.00%) - */ - function GSM_FEE_RATE_CHANGE_MAX() external view returns (uint256); - - /** - * @notice Returns maximum value that can be assigned to GHO borrow rate. - * @return The maximum value that can be assigned to GHO borrow rate in ray (e.g. 0.01e27 results in 1.0%) - */ - function GHO_BORROW_RATE_MAX() external view returns (uint256); - - /** - * @notice Returns the minimum delay that must be respected between parameters update. - * @return The minimum delay between parameter updates (in seconds) - */ - function MINIMUM_DELAY() external view returns (uint256); - - /** - * @notice Returns the address of the Pool Addresses Provider of the Aave V3 Ethereum Pool - * @return The address of the PoolAddressesProvider of Aave V3 Ethereum Pool - */ - function POOL_ADDRESSES_PROVIDER() external view returns (address); - - /** - * @notice Returns the address of the Gho Token - * @return The address of the GhoToken - */ - function GHO_TOKEN() external view returns (address); - - /** - * @notice Returns the address of the fixed rate strategy factory - * @return The address of the FixedRateStrategyFactory - */ - function FIXED_RATE_STRATEGY_FACTORY() external view returns (address); - - /** - * @notice Returns the address of the risk council - * @return The address of the RiskCouncil - */ - function RISK_COUNCIL() external view returns (address); - - /** - * @notice Returns the list of controlled facilitators by this steward. - * @return An array of facilitator addresses - */ - function getControlledFacilitators() external view returns (address[] memory); - - /** - * @notice Returns timestamp of the last update of GHO parameters - * @return The GhoDebounce struct describing the last update of GHO parameters - */ - function getGhoTimelocks() external view returns (GhoDebounce memory); - - /** - * @notice Returns timestamp of the last update of Gsm parameters - * @param gsm The GSM address - * @return The GsmDebounce struct describing the last update of GSM parameters - */ - function getGsmTimelocks(address gsm) external view returns (GsmDebounce memory); - - /** - * @notice Returns timestamp of the facilitators last bucket capacity update - * @param facilitator The facilitator address - * @return The unix time of the last bucket capacity (in seconds). - */ - function getFacilitatorBucketCapacityTimelock(address facilitator) external view returns (uint40); - - /** - * @notice Returns the list of Fixed Fee Strategies for GSM - * @return An array of FixedFeeStrategy addresses - */ - function getGsmFeeStrategies() external view returns (address[] memory); -} diff --git a/src/test/TestGhoAaveSteward.t.sol b/src/test/TestGhoAaveSteward.t.sol new file mode 100644 index 00000000..8b546b25 --- /dev/null +++ b/src/test/TestGhoAaveSteward.t.sol @@ -0,0 +1,813 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import './TestGhoBase.t.sol'; +import {Constants} from './helpers/Constants.sol'; +import {IGhoAaveSteward} from '../contracts/misc/interfaces/IGhoAaveSteward.sol'; +import {IDefaultInterestRateStrategyV2, DefaultReserveInterestRateStrategyV2} from '../contracts/misc/dependencies/AaveV3-1.sol'; + +contract TestGhoAaveSteward is TestGhoBase { + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + + IGhoAaveSteward.BorrowRateConfig public defaultBorrowRateConfig = + IGhoAaveSteward.BorrowRateConfig({ + optimalUsageRatioMaxChange: 5_00, + baseVariableBorrowRateMaxChange: 5_00, + variableRateSlope1MaxChange: 5_00, + variableRateSlope2MaxChange: 5_00 + }); + IDefaultInterestRateStrategyV2.InterestRateData public defaultRateParams = + IDefaultInterestRateStrategyV2.InterestRateData({ + optimalUsageRatio: 1_00, + baseVariableBorrowRate: 0.20e4, + variableRateSlope1: 0, + variableRateSlope2: 0 + }); + + function setUp() public { + // Deploy Gho Aave Steward + GHO_AAVE_STEWARD = new GhoAaveSteward( + SHORT_EXECUTOR, + address(PROVIDER), + address(MOCK_POOL_DATA_PROVIDER), + address(GHO_TOKEN), + RISK_COUNCIL, + defaultBorrowRateConfig + ); + + // Set a new strategy because the default is old strategy type + DefaultReserveInterestRateStrategyV2 newRateStrategy = new DefaultReserveInterestRateStrategyV2( + address(PROVIDER) + ); + CONFIGURATOR.setReserveInterestRateStrategyAddress( + address(GHO_TOKEN), + address(newRateStrategy), + abi.encode(defaultRateParams) + ); + + /// @dev Since block.timestamp starts at 0 this is a necessary condition (block.timestamp > `MINIMUM_DELAY`) for the timelocked contract methods to work. + vm.warp(GHO_AAVE_STEWARD.MINIMUM_DELAY() + 1); + } + + function testConstructor() public { + assertEq(GHO_AAVE_STEWARD.owner(), SHORT_EXECUTOR); + assertEq(GHO_AAVE_STEWARD.MINIMUM_DELAY(), MINIMUM_DELAY_V2); + + assertEq(GHO_AAVE_STEWARD.POOL_ADDRESSES_PROVIDER(), address(PROVIDER)); + assertEq(GHO_AAVE_STEWARD.POOL_DATA_PROVIDER(), address(MOCK_POOL_DATA_PROVIDER)); + assertEq(GHO_AAVE_STEWARD.GHO_TOKEN(), address(GHO_TOKEN)); + assertEq(GHO_AAVE_STEWARD.RISK_COUNCIL(), RISK_COUNCIL); + + IGhoAaveSteward.GhoDebounce memory ghoTimelocks = GHO_AAVE_STEWARD.getGhoTimelocks(); + assertEq(ghoTimelocks.ghoBorrowCapLastUpdate, 0); + } + + function testRevertConstructorInvalidOwner() public { + vm.expectRevert('INVALID_OWNER'); + new GhoAaveSteward( + address(0), + address(0x002), + address(0x003), + address(0x004), + address(0x005), + defaultBorrowRateConfig + ); + } + + function testRevertConstructorInvalidAddressesProvider() public { + vm.expectRevert('INVALID_ADDRESSES_PROVIDER'); + new GhoAaveSteward( + address(0x001), + address(0), + address(0x003), + address(0x004), + address(0x005), + defaultBorrowRateConfig + ); + } + + function testRevertConstructorInvalidDataProvider() public { + vm.expectRevert('INVALID_DATA_PROVIDER'); + new GhoAaveSteward( + address(0x001), + address(0x002), + address(0), + address(0x004), + address(0x005), + defaultBorrowRateConfig + ); + } + + function testRevertConstructorInvalidGhoToken() public { + vm.expectRevert('INVALID_GHO_TOKEN'); + new GhoAaveSteward( + address(0x001), + address(0x002), + address(0x003), + address(0), + address(0x005), + defaultBorrowRateConfig + ); + } + + function testRevertConstructorInvalidRiskCouncil() public { + vm.expectRevert('INVALID_RISK_COUNCIL'); + new GhoAaveSteward( + address(0x001), + address(0x002), + address(0x003), + address(0x004), + address(0), + defaultBorrowRateConfig + ); + } + + function testChangeOwnership() public { + address newOwner = makeAddr('newOwner'); + assertEq(GHO_AAVE_STEWARD.owner(), SHORT_EXECUTOR); + vm.prank(SHORT_EXECUTOR); + GHO_AAVE_STEWARD.transferOwnership(newOwner); + assertEq(GHO_AAVE_STEWARD.owner(), newOwner); + } + + function testChangeOwnershipRevert() public { + vm.expectRevert('Ownable: new owner is the zero address'); + vm.prank(SHORT_EXECUTOR); + GHO_AAVE_STEWARD.transferOwnership(address(0)); + } + + function testUpdateGhoBorrowCap() public { + uint256 oldBorrowCap = 1e6; + _setGhoBorrowCapViaConfigurator(oldBorrowCap); + uint256 newBorrowCap = oldBorrowCap + 1; + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowCap(newBorrowCap); + uint256 currentBorrowCap = _getGhoBorrowCap(); + assertEq(newBorrowCap, currentBorrowCap); + } + + function testUpdateGhoBorrowCapMaxIncrease() public { + uint256 oldBorrowCap = 1e6; + _setGhoBorrowCapViaConfigurator(oldBorrowCap); + uint256 newBorrowCap = oldBorrowCap * 2; + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowCap(newBorrowCap); + uint256 currentBorrowCap = _getGhoBorrowCap(); + assertEq(newBorrowCap, currentBorrowCap); + } + + function testUpdateGhoBorrowCapMaxDecrease() public { + uint256 oldBorrowCap = 1e6; + _setGhoBorrowCapViaConfigurator(oldBorrowCap); + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowCap(0); + uint256 currentBorrowCap = _getGhoBorrowCap(); + assertEq(currentBorrowCap, 0); + } + + function testUpdateGhoBorrowCapTimelock() public { + uint256 oldBorrowCap = 1e6; + _setGhoBorrowCapViaConfigurator(oldBorrowCap); + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowCap(oldBorrowCap + 1); + IGhoAaveSteward.GhoDebounce memory ghoTimelocks = GHO_AAVE_STEWARD.getGhoTimelocks(); + assertEq(ghoTimelocks.ghoBorrowCapLastUpdate, block.timestamp); + } + + function testUpdateGhoBorrowCapAfterTimelock() public { + uint256 oldBorrowCap = 1e6; + _setGhoBorrowCapViaConfigurator(oldBorrowCap); + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowCap(oldBorrowCap + 1); + skip(GHO_AAVE_STEWARD.MINIMUM_DELAY() + 1); + uint256 newBorrowCap = oldBorrowCap + 2; + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowCap(newBorrowCap); + uint256 currentBorrowCap = _getGhoBorrowCap(); + assertEq(newBorrowCap, currentBorrowCap); + } + + function testRevertUpdateGhoBorrowCapIfUnauthorized() public { + vm.prank(ALICE); + vm.expectRevert('INVALID_CALLER'); + GHO_AAVE_STEWARD.updateGhoBorrowCap(50e6); + } + + function testRevertUpdateGhoBorrowCapIfUpdatedTooSoon() public { + uint256 oldBorrowCap = 1e6; + _setGhoBorrowCapViaConfigurator(oldBorrowCap); + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowCap(oldBorrowCap + 1); + vm.prank(RISK_COUNCIL); + vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); + GHO_AAVE_STEWARD.updateGhoBorrowCap(oldBorrowCap + 2); + } + + function testRevertUpdateGhoBorrowCapNoChange() public { + uint256 oldBorrowCap = 1e6; + _setGhoBorrowCapViaConfigurator(oldBorrowCap); + vm.prank(RISK_COUNCIL); + vm.expectRevert('NO_CHANGE_IN_BORROW_CAP'); + GHO_AAVE_STEWARD.updateGhoBorrowCap(oldBorrowCap); + } + + function testRevertUpdateGhoBorrowCapIfValueMoreThanDouble() public { + uint256 oldBorrowCap = 1e6; + _setGhoBorrowCapViaConfigurator(oldBorrowCap); + vm.prank(RISK_COUNCIL); + vm.expectRevert('INVALID_BORROW_CAP_UPDATE'); + GHO_AAVE_STEWARD.updateGhoBorrowCap(oldBorrowCap * 2 + 1); + } + + function testUpdateGhoSupplyCap() public { + uint256 oldSupplyCap = 1e6; + _setGhoSupplyCapViaConfigurator(oldSupplyCap); + uint256 newSupplyCap = oldSupplyCap + 1; + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoSupplyCap(newSupplyCap); + uint256 currentSupplyCap = _getGhoSupplyCap(); + assertEq(newSupplyCap, currentSupplyCap); + } + + function testUpdateGhoSupplyCapMaxIncrease() public { + uint256 oldSupplyCap = 1e6; + _setGhoSupplyCapViaConfigurator(oldSupplyCap); + uint256 newSupplyCap = oldSupplyCap * 2; + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoSupplyCap(newSupplyCap); + uint256 currentSupplyCap = _getGhoSupplyCap(); + assertEq(newSupplyCap, currentSupplyCap); + } + + function testUpdateGhoSupplyCapMaxDecrease() public { + uint256 oldSupplyCap = 1e6; + _setGhoSupplyCapViaConfigurator(oldSupplyCap); + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoSupplyCap(0); + uint256 currentSupplyCap = _getGhoSupplyCap(); + assertEq(currentSupplyCap, 0); + } + + function testUpdateGhoSupplyCapTimelock() public { + uint256 oldSupplyCap = 1e6; + _setGhoSupplyCapViaConfigurator(oldSupplyCap); + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoSupplyCap(oldSupplyCap + 1); + IGhoAaveSteward.GhoDebounce memory ghoTimelocks = GHO_AAVE_STEWARD.getGhoTimelocks(); + assertEq(ghoTimelocks.ghoSupplyCapLastUpdate, block.timestamp); + } + + function testUpdateGhoSupplyCapAfterTimelock() public { + uint256 oldSupplyCap = 1e6; + _setGhoSupplyCapViaConfigurator(oldSupplyCap); + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoSupplyCap(oldSupplyCap + 1); + skip(GHO_AAVE_STEWARD.MINIMUM_DELAY() + 1); + uint256 newSupplyCap = oldSupplyCap + 2; + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoSupplyCap(newSupplyCap); + uint256 currentSupplyCap = _getGhoSupplyCap(); + assertEq(newSupplyCap, currentSupplyCap); + } + + function testRevertUpdateGhoSupplyCapIfUnauthorized() public { + vm.prank(ALICE); + vm.expectRevert('INVALID_CALLER'); + GHO_AAVE_STEWARD.updateGhoSupplyCap(50e6); + } + + function testRevertUpdateGhoSupplyCapIfUpdatedTooSoon() public { + uint256 oldSupplyCap = 1e6; + _setGhoSupplyCapViaConfigurator(oldSupplyCap); + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoSupplyCap(oldSupplyCap + 1); + vm.prank(RISK_COUNCIL); + vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); + GHO_AAVE_STEWARD.updateGhoSupplyCap(oldSupplyCap + 2); + } + + function testRevertUpdateGhoSupplyCapNoChange() public { + uint256 oldSupplyCap = 1e6; + _setGhoSupplyCapViaConfigurator(oldSupplyCap); + vm.prank(RISK_COUNCIL); + vm.expectRevert('NO_CHANGE_IN_SUPPLY_CAP'); + GHO_AAVE_STEWARD.updateGhoSupplyCap(oldSupplyCap); + } + + function testRevertUpdateGhoSupplyCapIfValueMoreThanDouble() public { + uint256 oldSupplyCap = 1e6; + _setGhoSupplyCapViaConfigurator(oldSupplyCap); + vm.prank(RISK_COUNCIL); + vm.expectRevert('INVALID_SUPPLY_CAP_UPDATE'); + GHO_AAVE_STEWARD.updateGhoSupplyCap(oldSupplyCap * 2 + 1); + } + + function testUpdateGhoBorrowRate() public { + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + 0.21e4, + defaultRateParams.variableRateSlope1, + defaultRateParams.variableRateSlope2 + ); + assertEq(_getGhoBorrowRate(), 0.21e4); + } + + function testUpdateGhoBorrowRateUpwards() public { + uint32 oldBorrowRate = _getGhoBorrowRate(); + uint32 newBorrowRate = oldBorrowRate + 1; + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + newBorrowRate, + defaultRateParams.variableRateSlope1, + defaultRateParams.variableRateSlope2 + ); + uint32 currentBorrowRate = _getGhoBorrowRate(); + assertEq(currentBorrowRate, newBorrowRate); + } + + function testUpdateGhoBorrowRateDownwards() public { + uint32 oldBorrowRate = _getGhoBorrowRate(); + uint32 newBorrowRate = oldBorrowRate - 1; + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + newBorrowRate, + defaultRateParams.variableRateSlope1, + defaultRateParams.variableRateSlope2 + ); + uint32 currentBorrowRate = _getGhoBorrowRate(); + assertEq(currentBorrowRate, newBorrowRate); + } + + function testUpdateGhoBorrowRateMaxValue() public { + uint32 ghoBorrowRateMax = GHO_AAVE_STEWARD.GHO_BORROW_RATE_MAX(); + _setGhoBorrowRateViaConfigurator(ghoBorrowRateMax - 1); + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + ghoBorrowRateMax, + defaultRateParams.variableRateSlope1, + defaultRateParams.variableRateSlope2 + ); + uint32 currentBorrowRate = _getGhoBorrowRate(); + assertEq(currentBorrowRate, ghoBorrowRateMax); + } + + function testUpdateGhoBorrowRateMaxIncrement() public { + uint32 oldBorrowRate = _getGhoBorrowRate(); + uint32 newBorrowRate = oldBorrowRate + GHO_BORROW_RATE_CHANGE_MAX; + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + newBorrowRate, + defaultRateParams.variableRateSlope1, + defaultRateParams.variableRateSlope2 + ); + uint32 currentBorrowRate = _getGhoBorrowRate(); + assertEq(currentBorrowRate, newBorrowRate); + } + + function testUpdateGhoBorrowRateDecrement() public { + uint32 oldBorrowRate = _getGhoBorrowRate(); + uint32 newBorrowRate = oldBorrowRate - 1; + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + newBorrowRate, + defaultRateParams.variableRateSlope1, + defaultRateParams.variableRateSlope2 + ); + uint32 currentBorrowRate = _getGhoBorrowRate(); + assertEq(currentBorrowRate, newBorrowRate); + } + + function testUpdateGhoBorrowRateMaxDecrement() public { + vm.startPrank(RISK_COUNCIL); + + // set a high borrow rate + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + _getGhoBorrowRate() + GHO_BORROW_RATE_CHANGE_MAX, + defaultRateParams.variableRateSlope1, + defaultRateParams.variableRateSlope2 + ); + vm.warp(block.timestamp + GHO_AAVE_STEWARD.MINIMUM_DELAY() + 1); + + uint32 oldBorrowRate = _getGhoBorrowRate(); + uint32 newBorrowRate = oldBorrowRate - GHO_BORROW_RATE_CHANGE_MAX; + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + newBorrowRate, + defaultRateParams.variableRateSlope1, + defaultRateParams.variableRateSlope2 + ); + uint32 currentBorrowRate = _getGhoBorrowRate(); + assertEq(currentBorrowRate, newBorrowRate); + + vm.stopPrank(); + } + + function testUpdateGhoBorrowRateTimelock() public { + uint32 oldBorrowRate = _getGhoBorrowRate(); + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + oldBorrowRate + 1, + defaultRateParams.variableRateSlope1, + defaultRateParams.variableRateSlope2 + ); + IGhoAaveSteward.GhoDebounce memory ghoTimelocks = GHO_AAVE_STEWARD.getGhoTimelocks(); + assertEq(ghoTimelocks.ghoBorrowRateLastUpdate, block.timestamp); + } + + function testUpdateGhoBorrowRateAfterTimelock() public { + uint32 oldBorrowRate = _getGhoBorrowRate(); + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + oldBorrowRate + 1, + defaultRateParams.variableRateSlope1, + defaultRateParams.variableRateSlope2 + ); + skip(GHO_AAVE_STEWARD.MINIMUM_DELAY() + 1); + uint32 newBorrowRate = oldBorrowRate + 2; + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + newBorrowRate, + defaultRateParams.variableRateSlope1, + defaultRateParams.variableRateSlope2 + ); + uint32 currentBorrowRate = _getGhoBorrowRate(); + assertEq(currentBorrowRate, newBorrowRate); + } + + function testUpdateGhoBorrowRateOptimalUsageRatio() public { + uint16 oldOptimalUsageRatio = _getOptimalUsageRatio(); + uint16 newOptimalUsageRatio = oldOptimalUsageRatio + 1; + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + newOptimalUsageRatio, + defaultRateParams.baseVariableBorrowRate, + defaultRateParams.variableRateSlope1, + defaultRateParams.variableRateSlope2 + ); + uint16 currentOptimalUsageRatio = _getOptimalUsageRatio(); + assertEq(currentOptimalUsageRatio, newOptimalUsageRatio); + } + + function testRevertUpdateGhoBorrowRateOptimalUsageRatioIfMaxExceededUpwards() public { + uint16 oldOptimalUsageRatio = _getOptimalUsageRatio(); + uint16 newOptimalUsageRatio = oldOptimalUsageRatio + + defaultBorrowRateConfig.optimalUsageRatioMaxChange + + 1; + vm.prank(RISK_COUNCIL); + vm.expectRevert('INVALID_OPTIMAL_USAGE_RATIO'); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + newOptimalUsageRatio, + defaultRateParams.baseVariableBorrowRate, + defaultRateParams.variableRateSlope1, + defaultRateParams.variableRateSlope2 + ); + } + + function testRevertUpdateGhoBorrowRateOptimalUsageRatioIfMaxExceededDownwards() public { + uint16 oldOptimalUsageRatio = _getOptimalUsageRatio(); + uint16 newOptimalUsageRatio = oldOptimalUsageRatio + + defaultBorrowRateConfig.optimalUsageRatioMaxChange; + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + newOptimalUsageRatio, + defaultRateParams.baseVariableBorrowRate, + defaultRateParams.variableRateSlope1, + defaultRateParams.variableRateSlope2 + ); + vm.warp(block.timestamp + GHO_AAVE_STEWARD.MINIMUM_DELAY() + 1); + vm.prank(RISK_COUNCIL); + vm.expectRevert('INVALID_OPTIMAL_USAGE_RATIO'); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + newOptimalUsageRatio - defaultBorrowRateConfig.optimalUsageRatioMaxChange - 1, + defaultRateParams.baseVariableBorrowRate, + defaultRateParams.variableRateSlope1, + defaultRateParams.variableRateSlope2 + ); + } + + function testUpdateGhoBorrowRateVariableRateSlope1() public { + uint32 oldVariableRateSlope1 = _getVariableRateSlope1(); + uint32 newVariableRateSlope1 = oldVariableRateSlope1 + 1; + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + defaultRateParams.baseVariableBorrowRate, + newVariableRateSlope1, + newVariableRateSlope1 + 1 // variableRateSlope2 has to be gte variableRateSlope1 + ); + uint32 currentVariableRateSlope1 = _getVariableRateSlope1(); + assertEq(currentVariableRateSlope1, newVariableRateSlope1); + } + + function testRevertUpdateGhoBorrowRateVariableRateSlope1IfMaxExceededUpwards() public { + uint32 oldVariableRateSlope1 = _getVariableRateSlope1(); + uint32 newVariableRateSlope1 = oldVariableRateSlope1 + + defaultBorrowRateConfig.variableRateSlope1MaxChange + + 1; + vm.prank(RISK_COUNCIL); + vm.expectRevert('INVALID_VARIABLE_RATE_SLOPE1'); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + defaultRateParams.baseVariableBorrowRate, + newVariableRateSlope1, + defaultRateParams.variableRateSlope2 + ); + } + + function testRevertUpdateGhoBorrowRateVariableRateSlope1IfMaxExceededDownwards() public { + uint32 oldVariableRateSlope1 = _getVariableRateSlope1(); + uint32 newVariableRateSlope1 = oldVariableRateSlope1 + + defaultBorrowRateConfig.variableRateSlope1MaxChange; + _setGhoBorrowRateViaConfigurator(1); // Change Gho borrow rate to not exceed max + uint32 ghoBorrowRate = _getGhoBorrowRate(); + vm.startPrank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + ghoBorrowRate, + newVariableRateSlope1, + newVariableRateSlope1 // variableRateSlope2 has to be gte variableRateSlope1 + ); + newVariableRateSlope1 += 1; // Set higher than max allowed + vm.warp(block.timestamp + GHO_AAVE_STEWARD.MINIMUM_DELAY() + 1); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + ghoBorrowRate, + newVariableRateSlope1, + newVariableRateSlope1 + ); + vm.warp(block.timestamp + GHO_AAVE_STEWARD.MINIMUM_DELAY() + 1); + vm.expectRevert('INVALID_VARIABLE_RATE_SLOPE1'); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + ghoBorrowRate, + newVariableRateSlope1 - defaultBorrowRateConfig.variableRateSlope1MaxChange - 1, + newVariableRateSlope1 + ); + vm.stopPrank(); + } + + function testUpdateGhoBorrowRateVariableRateSlope2() public { + uint32 oldVariableRateSlope2 = _getVariableRateSlope2(); + uint32 newVariableRateSlope2 = oldVariableRateSlope2 + 1; + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + defaultRateParams.baseVariableBorrowRate, + defaultRateParams.variableRateSlope1, + newVariableRateSlope2 + ); + uint32 currentVariableRateSlope2 = _getVariableRateSlope2(); + assertEq(currentVariableRateSlope2, newVariableRateSlope2); + } + + function testRevertUpdateGhoBorrowRateVariableRateSlope2IfMaxExceededUpwards() public { + uint32 oldVariableRateSlope2 = _getVariableRateSlope2(); + uint32 newVariableRateSlope2 = oldVariableRateSlope2 + + defaultBorrowRateConfig.variableRateSlope2MaxChange + + 1; + vm.prank(RISK_COUNCIL); + vm.expectRevert('INVALID_VARIABLE_RATE_SLOPE2'); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + defaultRateParams.baseVariableBorrowRate, + defaultRateParams.variableRateSlope1, + newVariableRateSlope2 + ); + } + + function testRevertUpdateGhoBorrowRateVariableRateSlope2IfMaxExceededDownwards() public { + uint32 oldVariableRateSlope2 = _getVariableRateSlope2(); + uint32 newVariableRateSlope2 = oldVariableRateSlope2 + + defaultBorrowRateConfig.variableRateSlope2MaxChange; + _setGhoBorrowRateViaConfigurator(1); + uint32 ghoBorrowRate = _getGhoBorrowRate(); + vm.startPrank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + ghoBorrowRate, + defaultRateParams.variableRateSlope1, + newVariableRateSlope2 + ); + newVariableRateSlope2 += 1; // Set higher than max allowed + vm.warp(block.timestamp + GHO_AAVE_STEWARD.MINIMUM_DELAY() + 1); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + ghoBorrowRate, + defaultRateParams.variableRateSlope1, + newVariableRateSlope2 + ); + vm.warp(block.timestamp + GHO_AAVE_STEWARD.MINIMUM_DELAY() + 1); + vm.expectRevert('INVALID_VARIABLE_RATE_SLOPE2'); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + ghoBorrowRate, + defaultRateParams.variableRateSlope1, + newVariableRateSlope2 - defaultBorrowRateConfig.variableRateSlope2MaxChange - 1 + ); + vm.stopPrank(); + } + + function testRevertUpdateGhoBorrowRateIfUnauthorized() public { + vm.expectRevert('INVALID_CALLER'); + vm.prank(ALICE); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + 0.07e4, + defaultRateParams.variableRateSlope1, + defaultRateParams.variableRateSlope2 + ); + } + + function testRevertUpdateGhoBorrowRateIfUpdatedTooSoon() public { + uint32 oldBorrowRate = _getGhoBorrowRate(); + vm.prank(RISK_COUNCIL); + uint32 newBorrowRate = oldBorrowRate + 1; + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + newBorrowRate, + defaultRateParams.variableRateSlope1, + defaultRateParams.variableRateSlope2 + ); + vm.prank(RISK_COUNCIL); + vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + newBorrowRate, + defaultRateParams.variableRateSlope1, + defaultRateParams.variableRateSlope2 + ); + } + + function testRevertUpdateGhoBorrowRateNoChange() public { + uint32 oldBorrowRate = _getGhoBorrowRate(); + vm.prank(RISK_COUNCIL); + vm.expectRevert('NO_CHANGE_IN_RATES'); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + oldBorrowRate, + defaultRateParams.variableRateSlope1, + defaultRateParams.variableRateSlope2 + ); + } + + function testRevertUpdateGhoBorrowRateIfValueMoreThanMax() public { + uint32 maxGhoBorrowRate = GHO_BORROW_RATE_MAX; + _setGhoBorrowRateViaConfigurator(maxGhoBorrowRate); + vm.prank(RISK_COUNCIL); + vm.expectRevert('BORROW_RATE_HIGHER_THAN_MAX'); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + maxGhoBorrowRate + 1, + defaultRateParams.variableRateSlope1, + defaultRateParams.variableRateSlope2 + ); + } + + function testRevertUpdateGhoBorrowRateIfMaxExceededUpwards() public { + uint32 oldBorrowRate = _getGhoBorrowRate(); + uint32 newBorrowRate = oldBorrowRate + GHO_BORROW_RATE_CHANGE_MAX + 1; + vm.prank(RISK_COUNCIL); + vm.expectRevert('INVALID_BORROW_RATE_UPDATE'); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + newBorrowRate, + defaultRateParams.variableRateSlope1, + defaultRateParams.variableRateSlope2 + ); + } + + function testRevertUpdateGhoBorrowRateIfMaxExceededDownwards() public { + vm.startPrank(RISK_COUNCIL); + + // set a high borrow rate + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + _getGhoBorrowRate() + GHO_BORROW_RATE_CHANGE_MAX, + defaultRateParams.variableRateSlope1, + defaultRateParams.variableRateSlope2 + ); + vm.warp(block.timestamp + GHO_AAVE_STEWARD.MINIMUM_DELAY() + 1); + + uint32 oldBorrowRate = _getGhoBorrowRate(); + uint32 newBorrowRate = oldBorrowRate - GHO_BORROW_RATE_CHANGE_MAX - 1; + vm.expectRevert('INVALID_BORROW_RATE_UPDATE'); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + defaultRateParams.optimalUsageRatio, + newBorrowRate, + defaultRateParams.variableRateSlope1, + defaultRateParams.variableRateSlope2 + ); + + vm.stopPrank(); + } + + function testSetRiskConfig() public { + defaultBorrowRateConfig.optimalUsageRatioMaxChange += 1; + vm.prank(SHORT_EXECUTOR); + GHO_AAVE_STEWARD.setBorrowRateConfig( + defaultBorrowRateConfig.optimalUsageRatioMaxChange, + defaultBorrowRateConfig.baseVariableBorrowRateMaxChange, + defaultBorrowRateConfig.variableRateSlope1MaxChange, + defaultBorrowRateConfig.variableRateSlope2MaxChange + ); + IGhoAaveSteward.BorrowRateConfig memory currentBorrowRateConfig = GHO_AAVE_STEWARD + .getBorrowRateConfig(); + assertEq( + currentBorrowRateConfig.optimalUsageRatioMaxChange, + defaultBorrowRateConfig.optimalUsageRatioMaxChange + ); + } + + function _setGhoBorrowCapViaConfigurator(uint256 newBorrowCap) internal { + CONFIGURATOR.setBorrowCap(address(GHO_TOKEN), newBorrowCap); + } + + function _getGhoBorrowCap() internal view returns (uint256) { + DataTypes.ReserveConfigurationMap memory configuration = POOL.getConfiguration( + address(GHO_TOKEN) + ); + return configuration.getBorrowCap(); + } + + function _setGhoSupplyCapViaConfigurator(uint256 newSupplyCap) internal { + CONFIGURATOR.setSupplyCap(address(GHO_TOKEN), newSupplyCap); + } + + function _getGhoSupplyCap() internal view returns (uint256) { + DataTypes.ReserveConfigurationMap memory configuration = POOL.getConfiguration( + address(GHO_TOKEN) + ); + return configuration.getSupplyCap(); + } + + function _setGhoBorrowRateViaConfigurator(uint32 newBorrowRate) internal { + IDefaultInterestRateStrategyV2.InterestRateData + memory rateParams = IDefaultInterestRateStrategyV2.InterestRateData({ + optimalUsageRatio: 1_00, + baseVariableBorrowRate: newBorrowRate, + variableRateSlope1: 0, + variableRateSlope2: 0 + }); + CONFIGURATOR.setReserveInterestRateData(address(GHO_TOKEN), abi.encode(rateParams)); + uint256 currentBorrowRate = _getGhoBorrowRate(); + assertEq(currentBorrowRate, newBorrowRate); + } + + function _getGhoBorrowRate() internal view returns (uint32) { + address currentInterestRateStrategy = POOL.getReserveInterestRateStrategyAddress( + address(GHO_TOKEN) + ); + return + uint32( + IDefaultInterestRateStrategyV2(currentInterestRateStrategy).getBaseVariableBorrowRate( + address(GHO_TOKEN) + ) / 1e23 + ); // Convert to bps + } + + function _getOptimalUsageRatio() internal view returns (uint16) { + address currentInterestRateStrategy = POOL.getReserveInterestRateStrategyAddress( + address(GHO_TOKEN) + ); + return + uint16( + IDefaultInterestRateStrategyV2(currentInterestRateStrategy).getOptimalUsageRatio( + address(GHO_TOKEN) + ) / 1e23 + ); // Convert to bps + } + + function _getVariableRateSlope1() internal view returns (uint32) { + address currentInterestRateStrategy = POOL.getReserveInterestRateStrategyAddress( + address(GHO_TOKEN) + ); + return + uint32( + IDefaultInterestRateStrategyV2(currentInterestRateStrategy).getVariableRateSlope1( + address(GHO_TOKEN) + ) / 1e23 + ); // Convert to bps + } + + function _getVariableRateSlope2() internal view returns (uint32) { + address currentInterestRateStrategy = POOL.getReserveInterestRateStrategyAddress( + address(GHO_TOKEN) + ); + return + uint32( + IDefaultInterestRateStrategyV2(currentInterestRateStrategy).getVariableRateSlope2( + address(GHO_TOKEN) + ) / 1e23 + ); // Convert to bps + } +} diff --git a/src/test/TestGhoBase.t.sol b/src/test/TestGhoBase.t.sol index e85097cc..b382234c 100644 --- a/src/test/TestGhoBase.t.sol +++ b/src/test/TestGhoBase.t.sol @@ -29,6 +29,7 @@ import {MockUpgradeable} from './mocks/MockUpgradeable.sol'; import {PriceOracle} from '@aave/core-v3/contracts/mocks/oracle/PriceOracle.sol'; import {TestnetERC20} from '@aave/periphery-v3/contracts/mocks/testnet-helpers/TestnetERC20.sol'; import {WETH9Mock} from '@aave/periphery-v3/contracts/mocks/WETH9Mock.sol'; +import {MockPoolDataProvider} from './mocks/MockPoolDataProvider.sol'; // interfaces import {IAaveIncentivesController} from '@aave/core-v3/contracts/interfaces/IAaveIncentivesController.sol'; @@ -42,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'; @@ -56,15 +56,13 @@ import {GhoAToken} from '../contracts/facilitators/aave/tokens/GhoAToken.sol'; import {GhoDiscountRateStrategy} from '../contracts/facilitators/aave/interestStrategy/GhoDiscountRateStrategy.sol'; import {GhoFlashMinter} from '../contracts/facilitators/flashMinter/GhoFlashMinter.sol'; import {GhoInterestRateStrategy} from '../contracts/facilitators/aave/interestStrategy/GhoInterestRateStrategy.sol'; -import {GhoSteward} from '../contracts/misc/GhoSteward.sol'; -import {IGhoSteward} from '../contracts/misc/interfaces/IGhoSteward.sol'; -import {IGhoStewardV2} from '../contracts/misc/interfaces/IGhoStewardV2.sol'; +import {IGhoAaveSteward} from '../contracts/misc/interfaces/IGhoAaveSteward.sol'; +import {GhoAaveSteward} from '../contracts/misc/GhoAaveSteward.sol'; import {GhoOracle} from '../contracts/facilitators/aave/oracle/GhoOracle.sol'; import {GhoStableDebtToken} from '../contracts/facilitators/aave/tokens/GhoStableDebtToken.sol'; import {GhoToken} from '../contracts/gho/GhoToken.sol'; import {UpgradeableGhoToken} from '../contracts/gho/UpgradeableGhoToken.sol'; import {GhoVariableDebtToken} from '../contracts/facilitators/aave/tokens/GhoVariableDebtToken.sol'; -import {GhoStewardV2} from '../contracts/misc/GhoStewardV2.sol'; import {FixedRateStrategyFactory} from '../contracts/facilitators/aave/interestStrategy/FixedRateStrategyFactory.sol'; // GSM contracts @@ -78,6 +76,16 @@ import {FixedFeeStrategy} from '../contracts/facilitators/gsm/feeStrategy/FixedF import {SampleLiquidator} from '../contracts/facilitators/gsm/misc/SampleLiquidator.sol'; import {SampleSwapFreezer} from '../contracts/facilitators/gsm/misc/SampleSwapFreezer.sol'; 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 '../contracts/facilitators/gsm/feeStrategy/FixedFeeStrategyFactory.sol'; + +// CCIP contracts +import {MockUpgradeableLockReleaseTokenPool} from './mocks/MockUpgradeableLockReleaseTokenPool.sol'; +import {RateLimiter} from '../contracts/misc/dependencies/Ccip.sol'; +import {IGhoCcipSteward} from '../contracts/misc/interfaces/IGhoCcipSteward.sol'; +import {GhoCcipSteward} from '../contracts/misc/GhoCcipSteward.sol'; +import {GhoBucketSteward} from '../contracts/misc/GhoBucketSteward.sol'; contract TestGhoBase is Test, Constants, Events { using WadRayMath for uint256; @@ -124,9 +132,15 @@ contract TestGhoBase is Test, Constants, Events { SampleSwapFreezer GHO_GSM_SWAP_FREEZER; GsmRegistry GHO_GSM_REGISTRY; GhoOracle GHO_ORACLE; - GhoSteward GHO_STEWARD; - GhoStewardV2 GHO_STEWARD_V2; + GhoAaveSteward GHO_AAVE_STEWARD; + GhoCcipSteward GHO_CCIP_STEWARD; + GhoGsmSteward GHO_GSM_STEWARD; + GhoBucketSteward GHO_BUCKET_STEWARD; + MockPoolDataProvider MOCK_POOL_DATA_PROVIDER; + FixedRateStrategyFactory FIXED_RATE_STRATEGY_FACTORY; + FixedFeeStrategyFactory FIXED_FEE_STRATEGY_FACTORY; + MockUpgradeableLockReleaseTokenPool GHO_TOKEN_POOL; constructor() { setupGho(); @@ -141,6 +155,7 @@ contract TestGhoBase is Test, Constants, Events { bytes memory empty; ACL_MANAGER = new MockAclManager(); PROVIDER = new MockAddressesProvider(address(ACL_MANAGER)); + MOCK_POOL_DATA_PROVIDER = new MockPoolDataProvider(address(PROVIDER)); POOL = new MockPool(IPoolAddressesProvider(address(PROVIDER))); CONFIGURATOR = new MockConfigurator(IPool(POOL)); PRICE_ORACLE = new PriceOracle(); @@ -175,8 +190,6 @@ contract TestGhoBase is Test, Constants, Events { STK_TOKEN = IStakedAaveV3(address(stkAaveProxy)); USDC_TOKEN = new TestnetERC20('USD Coin', 'USDC', 6, FAUCET); USDC_4626_TOKEN = new MockERC4626('USD Coin 4626', '4626', address(USDC_TOKEN)); - address ghoTokenAddress = address(GHO_TOKEN); - address discountToken = address(STK_TOKEN); IPool iPool = IPool(address(POOL)); WETH = new WETH9Mock('Wrapped Ether', 'WETH', FAUCET); GHO_DEBT_TOKEN = new GhoVariableDebtToken(iPool); @@ -184,7 +197,7 @@ contract TestGhoBase is Test, Constants, Events { GHO_ATOKEN = new GhoAToken(iPool); GHO_DEBT_TOKEN.initialize( iPool, - ghoTokenAddress, + address(GHO_TOKEN), IAaveIncentivesController(address(0)), 18, 'Aave Variable Debt GHO', @@ -193,7 +206,7 @@ contract TestGhoBase is Test, Constants, Events { ); GHO_STABLE_DEBT_TOKEN.initialize( iPool, - ghoTokenAddress, + address(GHO_TOKEN), IAaveIncentivesController(address(0)), 18, 'Aave Stable Debt GHO', @@ -203,7 +216,7 @@ contract TestGhoBase is Test, Constants, Events { GHO_ATOKEN.initialize( iPool, TREASURY, - ghoTokenAddress, + address(GHO_TOKEN), IAaveIncentivesController(address(0)), 18, 'Aave GHO', @@ -211,7 +224,7 @@ contract TestGhoBase is Test, Constants, Events { empty ); GHO_ATOKEN.updateGhoTreasury(TREASURY); - GHO_DEBT_TOKEN.updateDiscountToken(discountToken); + GHO_DEBT_TOKEN.updateDiscountToken(address(STK_TOKEN)); GHO_DISCOUNT_STRATEGY = new GhoDiscountRateStrategy(); GHO_DEBT_TOKEN.updateDiscountRateStrategy(address(GHO_DISCOUNT_STRATEGY)); GHO_DEBT_TOKEN.setAToken(address(GHO_ATOKEN)); @@ -246,7 +259,6 @@ contract TestGhoBase is Test, Constants, Events { address(USDC_4626_TOKEN), 6 ); - GHO_GSM_FIXED_FEE_STRATEGY = new FixedFeeStrategy(DEFAULT_GSM_BUY_FEE, DEFAULT_GSM_SELL_FEE); GHO_GSM_LAST_RESORT_LIQUIDATOR = new SampleLiquidator(); GHO_GSM_SWAP_FREEZER = new SampleSwapFreezer(); Gsm gsm = new Gsm( @@ -284,28 +296,63 @@ contract TestGhoBase is Test, Constants, Events { GHO_TOKEN.addFacilitator(FAUCET, 'Faucet Facilitator', type(uint128).max); GHO_GSM_REGISTRY = new GsmRegistry(address(this)); - GHO_STEWARD = new GhoSteward( - address(PROVIDER), - address(GHO_TOKEN), - RISK_COUNCIL, - SHORT_EXECUTOR - ); - GHO_TOKEN.grantRole(GHO_TOKEN_BUCKET_MANAGER_ROLE, address(GHO_STEWARD)); FIXED_RATE_STRATEGY_FACTORY = new FixedRateStrategyFactory(address(PROVIDER)); - GHO_STEWARD_V2 = new GhoStewardV2( - SHORT_EXECUTOR, - address(PROVIDER), + + // Deploy Gho Token Pool + address ARM_PROXY = makeAddr('ARM_PROXY'); + address OWNER = makeAddr('OWNER'); + address ROUTER = makeAddr('ROUTER'); + address PROXY_ADMIN = makeAddr('PROXY_ADMIN'); + uint256 INITIAL_BRIDGE_LIMIT = 100e6 * 1e18; + MockUpgradeableLockReleaseTokenPool tokenPoolImpl = new MockUpgradeableLockReleaseTokenPool( address(GHO_TOKEN), - address(FIXED_RATE_STRATEGY_FACTORY), - RISK_COUNCIL - ); - GHO_TOKEN.grantRole(GHO_TOKEN_BUCKET_MANAGER_ROLE, address(GHO_STEWARD_V2)); - GHO_GSM.grantRole(GSM_CONFIGURATOR_ROLE, address(GHO_STEWARD_V2)); - address[] memory controlledFacilitators = new address[](2); - controlledFacilitators[0] = address(GHO_ATOKEN); - controlledFacilitators[1] = address(GHO_GSM); - vm.prank(SHORT_EXECUTOR); - GHO_STEWARD_V2.setControlledFacilitator(controlledFacilitators, true); + ARM_PROXY, + false, + true + ); + // proxy deploy and init + address[] memory emptyArray = new address[](0); + bytes memory tokenPoolInitParams = abi.encodeWithSignature( + 'initialize(address,address[],address,uint256)', + OWNER, + emptyArray, + ROUTER, + INITIAL_BRIDGE_LIMIT + ); + TransparentUpgradeableProxy tokenPoolProxy = new TransparentUpgradeableProxy( + address(tokenPoolImpl), + PROXY_ADMIN, + tokenPoolInitParams + ); + + // Manage ownership + vm.prank(OWNER); + MockUpgradeableLockReleaseTokenPool(address(tokenPoolProxy)).acceptOwnership(); + GHO_TOKEN_POOL = MockUpgradeableLockReleaseTokenPool(address(tokenPoolProxy)); + + // Setup GHO Token Pool + uint64 SOURCE_CHAIN_SELECTOR = 1; + uint64 DEST_CHAIN_SELECTOR = 2; + RateLimiter.Config memory initialOutboundRateLimit = RateLimiter.Config({ + isEnabled: true, + capacity: 100e28, + rate: 1e15 + }); + RateLimiter.Config memory initialInboundRateLimit = RateLimiter.Config({ + isEnabled: true, + capacity: 222e30, + rate: 1e18 + }); + MockUpgradeableLockReleaseTokenPool.ChainUpdate[] + memory chainUpdate = new MockUpgradeableLockReleaseTokenPool.ChainUpdate[](1); + chainUpdate[0] = MockUpgradeableLockReleaseTokenPool.ChainUpdate({ + remoteChainSelector: DEST_CHAIN_SELECTOR, + allowed: true, + outboundRateLimiterConfig: initialOutboundRateLimit, + inboundRateLimiterConfig: initialInboundRateLimit + }); + vm.prank(OWNER); + GHO_TOKEN_POOL.applyChainUpdates(chainUpdate); } function ghoFaucet(address to, uint256 amount) public { diff --git a/src/test/TestGhoBucketSteward.t.sol b/src/test/TestGhoBucketSteward.t.sol new file mode 100644 index 00000000..89e3641e --- /dev/null +++ b/src/test/TestGhoBucketSteward.t.sol @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import './TestGhoBase.t.sol'; + +contract TestGhoBucketSteward is TestGhoBase { + function setUp() public { + // Deploy Gho Bucket Steward + GHO_BUCKET_STEWARD = new GhoBucketSteward(SHORT_EXECUTOR, address(GHO_TOKEN), RISK_COUNCIL); + address[] memory controlledFacilitators = new address[](2); + controlledFacilitators[0] = address(GHO_ATOKEN); + controlledFacilitators[1] = address(GHO_GSM); + vm.prank(SHORT_EXECUTOR); + GHO_BUCKET_STEWARD.setControlledFacilitator(controlledFacilitators, true); + + /// @dev Since block.timestamp starts at 0 this is a necessary condition (block.timestamp > `MINIMUM_DELAY`) for the timelocked contract methods to work. + vm.warp(GHO_BUCKET_STEWARD.MINIMUM_DELAY() + 1); + + // Grant roles + GHO_TOKEN.grantRole(GHO_TOKEN_BUCKET_MANAGER_ROLE, address(GHO_BUCKET_STEWARD)); + } + + function testConstructor() public { + assertEq(GHO_BUCKET_STEWARD.owner(), SHORT_EXECUTOR); + assertEq(GHO_BUCKET_STEWARD.GHO_TOKEN(), address(GHO_TOKEN)); + assertEq(GHO_BUCKET_STEWARD.RISK_COUNCIL(), RISK_COUNCIL); + + address[] memory controlledFacilitators = GHO_BUCKET_STEWARD.getControlledFacilitators(); + assertEq(controlledFacilitators.length, 2); + + uint40 facilitatorTimelock = GHO_BUCKET_STEWARD.getFacilitatorBucketCapacityTimelock( + controlledFacilitators[0] + ); + assertEq(facilitatorTimelock, 0); + } + + function testRevertConstructorInvalidOwner() public { + vm.expectRevert('INVALID_OWNER'); + new GhoBucketSteward(address(0), address(0x002), address(0x003)); + } + + function testRevertConstructorInvalidGhoToken() public { + vm.expectRevert('INVALID_GHO_TOKEN'); + new GhoBucketSteward(address(0x001), address(0), address(0x003)); + } + + function testRevertConstructorInvalidRiskCouncil() public { + vm.expectRevert('INVALID_RISK_COUNCIL'); + new GhoBucketSteward(address(0x001), address(0x002), address(0)); + } + + function testChangeOwnership() public { + address newOwner = makeAddr('newOwner'); + assertEq(GHO_BUCKET_STEWARD.owner(), SHORT_EXECUTOR); + vm.prank(SHORT_EXECUTOR); + GHO_BUCKET_STEWARD.transferOwnership(newOwner); + assertEq(GHO_BUCKET_STEWARD.owner(), newOwner); + } + + function testChangeOwnershipRevert() public { + vm.expectRevert('Ownable: new owner is the zero address'); + vm.prank(SHORT_EXECUTOR); + GHO_BUCKET_STEWARD.transferOwnership(address(0)); + } + + function testUpdateFacilitatorBucketCapacity() public { + (uint256 currentBucketCapacity, ) = 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, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); + assertEq(newBucketCapacity, capacity); + } + + function testUpdateFacilitatorBucketCapacityMaxValue() public { + (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); + uint128 newBucketCapacity = uint128(currentBucketCapacity * 2); + vm.prank(RISK_COUNCIL); + GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity(address(GHO_ATOKEN), newBucketCapacity); + (uint256 capacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); + assertEq(capacity, newBucketCapacity); + } + + function testUpdateFacilitatorBucketCapacityTimelock() public { + (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); + vm.prank(RISK_COUNCIL); + GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity( + address(GHO_ATOKEN), + uint128(currentBucketCapacity) + 1 + ); + uint40 timelock = GHO_BUCKET_STEWARD.getFacilitatorBucketCapacityTimelock(address(GHO_ATOKEN)); + assertEq(timelock, block.timestamp); + } + + function testUpdateFacilitatorBucketCapacityAfterTimelock() public { + (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); + vm.prank(RISK_COUNCIL); + uint128 newBucketCapacity = uint128(currentBucketCapacity) + 1; + GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity(address(GHO_ATOKEN), newBucketCapacity); + skip(GHO_BUCKET_STEWARD.MINIMUM_DELAY() + 1); + uint128 newBucketCapacityAfterTimelock = newBucketCapacity + 1; + vm.prank(RISK_COUNCIL); + GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity( + address(GHO_ATOKEN), + newBucketCapacityAfterTimelock + ); + (uint256 capacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); + assertEq(capacity, newBucketCapacityAfterTimelock); + } + + function testRevertUpdateFacilitatorBucketCapacityIfUnauthorized() public { + vm.expectRevert('INVALID_CALLER'); + vm.prank(ALICE); + GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity(address(GHO_ATOKEN), 123); + } + + function testRevertUpdateFacilitatorBucketCapacityIfUpdatedTooSoon() public { + (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); + vm.prank(RISK_COUNCIL); + GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity( + address(GHO_ATOKEN), + uint128(currentBucketCapacity) + 1 + ); + vm.prank(RISK_COUNCIL); + vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); + GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity( + address(GHO_ATOKEN), + uint128(currentBucketCapacity) + 2 + ); + } + + function testRevertUpdateFacilitatorBucketCapacityNoChange() public { + (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); + vm.prank(RISK_COUNCIL); + vm.expectRevert('NO_CHANGE_IN_BUCKET_CAPACITY'); + GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity( + address(GHO_ATOKEN), + uint128(currentBucketCapacity) + ); + } + + function testRevertUpdateFacilitatorBucketCapacityIfFacilitatorNotInControl() public { + (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); + vm.prank(RISK_COUNCIL); + vm.expectRevert('FACILITATOR_NOT_CONTROLLED'); + GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity( + address(GHO_GSM_4626), + uint128(currentBucketCapacity) + 1 + ); + } + + function testRevertUpdateFacilitatorBucketCapacityIfStewardLostBucketManagerRole() public { + (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); + GHO_TOKEN.revokeRole(GHO_TOKEN_BUCKET_MANAGER_ROLE, address(GHO_BUCKET_STEWARD)); + vm.expectRevert( + AccessControlErrorsLib.MISSING_ROLE( + GHO_TOKEN_BUCKET_MANAGER_ROLE, + address(GHO_BUCKET_STEWARD) + ) + ); + vm.prank(RISK_COUNCIL); + GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity( + address(GHO_ATOKEN), + uint128(currentBucketCapacity) + 1 + ); + } + + function testRevertUpdateFacilitatorBucketCapacityIfMoreThanDouble() public { + (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); + vm.prank(RISK_COUNCIL); + vm.expectRevert('INVALID_BUCKET_CAPACITY_UPDATE'); + GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity( + address(GHO_ATOKEN), + uint128(currentBucketCapacity * 2) + 1 + ); + } + + function testRevertUpdateFacilitatorBucketCapacityDecrement() public { + (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); + vm.prank(RISK_COUNCIL); + uint128 newBucketCapacity = uint128(currentBucketCapacity) - 1; + vm.expectRevert('INVALID_BUCKET_CAPACITY_UPDATE'); + GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity(address(GHO_ATOKEN), newBucketCapacity); + } + + function testSetControlledFacilitatorAdd() public { + address[] memory oldControlledFacilitators = GHO_BUCKET_STEWARD.getControlledFacilitators(); + address[] memory newGsmList = new address[](1); + newGsmList[0] = address(GHO_GSM_4626); + vm.prank(SHORT_EXECUTOR); + GHO_BUCKET_STEWARD.setControlledFacilitator(newGsmList, true); + address[] memory newControlledFacilitators = GHO_BUCKET_STEWARD.getControlledFacilitators(); + assertEq(newControlledFacilitators.length, oldControlledFacilitators.length + 1); + assertTrue(_contains(newControlledFacilitators, address(GHO_GSM_4626))); + } + + function testSetControlledFacilitatorsRemove() public { + address[] memory oldControlledFacilitators = GHO_BUCKET_STEWARD.getControlledFacilitators(); + address[] memory disableGsmList = new address[](1); + disableGsmList[0] = address(GHO_GSM); + vm.prank(SHORT_EXECUTOR); + GHO_BUCKET_STEWARD.setControlledFacilitator(disableGsmList, false); + address[] memory newControlledFacilitators = GHO_BUCKET_STEWARD.getControlledFacilitators(); + assertEq(newControlledFacilitators.length, oldControlledFacilitators.length - 1); + assertFalse(_contains(newControlledFacilitators, address(GHO_GSM))); + } + + function testRevertSetControlledFacilitatorIfUnauthorized() public { + vm.expectRevert(OwnableErrorsLib.CALLER_NOT_OWNER()); + vm.prank(RISK_COUNCIL); + address[] memory newGsmList = new address[](1); + newGsmList[0] = address(GHO_GSM_4626); + GHO_BUCKET_STEWARD.setControlledFacilitator(newGsmList, true); + } +} diff --git a/src/test/TestGhoCcipSteward.t.sol b/src/test/TestGhoCcipSteward.t.sol new file mode 100644 index 00000000..2295ed4a --- /dev/null +++ b/src/test/TestGhoCcipSteward.t.sol @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import './TestGhoBase.t.sol'; +import {RateLimiter} from '../contracts/misc/dependencies/Ccip.sol'; + +contract TestGhoCcipSteward is TestGhoBase { + RateLimiter.Config rateLimitConfig = + RateLimiter.Config({isEnabled: true, capacity: type(uint128).max, rate: 1e15}); + uint64 remoteChainSelector = 2; + + event ChainConfigured( + uint64 remoteChainSelector, + RateLimiter.Config outboundRateLimiterConfig, + RateLimiter.Config inboundRateLimiterConfig + ); + + function setUp() public { + // Deploy Gho CCIP Steward + GHO_CCIP_STEWARD = new GhoCcipSteward( + address(GHO_TOKEN), + address(GHO_TOKEN_POOL), + RISK_COUNCIL, + true + ); + + /// @dev Since block.timestamp starts at 0 this is a necessary condition (block.timestamp > `MINIMUM_DELAY`) for the timelocked contract methods to work. + vm.warp(GHO_CCIP_STEWARD.MINIMUM_DELAY() + 1); + + // Grant accesses to the Steward + vm.startPrank(GHO_TOKEN_POOL.owner()); + GHO_TOKEN_POOL.setRateLimitAdmin(address(GHO_CCIP_STEWARD)); + GHO_TOKEN_POOL.setBridgeLimitAdmin(address(GHO_CCIP_STEWARD)); + vm.stopPrank(); + } + + function testConstructor() public { + assertEq(GHO_CCIP_STEWARD.MINIMUM_DELAY(), MINIMUM_DELAY_V2); + + assertEq(GHO_CCIP_STEWARD.GHO_TOKEN(), address(GHO_TOKEN)); + assertEq(GHO_CCIP_STEWARD.GHO_TOKEN_POOL(), address(GHO_TOKEN_POOL)); + assertEq(GHO_CCIP_STEWARD.RISK_COUNCIL(), RISK_COUNCIL); + } + + function testRevertConstructorInvalidGhoToken() public { + vm.expectRevert('INVALID_GHO_TOKEN'); + new GhoCcipSteward(address(0), address(0x002), address(0x003), true); + } + + function testRevertConstructorInvalidGhoTokenPool() public { + vm.expectRevert('INVALID_GHO_TOKEN_POOL'); + new GhoCcipSteward(address(0x001), address(0), address(0x003), true); + } + + function testRevertConstructorInvalidRiskCouncil() public { + vm.expectRevert('INVALID_RISK_COUNCIL'); + new GhoCcipSteward(address(0x001), address(0x002), address(0), true); + } + + function testUpdateBridgeLimit() public { + uint256 oldBridgeLimit = GHO_TOKEN_POOL.getBridgeLimit(); + uint256 newBridgeLimit = oldBridgeLimit + 1; + vm.prank(RISK_COUNCIL); + GHO_CCIP_STEWARD.updateBridgeLimit(newBridgeLimit); + uint256 currentBridgeLimit = GHO_TOKEN_POOL.getBridgeLimit(); + assertEq(currentBridgeLimit, newBridgeLimit); + } + + function testRevertUpdateBridgeLimitIfUnauthorized() public { + uint256 oldBridgeLimit = GHO_TOKEN_POOL.getBridgeLimit(); + uint256 newBridgeLimit = oldBridgeLimit + 1; + vm.prank(ALICE); + vm.expectRevert('INVALID_CALLER'); + GHO_CCIP_STEWARD.updateBridgeLimit(newBridgeLimit); + } + + function testRevertUpdateBridgeLimitIfUpdatedTooSoon() public { + uint256 oldBridgeLimit = GHO_TOKEN_POOL.getBridgeLimit(); + uint256 newBridgeLimit = oldBridgeLimit + 1; + vm.prank(RISK_COUNCIL); + GHO_CCIP_STEWARD.updateBridgeLimit(newBridgeLimit); + vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); + vm.prank(RISK_COUNCIL); + GHO_CCIP_STEWARD.updateBridgeLimit(newBridgeLimit); + } + + function testRevertUpdateBridgeLimitNoChange() public { + uint256 oldBridgeLimit = GHO_TOKEN_POOL.getBridgeLimit(); + vm.prank(RISK_COUNCIL); + vm.expectRevert('NO_CHANGE_IN_BRIDGE_LIMIT'); + GHO_CCIP_STEWARD.updateBridgeLimit(oldBridgeLimit); + } + + function testRevertUpdateBridgeLimitIfDisabled() public { + // Deploy new Gho CCIP Steward with bridge limit disabled + GHO_CCIP_STEWARD = new GhoCcipSteward( + address(GHO_TOKEN), + address(GHO_TOKEN_POOL), + RISK_COUNCIL, + false + ); + + /// @dev Since block.timestamp starts at 0 this is a necessary condition (block.timestamp > `MINIMUM_DELAY`) for the timelocked contract methods to work. + vm.warp(GHO_CCIP_STEWARD.MINIMUM_DELAY() + 1); + + // Grant accesses to the Steward + vm.startPrank(GHO_TOKEN_POOL.owner()); + GHO_TOKEN_POOL.setRateLimitAdmin(address(GHO_CCIP_STEWARD)); + GHO_TOKEN_POOL.setBridgeLimitAdmin(address(GHO_CCIP_STEWARD)); + vm.stopPrank(); + + uint256 oldBridgeLimit = GHO_TOKEN_POOL.getBridgeLimit(); + uint256 newBridgeLimit = oldBridgeLimit + 1; + vm.expectRevert('BRIDGE_LIMIT_DISABLED'); + vm.prank(RISK_COUNCIL); + GHO_CCIP_STEWARD.updateBridgeLimit(newBridgeLimit); + } + + function testUpdateBridgeLimitTooHigh() public { + uint256 oldBridgeLimit = GHO_TOKEN_POOL.getBridgeLimit(); + uint256 newBridgeLimit = (oldBridgeLimit + 1) * 2; + vm.prank(RISK_COUNCIL); + vm.expectRevert('INVALID_BRIDGE_LIMIT_UPDATE'); + GHO_CCIP_STEWARD.updateBridgeLimit(newBridgeLimit); + } + + function testUpdateBridgeLimitFuzz(uint256 newBridgeLimit) public { + uint256 oldBridgeLimit = GHO_TOKEN_POOL.getBridgeLimit(); + newBridgeLimit = bound(newBridgeLimit, 0, oldBridgeLimit * 2); + vm.prank(RISK_COUNCIL); + GHO_CCIP_STEWARD.updateBridgeLimit(newBridgeLimit); + uint256 currentBridgeLimit = GHO_TOKEN_POOL.getBridgeLimit(); + assertEq(currentBridgeLimit, newBridgeLimit); + } + + function testUpdateRateLimit() 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: true, + capacity: outboundConfig.capacity + 1, + rate: outboundConfig.rate + 1 + }); + + RateLimiter.Config memory newInboundConfig = RateLimiter.Config({ + isEnabled: true, + capacity: inboundConfig.capacity + 1, + rate: inboundConfig.rate + 1 + }); + + 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 testRevertUpdateRateLimitIfUnauthorized() public { + vm.prank(ALICE); + vm.expectRevert('INVALID_CALLER'); + GHO_CCIP_STEWARD.updateRateLimit( + remoteChainSelector, + rateLimitConfig.isEnabled, + rateLimitConfig.capacity, + rateLimitConfig.rate, + rateLimitConfig.isEnabled, + rateLimitConfig.capacity, + rateLimitConfig.rate + ); + } + + function testRevertUpdateRateLimitIfUpdatedTooSoon() public { + RateLimiter.TokenBucket memory outboundConfig = MockUpgradeableLockReleaseTokenPool( + GHO_TOKEN_POOL + ).getCurrentOutboundRateLimiterState(remoteChainSelector); + RateLimiter.TokenBucket memory inboundConfig = MockUpgradeableLockReleaseTokenPool( + GHO_TOKEN_POOL + ).getCurrentInboundRateLimiterState(remoteChainSelector); + + vm.prank(RISK_COUNCIL); + GHO_CCIP_STEWARD.updateRateLimit( + remoteChainSelector, + outboundConfig.isEnabled, + outboundConfig.capacity + 1, + outboundConfig.rate, + inboundConfig.isEnabled, + inboundConfig.capacity, + inboundConfig.rate + ); + vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); + vm.prank(RISK_COUNCIL); + GHO_CCIP_STEWARD.updateRateLimit( + remoteChainSelector, + outboundConfig.isEnabled, + outboundConfig.capacity + 2, + outboundConfig.rate, + inboundConfig.isEnabled, + inboundConfig.capacity, + inboundConfig.rate + ); + } + + function testRevertUpdateRateLimitNoChange() public { + RateLimiter.TokenBucket memory outboundConfig = MockUpgradeableLockReleaseTokenPool( + GHO_TOKEN_POOL + ).getCurrentOutboundRateLimiterState(remoteChainSelector); + RateLimiter.TokenBucket memory inboundConfig = MockUpgradeableLockReleaseTokenPool( + GHO_TOKEN_POOL + ).getCurrentInboundRateLimiterState(remoteChainSelector); + + vm.prank(RISK_COUNCIL); + vm.expectRevert('NO_CHANGE_IN_RATE_LIMIT'); + GHO_CCIP_STEWARD.updateRateLimit( + remoteChainSelector, + outboundConfig.isEnabled, + outboundConfig.capacity, + outboundConfig.rate, + inboundConfig.isEnabled, + inboundConfig.capacity, + inboundConfig.rate + ); + } + + function testRevertUpdateRateLimitToZero() public { + RateLimiter.Config memory invalidConfig = RateLimiter.Config({ + isEnabled: true, + capacity: 0, + rate: 0 + }); + vm.prank(RISK_COUNCIL); + vm.expectRevert(); + GHO_CCIP_STEWARD.updateRateLimit( + remoteChainSelector, + invalidConfig.isEnabled, + invalidConfig.capacity, + invalidConfig.rate, + rateLimitConfig.isEnabled, + rateLimitConfig.capacity, + rateLimitConfig.rate + ); + } + + function testRevertUpdateRateLimitRateGreaterThanCapacity() public { + RateLimiter.Config memory invalidConfig = RateLimiter.Config({ + isEnabled: true, + capacity: 10, + rate: 100 + }); + vm.prank(RISK_COUNCIL); + vm.expectRevert(); + GHO_CCIP_STEWARD.updateRateLimit( + remoteChainSelector, + invalidConfig.isEnabled, + invalidConfig.capacity, + invalidConfig.rate, + rateLimitConfig.isEnabled, + rateLimitConfig.capacity, + rateLimitConfig.rate + ); + } + + function testUpdateRateLimitFuzz( + uint128 outboundCapacity, + uint128 outboundRate, + uint128 inboundCapacity, + uint128 inboundRate + ) public { + RateLimiter.TokenBucket memory currentOutboundConfig = MockUpgradeableLockReleaseTokenPool( + GHO_TOKEN_POOL + ).getCurrentOutboundRateLimiterState(remoteChainSelector); + RateLimiter.TokenBucket memory currentInboundConfig = MockUpgradeableLockReleaseTokenPool( + GHO_TOKEN_POOL + ).getCurrentInboundRateLimiterState(remoteChainSelector); + + // Capacity must be strictly greater than rate and nothing can change more than 100% + outboundRate = uint128(bound(outboundRate, 1, currentOutboundConfig.rate * 2)); + outboundCapacity = uint128( + bound(outboundCapacity, outboundRate + 1, currentOutboundConfig.capacity * 2) + ); + inboundRate = uint128(bound(inboundRate, 1, currentInboundConfig.rate * 2)); + inboundCapacity = uint128( + bound(inboundCapacity, inboundRate + 1, currentInboundConfig.capacity * 2) + ); + + vm.prank(RISK_COUNCIL); + GHO_CCIP_STEWARD.updateRateLimit( + remoteChainSelector, + rateLimitConfig.isEnabled, + outboundCapacity, + outboundRate, + rateLimitConfig.isEnabled, + inboundCapacity, + inboundRate + ); + } +} diff --git a/src/test/TestGhoGsmSteward.t.sol b/src/test/TestGhoGsmSteward.t.sol new file mode 100644 index 00000000..32e9127d --- /dev/null +++ b/src/test/TestGhoGsmSteward.t.sol @@ -0,0 +1,466 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import './TestGhoBase.t.sol'; +import {IGhoGsmSteward} from '../contracts/misc/interfaces/IGhoGsmSteward.sol'; + +contract TestGhoGsmSteward is TestGhoBase { + function setUp() public { + // Deploy Gho GSM Steward + FIXED_FEE_STRATEGY_FACTORY = new FixedFeeStrategyFactory(); + GHO_GSM_STEWARD = new GhoGsmSteward(address(FIXED_FEE_STRATEGY_FACTORY), RISK_COUNCIL); + + /// @dev Since block.timestamp starts at 0 this is a necessary condition (block.timestamp > `MINIMUM_DELAY`) for the timelocked contract methods to work. + vm.warp(GHO_GSM_STEWARD.MINIMUM_DELAY() + 1); + + // Grant required roles + GHO_GSM.grantRole(GSM_CONFIGURATOR_ROLE, address(GHO_GSM_STEWARD)); + } + + function testConstructor() public { + assertEq(GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(), GSM_FEE_RATE_CHANGE_MAX); + assertEq(GHO_GSM_STEWARD.MINIMUM_DELAY(), MINIMUM_DELAY_V2); + + assertEq(GHO_GSM_STEWARD.FIXED_FEE_STRATEGY_FACTORY(), address(FIXED_FEE_STRATEGY_FACTORY)); + assertEq(GHO_GSM_STEWARD.RISK_COUNCIL(), RISK_COUNCIL); + + address[] memory gsmFeeStrategies = FIXED_FEE_STRATEGY_FACTORY.getFixedFeeStrategies(); + assertEq(gsmFeeStrategies.length, 0); + } + + function testRevertConstructorInvalidGsmFeeStrategyFactory() public { + vm.expectRevert('INVALID_FIXED_FEE_STRATEGY_FACTORY'); + new GhoGsmSteward(address(0), address(0x002)); + } + + function testRevertConstructorInvalidRiskCouncil() public { + vm.expectRevert('INVALID_RISK_COUNCIL'); + new GhoGsmSteward(address(0x001), address(0)); + } + + function testUpdateGsmExposureCapUpwards() public { + uint128 oldExposureCap = GHO_GSM.getExposureCap(); + vm.prank(RISK_COUNCIL); + uint128 newExposureCap = oldExposureCap + 1; + GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), newExposureCap); + uint128 currentExposureCap = GHO_GSM.getExposureCap(); + assertEq(currentExposureCap, newExposureCap); + } + + function testUpdateGsmExposureCapDownwards() public { + uint128 oldExposureCap = GHO_GSM.getExposureCap(); + vm.prank(RISK_COUNCIL); + uint128 newExposureCap = oldExposureCap - 1; + GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), newExposureCap); + uint128 currentExposureCap = GHO_GSM.getExposureCap(); + assertEq(currentExposureCap, newExposureCap); + } + + function testUpdateGsmExposureCapMaxIncrease() public { + uint128 oldExposureCap = GHO_GSM.getExposureCap(); + uint128 newExposureCap = oldExposureCap * 2; + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), newExposureCap); + uint128 currentExposureCap = GHO_GSM.getExposureCap(); + assertEq(currentExposureCap, newExposureCap); + } + + function testUpdateGsmExposureCapMaxDecrease() public { + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), 0); + uint128 currentExposureCap = GHO_GSM.getExposureCap(); + assertEq(currentExposureCap, 0); + } + + function testUpdateGsmExposureCapTimelock() public { + uint128 oldExposureCap = GHO_GSM.getExposureCap(); + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), oldExposureCap + 1); + IGhoGsmSteward.GsmDebounce memory timelocks = GHO_GSM_STEWARD.getGsmTimelocks(address(GHO_GSM)); + assertEq(timelocks.gsmExposureCapLastUpdated, block.timestamp); + } + + function testUpdateGsmExposureCapAfterTimelock() public { + uint128 oldExposureCap = GHO_GSM.getExposureCap(); + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), oldExposureCap + 1); + skip(GHO_GSM_STEWARD.MINIMUM_DELAY() + 1); + uint128 newExposureCap = oldExposureCap + 2; + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), newExposureCap); + uint128 currentExposureCap = GHO_GSM.getExposureCap(); + assertEq(currentExposureCap, newExposureCap); + } + + function testRevertUpdateGsmExposureCapIfUnauthorized() public { + vm.expectRevert('INVALID_CALLER'); + vm.prank(ALICE); + GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), 50_000_000e18); + } + + function testRevertUpdateGsmExposureCapIfTooSoon() public { + uint128 oldExposureCap = GHO_GSM.getExposureCap(); + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), oldExposureCap + 1); + vm.prank(RISK_COUNCIL); + vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); + GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), oldExposureCap + 2); + } + + function testRevertUpdateGsmExposureCapNoChange() public { + uint128 oldExposureCap = GHO_GSM.getExposureCap(); + vm.prank(RISK_COUNCIL); + vm.expectRevert('NO_CHANGE_IN_EXPOSURE_CAP'); + GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), oldExposureCap); + } + + function testRevertUpdateGsmExposureCapIfValueMoreThanDouble() public { + uint128 oldExposureCap = GHO_GSM.getExposureCap(); + vm.prank(RISK_COUNCIL); + vm.expectRevert('INVALID_EXPOSURE_CAP_UPDATE'); + GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), oldExposureCap * 2 + 1); + } + + function testRevertUpdateGsmExposureCapIfStewardLostConfiguratorRole() public { + uint128 oldExposureCap = GHO_GSM.getExposureCap(); + GHO_GSM.revokeRole(GSM_CONFIGURATOR_ROLE, address(GHO_GSM_STEWARD)); + vm.expectRevert( + AccessControlErrorsLib.MISSING_ROLE(GSM_CONFIGURATOR_ROLE, address(GHO_GSM_STEWARD)) + ); + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), oldExposureCap + 1); + } + + function testUpdateGsmBuySellFeesBuyFeeUpwards() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee); + address newStrategy = GHO_GSM.getFeeStrategy(); + uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); + assertEq(newBuyFee, buyFee + 1); + } + + function testUpdateGsmBuySellFeesBuyFeeDownwards() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee - 1, sellFee); + address newStrategy = GHO_GSM.getFeeStrategy(); + uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); + assertEq(newBuyFee, buyFee - 1); + } + + function testUpdateGsmBuySellFeesBuyFeeMax() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + uint256 maxFeeUpdate = GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(); + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee + maxFeeUpdate, sellFee); + address newStrategy = GHO_GSM.getFeeStrategy(); + uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); + assertEq(newBuyFee, buyFee + maxFeeUpdate); + } + + function testUpdateGsmBuySellFeesBuyFeeMin() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + uint256 maxFeeUpdate = GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(); + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee - maxFeeUpdate, sellFee); + address newStrategy = GHO_GSM.getFeeStrategy(); + uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); + assertEq(newBuyFee, buyFee - maxFeeUpdate); + } + + function testUpdateGsmBuySellFeesSellFeeUpwards() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee, sellFee + 1); + address newStrategy = GHO_GSM.getFeeStrategy(); + uint256 newSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); + assertEq(newSellFee, sellFee + 1); + } + + function testUpdateGsmBuySellFeesSellFeeDownwards() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee, sellFee - 1); + address newStrategy = GHO_GSM.getFeeStrategy(); + uint256 newSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); + assertEq(newSellFee, sellFee - 1); + } + + function testUpdateGsmBuySellFeesSellFeeMax() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + uint256 maxFeeUpdate = GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(); + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee, sellFee + maxFeeUpdate); + address newStrategy = GHO_GSM.getFeeStrategy(); + uint256 newSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); + assertEq(newSellFee, sellFee + maxFeeUpdate); + } + + function testUpdateGsmBuySellFeesSellFeeMin() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + uint256 maxFeeUpdate = GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(); + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee, sellFee - maxFeeUpdate); + address newStrategy = GHO_GSM.getFeeStrategy(); + uint256 newSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); + assertEq(newSellFee, sellFee - maxFeeUpdate); + } + + function testUpdateGsmBuySellFeesBothFeesUpwards() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee + 1); + address newStrategy = GHO_GSM.getFeeStrategy(); + uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); + uint256 newSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); + assertEq(newBuyFee, buyFee + 1); + assertEq(newSellFee, sellFee + 1); + } + + function testUpdateGsmBuySellFeesBothFeesDownwards() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee - 1, sellFee - 1); + address newStrategy = GHO_GSM.getFeeStrategy(); + uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); + uint256 newSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); + assertEq(newBuyFee, buyFee - 1); + assertEq(newSellFee, sellFee - 1); + } + + function testUpdateGsmBuySellFeesBothFeesMax() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + uint256 maxFeeUpdate = GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(); + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmBuySellFees( + address(GHO_GSM), + buyFee + maxFeeUpdate, + sellFee + maxFeeUpdate + ); + address newStrategy = GHO_GSM.getFeeStrategy(); + uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); + uint256 newSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); + assertEq(newBuyFee, buyFee + maxFeeUpdate); + assertEq(newSellFee, sellFee + maxFeeUpdate); + } + + function testUpdateGsmBuySellFeesBothFeesMin() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + uint256 maxFeeUpdate = GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(); + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmBuySellFees( + address(GHO_GSM), + buyFee - maxFeeUpdate, + sellFee - maxFeeUpdate + ); + address newStrategy = GHO_GSM.getFeeStrategy(); + uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); + uint256 newSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); + assertEq(newBuyFee, buyFee - maxFeeUpdate); + assertEq(newSellFee, sellFee - maxFeeUpdate); + } + + function testUpdateGsmBuySellFeesTimelock() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee + 1); + IGhoGsmSteward.GsmDebounce memory timelocks = GHO_GSM_STEWARD.getGsmTimelocks(address(GHO_GSM)); + assertEq(timelocks.gsmFeeStrategyLastUpdated, block.timestamp); + } + + function testUpdateGsmBuySellFeesAfterTimelock() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee + 1); + skip(GHO_GSM_STEWARD.MINIMUM_DELAY() + 1); + uint256 newBuyFee = buyFee + 2; + uint256 newSellFee = sellFee + 2; + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), newBuyFee, newSellFee); + address newStrategy = GHO_GSM.getFeeStrategy(); + uint256 currentBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); + uint256 currentSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); + assertEq(currentBuyFee, newBuyFee); + assertEq(currentSellFee, newSellFee); + } + + function testUpdateGsmBuySellFeesNewStrategy() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee + 1); + address[] memory cachedStrategies = FIXED_FEE_STRATEGY_FACTORY.getFixedFeeStrategies(); + assertEq(cachedStrategies.length, 1); + address newStrategy = GHO_GSM.getFeeStrategy(); + assertEq(newStrategy, cachedStrategies[0]); + } + + function testUpdateGsmBuySellFeesIfZeroFees() public { + address currentFeeStrategy = GHO_GSM.getFeeStrategy(); + vm.mockCall( + currentFeeStrategy, + abi.encodeWithSelector(GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee.selector), + abi.encode(0) + ); + vm.mockCall( + currentFeeStrategy, + abi.encodeWithSelector(GHO_GSM_FIXED_FEE_STRATEGY.getSellFee.selector), + abi.encode(0) + ); + uint256 buyFee = IGsmFeeStrategy(currentFeeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(currentFeeStrategy).getSellFee(1e4); + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee); + address[] memory cachedStrategies = FIXED_FEE_STRATEGY_FACTORY.getFixedFeeStrategies(); + assertEq(cachedStrategies.length, 1); + address newStrategy = GHO_GSM.getFeeStrategy(); + assertEq(newStrategy, cachedStrategies[0]); + } + + function testRevertUpdateGsmBuySellFeesIfZeroFeeStrategyAddress() public { + vm.mockCall( + address(GHO_GSM), + abi.encodeWithSelector(GHO_GSM.getFeeStrategy.selector), + abi.encode(address(0)) + ); + vm.expectRevert('FIXED_FEE_STRATEGY_NOT_FOUND'); + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), 0.01e4, 0.01e4); + } + + function testRevertUpdateGsmBuySellFeesIfUnauthorized() public { + vm.prank(ALICE); + vm.expectRevert('INVALID_CALLER'); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), 0.01e4, 0.01e4); + } + + function testRevertUpdateGsmBuySellFeesIfTooSoon() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee + 1); + vm.prank(RISK_COUNCIL); + vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee + 2, sellFee + 2); + } + + function testRevertUpdateGsmBuySellFeesNoChange() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + vm.prank(RISK_COUNCIL); + vm.expectRevert('NO_CHANGE_IN_FEES'); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee, sellFee); + } + + function testRevertUpdateGsmBuySellFeesIfBuyFeeMoreThanMax() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + uint256 maxFeeUpdate = GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + vm.prank(RISK_COUNCIL); + vm.expectRevert('INVALID_BUY_FEE_UPDATE'); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee + maxFeeUpdate + 1, sellFee); + } + + function testRevertUpdateGsmBuySellFeesIfBuyFeeLessThanMin() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + uint256 maxFeeUpdate = GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + vm.prank(RISK_COUNCIL); + vm.expectRevert('INVALID_BUY_FEE_UPDATE'); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee - maxFeeUpdate - 1, sellFee); + } + + function testRevertUpdateGsmBuySellFeesIfSellFeeMoreThanMax() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + uint256 maxFeeUpdate = GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + vm.prank(RISK_COUNCIL); + vm.expectRevert('INVALID_SELL_FEE_UPDATE'); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee, sellFee + maxFeeUpdate + 1); + } + + function testRevertUpdateGsmBuySellFeesIfSellFeeLessThanMin() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + uint256 maxFeeUpdate = GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + vm.prank(RISK_COUNCIL); + vm.expectRevert('INVALID_SELL_FEE_UPDATE'); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee, sellFee - maxFeeUpdate - 1); + } + + function testRevertUpdateGsmBuySellFeesIfBothMoreThanMax() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + uint256 maxFeeUpdate = GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + vm.prank(RISK_COUNCIL); + vm.expectRevert('INVALID_BUY_FEE_UPDATE'); + GHO_GSM_STEWARD.updateGsmBuySellFees( + address(GHO_GSM), + buyFee + maxFeeUpdate + 1, + sellFee + maxFeeUpdate + 1 + ); + } + + function testRevertUpdateGsmBuySellFeesIfBothLessThanMin() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + uint256 maxFeeUpdate = GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + vm.prank(RISK_COUNCIL); + vm.expectRevert('INVALID_BUY_FEE_UPDATE'); + GHO_GSM_STEWARD.updateGsmBuySellFees( + address(GHO_GSM), + buyFee - maxFeeUpdate - 1, + sellFee - maxFeeUpdate - 1 + ); + } + + function testRevertUpdateGsmBuySellFeesIfStewardLostConfiguratorRole() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); + uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); + GHO_GSM.revokeRole(GSM_CONFIGURATOR_ROLE, address(GHO_GSM_STEWARD)); + vm.expectRevert( + AccessControlErrorsLib.MISSING_ROLE(GSM_CONFIGURATOR_ROLE, address(GHO_GSM_STEWARD)) + ); + vm.prank(RISK_COUNCIL); + GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee + 1); + } +} diff --git a/src/test/TestGhoSteward.t.sol b/src/test/TestGhoSteward.t.sol deleted file mode 100644 index 10737afe..00000000 --- a/src/test/TestGhoSteward.t.sol +++ /dev/null @@ -1,415 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import './TestGhoBase.t.sol'; - -contract TestGhoSteward is TestGhoBase { - using PercentageMath for uint256; - - function testConstructor() public { - assertEq(GHO_STEWARD.MINIMUM_DELAY(), MINIMUM_DELAY); - assertEq(GHO_STEWARD.BORROW_RATE_CHANGE_MAX(), BORROW_RATE_CHANGE_MAX); - assertEq(GHO_STEWARD.STEWARD_LIFESPAN(), STEWARD_LIFESPAN); - - assertEq(GHO_STEWARD.POOL_ADDRESSES_PROVIDER(), address(PROVIDER)); - assertEq(GHO_STEWARD.GHO_TOKEN(), address(GHO_TOKEN)); - assertEq(GHO_STEWARD.RISK_COUNCIL(), RISK_COUNCIL); - assertEq(GHO_STEWARD.owner(), SHORT_EXECUTOR); - - IGhoSteward.Debounce memory timelocks = GHO_STEWARD.getTimelock(); - assertEq(timelocks.borrowRateLastUpdated, 0); - assertEq(timelocks.bucketCapacityLastUpdated, 0); - - assertEq(GHO_STEWARD.getStewardExpiration(), block.timestamp + GHO_STEWARD.STEWARD_LIFESPAN()); - } - - function testRevertConstructorInvalidAddressesProvider() public { - vm.expectRevert('INVALID_ADDRESSES_PROVIDER'); - new GhoSteward(address(0), address(0x002), address(0x003), address(0x004)); - } - - function testRevertConstructorInvalidGhoToken() public { - vm.expectRevert('INVALID_GHO_TOKEN'); - new GhoSteward(address(0x001), address(0), address(0x003), address(0x004)); - } - - function testRevertConstructorInvalidRiskCouncil() public { - vm.expectRevert('INVALID_RISK_COUNCIL'); - new GhoSteward(address(0x001), address(0x002), address(0), address(0x004)); - } - - function testRevertConstructorInvalidShortExecutor() public { - vm.expectRevert('INVALID_SHORT_EXECUTOR'); - new GhoSteward(address(0x001), address(0x002), address(0x003), address(0)); - } - - function testExtendStewardExpiration() public { - uint40 oldExpirationTime = GHO_STEWARD.getStewardExpiration(); - uint40 newExpirationTime = oldExpirationTime + GHO_STEWARD.STEWARD_LIFESPAN(); - vm.prank(GHO_STEWARD.owner()); - vm.expectEmit(true, true, true, true, address(GHO_STEWARD)); - emit StewardExpirationUpdated(oldExpirationTime, newExpirationTime); - GHO_STEWARD.extendStewardExpiration(); - assertEq(GHO_STEWARD.getStewardExpiration(), newExpirationTime); - } - - function testRevertExtendStewardExpiration() public { - vm.expectRevert(OwnableErrorsLib.CALLER_NOT_OWNER()); - vm.prank(ALICE); - GHO_STEWARD.extendStewardExpiration(); - } - - function testUpdateBorrowRate() public { - address oldInterestStrategy = POOL.getReserveInterestRateStrategyAddress(address(GHO_TOKEN)); - uint256 oldBorrowRate = GhoInterestRateStrategy(oldInterestStrategy) - .getBaseVariableBorrowRate(); - vm.expectEmit(true, true, true, false, address(CONFIGURATOR)); - emit ReserveInterestRateStrategyChanged( - address(GHO_TOKEN), - oldInterestStrategy, - address(0) // deployed by GhoSteward - ); - uint256 newBorrowRate = oldBorrowRate + 1; - vm.warp(GHO_STEWARD.MINIMUM_DELAY() + 1); - - IGhoSteward.Debounce memory timelocksBefore = GHO_STEWARD.getTimelock(); - - assertEq(GHO_STEWARD.getAllStrategies().length, 0); - - vm.prank(RISK_COUNCIL); - GHO_STEWARD.updateBorrowRate(newBorrowRate); - - address[] memory strategies = GHO_STEWARD.getAllStrategies(); - assertEq(strategies.length, 1); - - address newInterestStrategy = POOL.getReserveInterestRateStrategyAddress(address(GHO_TOKEN)); - assertEq(strategies[0], newInterestStrategy); - assertEq( - GhoInterestRateStrategy(newInterestStrategy).getBaseVariableBorrowRate(), - newBorrowRate - ); - IGhoSteward.Debounce memory timelocks = GHO_STEWARD.getTimelock(); - assertEq(timelocks.borrowRateLastUpdated, block.timestamp); - assertEq(timelocks.bucketCapacityLastUpdated, timelocksBefore.bucketCapacityLastUpdated); - } - - function testUpdateBorrowRateReuseStrategy() public { - address oldInterestStrategy = POOL.getReserveInterestRateStrategyAddress(address(GHO_TOKEN)); - uint256 oldBorrowRate = GhoInterestRateStrategy(oldInterestStrategy) - .getBaseVariableBorrowRate(); - - vm.warp(GHO_STEWARD.MINIMUM_DELAY() + 1); - - assertEq(GHO_STEWARD.getAllStrategies().length, 0); - - vm.prank(RISK_COUNCIL); - GHO_STEWARD.updateBorrowRate(oldBorrowRate); - - assertEq(GHO_STEWARD.getAllStrategies().length, 1); - - address[] memory strategies = GHO_STEWARD.getAllStrategies(); - assertEq(strategies.length, 1); - - // New borrow rate - uint256 newBorrowRate = oldBorrowRate + 1; - vm.warp(block.timestamp + GHO_STEWARD.MINIMUM_DELAY() + 1); - - vm.prank(RISK_COUNCIL); - GHO_STEWARD.updateBorrowRate(newBorrowRate); - - strategies = GHO_STEWARD.getAllStrategies(); - assertEq(strategies.length, 2); - - address newInterestStrategy = POOL.getReserveInterestRateStrategyAddress(address(GHO_TOKEN)); - assertEq(strategies[1], newInterestStrategy); - assertEq( - GhoInterestRateStrategy(newInterestStrategy).getBaseVariableBorrowRate(), - newBorrowRate - ); - - // Come back to old rate - vm.warp(block.timestamp + GHO_STEWARD.MINIMUM_DELAY() + 1); - - vm.prank(RISK_COUNCIL); - GHO_STEWARD.updateBorrowRate(oldBorrowRate); - - assertEq(GHO_STEWARD.getAllStrategies().length, 2); - assertEq( - GhoInterestRateStrategy(POOL.getReserveInterestRateStrategyAddress(address(GHO_TOKEN))) - .getBaseVariableBorrowRate(), - oldBorrowRate - ); - } - - function testUpdateBorrowRateIdempotent() public { - address oldInterestStrategy = POOL.getReserveInterestRateStrategyAddress(address(GHO_TOKEN)); - uint256 oldBorrowRate = GhoInterestRateStrategy(oldInterestStrategy) - .getBaseVariableBorrowRate(); - vm.expectEmit(true, true, true, false, address(CONFIGURATOR)); - emit ReserveInterestRateStrategyChanged( - address(GHO_TOKEN), - oldInterestStrategy, - address(0) // deployed by GhoSteward - ); - vm.warp(GHO_STEWARD.MINIMUM_DELAY() + 1); - - vm.prank(RISK_COUNCIL); - GHO_STEWARD.updateBorrowRate(oldBorrowRate); - - address newInterestStrategy = POOL.getReserveInterestRateStrategyAddress(address(GHO_TOKEN)); - assertEq( - GhoInterestRateStrategy(newInterestStrategy).getBaseVariableBorrowRate(), - oldBorrowRate - ); - } - - function testUpdateBorrowRateMaximumIncrease() public { - address oldInterestStrategy = POOL.getReserveInterestRateStrategyAddress(address(GHO_TOKEN)); - uint256 oldBorrowRate = GhoInterestRateStrategy(oldInterestStrategy) - .getBaseVariableBorrowRate(); - vm.expectEmit(true, true, true, false, address(CONFIGURATOR)); - emit ReserveInterestRateStrategyChanged( - address(GHO_TOKEN), - oldInterestStrategy, - address(0) // deployed by GhoSteward - ); - - uint256 newBorrowRate = oldBorrowRate + - oldBorrowRate.percentMul(GHO_STEWARD.BORROW_RATE_CHANGE_MAX()); - - vm.warp(GHO_STEWARD.MINIMUM_DELAY() + 1); - vm.prank(RISK_COUNCIL); - GHO_STEWARD.updateBorrowRate(newBorrowRate); - - address newInterestStrategy = POOL.getReserveInterestRateStrategyAddress(address(GHO_TOKEN)); - assertEq( - GhoInterestRateStrategy(newInterestStrategy).getBaseVariableBorrowRate(), - newBorrowRate - ); - } - - function testUpdateBorrowRateMaximumDecrease() public { - address oldInterestStrategy = POOL.getReserveInterestRateStrategyAddress(address(GHO_TOKEN)); - uint256 oldBorrowRate = GhoInterestRateStrategy(oldInterestStrategy) - .getBaseVariableBorrowRate(); - vm.expectEmit(true, true, true, false, address(CONFIGURATOR)); - emit ReserveInterestRateStrategyChanged( - address(GHO_TOKEN), - oldInterestStrategy, - address(0) // deployed by GhoSteward - ); - - uint256 newBorrowRate = oldBorrowRate + - oldBorrowRate.percentMul(GHO_STEWARD.BORROW_RATE_CHANGE_MAX()); - vm.warp(block.timestamp + GHO_STEWARD.MINIMUM_DELAY() + 1); - - vm.prank(RISK_COUNCIL); - GHO_STEWARD.updateBorrowRate(newBorrowRate); - - address newInterestStrategy = POOL.getReserveInterestRateStrategyAddress(address(GHO_TOKEN)); - assertEq( - GhoInterestRateStrategy(newInterestStrategy).getBaseVariableBorrowRate(), - newBorrowRate - ); - } - - function testRevertUpdateBorrowRateUnauthorized() public { - vm.expectRevert('INVALID_CALLER'); - GHO_STEWARD.updateBorrowRate(123); - } - - function testRevertUpdateBorrowRateExpiredSteward() public { - vm.warp(block.timestamp + GHO_STEWARD.getStewardExpiration()); - vm.prank(RISK_COUNCIL); - vm.expectRevert('STEWARD_EXPIRED'); - GHO_STEWARD.updateBorrowRate(123); - } - - function testRevertUpdateBorrowRateDebounceNotRespectedAtLaunch() public { - vm.warp(GHO_STEWARD.MINIMUM_DELAY()); - - vm.prank(RISK_COUNCIL); - vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); - GHO_STEWARD.updateBorrowRate(123); - } - - function testRevertUpdateBorrowRateDebounceNotRespected() public { - // first borrow rate update - address oldInterestStrategy = POOL.getReserveInterestRateStrategyAddress(address(GHO_TOKEN)); - uint256 oldBorrowRate = GhoInterestRateStrategy(oldInterestStrategy) - .getBaseVariableBorrowRate(); - vm.warp(GHO_STEWARD.MINIMUM_DELAY() + 1); - vm.prank(RISK_COUNCIL); - GHO_STEWARD.updateBorrowRate(oldBorrowRate); - - vm.prank(RISK_COUNCIL); - vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); - GHO_STEWARD.updateBorrowRate(123); - } - - function testRevertUpdateBorrowRateInterestRateNotFound() public { - vm.warp(GHO_STEWARD.MINIMUM_DELAY() + 1); - DataTypes.ReserveData memory mockData = POOL.getReserveData(address(GHO_TOKEN)); - mockData.interestRateStrategyAddress = address(0); - vm.mockCall( - address(POOL), - abi.encodeWithSelector(IPool.getReserveData.selector, address(GHO_TOKEN)), - abi.encode(mockData) - ); - - vm.prank(RISK_COUNCIL); - vm.expectRevert('GHO_INTEREST_RATE_STRATEGY_NOT_FOUND'); - GHO_STEWARD.updateBorrowRate(123); - } - - function testRevertUpdateBorrowRateAboveMaximumIncrease() public { - address oldInterestStrategy = POOL.getReserveInterestRateStrategyAddress(address(GHO_TOKEN)); - uint256 oldBorrowRate = GhoInterestRateStrategy(oldInterestStrategy) - .getBaseVariableBorrowRate(); - uint256 newBorrowRate = oldBorrowRate + - oldBorrowRate.percentMul(GHO_STEWARD.BORROW_RATE_CHANGE_MAX()) + - 1; - vm.warp(GHO_STEWARD.MINIMUM_DELAY() + 1); - - vm.prank(RISK_COUNCIL); - vm.expectRevert('INVALID_BORROW_RATE_UPDATE'); - GHO_STEWARD.updateBorrowRate(newBorrowRate); - } - - function testRevertUpdateBorrowRateBelowMaximumDecrease() public { - address oldInterestStrategy = POOL.getReserveInterestRateStrategyAddress(address(GHO_TOKEN)); - uint256 oldBorrowRate = GhoInterestRateStrategy(oldInterestStrategy) - .getBaseVariableBorrowRate(); - uint256 newBorrowRate = oldBorrowRate - - oldBorrowRate.percentMul(GHO_STEWARD.BORROW_RATE_CHANGE_MAX()) - - 1; - vm.warp(GHO_STEWARD.MINIMUM_DELAY() + 1); - - vm.prank(RISK_COUNCIL); - vm.expectRevert('INVALID_BORROW_RATE_UPDATE'); - GHO_STEWARD.updateBorrowRate(newBorrowRate); - } - - function testUpdateBucketCapacity() public { - (uint256 oldCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); - uint128 newCapacity = uint128(oldCapacity) + 1; - vm.warp(GHO_STEWARD.MINIMUM_DELAY() + 1); - - IGhoSteward.Debounce memory timelocksBefore = GHO_STEWARD.getTimelock(); - - vm.expectEmit(true, true, true, false, address(GHO_TOKEN)); - emit FacilitatorBucketCapacityUpdated(address(GHO_ATOKEN), oldCapacity, newCapacity); - vm.prank(RISK_COUNCIL); - GHO_STEWARD.updateBucketCapacity(newCapacity); - - (uint256 capacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); - assertEq(capacity, newCapacity); - IGhoSteward.Debounce memory timelocks = GHO_STEWARD.getTimelock(); - assertEq(timelocks.borrowRateLastUpdated, timelocksBefore.borrowRateLastUpdated); - assertEq(timelocks.bucketCapacityLastUpdated, block.timestamp); - } - - function testUpdateBucketCapacityIdempotent() public { - (uint256 oldCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); - vm.warp(GHO_STEWARD.MINIMUM_DELAY() + 1); - - IGhoSteward.Debounce memory timelocksBefore = GHO_STEWARD.getTimelock(); - - vm.expectEmit(true, true, true, false, address(GHO_TOKEN)); - emit FacilitatorBucketCapacityUpdated(address(GHO_ATOKEN), oldCapacity, oldCapacity); - vm.prank(RISK_COUNCIL); - GHO_STEWARD.updateBucketCapacity(uint128(oldCapacity)); - - (uint256 capacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); - assertEq(capacity, oldCapacity); - IGhoSteward.Debounce memory timelocks = GHO_STEWARD.getTimelock(); - assertEq(timelocks.borrowRateLastUpdated, timelocksBefore.borrowRateLastUpdated); - assertEq(timelocks.bucketCapacityLastUpdated, block.timestamp); - } - - function testUpdateBucketCapacityMaximumIncrease() public { - (uint256 oldCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); - uint128 newCapacity = uint128(oldCapacity * 2); - vm.warp(GHO_STEWARD.MINIMUM_DELAY() + 1); - - IGhoSteward.Debounce memory timelocksBefore = GHO_STEWARD.getTimelock(); - - vm.expectEmit(true, true, true, false, address(GHO_TOKEN)); - emit FacilitatorBucketCapacityUpdated(address(GHO_ATOKEN), oldCapacity, newCapacity); - vm.prank(RISK_COUNCIL); - GHO_STEWARD.updateBucketCapacity(newCapacity); - - (uint256 capacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); - assertEq(capacity, newCapacity); - IGhoSteward.Debounce memory timelocks = GHO_STEWARD.getTimelock(); - assertEq(timelocks.borrowRateLastUpdated, timelocksBefore.borrowRateLastUpdated); - assertEq(timelocks.bucketCapacityLastUpdated, block.timestamp); - } - - function testRevertUpdateBucketCapacityUnauthorized() public { - vm.expectRevert('INVALID_CALLER'); - GHO_STEWARD.updateBucketCapacity(123); - } - - function testRevertUpdateBucketCapacityExpiredSteward() public { - vm.warp(block.timestamp + GHO_STEWARD.getStewardExpiration()); - vm.prank(RISK_COUNCIL); - vm.expectRevert('STEWARD_EXPIRED'); - GHO_STEWARD.updateBucketCapacity(123); - } - - function testRevertUpdateBucketCapacityDebounceNotRespectedAtLaunch() public { - vm.warp(GHO_STEWARD.MINIMUM_DELAY()); - - vm.prank(RISK_COUNCIL); - vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); - GHO_STEWARD.updateBucketCapacity(123); - } - - function testRevertUpdateBucketCapacityDebounceNotRespected() public { - // first bucket capacity update - (uint256 oldCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); - vm.warp(GHO_STEWARD.MINIMUM_DELAY() + 1); - vm.prank(RISK_COUNCIL); - GHO_STEWARD.updateBucketCapacity(uint128(oldCapacity)); - - vm.prank(RISK_COUNCIL); - vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); - GHO_STEWARD.updateBucketCapacity(123); - } - - function testRevertUpdateBucketCapacityGhoATokenNotFound() public { - vm.warp(GHO_STEWARD.MINIMUM_DELAY() + 1); - DataTypes.ReserveData memory mockData = POOL.getReserveData(address(GHO_TOKEN)); - mockData.aTokenAddress = address(0); - vm.mockCall( - address(POOL), - abi.encodeWithSelector(IPool.getReserveData.selector, address(GHO_TOKEN)), - abi.encode(mockData) - ); - - vm.prank(RISK_COUNCIL); - vm.expectRevert('GHO_ATOKEN_NOT_FOUND'); - GHO_STEWARD.updateBucketCapacity(123); - } - - function testRevertUpdateBucketCapacityAboveMaximumIncrease() public { - (uint256 oldCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); - uint128 newCapacity = uint128(oldCapacity * 2 + 1); - vm.warp(GHO_STEWARD.MINIMUM_DELAY() + 1); - - vm.prank(RISK_COUNCIL); - vm.expectRevert('INVALID_BUCKET_CAPACITY_UPDATE'); - GHO_STEWARD.updateBucketCapacity(newCapacity); - } - - function testRevertUpdateBucketCapacityBelowMaximumDecrease() public { - (uint256 oldCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); - uint128 newCapacity = uint128(oldCapacity - 1); - vm.warp(GHO_STEWARD.MINIMUM_DELAY() + 1); - - vm.prank(RISK_COUNCIL); - vm.expectRevert('INVALID_BUCKET_CAPACITY_UPDATE'); - GHO_STEWARD.updateBucketCapacity(newCapacity); - } -} diff --git a/src/test/TestGhoStewardV2.t.sol b/src/test/TestGhoStewardV2.t.sol deleted file mode 100644 index 801fc373..00000000 --- a/src/test/TestGhoStewardV2.t.sol +++ /dev/null @@ -1,858 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import './TestGhoBase.t.sol'; - -contract TestGhoStewardV2 is TestGhoBase { - using ReserveConfiguration for DataTypes.ReserveConfigurationMap; - - function setUp() public { - /// @dev Since block.timestamp starts at 0 this is a necessary condition (block.timestamp > `MINIMUM_DELAY`) for the timelocked contract methods to work. - vm.warp(GHO_STEWARD_V2.MINIMUM_DELAY() + 1); - } - - function testConstructor() public { - assertEq(GHO_STEWARD_V2.GHO_BORROW_RATE_CHANGE_MAX(), GHO_BORROW_RATE_CHANGE_MAX); - assertEq(GHO_STEWARD_V2.GSM_FEE_RATE_CHANGE_MAX(), GSM_FEE_RATE_CHANGE_MAX); - assertEq(GHO_STEWARD_V2.GHO_BORROW_RATE_MAX(), GHO_BORROW_RATE_MAX); - assertEq(GHO_STEWARD_V2.MINIMUM_DELAY(), MINIMUM_DELAY_V2); - - assertEq(GHO_STEWARD.owner(), SHORT_EXECUTOR); - assertEq(GHO_STEWARD_V2.POOL_ADDRESSES_PROVIDER(), address(PROVIDER)); - assertEq(GHO_STEWARD_V2.GHO_TOKEN(), address(GHO_TOKEN)); - assertEq(GHO_STEWARD_V2.FIXED_RATE_STRATEGY_FACTORY(), address(FIXED_RATE_STRATEGY_FACTORY)); - assertEq(GHO_STEWARD_V2.RISK_COUNCIL(), RISK_COUNCIL); - - IGhoStewardV2.GhoDebounce memory ghoTimelocks = GHO_STEWARD_V2.getGhoTimelocks(); - assertEq(ghoTimelocks.ghoBorrowCapLastUpdate, 0); - assertEq(ghoTimelocks.ghoBorrowRateLastUpdate, 0); - - address[] memory controlledFacilitators = GHO_STEWARD_V2.getControlledFacilitators(); - assertEq(controlledFacilitators.length, 2); - - uint40 facilitatorTimelock = GHO_STEWARD_V2.getFacilitatorBucketCapacityTimelock( - controlledFacilitators[0] - ); - assertEq(facilitatorTimelock, 0); - - address[] memory gsmFeeStrategies = GHO_STEWARD_V2.getGsmFeeStrategies(); - assertEq(gsmFeeStrategies.length, 0); - } - - function testRevertConstructorInvalidExecutor() public { - vm.expectRevert('INVALID_OWNER'); - new GhoStewardV2(address(0), address(0x002), address(0x003), address(0x004), address(0x005)); - } - - function testRevertConstructorInvalidAddressesProvider() public { - vm.expectRevert('INVALID_ADDRESSES_PROVIDER'); - new GhoStewardV2(address(0x001), address(0), address(0x003), address(0x004), address(0x005)); - } - - function testRevertConstructorInvalidGhoToken() public { - vm.expectRevert('INVALID_GHO_TOKEN'); - new GhoStewardV2(address(0x001), address(0x002), address(0), address(0x004), address(0x005)); - } - - function testRevertConstructorInvalidFixedRateStrategyFactory() public { - vm.expectRevert('INVALID_FIXED_RATE_STRATEGY_FACTORY'); - new GhoStewardV2(address(0x001), address(0x002), address(0x003), address(0), address(0x005)); - } - - function testRevertConstructorInvalidRiskCouncil() public { - vm.expectRevert('INVALID_RISK_COUNCIL'); - new GhoStewardV2(address(0x001), address(0x002), address(0x003), address(0x005), address(0)); - } - - function testUpdateFacilitatorBucketCapacity() public { - (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); - vm.prank(RISK_COUNCIL); - uint128 newBucketCapacity = uint128(currentBucketCapacity) + 1; - GHO_STEWARD_V2.updateFacilitatorBucketCapacity(address(GHO_ATOKEN), newBucketCapacity); - (uint256 capacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); - assertEq(newBucketCapacity, capacity); - } - - function testUpdateFacilitatorBucketCapacityMaxValue() public { - (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); - uint128 newBucketCapacity = uint128(currentBucketCapacity * 2); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateFacilitatorBucketCapacity(address(GHO_ATOKEN), newBucketCapacity); - (uint256 capacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); - assertEq(capacity, newBucketCapacity); - } - - function testUpdateFacilitatorBucketCapacityTimelock() public { - (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateFacilitatorBucketCapacity( - address(GHO_ATOKEN), - uint128(currentBucketCapacity) + 1 - ); - uint40 timelock = GHO_STEWARD_V2.getFacilitatorBucketCapacityTimelock(address(GHO_ATOKEN)); - assertEq(timelock, block.timestamp); - } - - function testUpdateFacilitatorBucketCapacityAfterTimelock() public { - (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); - vm.prank(RISK_COUNCIL); - uint128 newBucketCapacity = uint128(currentBucketCapacity) + 1; - GHO_STEWARD_V2.updateFacilitatorBucketCapacity(address(GHO_ATOKEN), newBucketCapacity); - skip(GHO_STEWARD_V2.MINIMUM_DELAY() + 1); - uint128 newBucketCapacityAfterTimelock = newBucketCapacity + 1; - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateFacilitatorBucketCapacity( - address(GHO_ATOKEN), - newBucketCapacityAfterTimelock - ); - (uint256 capacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); - assertEq(capacity, newBucketCapacityAfterTimelock); - } - - function testRevertUpdateFacilitatorBucketCapacityIfUnauthorized() public { - vm.expectRevert('INVALID_CALLER'); - vm.prank(ALICE); - GHO_STEWARD_V2.updateFacilitatorBucketCapacity(address(GHO_ATOKEN), 123); - } - - function testRevertUpdateFaciltatorBucketCapacityIfUpdatedTooSoon() public { - (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateFacilitatorBucketCapacity( - address(GHO_ATOKEN), - uint128(currentBucketCapacity) + 1 - ); - vm.prank(RISK_COUNCIL); - vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); - GHO_STEWARD_V2.updateFacilitatorBucketCapacity( - address(GHO_ATOKEN), - uint128(currentBucketCapacity) + 2 - ); - } - - function testRevertUpdateFacilitatorBucketCapacityIfFacilitatorNotInControl() public { - (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); - vm.prank(RISK_COUNCIL); - vm.expectRevert('FACILITATOR_NOT_CONTROLLED'); - GHO_STEWARD_V2.updateFacilitatorBucketCapacity( - address(GHO_GSM_4626), - uint128(currentBucketCapacity) + 1 - ); - } - - function testRevertUpdateFacilitatorBucketCapacityIfStewardLostBucketManagerRole() public { - (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); - GHO_TOKEN.revokeRole(GHO_TOKEN_BUCKET_MANAGER_ROLE, address(GHO_STEWARD_V2)); - vm.expectRevert( - AccessControlErrorsLib.MISSING_ROLE(GHO_TOKEN_BUCKET_MANAGER_ROLE, address(GHO_STEWARD_V2)) - ); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateFacilitatorBucketCapacity( - address(GHO_ATOKEN), - uint128(currentBucketCapacity) + 1 - ); - } - - function testRevertUpdateFacilitatorBucketCapacityIfMoreThanDouble() public { - (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); - vm.prank(RISK_COUNCIL); - vm.expectRevert('INVALID_BUCKET_CAPACITY_UPDATE'); - GHO_STEWARD_V2.updateFacilitatorBucketCapacity( - address(GHO_ATOKEN), - uint128(currentBucketCapacity * 2) + 1 - ); - } - - function testUpdateGhoBorrowCap() public { - uint256 oldBorrowCap = 1e6; - _setGhoBorrowCapViaConfigurator(oldBorrowCap); - uint256 newBorrowCap = oldBorrowCap + 1; - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGhoBorrowCap(newBorrowCap); - uint256 currentBorrowCap = _getGhoBorrowCap(); - assertEq(newBorrowCap, currentBorrowCap); - } - - function testUpdateGhoBorrowCapMaxIncrease() public { - uint256 oldBorrowCap = 1e6; - _setGhoBorrowCapViaConfigurator(oldBorrowCap); - uint256 newBorrowCap = oldBorrowCap * 2; - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGhoBorrowCap(newBorrowCap); - uint256 currentBorrowCap = _getGhoBorrowCap(); - assertEq(newBorrowCap, currentBorrowCap); - } - - function testUpdateGhoBorrowCapMaxDecrease() public { - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGhoBorrowCap(0); - uint256 currentBorrowCap = _getGhoBorrowCap(); - assertEq(currentBorrowCap, 0); - } - - function testUpdateGhoBorrowCapTimelock() public { - uint256 oldBorrowCap = 1e6; - _setGhoBorrowCapViaConfigurator(oldBorrowCap); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGhoBorrowCap(oldBorrowCap + 1); - IGhoStewardV2.GhoDebounce memory ghoTimelocks = GHO_STEWARD_V2.getGhoTimelocks(); - assertEq(ghoTimelocks.ghoBorrowCapLastUpdate, block.timestamp); - } - - function testUpdateGhoBorrowCapAfterTimelock() public { - uint256 oldBorrowCap = 1e6; - _setGhoBorrowCapViaConfigurator(oldBorrowCap); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGhoBorrowCap(oldBorrowCap + 1); - skip(GHO_STEWARD_V2.MINIMUM_DELAY() + 1); - uint256 newBorrowCap = oldBorrowCap + 2; - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGhoBorrowCap(newBorrowCap); - uint256 currentBorrowCap = _getGhoBorrowCap(); - assertEq(newBorrowCap, currentBorrowCap); - } - - function testRevertUpdateGhoBorrowCapIfUnauthorized() public { - vm.prank(ALICE); - vm.expectRevert('INVALID_CALLER'); - GHO_STEWARD_V2.updateGhoBorrowCap(50e6); - } - - function testRevertUpdateGhoBorrowCapIfUpdatedTooSoon() public { - uint256 oldBorrowCap = 1e6; - _setGhoBorrowCapViaConfigurator(oldBorrowCap); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGhoBorrowCap(oldBorrowCap + 1); - vm.prank(RISK_COUNCIL); - vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); - GHO_STEWARD_V2.updateGhoBorrowCap(oldBorrowCap + 2); - } - - function testRevertUpdateGhoBorrowCapIfValueMoreThanDouble() public { - uint256 oldBorrowCap = 1e6; - _setGhoBorrowCapViaConfigurator(oldBorrowCap); - vm.prank(RISK_COUNCIL); - vm.expectRevert('INVALID_BORROW_CAP_UPDATE'); - GHO_STEWARD_V2.updateGhoBorrowCap(oldBorrowCap * 2 + 1); - } - - function testUpdateGhoBorrowRateUpwards() public { - uint256 oldBorrowRate = _getGhoBorrowRate(); - uint256 newBorrowRate = oldBorrowRate + 1; - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGhoBorrowRate(newBorrowRate); - uint256 currentBorrowRate = _getGhoBorrowRate(); - assertEq(currentBorrowRate, newBorrowRate); - } - - function testUpdateGhoBorrowRateDownwards() public { - uint256 oldBorrowRate = _getGhoBorrowRate(); - uint256 newBorrowRate = oldBorrowRate - 1; - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGhoBorrowRate(newBorrowRate); - uint256 currentBorrowRate = _getGhoBorrowRate(); - assertEq(currentBorrowRate, newBorrowRate); - } - - function testUpdateGhoBorrowRateMaxValue() public { - uint256 ghoBorrowRateMax = GHO_STEWARD_V2.GHO_BORROW_RATE_MAX(); - (, uint256 oldBorrowRate) = _setGhoBorrowRateViaConfigurator(ghoBorrowRateMax - 1); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGhoBorrowRate(ghoBorrowRateMax); - uint256 currentBorrowRate = _getGhoBorrowRate(); - assertEq(currentBorrowRate, ghoBorrowRateMax); - } - - function testUpdateGhoBorrowRateMaxIncrement() public { - uint256 oldBorrowRate = _getGhoBorrowRate(); - uint256 newBorrowRate = oldBorrowRate + GHO_STEWARD_V2.GHO_BORROW_RATE_CHANGE_MAX(); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGhoBorrowRate(newBorrowRate); - uint256 currentBorrowRate = _getGhoBorrowRate(); - assertEq(currentBorrowRate, newBorrowRate); - } - - function testUpdateGhoBorrowRateDecrement() public { - uint256 oldBorrowRate = _getGhoBorrowRate(); - uint256 newBorrowRate = oldBorrowRate - 1; - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGhoBorrowRate(newBorrowRate); - uint256 currentBorrowRate = _getGhoBorrowRate(); - assertEq(currentBorrowRate, newBorrowRate); - } - - function testUpdateGhoBorrowRateMaxDecrement() public { - vm.startPrank(RISK_COUNCIL); - - // set a high borrow rate - GHO_STEWARD_V2.updateGhoBorrowRate(GHO_STEWARD_V2.GHO_BORROW_RATE_CHANGE_MAX() + 1); - vm.warp(block.timestamp + GHO_STEWARD_V2.MINIMUM_DELAY() + 1); - - uint256 oldBorrowRate = _getGhoBorrowRate(); - uint256 newBorrowRate = oldBorrowRate - GHO_STEWARD_V2.GHO_BORROW_RATE_CHANGE_MAX(); - GHO_STEWARD_V2.updateGhoBorrowRate(newBorrowRate); - uint256 currentBorrowRate = _getGhoBorrowRate(); - assertEq(currentBorrowRate, newBorrowRate); - - vm.stopPrank(); - } - - function testUpdateGhoBorrowRateTimelock() public { - uint256 oldBorrowRate = _getGhoBorrowRate(); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGhoBorrowRate(oldBorrowRate + 1); - IGhoStewardV2.GhoDebounce memory ghoTimelocks = GHO_STEWARD_V2.getGhoTimelocks(); - assertEq(ghoTimelocks.ghoBorrowRateLastUpdate, block.timestamp); - } - - function testUpdateGhoBorrowRateAfterTimelock() public { - uint256 oldBorrowRate = _getGhoBorrowRate(); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGhoBorrowRate(oldBorrowRate + 1); - skip(GHO_STEWARD_V2.MINIMUM_DELAY() + 1); - uint256 newBorrowRate = oldBorrowRate + 2; - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGhoBorrowRate(newBorrowRate); - uint256 currentBorrowRate = _getGhoBorrowRate(); - assertEq(currentBorrowRate, newBorrowRate); - } - - function testRevertUpdateGhoBorrowRateIfUnauthorized() public { - vm.expectRevert('INVALID_CALLER'); - vm.prank(ALICE); - GHO_STEWARD_V2.updateGhoBorrowRate(0.07e4); - } - - function testRevertUpdateGhoBorrowRateIfUpdatedTooSoon() public { - address oldInterestStrategy = POOL.getReserveInterestRateStrategyAddress(address(GHO_TOKEN)); - uint256 oldBorrowRate = GhoInterestRateStrategy(oldInterestStrategy) - .getBaseVariableBorrowRate(); - vm.prank(RISK_COUNCIL); - uint256 newBorrowRate = oldBorrowRate + 1; - GHO_STEWARD_V2.updateGhoBorrowRate(newBorrowRate); - vm.prank(RISK_COUNCIL); - vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); - GHO_STEWARD_V2.updateGhoBorrowRate(newBorrowRate); - } - - function testRevertUpdateGhoBorrowRateIfInterestRateNotFound() public { - uint256 oldBorrowRate = _getGhoBorrowRate(); - DataTypes.ReserveData memory mockData = POOL.getReserveData(address(GHO_TOKEN)); - mockData.interestRateStrategyAddress = address(0); - vm.mockCall( - address(POOL), - abi.encodeWithSelector(IPool.getReserveData.selector, address(GHO_TOKEN)), - abi.encode(mockData) - ); - vm.expectRevert('GHO_INTEREST_RATE_STRATEGY_NOT_FOUND'); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGhoBorrowRate(oldBorrowRate + 1); - } - - function testRevertUpdateGhoBorrowRateIfValueMoreThanMax() public { - uint256 maxGhoBorrowRate = GHO_STEWARD_V2.GHO_BORROW_RATE_MAX(); - _setGhoBorrowRateViaConfigurator(maxGhoBorrowRate); - vm.prank(RISK_COUNCIL); - vm.expectRevert('BORROW_RATE_HIGHER_THAN_MAX'); - GHO_STEWARD_V2.updateGhoBorrowRate(maxGhoBorrowRate + 1); - } - - function testRevertUpdateGhoBorrowRateIfMaxExceededUpwards() public { - uint256 oldBorrowRate = _getGhoBorrowRate(); - uint256 newBorrowRate = oldBorrowRate + GHO_STEWARD_V2.GHO_BORROW_RATE_CHANGE_MAX() + 1; - vm.prank(RISK_COUNCIL); - vm.expectRevert('INVALID_BORROW_RATE_UPDATE'); - GHO_STEWARD_V2.updateGhoBorrowRate(newBorrowRate); - } - - function testRevertUpdateGhoBorrowRateIfMaxExceededDownwards() public { - vm.startPrank(RISK_COUNCIL); - - // set a high borrow rate - GHO_STEWARD_V2.updateGhoBorrowRate(GHO_STEWARD_V2.GHO_BORROW_RATE_CHANGE_MAX() + 1); - vm.warp(block.timestamp + GHO_STEWARD_V2.MINIMUM_DELAY() + 1); - - uint256 oldBorrowRate = _getGhoBorrowRate(); - uint256 newBorrowRate = oldBorrowRate - GHO_STEWARD_V2.GHO_BORROW_RATE_CHANGE_MAX() - 1; - vm.expectRevert('INVALID_BORROW_RATE_UPDATE'); - GHO_STEWARD_V2.updateGhoBorrowRate(newBorrowRate); - - vm.stopPrank(); - } - - function testUpdateGsmExposureCapUpwards() public { - uint128 oldExposureCap = GHO_GSM.getExposureCap(); - vm.prank(RISK_COUNCIL); - uint128 newExposureCap = oldExposureCap + 1; - GHO_STEWARD_V2.updateGsmExposureCap(address(GHO_GSM), newExposureCap); - uint128 currentExposureCap = GHO_GSM.getExposureCap(); - assertEq(currentExposureCap, newExposureCap); - } - - function testUpdateGsmExposureCapDownwards() public { - uint128 oldExposureCap = GHO_GSM.getExposureCap(); - vm.prank(RISK_COUNCIL); - uint128 newExposureCap = oldExposureCap - 1; - GHO_STEWARD_V2.updateGsmExposureCap(address(GHO_GSM), newExposureCap); - uint128 currentExposureCap = GHO_GSM.getExposureCap(); - assertEq(currentExposureCap, newExposureCap); - } - - function testUpdateGsmExposureCapMaxIncrease() public { - uint128 oldExposureCap = GHO_GSM.getExposureCap(); - uint128 newExposureCap = oldExposureCap * 2; - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmExposureCap(address(GHO_GSM), newExposureCap); - uint128 currentExposureCap = GHO_GSM.getExposureCap(); - assertEq(currentExposureCap, newExposureCap); - } - - function testUpdateGsmExposureCapMaxDecrease() public { - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmExposureCap(address(GHO_GSM), 0); - uint128 currentExposureCap = GHO_GSM.getExposureCap(); - assertEq(currentExposureCap, 0); - } - - function testUpdateGsmExposureCapTimelock() public { - uint128 oldExposureCap = GHO_GSM.getExposureCap(); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmExposureCap(address(GHO_GSM), oldExposureCap + 1); - IGhoStewardV2.GsmDebounce memory timelocks = GHO_STEWARD_V2.getGsmTimelocks(address(GHO_GSM)); - assertEq(timelocks.gsmExposureCapLastUpdated, block.timestamp); - } - - function testUpdateGsmExposureCapAfterTimelock() public { - uint128 oldExposureCap = GHO_GSM.getExposureCap(); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmExposureCap(address(GHO_GSM), oldExposureCap + 1); - skip(GHO_STEWARD_V2.MINIMUM_DELAY() + 1); - uint128 newExposureCap = oldExposureCap + 2; - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmExposureCap(address(GHO_GSM), newExposureCap); - uint128 currentExposureCap = GHO_GSM.getExposureCap(); - assertEq(currentExposureCap, newExposureCap); - } - - function testRevertUpdateGsmExposureCapIfUnauthorized() public { - vm.expectRevert('INVALID_CALLER'); - vm.prank(ALICE); - GHO_STEWARD_V2.updateGsmExposureCap(address(GHO_GSM), 50_000_000e18); - } - - function testRevertUpdateGsmExposureCapIfTooSoon() public { - uint128 oldExposureCap = GHO_GSM.getExposureCap(); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmExposureCap(address(GHO_GSM), oldExposureCap + 1); - vm.prank(RISK_COUNCIL); - vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); - GHO_STEWARD_V2.updateGsmExposureCap(address(GHO_GSM), oldExposureCap + 2); - } - - function testRevertUpdateGsmExposureCapIfValueMoreThanDouble() public { - uint128 oldExposureCap = GHO_GSM.getExposureCap(); - vm.prank(RISK_COUNCIL); - vm.expectRevert('INVALID_EXPOSURE_CAP_UPDATE'); - GHO_STEWARD_V2.updateGsmExposureCap(address(GHO_GSM), oldExposureCap * 2 + 1); - } - - function testRevertUpdateGsmExposureCapIfStewardLostConfiguratorRole() public { - uint128 oldExposureCap = GHO_GSM.getExposureCap(); - GHO_GSM.revokeRole(GSM_CONFIGURATOR_ROLE, address(GHO_STEWARD_V2)); - vm.expectRevert( - AccessControlErrorsLib.MISSING_ROLE(GSM_CONFIGURATOR_ROLE, address(GHO_STEWARD_V2)) - ); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmExposureCap(address(GHO_GSM), oldExposureCap + 1); - } - - function testUpdateGsmBuySellFeesBuyFeeUpwards() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee); - address newStrategy = GHO_GSM.getFeeStrategy(); - uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); - assertEq(newBuyFee, buyFee + 1); - } - - function testUpdateGsmBuySellFeesBuyFeeDownwards() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), buyFee - 1, sellFee); - address newStrategy = GHO_GSM.getFeeStrategy(); - uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); - assertEq(newBuyFee, buyFee - 1); - } - - function testUpdateGsmBuySellFeesBuyFeeMax() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - uint256 maxFeeUpdate = GHO_STEWARD_V2.GSM_FEE_RATE_CHANGE_MAX(); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), buyFee + maxFeeUpdate, sellFee); - address newStrategy = GHO_GSM.getFeeStrategy(); - uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); - assertEq(newBuyFee, buyFee + maxFeeUpdate); - } - - function testUpdateGsmBuySellFeesBuyFeeMin() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - uint256 maxFeeUpdate = GHO_STEWARD_V2.GSM_FEE_RATE_CHANGE_MAX(); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), buyFee - maxFeeUpdate, sellFee); - address newStrategy = GHO_GSM.getFeeStrategy(); - uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); - assertEq(newBuyFee, buyFee - maxFeeUpdate); - } - - function testUpdateGsmBuySellFeesSellFeeUpwards() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), buyFee, sellFee + 1); - address newStrategy = GHO_GSM.getFeeStrategy(); - uint256 newSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); - assertEq(newSellFee, sellFee + 1); - } - - function testUpdateGsmBuySellFeesSellFeeDownwards() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), buyFee, sellFee - 1); - address newStrategy = GHO_GSM.getFeeStrategy(); - uint256 newSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); - assertEq(newSellFee, sellFee - 1); - } - - function testUpdateGsmBuySellFeesSellFeeMax() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - uint256 maxFeeUpdate = GHO_STEWARD_V2.GSM_FEE_RATE_CHANGE_MAX(); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), buyFee, sellFee + maxFeeUpdate); - address newStrategy = GHO_GSM.getFeeStrategy(); - uint256 newSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); - assertEq(newSellFee, sellFee + maxFeeUpdate); - } - - function testUpdateGsmBuySellFeesSellFeeMin() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - uint256 maxFeeUpdate = GHO_STEWARD_V2.GSM_FEE_RATE_CHANGE_MAX(); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), buyFee, sellFee - maxFeeUpdate); - address newStrategy = GHO_GSM.getFeeStrategy(); - uint256 newSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); - assertEq(newSellFee, sellFee - maxFeeUpdate); - } - - function testUpdateGsmBuySellFeesBothFeesUpwards() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee + 1); - address newStrategy = GHO_GSM.getFeeStrategy(); - uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); - uint256 newSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); - assertEq(newBuyFee, buyFee + 1); - assertEq(newSellFee, sellFee + 1); - } - - function testUpdateGsmBuySellFeesBothFeesDownwards() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), buyFee - 1, sellFee - 1); - address newStrategy = GHO_GSM.getFeeStrategy(); - uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); - uint256 newSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); - assertEq(newBuyFee, buyFee - 1); - assertEq(newSellFee, sellFee - 1); - } - - function testUpdateGsmBuySellFeesBothFeesMax() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - uint256 maxFeeUpdate = GHO_STEWARD_V2.GSM_FEE_RATE_CHANGE_MAX(); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmBuySellFees( - address(GHO_GSM), - buyFee + maxFeeUpdate, - sellFee + maxFeeUpdate - ); - address newStrategy = GHO_GSM.getFeeStrategy(); - uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); - uint256 newSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); - assertEq(newBuyFee, buyFee + maxFeeUpdate); - assertEq(newSellFee, sellFee + maxFeeUpdate); - } - - function testUpdateGsmBuySellFeesBothFeesMin() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - uint256 maxFeeUpdate = GHO_STEWARD_V2.GSM_FEE_RATE_CHANGE_MAX(); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmBuySellFees( - address(GHO_GSM), - buyFee - maxFeeUpdate, - sellFee - maxFeeUpdate - ); - address newStrategy = GHO_GSM.getFeeStrategy(); - uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); - uint256 newSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); - assertEq(newBuyFee, buyFee - maxFeeUpdate); - assertEq(newSellFee, sellFee - maxFeeUpdate); - } - - function testUpdateGsmBuySellFeesTimelock() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee + 1); - IGhoStewardV2.GsmDebounce memory timelocks = GHO_STEWARD_V2.getGsmTimelocks(address(GHO_GSM)); - assertEq(timelocks.gsmFeeStrategyLastUpdated, block.timestamp); - } - - function testUpdateGsmBuySellFeesAfterTimelock() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee + 1); - skip(GHO_STEWARD_V2.MINIMUM_DELAY() + 1); - uint256 newBuyFee = buyFee + 2; - uint256 newSellFee = sellFee + 2; - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), newBuyFee, newSellFee); - address newStrategy = GHO_GSM.getFeeStrategy(); - uint256 currentBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); - uint256 currentSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); - assertEq(currentBuyFee, newBuyFee); - assertEq(currentSellFee, newSellFee); - } - - function testUpdateGsmBuySellFeesNewStrategy() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee + 1); - address[] memory cachedStrategies = GHO_STEWARD_V2.getGsmFeeStrategies(); - assertEq(cachedStrategies.length, 1); - address newStrategy = GHO_GSM.getFeeStrategy(); - assertEq(newStrategy, cachedStrategies[0]); - } - - function testUpdateGsmBuySellFeesSameStrategy() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee + 1); - address oldStrategy = GHO_GSM.getFeeStrategy(); - skip(GHO_STEWARD_V2.MINIMUM_DELAY() + 1); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee + 1); - address[] memory cachedStrategies = GHO_STEWARD_V2.getGsmFeeStrategies(); - assertEq(cachedStrategies.length, 1); - address newStrategy = GHO_GSM.getFeeStrategy(); - assertEq(oldStrategy, newStrategy); - } - - function testRevertUpdateGsmBuySellFeesIfUnauthorized() public { - vm.prank(ALICE); - vm.expectRevert('INVALID_CALLER'); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), 0.01e4, 0.01e4); - } - - function testRevertUpdateGsmBuySellFeesIfTooSoon() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee + 1); - vm.prank(RISK_COUNCIL); - vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), buyFee + 2, sellFee + 2); - } - - function testRevertUpdateGsmBuySellFeesIfStrategyNotFound() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - vm.mockCall( - address(GHO_GSM), - abi.encodeWithSelector(GHO_GSM.getFeeStrategy.selector), - abi.encode(address(0)) - ); - vm.expectRevert('GSM_FEE_STRATEGY_NOT_FOUND'); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee + 1); - } - - function testRevertUpdateGsmBuySellFeesIfBuyFeeMoreThanMax() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 maxFeeUpdate = GHO_STEWARD_V2.GSM_FEE_RATE_CHANGE_MAX(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - vm.prank(RISK_COUNCIL); - vm.expectRevert('INVALID_BUY_FEE_UPDATE'); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), buyFee + maxFeeUpdate + 1, sellFee); - } - - function testRevertUpdateGsmBuySellFeesIfBuyFeeLessThanMin() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 maxFeeUpdate = GHO_STEWARD_V2.GSM_FEE_RATE_CHANGE_MAX(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - vm.prank(RISK_COUNCIL); - vm.expectRevert('INVALID_BUY_FEE_UPDATE'); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), buyFee - maxFeeUpdate - 1, sellFee); - } - - function testRevertUpdateGsmBuySellFeesIfSellFeeMoreThanMax() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 maxFeeUpdate = GHO_STEWARD_V2.GSM_FEE_RATE_CHANGE_MAX(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - vm.prank(RISK_COUNCIL); - vm.expectRevert('INVALID_SELL_FEE_UPDATE'); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), buyFee, sellFee + maxFeeUpdate + 1); - } - - function testRevertUpdateGsmBuySellFeesIfSellFeeLessThanMin() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 maxFeeUpdate = GHO_STEWARD_V2.GSM_FEE_RATE_CHANGE_MAX(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - vm.prank(RISK_COUNCIL); - vm.expectRevert('INVALID_SELL_FEE_UPDATE'); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), buyFee, sellFee - maxFeeUpdate - 1); - } - - function testRevertUpdateGsmBuySellFeesIfBothMoreThanMax() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 maxFeeUpdate = GHO_STEWARD_V2.GSM_FEE_RATE_CHANGE_MAX(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - vm.prank(RISK_COUNCIL); - vm.expectRevert('INVALID_BUY_FEE_UPDATE'); - GHO_STEWARD_V2.updateGsmBuySellFees( - address(GHO_GSM), - buyFee + maxFeeUpdate + 1, - sellFee + maxFeeUpdate + 1 - ); - } - - function testRevertUpdateGsmBuySellFeesIfBothLessThanMin() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 maxFeeUpdate = GHO_STEWARD_V2.GSM_FEE_RATE_CHANGE_MAX(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - vm.prank(RISK_COUNCIL); - vm.expectRevert('INVALID_BUY_FEE_UPDATE'); - GHO_STEWARD_V2.updateGsmBuySellFees( - address(GHO_GSM), - buyFee - maxFeeUpdate - 1, - sellFee - maxFeeUpdate - 1 - ); - } - - function testRevertUpdateGsmBuySellFeesIfStewardLostConfiguratorRole() public { - address feeStrategy = GHO_GSM.getFeeStrategy(); - uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); - uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); - GHO_GSM.revokeRole(GSM_CONFIGURATOR_ROLE, address(GHO_STEWARD_V2)); - vm.expectRevert( - AccessControlErrorsLib.MISSING_ROLE(GSM_CONFIGURATOR_ROLE, address(GHO_STEWARD_V2)) - ); - vm.prank(RISK_COUNCIL); - GHO_STEWARD_V2.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee + 1); - } - - function testSetControlledFacilitatorAdd() public { - address[] memory oldControlledFacilitators = GHO_STEWARD_V2.getControlledFacilitators(); - address[] memory newGsmList = new address[](1); - newGsmList[0] = address(GHO_GSM_4626); - vm.prank(SHORT_EXECUTOR); - GHO_STEWARD_V2.setControlledFacilitator(newGsmList, true); - address[] memory newControlledFacilitators = GHO_STEWARD_V2.getControlledFacilitators(); - assertEq(newControlledFacilitators.length, oldControlledFacilitators.length + 1); - assertTrue(_contains(newControlledFacilitators, address(GHO_GSM_4626))); - } - - function testSetControlledFacilitatorsRemove() public { - address[] memory oldControlledFacilitators = GHO_STEWARD_V2.getControlledFacilitators(); - address[] memory disableGsmList = new address[](1); - disableGsmList[0] = address(GHO_GSM); - vm.prank(SHORT_EXECUTOR); - GHO_STEWARD_V2.setControlledFacilitator(disableGsmList, false); - address[] memory newControlledFacilitators = GHO_STEWARD_V2.getControlledFacilitators(); - assertEq(newControlledFacilitators.length, oldControlledFacilitators.length - 1); - assertFalse(_contains(newControlledFacilitators, address(GHO_GSM))); - } - - function testRevertSetControlledFacilitatorIfUnauthorized() public { - vm.expectRevert(OwnableErrorsLib.CALLER_NOT_OWNER()); - vm.prank(RISK_COUNCIL); - address[] memory newGsmList = new address[](1); - newGsmList[0] = address(GHO_GSM_4626); - GHO_STEWARD_V2.setControlledFacilitator(newGsmList, true); - } - - function _setGhoBorrowCapViaConfigurator(uint256 newBorrowCap) internal { - CONFIGURATOR.setBorrowCap(address(GHO_TOKEN), newBorrowCap); - } - - function _setGhoBorrowRateViaConfigurator( - uint256 newBorrowRate - ) internal returns (GhoInterestRateStrategy, uint256) { - GhoInterestRateStrategy newRateStrategy = new GhoInterestRateStrategy( - address(PROVIDER), - newBorrowRate - ); - CONFIGURATOR.setReserveInterestRateStrategyAddress( - address(GHO_TOKEN), - address(newRateStrategy) - ); - address currentInterestRateStrategy = POOL.getReserveInterestRateStrategyAddress( - address(GHO_TOKEN) - ); - uint256 currentBorrowRate = GhoInterestRateStrategy(currentInterestRateStrategy) - .getBaseVariableBorrowRate(); - assertEq(currentInterestRateStrategy, address(newRateStrategy)); - assertEq(currentBorrowRate, newBorrowRate); - return (newRateStrategy, newBorrowRate); - } - - function _getGhoBorrowRate() internal view returns (uint256) { - address currentInterestRateStrategy = POOL.getReserveInterestRateStrategyAddress( - address(GHO_TOKEN) - ); - return GhoInterestRateStrategy(currentInterestRateStrategy).getBaseVariableBorrowRate(); - } - - function _getGhoBorrowCap() internal view returns (uint256) { - DataTypes.ReserveConfigurationMap memory configuration = POOL.getConfiguration( - address(GHO_TOKEN) - ); - return configuration.getBorrowCap(); - } -} diff --git a/src/test/TestGhoStewardsForkEthereum.t.sol b/src/test/TestGhoStewardsForkEthereum.t.sol new file mode 100644 index 00000000..ea0b7726 --- /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: 5_00, + baseVariableBorrowRateMaxChange: 5_00, + variableRateSlope1MaxChange: 5_00, + variableRateSlope2MaxChange: 5_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..ccfae8df --- /dev/null +++ b/src/test/TestGhoStewardsForkRemote.t.sol @@ -0,0 +1,361 @@ +// 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: 5_00, + baseVariableBorrowRateMaxChange: 5_00, + variableRateSlope1MaxChange: 5_00, + variableRateSlope2MaxChange: 5_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 { + address rateStrategyAddress = POOL_DATA_PROVIDER.getInterestRateStrategyAddress(GHO_TOKEN); + + IDefaultInterestRateStrategyV2.InterestRateData + memory mockResponse = IDefaultInterestRateStrategyV2.InterestRateData({ + optimalUsageRatio: 100, + baseVariableBorrowRate: 100, + variableRateSlope1: 100, + variableRateSlope2: 100 + }); + vm.mockCall( + rateStrategyAddress, + abi.encodeWithSelector( + IDefaultInterestRateStrategyV2(rateStrategyAddress).getInterestRateDataBps.selector, + GHO_TOKEN + ), + abi.encode(mockResponse) + ); + + IDefaultInterestRateStrategyV2.InterestRateData memory currentRates = _getGhoBorrowRates(); + uint16 newOptimalUsageRatio = currentRates.optimalUsageRatio + 1; + uint32 newBaseVariableBorrowRate = currentRates.baseVariableBorrowRate + 1; + uint32 newVariableRateSlope1 = currentRates.variableRateSlope1 - 1; + uint32 newVariableRateSlope2 = currentRates.variableRateSlope2 - 1; + + vm.prank(RISK_COUNCIL); + GHO_AAVE_STEWARD.updateGhoBorrowRate( + newOptimalUsageRatio, + newBaseVariableBorrowRate, + newVariableRateSlope1, + newVariableRateSlope2 + ); + + vm.clearMockedCalls(); + + assertEq(_getOptimalUsageRatio(), newOptimalUsageRatio); + assertEq(_getBaseVariableBorrowRate(), newBaseVariableBorrowRate); + assertEq(_getVariableRateSlope1(), newVariableRateSlope1); + assertEq(_getVariableRateSlope2(), newVariableRateSlope2); + } + + 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/TestGsmRegistry.t.sol b/src/test/TestGsmRegistry.t.sol index 4b135e1b..38369995 100644 --- a/src/test/TestGsmRegistry.t.sol +++ b/src/test/TestGsmRegistry.t.sol @@ -5,7 +5,7 @@ import './TestGhoBase.t.sol'; contract TestGsmRegistry is TestGhoBase { function testConstructor(address newOwner) public { - vm.assume(newOwner != address(this)); + vm.assume(newOwner != address(this) && newOwner != address(0)); vm.expectEmit(true, true, false, true); emit OwnershipTransferred(address(0), address(this)); diff --git a/src/test/helpers/Constants.sol b/src/test/helpers/Constants.sol index 68952b7b..17b9c5b7 100644 --- a/src/test/helpers/Constants.sol +++ b/src/test/helpers/Constants.sol @@ -49,16 +49,11 @@ contract Constants { uint128 constant DEFAULT_GSM_USDC_AMOUNT = 100e6; // 6 decimals for USDC uint128 constant DEFAULT_GSM_GHO_AMOUNT = 100e18; - // GhoSteward - uint256 constant MINIMUM_DELAY = 5 days; - uint256 constant BORROW_RATE_CHANGE_MAX = 0.01e4; - uint40 constant STEWARD_LIFESPAN = 90 days; - - // GhoStewardV2 - uint256 constant GHO_BORROW_RATE_CHANGE_MAX = 0.0500e27; + // Gho Stewards + uint32 constant GHO_BORROW_RATE_CHANGE_MAX = 0.05e4; uint256 constant GSM_FEE_RATE_CHANGE_MAX = 0.0050e4; - uint256 constant GHO_BORROW_RATE_MAX = 0.2500e27; - uint256 constant MINIMUM_DELAY_V2 = 2 days; + uint32 constant GHO_BORROW_RATE_MAX = 0.25e4; + uint256 constant MINIMUM_DELAY_V2 = 1 days; uint256 constant FIXED_RATE_STRATEGY_FACTORY_REVISION = 1; // sample users used across unit tests diff --git a/src/test/helpers/Events.sol b/src/test/helpers/Events.sol index 23488186..c4fdc9fb 100644 --- a/src/test/helpers/Events.sol +++ b/src/test/helpers/Events.sol @@ -111,9 +111,6 @@ interface Events { uint256 amount ); - // GhoSteward - event StewardExpirationUpdated(uint40 oldStewardExpiration, uint40 newStewardExpiration); - // FixedRateStrategyFactory event RateStrategyCreated(address indexed strategy, uint256 indexed rate); diff --git a/src/test/mocks/MockConfigurator.sol b/src/test/mocks/MockConfigurator.sol index c7f6d2e1..0a5093f6 100644 --- a/src/test/mocks/MockConfigurator.sol +++ b/src/test/mocks/MockConfigurator.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.0; import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; import {DataTypes} from '@aave/core-v3/contracts/protocol/libraries/types/DataTypes.sol'; import {ReserveConfiguration} from '@aave/core-v3/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; +import {DefaultReserveInterestRateStrategyV2} from '../../contracts/misc/dependencies/AaveV3-1.sol'; +import {IDefaultInterestRateStrategyV2} from '../../contracts/misc/dependencies/AaveV3-1.sol'; contract MockConfigurator { using ReserveConfiguration for DataTypes.ReserveConfigurationMap; @@ -18,6 +20,8 @@ contract MockConfigurator { event BorrowCapChanged(address indexed asset, uint256 oldBorrowCap, uint256 newBorrowCap); + event SupplyCapChanged(address indexed asset, uint256 oldSupplyCap, uint256 newSupplyCap); + constructor(IPool pool) { _pool = pool; } @@ -37,6 +41,37 @@ contract MockConfigurator { emit ReserveInterestRateStrategyChanged(asset, oldRateStrategyAddress, newRateStrategyAddress); } + function setReserveInterestRateParams( + address asset, + IDefaultInterestRateStrategyV2.InterestRateData calldata rateParams + ) external { + DataTypes.ReserveData memory reserve = _pool.getReserveData(asset); + address rateStrategyAddress = reserve.interestRateStrategyAddress; + DefaultReserveInterestRateStrategyV2(rateStrategyAddress).setInterestRateParams( + asset, + rateParams + ); + } + + function setReserveInterestRateData(address asset, bytes calldata rateData) external { + this.setReserveInterestRateParams( + asset, + abi.decode(rateData, (IDefaultInterestRateStrategyV2.InterestRateData)) + ); + } + + function setReserveInterestRateStrategyAddress( + address asset, + address rateStrategyAddress, + bytes calldata rateData + ) external { + this.setReserveInterestRateStrategyAddress(asset, rateStrategyAddress); + this.setReserveInterestRateParams( + asset, + abi.decode(rateData, (IDefaultInterestRateStrategyV2.InterestRateData)) + ); + } + function setBorrowCap(address asset, uint256 newBorrowCap) external { DataTypes.ReserveConfigurationMap memory currentConfig = _pool.getConfiguration(asset); uint256 oldBorrowCap = currentConfig.getBorrowCap(); @@ -44,4 +79,12 @@ contract MockConfigurator { _pool.setConfiguration(asset, currentConfig); emit BorrowCapChanged(asset, oldBorrowCap, newBorrowCap); } + + function setSupplyCap(address asset, uint256 newSupplyCap) external { + DataTypes.ReserveConfigurationMap memory currentConfig = _pool.getConfiguration(asset); + uint256 oldSupplyCap = currentConfig.getSupplyCap(); + currentConfig.setSupplyCap(newSupplyCap); + _pool.setConfiguration(asset, currentConfig); + emit SupplyCapChanged(asset, oldSupplyCap, newSupplyCap); + } } diff --git a/src/test/mocks/MockPoolDataProvider.sol b/src/test/mocks/MockPoolDataProvider.sol new file mode 100644 index 00000000..fefda5a8 --- /dev/null +++ b/src/test/mocks/MockPoolDataProvider.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import {IPoolDataProvider} from '@aave/core-v3/contracts/interfaces/IPoolDataProvider.sol'; +import {IPoolAddressesProvider} from '@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol'; +import {DataTypes} from '@aave/core-v3/contracts/protocol/libraries/types/DataTypes.sol'; +import {ReserveConfiguration} from '@aave/core-v3/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; +import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; + +contract MockPoolDataProvider is IPoolDataProvider { + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + + IPoolAddressesProvider public immutable POOL_ADDRESSES_PROVIDER; + function ADDRESSES_PROVIDER() external view returns (IPoolAddressesProvider) { + return POOL_ADDRESSES_PROVIDER; + } + + constructor(address addressesProvider) { + POOL_ADDRESSES_PROVIDER = IPoolAddressesProvider(addressesProvider); + } + + function getInterestRateStrategyAddress(address asset) external view returns (address) { + DataTypes.ReserveData memory reserveData = IPool( + IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER).getPool() + ).getReserveData(asset); + return reserveData.interestRateStrategyAddress; + } + + function getATokenTotalSupply(address asset) external view returns (uint256) { + return 0; + } + + function getAllATokens() external view returns (TokenData[] memory) { + return new TokenData[](0); + } + + function getAllReservesTokens() external view returns (TokenData[] memory) { + return new TokenData[](0); + } + + function getDebtCeiling(address asset) external view returns (uint256) { + return 0; + } + + function getDebtCeilingDecimals() external pure returns (uint256) { + return 0; + } + + function getFlashLoanEnabled(address asset) external view returns (bool) { + return false; + } + + function getLiquidationProtocolFee(address asset) external view returns (uint256) { + return 0; + } + + function getPaused(address asset) external view returns (bool isPaused) { + return false; + } + function getReserveCaps( + address asset + ) external view returns (uint256 borrowCap, uint256 supplyCap) { + return (0, 0); + } + + function getReserveConfigurationData( + address asset + ) + external + view + returns ( + uint256 decimals, + uint256 ltv, + uint256 liquidationThreshold, + uint256 liquidationBonus, + uint256 reserveFactor, + bool usageAsCollateralEnabled, + bool borrowingEnabled, + bool stableBorrowRateEnabled, + bool isActive, + bool isFrozen + ) + { + return (0, 0, 0, 0, 0, false, false, false, false, false); + } + + function getReserveData( + address asset + ) + external + view + returns ( + uint256 unbacked, + uint256 accruedToTreasuryScaled, + uint256 totalAToken, + uint256 totalStableDebt, + uint256 totalVariableDebt, + uint256 liquidityRate, + uint256 variableBorrowRate, + uint256 stableBorrowRate, + uint256 averageStableBorrowRate, + uint256 liquidityIndex, + uint256 variableBorrowIndex, + uint40 lastUpdateTimestamp + ) + { + return (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + } + + function getReserveEModeCategory(address asset) external view returns (uint256) { + return 0; + } + + function getReserveTokensAddresses( + address asset + ) + external + view + returns ( + address aTokenAddress, + address stableDebtTokenAddress, + address variableDebtTokenAddress + ) + { + return (address(0), address(0), address(0)); + } + + function getSiloedBorrowing(address asset) external view returns (bool) { + return false; + } + + function getTotalDebt(address asset) external view returns (uint256) { + return 0; + } + + function getUnbackedMintCap(address asset) external view returns (uint256) { + return 0; + } + + function getUserReserveData( + address asset, + address user + ) + external + view + returns ( + uint256 currentATokenBalance, + uint256 currentStableDebt, + uint256 currentVariableDebt, + uint256 principalStableDebt, + uint256 scaledVariableDebt, + uint256 stableBorrowRate, + uint256 liquidityRate, + uint40 stableRateLastUpdated, + bool usageAsCollateralEnabled + ) + { + return (0, 0, 0, 0, 0, 0, 0, 0, false); + } +} diff --git a/src/test/mocks/MockUpgradeableBurnMintTokenPool.sol b/src/test/mocks/MockUpgradeableBurnMintTokenPool.sol new file mode 100644 index 00000000..4865ac90 --- /dev/null +++ b/src/test/mocks/MockUpgradeableBurnMintTokenPool.sol @@ -0,0 +1,158 @@ +// 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 { + allowlist; + 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); + s_bridgeLimit = newBridgeLimit; + } + + function setBridgeLimitAdmin(address bridgeLimitAdmin) external { + 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; + } +} diff --git a/src/test/mocks/MockUpgradeableLockReleaseTokenPool.sol b/src/test/mocks/MockUpgradeableLockReleaseTokenPool.sol new file mode 100644 index 00000000..774671ee --- /dev/null +++ b/src/test/mocks/MockUpgradeableLockReleaseTokenPool.sol @@ -0,0 +1,158 @@ +// 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 MockUpgradeableLockReleaseTokenPool 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 { + allowlist; + 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); + s_bridgeLimit = newBridgeLimit; + } + + function setBridgeLimitAdmin(address bridgeLimitAdmin) external { + 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; + } +} diff --git a/tasks/main/gho-testnet-setup.ts b/tasks/main/gho-testnet-setup.ts index 15f48086..a457160d 100644 --- a/tasks/main/gho-testnet-setup.ts +++ b/tasks/main/gho-testnet-setup.ts @@ -43,15 +43,7 @@ task('gho-testnet-setup', 'Deploy and Configure Gho').setAction(async (params, h blankSpace(); await hre.run('upgrade-stkAave'); - /***************************************** - * ADD GhoSteward * - ******************************************/ blankSpace(); - - await hre.run('add-gho-steward'); - - console.log(`\nGho Setup Complete!\n`); - await hre.run('print-all-deployments'); }); diff --git a/tasks/testnet-setup/07_add-gho-steward.ts b/tasks/testnet-setup/07_add-gho-steward.ts deleted file mode 100644 index 3fd7854e..00000000 --- a/tasks/testnet-setup/07_add-gho-steward.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { task } from 'hardhat/config'; -import { getACLManager } from '@aave/deploy-v3'; -import { GhoSteward } from '../../../types/src/contracts/facilitators/aave/misc/GhoSteward'; -import { getGhoToken } from '../../helpers/contract-getters'; -import { ethers } from 'ethers'; - -const BUCKET_MANAGER_ROLE = ethers.utils.id('BUCKET_MANAGER_ROLE'); - -task('add-gho-steward', 'Adds ghoSteward poolAdmin role').setAction(async (_, hre) => { - const { ethers } = hre; - - const ghoSteward = (await ethers.getContract('GhoSteward')) as GhoSteward; - const aclArtifact = await getACLManager(); - const addPoolAdminTx = await aclArtifact.addPoolAdmin(ghoSteward.address); - - const addPoolAdminTxReceipt = await addPoolAdminTx.wait(); - const newPoolAdminEvents = addPoolAdminTxReceipt.events?.find((e) => { - return e.event === 'RoleGranted'; - }); - if (newPoolAdminEvents?.args) { - console.log(`Gho steward added as a poolAdmin: ${JSON.stringify(newPoolAdminEvents.args[0])}`); - } else { - throw new Error(`Error at adding entity. Check tx: ${addPoolAdminTx.hash}`); - } - - const ghoToken = await getGhoToken(); - await (await ghoToken.grantRole(BUCKET_MANAGER_ROLE, ghoSteward.address)).wait(); - const added = await ghoToken.hasRole(BUCKET_MANAGER_ROLE, ghoSteward.address); - if (added) { - console.log('Gho steward added as bucketManager'); - } else { - throw new Error(`Error at adding entity ad BUCKET_MANAGER`); - } - - return; -}); diff --git a/test/gho-steward.test.ts b/test/gho-steward.test.ts deleted file mode 100644 index 44e85fe5..00000000 --- a/test/gho-steward.test.ts +++ /dev/null @@ -1,218 +0,0 @@ -import hre from 'hardhat'; -import { expect } from 'chai'; -import { makeSuite, TestEnv } from './helpers/make-suite'; -import { - advanceTimeAndBlock, - impersonateAccountHardhat, - mine, - setBlocktime, - timeLatest, -} from '../helpers/misc-utils'; -import { ONE_ADDRESS } from '../helpers/constants'; -import { ProtocolErrors } from '@aave/core-v3'; -import { evmRevert, evmSnapshot, getPoolConfiguratorProxy } from '@aave/deploy-v3'; -import { BigNumber } from 'ethers'; -import { GhoInterestRateStrategy__factory } from '../types'; - -export const TWO_ADDRESS = '0x0000000000000000000000000000000000000002'; - -makeSuite('Gho Steward End-To-End', (testEnv: TestEnv) => { - let ethers; - - let poolSigner; - let randomSigner; - let poolConfigurator; - - const BUCKET_MANAGER_ROLE = hre.ethers.utils.id('BUCKET_MANAGER_ROLE'); - - before(async () => { - ethers = hre.ethers; - - const { pool } = testEnv; - - poolSigner = await impersonateAccountHardhat(pool.address); - randomSigner = await impersonateAccountHardhat(ONE_ADDRESS); - poolConfigurator = await getPoolConfiguratorProxy(); - }); - - it('Check GhoSteward is PoolAdmin and BucketManager', async function () { - const { gho, ghoSteward, aclManager } = testEnv; - expect(await aclManager.isPoolAdmin(ghoSteward.address)).to.be.equal(true); - expect(await gho.hasRole(BUCKET_MANAGER_ROLE, ghoSteward.address)).to.be.equal(true); - }); - - it('Extends steward expiration', async function () { - const { ghoSteward } = testEnv; - - const expirationTimeBefore = await ghoSteward.getStewardExpiration(); - const newExpirationTime = BigNumber.from(expirationTimeBefore).add( - await ghoSteward.STEWARD_LIFESPAN() - ); - - const ownerAddress = await ghoSteward.owner(); - const owner = await impersonateAccountHardhat(ownerAddress); - await expect(ghoSteward.connect(owner).extendStewardExpiration()) - .to.emit(ghoSteward, 'StewardExpirationUpdated') - .withArgs(expirationTimeBefore, newExpirationTime); - - expect(await ghoSteward.getStewardExpiration()).to.be.eq(newExpirationTime.toNumber()); - }); - - it('Tries to extend steward expiration with no authorization (revert expected)', async function () { - const { ghoSteward, users } = testEnv; - const nonPoolAdmin = users[2]; - - await expect( - ghoSteward.connect(nonPoolAdmin.signer).extendStewardExpiration() - ).to.be.revertedWith('Ownable: caller is not the owner'); - }); - - it('Updates gho variable borrow rate', async function () { - const { ghoSteward, poolAdmin, aaveDataProvider, gho, deployer } = testEnv; - const oldInterestRateStrategyAddress = await aaveDataProvider.getInterestRateStrategyAddress( - gho.address - ); - const oldRate = await GhoInterestRateStrategy__factory.connect( - oldInterestRateStrategyAddress, - deployer.signer - ).getBaseVariableBorrowRate(); - await advanceTimeAndBlock((await ghoSteward.MINIMUM_DELAY()).toNumber()); - await expect(ghoSteward.connect(poolAdmin.signer).updateBorrowRate(oldRate)).to.emit( - poolConfigurator, - 'ReserveInterestRateStrategyChanged' - ); - - expect(await aaveDataProvider.getInterestRateStrategyAddress(gho.address)).not.to.be.equal( - oldInterestRateStrategyAddress - ); - }); - - it('GhoSteward tries to update gho variable borrow rate without PoolAdmin role (revert expected)', async function () { - const { ghoSteward, poolAdmin, aclAdmin, aclManager, aaveDataProvider, deployer, gho } = - testEnv; - - const snapId = await evmSnapshot(); - - const oldInterestRateStrategyAddress = await aaveDataProvider.getInterestRateStrategyAddress( - gho.address - ); - const oldRate = await GhoInterestRateStrategy__factory.connect( - oldInterestRateStrategyAddress, - deployer.signer - ).getBaseVariableBorrowRate(); - await advanceTimeAndBlock((await ghoSteward.MINIMUM_DELAY()).toNumber()); - - expect(await aclManager.connect(aclAdmin.signer).removePoolAdmin(ghoSteward.address)); - expect(await aclManager.isPoolAdmin(ghoSteward.address)).to.be.false; - - await expect(ghoSteward.connect(poolAdmin.signer).updateBorrowRate(oldRate)).to.be.revertedWith( - ProtocolErrors.CALLER_NOT_RISK_OR_POOL_ADMIN - ); - - await evmRevert(snapId); - }); - - it('Updates facilitator bucket', async function () { - const { ghoSteward, poolAdmin, gho, aToken } = testEnv; - - const [oldCapacity] = await gho.getFacilitatorBucket(aToken.address); - await advanceTimeAndBlock((await ghoSteward.MINIMUM_DELAY()).toNumber()); - await expect(ghoSteward.connect(poolAdmin.signer).updateBucketCapacity(oldCapacity.add(1))) - .to.emit(gho, 'FacilitatorBucketCapacityUpdated') - .withArgs(aToken.address, oldCapacity, oldCapacity.add(1)); - }); - - it('GhoSteward tries to update bucket capacity without BucketManager role (revert expected)', async function () { - const { ghoSteward, poolAdmin, gho, deployer, aToken } = testEnv; - - const snapId = await evmSnapshot(); - - const [oldCapacity] = await gho.getFacilitatorBucket(aToken.address); - await advanceTimeAndBlock((await ghoSteward.MINIMUM_DELAY()).toNumber()); - expect(await gho.connect(deployer.signer).revokeRole(BUCKET_MANAGER_ROLE, ghoSteward.address)); - expect(await gho.hasRole(BUCKET_MANAGER_ROLE, ghoSteward.address)).to.be.false; - - await expect( - ghoSteward.connect(poolAdmin.signer).updateBucketCapacity(oldCapacity) - ).to.be.revertedWith( - `AccessControl: account ${ghoSteward.address.toLowerCase()} is missing role ${BUCKET_MANAGER_ROLE}` - ); - - await evmRevert(snapId); - }); - - it('Check permissions of owner modified functions (revert expected)', async () => { - const { users, ghoSteward } = testEnv; - const nonPoolAdmin = users[2]; - - const calls = [ - { fn: 'updateBorrowRate', args: [0] }, - { fn: 'updateBucketCapacity', args: [ONE_ADDRESS] }, - ]; - for (const call of calls) { - await expect( - ghoSteward.connect(nonPoolAdmin.signer)[call.fn](...call.args) - ).to.be.revertedWith('INVALID_CALLER'); - } - }); - - it('RiskCouncil updates both parameters, steward expires, expiration time extends, more updates', async function () { - const { ghoSteward, poolAdmin, gho, aToken, aaveDataProvider, deployer } = testEnv; - - const oldInterestRateStrategyAddress = await aaveDataProvider.getInterestRateStrategyAddress( - gho.address - ); - const oldRate = await GhoInterestRateStrategy__factory.connect( - oldInterestRateStrategyAddress, - deployer.signer - ).getBaseVariableBorrowRate(); - const [oldCapacity] = await gho.getFacilitatorBucket(aToken.address); - await advanceTimeAndBlock((await ghoSteward.MINIMUM_DELAY()).toNumber()); - - // Update Bucket Capacity - await expect(ghoSteward.connect(poolAdmin.signer).updateBucketCapacity(oldCapacity.add(1))); - expect((await ghoSteward.getTimelock()).bucketCapacityLastUpdated).to.be.eq(await timeLatest()); - // Update Borrow Rate - await expect(ghoSteward.connect(poolAdmin.signer).updateBorrowRate(oldRate)); - expect((await ghoSteward.getTimelock()).borrowRateLastUpdated).to.be.eq(await timeLatest()); - - // Advance until expiration - await setBlocktime(await ghoSteward.getStewardExpiration()); - await mine(); - - // Tries to update bucket capacity or borrow rate - await expect( - ghoSteward.connect(poolAdmin.signer).updateBucketCapacity(oldCapacity) - ).to.be.revertedWith('STEWARD_EXPIRED'); - await expect(ghoSteward.connect(poolAdmin.signer).updateBorrowRate(oldRate)).to.be.revertedWith( - 'STEWARD_EXPIRED' - ); - - // Extend - const ownerAddress = await ghoSteward.owner(); - const owner = await impersonateAccountHardhat(ownerAddress); - await expect(ghoSteward.connect(owner).extendStewardExpiration()).to.emit( - ghoSteward, - 'StewardExpirationUpdated' - ); - - // New updates are possible - await expect(ghoSteward.connect(poolAdmin.signer).updateBucketCapacity(oldCapacity.add(1))); - expect((await ghoSteward.getTimelock()).bucketCapacityLastUpdated).to.be.eq(await timeLatest()); - // Update Borrow Rate - await expect(ghoSteward.connect(poolAdmin.signer).updateBorrowRate(oldRate)); - expect((await ghoSteward.getTimelock()).borrowRateLastUpdated).to.be.eq(await timeLatest()); - }); - - it('Deactivate Steward', async function () { - const { gho, ghoSteward, aclManager, deployer } = testEnv; - expect(await aclManager.isPoolAdmin(ghoSteward.address)).to.be.equal(true); - expect(await gho.hasRole(BUCKET_MANAGER_ROLE, ghoSteward.address)).to.be.equal(true); - - expect(await aclManager.connect(deployer.signer).removePoolAdmin(ghoSteward.address)); - expect(await gho.connect(deployer.signer).revokeRole(BUCKET_MANAGER_ROLE, ghoSteward.address)); - - expect(await aclManager.isPoolAdmin(ghoSteward.address)).to.be.equal(false); - expect(await gho.hasRole(BUCKET_MANAGER_ROLE, ghoSteward.address)).to.be.equal(false); - }); -}); diff --git a/test/helpers/make-suite.ts b/test/helpers/make-suite.ts index f9649104..2d45c1cf 100644 --- a/test/helpers/make-suite.ts +++ b/test/helpers/make-suite.ts @@ -19,7 +19,6 @@ import { StakedAaveV3, MintableERC20, GhoFlashMinter, - GhoSteward, } from '../../types'; import { getGhoDiscountRateStrategy, @@ -31,7 +30,6 @@ import { getStakedAave, getMintableErc20, getGhoFlashMinter, - getGhoSteward, getGhoStableDebtToken, } from '../../helpers/contract-getters'; import { @@ -85,7 +83,6 @@ export interface TestEnv { aaveToken: IERC20; flashMinter: GhoFlashMinter; faucetOwner: Faucet; - ghoSteward: GhoSteward; } let HardhatSnapshotId: string = '0x1'; @@ -123,7 +120,6 @@ const testEnv: TestEnv = { aaveToken: {} as IERC20, flashMinter: {} as GhoFlashMinter, faucetOwner: {} as Faucet, - ghoSteward: {} as GhoSteward, } as TestEnv; export async function initializeMakeSuite() { @@ -166,8 +162,6 @@ export async function initializeMakeSuite() { tokenProxyAddresses.variableDebtTokenAddress ); - testEnv.ghoSteward = await getGhoSteward(); - testEnv.aTokenImplementation = await getGhoAToken(); testEnv.stableDebtTokenImplementation = await getGhoStableDebtToken(); testEnv.variableDebtTokenImplementation = await getGhoVariableDebtToken();