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/README.md b/README.md index edcd32a3..6f86d711 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ You can find all audit reports under the [audits](./audits/) folder - [2023-12-07 - Certora Formal Verification (GHO Stability Module)](./certora/reports/Formal_Verification_Report_of_GHO_Stability_Module.pdf) - [2024-03-14 - Certora Formal Verification (GhoStewardV2)](./audits/2024-03-14_GhoStewardV2_Certora.pdf) - [2024-06-11 - Certora Formal Verification (UpgradeableGHO)](./audits/2024-06-11_UpgradeableGHO_Certora.pdf) +- [2024-06-11 - Certora Formal Verification (Modular Gho Stewards)](./audits/2024-09-15_ModularGhoStewards_Certora.pdf) ## Getting Started diff --git a/audits/2024-09-15_ModularGhoStewards_Certora.pdf b/audits/2024-09-15_ModularGhoStewards_Certora.pdf new file mode 100644 index 00000000..4c7c6691 Binary files /dev/null and b/audits/2024-09-15_ModularGhoStewards_Certora.pdf differ diff --git a/certora/steward/Makefile b/certora/steward/Makefile new file mode 100644 index 00000000..611d7a80 --- /dev/null +++ b/certora/steward/Makefile @@ -0,0 +1,28 @@ +default: help + +PATCH = applyHarness.patch +CONTRACTS_DIR = ../../src +MUNGED_DIR = munged + +help: + @echo "usage:" + @echo " make clean: remove all generated files (those ignored by git)" + @echo " make $(MUNGED_DIR): create $(MUNGED_DIR) directory by applying the patch file to $(CONTRACTS_DIR)" + @echo " make record: record a new patch file capturing the differences between $(CONTRACTS_DIR) and $(MUNGED_DIR)" + +munged: $(wildcard $(CONTRACTS_DIR)/*.sol) $(PATCH) + rm -rf $@ + mkdir $@ + cp -r ../../src $@ + patch -p0 -d $@ < $(PATCH) + +record: + mkdir tmp + cp -r ../../src tmp + diff -ruN tmp $(MUNGED_DIR) | sed 's+tmp/++g' | sed 's+$(MUNGED_DIR)/++g' > $(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 5d9492c1..354a7451 100644 --- a/src/test/TestGhoBase.t.sol +++ b/src/test/TestGhoBase.t.sol @@ -37,6 +37,7 @@ import {MockRedemptionFailed} from './mocks/MockRedemptionFailed.sol'; import {MockIssuanceReceiver} from './mocks/MockIssuanceReceiver.sol'; import {MockIssuanceReceiverFailed} from './mocks/MockIssuanceReceiverFailed.sol'; import {MockIssuanceReceiverFailedInvalidUSDCAccepted} from './mocks/MockIssuanceReceiverFailedInvalidUSDCAccepted.sol'; +import {MockPoolDataProvider} from './mocks/MockPoolDataProvider.sol'; // interfaces import {IAaveIncentivesController} from '@aave/core-v3/contracts/interfaces/IAaveIncentivesController.sol'; @@ -50,7 +51,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'; @@ -64,15 +64,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 @@ -86,6 +84,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'; import {GsmConverter} from '../contracts/facilitators/gsm/converter/GsmConverter.sol'; contract TestGhoBase is Test, Constants, Events { @@ -143,9 +151,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(); @@ -160,6 +174,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(); @@ -200,8 +215,6 @@ contract TestGhoBase is Test, Constants, Events { 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); @@ -209,7 +222,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', @@ -218,7 +231,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', @@ -228,7 +241,7 @@ contract TestGhoBase is Test, Constants, Events { GHO_ATOKEN.initialize( iPool, TREASURY, - ghoTokenAddress, + address(GHO_TOKEN), IAaveIncentivesController(address(0)), 18, 'Aave GHO', @@ -236,7 +249,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)); @@ -328,28 +341,63 @@ contract TestGhoBase is Test, Constants, Events { GHO_TOKEN.addFacilitator(address(GHO_BUIDL_GSM), 'GSM BUIDL Facilitator', DEFAULT_CAPACITY); 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); BUIDL_USDC_REDEMPTION = new MockRedemption(address(BUIDL_TOKEN), address(USDC_TOKEN)); BUIDL_USDC_REDEMPTION_FAILED_ISSUED_ASSET_AMOUNT = new MockRedemptionFailedIssuedAssetAmount( 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 0c67bd59..38369995 100644 --- a/src/test/TestGsmRegistry.t.sol +++ b/src/test/TestGsmRegistry.t.sol @@ -5,8 +5,7 @@ import './TestGhoBase.t.sol'; contract TestGsmRegistry is TestGhoBase { function testConstructor(address newOwner) public { - vm.assume(newOwner != address(this)); - vm.assume(newOwner != address(0)); + 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 a4f45ba1..7948059f 100644 --- a/src/test/helpers/Constants.sol +++ b/src/test/helpers/Constants.sol @@ -61,16 +61,11 @@ contract Constants { uint128 constant DEFAULT_GSM_BUIDL_AMOUNT = 100e6; // 6 decimals for BUIDL 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 aa6193e9..1fd32968 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); - // GsmConverter events event BuyAssetThroughRedemption( address indexed originator, 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();