From 77dd627126176b55f806f17fe43423c8536ffef9 Mon Sep 17 00:00:00 2001 From: JoaquinBattilana Date: Fri, 15 Mar 2024 13:33:12 +0000 Subject: [PATCH] feat: Add GhoStewardV2 (#388) * feat: basic gho steward * feat: init tests for gho steward * feat:updateBorrowCap tests * feat: polish tests for GhoStewardV2 * feat: new parameters requeriments and natspec for GhoStewardV2 * feat: initial test suit for GhoStewardV2 * feat: refactor to updateFacilitator instead of GHO and GSM by separate * feat: natspec polish * feat: fixed tests * feat: PR feedback + new tests for GhoStewardV2 * feat: deleted not used libraries and added setFacilitators tests * feat: fixed PR feedback * feat: format fix * feat: PR reviews 3 * feat: added updateGhoBorrowCap * feat: pr reviews and added test for roles removed * feat: added fixed rate strategy factory and deleted requeriment for borrow rate only increase * feat: fixed natspec * feat: more natspec cleaning * feat: deleted old test * feat: added tests for decrease gho borrow rate * feat: added test for missing constructor * feat: added FixedRateStrategyFactory tests * feat: made strategy factory initializable and moved it to correct directory * feat: PR feedback * feat: moved event test to common cases instead of separate test * fix: Update values for input validation --------- Co-authored-by: miguelmtzinf --- .../FixedRateStrategyFactory.sol | 88 ++ .../interfaces/IFixedRateStrategyFactory.sol | 45 + src/contracts/misc/GhoStewardV2.sol | 302 +++++++ .../misc/interfaces/IGhoStewardV2.sol | 158 ++++ src/test/TestFixedRateStrategyFactory.t.sol | 174 ++++ src/test/TestGhoBase.t.sol | 32 + src/test/TestGhoStewardV2.t.sol | 767 ++++++++++++++++++ src/test/helpers/Constants.sol | 7 + src/test/helpers/Events.sol | 3 + src/test/mocks/MockConfigurator.sol | 13 + src/test/mocks/MockPool.sol | 8 + 11 files changed, 1597 insertions(+) create mode 100644 src/contracts/facilitators/aave/interestStrategy/FixedRateStrategyFactory.sol create mode 100644 src/contracts/facilitators/aave/interestStrategy/interfaces/IFixedRateStrategyFactory.sol create mode 100644 src/contracts/misc/GhoStewardV2.sol create mode 100644 src/contracts/misc/interfaces/IGhoStewardV2.sol create mode 100644 src/test/TestFixedRateStrategyFactory.t.sol create mode 100644 src/test/TestGhoStewardV2.t.sol diff --git a/src/contracts/facilitators/aave/interestStrategy/FixedRateStrategyFactory.sol b/src/contracts/facilitators/aave/interestStrategy/FixedRateStrategyFactory.sol new file mode 100644 index 00000000..3b70608d --- /dev/null +++ b/src/contracts/facilitators/aave/interestStrategy/FixedRateStrategyFactory.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {IDefaultInterestRateStrategy} from '@aave/core-v3/contracts/interfaces/IDefaultInterestRateStrategy.sol'; +import {VersionedInitializable} from '@aave/core-v3/contracts/protocol/libraries/aave-upgradeability/VersionedInitializable.sol'; +import {IFixedRateStrategyFactory} from './interfaces/IFixedRateStrategyFactory.sol'; +import {GhoInterestRateStrategy} from './GhoInterestRateStrategy.sol'; + +/** + * @title FixedRateStrategyFactory + * @author Aave Labs + * @notice Factory contract to create and keep record of Aave v3 fixed rate strategy contracts + * @dev `GhoInterestRateStrategy` is used to provide a fixed interest rate strategy. + */ +contract FixedRateStrategyFactory is VersionedInitializable, IFixedRateStrategyFactory { + ///@inheritdoc IFixedRateStrategyFactory + address public immutable POOL_ADDRESSES_PROVIDER; + + mapping(uint256 => address) internal _strategiesByRate; + address[] internal _strategies; + + /** + * @dev Constructor + * @param addressesProvider The address of the PoolAddressesProvider of Aave V3 Pool + */ + constructor(address addressesProvider) { + require(addressesProvider != address(0), 'INVALID_ADDRESSES_PROVIDER'); + POOL_ADDRESSES_PROVIDER = addressesProvider; + } + + /** + * @notice FixedRateStrategyFactory initializer + * @dev asumes that the addresses provided are fixed rate deployed strategies. + * @param fixedRateStrategiesList List of fixed rate strategies + */ + function initialize(address[] memory fixedRateStrategiesList) external initializer { + for (uint256 i = 0; i < fixedRateStrategiesList.length; i++) { + address fixedRateStrategy = fixedRateStrategiesList[i]; + uint256 rate = IDefaultInterestRateStrategy(fixedRateStrategy).getBaseVariableBorrowRate(); + + _strategiesByRate[rate] = fixedRateStrategy; + _strategies.push(fixedRateStrategy); + + emit RateStrategyCreated(fixedRateStrategy, rate); + } + } + + ///@inheritdoc IFixedRateStrategyFactory + function createStrategies(uint256[] memory fixedRateList) public returns (address[] memory) { + address[] memory strategies = new address[](fixedRateList.length); + for (uint256 i = 0; i < fixedRateList.length; i++) { + uint256 rate = fixedRateList[i]; + address cachedStrategy = _strategiesByRate[rate]; + + if (cachedStrategy == address(0)) { + cachedStrategy = address(new GhoInterestRateStrategy(POOL_ADDRESSES_PROVIDER, rate)); + _strategiesByRate[rate] = cachedStrategy; + _strategies.push(cachedStrategy); + + emit RateStrategyCreated(cachedStrategy, rate); + } + + strategies[i] = cachedStrategy; + } + + return strategies; + } + + ///@inheritdoc IFixedRateStrategyFactory + function getAllStrategies() external view returns (address[] memory) { + return _strategies; + } + + ///@inheritdoc IFixedRateStrategyFactory + function getStrategyByRate(uint256 borrowRate) external view returns (address) { + return _strategiesByRate[borrowRate]; + } + + /// @inheritdoc IFixedRateStrategyFactory + 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/aave/interestStrategy/interfaces/IFixedRateStrategyFactory.sol b/src/contracts/facilitators/aave/interestStrategy/interfaces/IFixedRateStrategyFactory.sol new file mode 100644 index 00000000..caa28544 --- /dev/null +++ b/src/contracts/facilitators/aave/interestStrategy/interfaces/IFixedRateStrategyFactory.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IFixedRateStrategyFactory { + /** + * @dev Emitted when a new strategy is created + * @param strategy The address of the new fixed rate strategy + * @param rate The rate of the new strategy, expressed in ray (e.g. 0.0150e27 results in 1.50%) + */ + event RateStrategyCreated(address indexed strategy, uint256 indexed rate); + + /** + * @notice Creates new fixed rate strategy contracts from a list of rates. + * @dev Returns the address of a cached contract if a strategy with same rate already exists + * @param fixedRateList The list of rates for interest rates strategies, expressed in ray (e.g. 0.0150e27 results in 1.50%) + * @return The list of fixed interest rate strategy contracts + */ + function createStrategies(uint256[] memory fixedRateList) external returns (address[] memory); + + /** + * @notice Returns the address of the Pool Addresses Provider of Aave + * @return The address of the PoolAddressesProvider of Aave + */ + function POOL_ADDRESSES_PROVIDER() external view returns (address); + + /** + * @notice Returns all the fixed interest rate strategy contracts of the factory + * @return The list of fixed interest rate strategy contracts + */ + function getAllStrategies() external view returns (address[] memory); + + /** + * @notice Returns the fixed interest rate strategy contract which corresponds to the given rate. + * @dev Returns `address(0)` if there is no interest rate strategy for the given rate + * @param rate The rate of the fixed interest rate strategy contract + * @return The address of the fixed interest rate strategy contract + */ + function getStrategyByRate(uint256 rate) external view returns (address); + + /** + * @notice Returns the FixedRateStrategyFactory revision number + * @return The revision number + */ + function REVISION() external pure returns (uint256); +} diff --git a/src/contracts/misc/GhoStewardV2.sol b/src/contracts/misc/GhoStewardV2.sol new file mode 100644 index 00000000..3553848b --- /dev/null +++ b/src/contracts/misc/GhoStewardV2.sol @@ -0,0 +1,302 @@ +// 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 `PoolAdmin` 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 asumes 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( + _isIncreaseLowerThanMax(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( + _isIncreaseLowerThanMax(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( + _isIncreaseLowerThanMax(currentBuyFee, buyFee, GSM_FEE_RATE_CHANGE_MAX), + 'INVALID_BUY_FEE_UPDATE' + ); + require( + _isIncreaseLowerThanMax(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/interfaces/IGhoStewardV2.sol b/src/contracts/misc/interfaces/IGhoStewardV2.sol new file mode 100644 index 00000000..5f3aa7a8 --- /dev/null +++ b/src/contracts/misc/interfaces/IGhoStewardV2.sol @@ -0,0 +1,158 @@ +// 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 + * @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 + * @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 (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/TestFixedRateStrategyFactory.t.sol b/src/test/TestFixedRateStrategyFactory.t.sol new file mode 100644 index 00000000..a3b9bd85 --- /dev/null +++ b/src/test/TestFixedRateStrategyFactory.t.sol @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import './TestGhoBase.t.sol'; + +contract TestFixedRateStrategyFactory is TestGhoBase { + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + + function testConstructor() public { + assertEq(FIXED_RATE_STRATEGY_FACTORY.POOL_ADDRESSES_PROVIDER(), address(PROVIDER)); + address[] memory strategies = FIXED_RATE_STRATEGY_FACTORY.getAllStrategies(); + + assertEq(strategies.length, 0); + } + + function testRevertConstructorInvalidExecutor() public { + vm.expectRevert('INVALID_ADDRESSES_PROVIDER'); + new FixedRateStrategyFactory(address(0)); + } + + function testInitialize() public { + address[] memory strategies = new address[](1); + strategies[0] = address(new GhoInterestRateStrategy(address(PROVIDER), 100)); + + vm.expectEmit(true, true, false, false, address(FIXED_RATE_STRATEGY_FACTORY)); + emit RateStrategyCreated(strategies[0], 100); + + FIXED_RATE_STRATEGY_FACTORY.initialize(strategies); + address[] memory strategiesCall = FIXED_RATE_STRATEGY_FACTORY.getAllStrategies(); + + assertEq(strategiesCall.length, 1); + assertEq(strategiesCall[0], strategies[0]); + } + + function testInitializeMultiple() public { + address[] memory strategies = new address[](3); + strategies[0] = address(new GhoInterestRateStrategy(address(PROVIDER), 100)); + strategies[1] = address(new GhoInterestRateStrategy(address(PROVIDER), 200)); + strategies[2] = address(new GhoInterestRateStrategy(address(PROVIDER), 300)); + + vm.expectEmit(true, true, false, false, address(FIXED_RATE_STRATEGY_FACTORY)); + emit RateStrategyCreated(strategies[0], 100); + vm.expectEmit(true, true, false, false, address(FIXED_RATE_STRATEGY_FACTORY)); + emit RateStrategyCreated(strategies[1], 200); + vm.expectEmit(true, true, false, false, address(FIXED_RATE_STRATEGY_FACTORY)); + emit RateStrategyCreated(strategies[2], 300); + + FIXED_RATE_STRATEGY_FACTORY.initialize(strategies); + address[] memory strategiesCall = FIXED_RATE_STRATEGY_FACTORY.getAllStrategies(); + + assertEq(strategiesCall.length, 3); + assertEq(strategiesCall[0], strategies[0]); + assertEq(strategiesCall[1], strategies[1]); + assertEq(strategiesCall[2], strategies[2]); + } + + function testRevertInitializeTwice() public { + address[] memory strategies = new address[](1); + strategies[0] = address(new GhoInterestRateStrategy(address(PROVIDER), 100)); + + FIXED_RATE_STRATEGY_FACTORY.initialize(strategies); + vm.expectRevert('Contract instance has already been initialized'); + FIXED_RATE_STRATEGY_FACTORY.initialize(strategies); + } + + function testCreateStrategies() public { + uint256[] memory rates = new uint256[](1); + rates[0] = 100; + + uint256 nonce = vm.getNonce(address(FIXED_RATE_STRATEGY_FACTORY)); + address deployedStrategy = computeCreateAddress(address(FIXED_RATE_STRATEGY_FACTORY), nonce); + vm.expectEmit(true, true, false, false, address(FIXED_RATE_STRATEGY_FACTORY)); + emit RateStrategyCreated(deployedStrategy, 100); + + address[] memory strategies = FIXED_RATE_STRATEGY_FACTORY.createStrategies(rates); + + assertEq(strategies.length, 1); + assertEq(GhoInterestRateStrategy(strategies[0]).getBaseVariableBorrowRate(), rates[0]); + } + + function testCreateStrategiesMultiple() public { + uint256[] memory rates = new uint256[](3); + rates[0] = 100; + rates[1] = 200; + rates[2] = 300; + + uint256 nonce = vm.getNonce(address(FIXED_RATE_STRATEGY_FACTORY)); + + address deployedStrategy1 = computeCreateAddress(address(FIXED_RATE_STRATEGY_FACTORY), nonce); + vm.expectEmit(true, true, false, false, address(FIXED_RATE_STRATEGY_FACTORY)); + emit RateStrategyCreated(deployedStrategy1, 100); + + address deployedStrategy2 = computeCreateAddress( + address(FIXED_RATE_STRATEGY_FACTORY), + nonce + 1 + ); + vm.expectEmit(true, true, false, false, address(FIXED_RATE_STRATEGY_FACTORY)); + emit RateStrategyCreated(deployedStrategy2, 200); + + address deployedStrategy3 = computeCreateAddress( + address(FIXED_RATE_STRATEGY_FACTORY), + nonce + 2 + ); + vm.expectEmit(true, true, false, false, address(FIXED_RATE_STRATEGY_FACTORY)); + emit RateStrategyCreated(deployedStrategy3, 300); + + address[] memory strategies = FIXED_RATE_STRATEGY_FACTORY.createStrategies(rates); + + assertEq(strategies.length, 3); + assertEq(GhoInterestRateStrategy(strategies[0]).getBaseVariableBorrowRate(), rates[0]); + assertEq(GhoInterestRateStrategy(strategies[1]).getBaseVariableBorrowRate(), rates[1]); + assertEq(GhoInterestRateStrategy(strategies[2]).getBaseVariableBorrowRate(), rates[2]); + } + + function testCreateStrategiesCached() public { + uint256[] memory rates = new uint256[](2); + rates[0] = 100; + rates[1] = 100; + address[] memory strategies = FIXED_RATE_STRATEGY_FACTORY.createStrategies(rates); + + assertEq(strategies.length, 2); + assertEq(strategies[0], strategies[1]); + } + + function testCreatedStrategiesCachedDifferentCalls() public { + uint256[] memory rates = new uint256[](1); + rates[0] = 100; + address[] memory strategies = FIXED_RATE_STRATEGY_FACTORY.createStrategies(rates); + address[] memory strategies2 = FIXED_RATE_STRATEGY_FACTORY.createStrategies(rates); + assertEq(strategies[0], strategies2[0]); + } + + function testGetAllStrategies() public { + uint256[] memory rates = new uint256[](3); + rates[0] = 100; + rates[1] = 200; + rates[2] = 300; + + address[] memory strategies = FIXED_RATE_STRATEGY_FACTORY.createStrategies(rates); + address[] memory strategiesCall = FIXED_RATE_STRATEGY_FACTORY.getAllStrategies(); + + assertEq(strategies.length, strategiesCall.length); + assertEq(strategies[0], strategiesCall[0]); + assertEq(strategies[1], strategiesCall[1]); + assertEq(strategies[2], strategiesCall[2]); + } + + function testGetAllStrategiesCached() public { + uint256[] memory rates = new uint256[](2); + rates[0] = 100; + rates[1] = 100; + + FIXED_RATE_STRATEGY_FACTORY.createStrategies(rates); + address[] memory strategies = FIXED_RATE_STRATEGY_FACTORY.getAllStrategies(); + assertEq(strategies.length, 1); + } + + function testGetStrategyByRate() public { + uint256[] memory rates = new uint256[](3); + rates[0] = 100; + rates[1] = 200; + rates[2] = 300; + + address[] memory strategies = FIXED_RATE_STRATEGY_FACTORY.createStrategies(rates); + + assertEq(FIXED_RATE_STRATEGY_FACTORY.getStrategyByRate(rates[0]), strategies[0]); + assertEq(FIXED_RATE_STRATEGY_FACTORY.getStrategyByRate(rates[1]), strategies[1]); + assertEq(FIXED_RATE_STRATEGY_FACTORY.getStrategyByRate(rates[2]), strategies[2]); + } + + function testGetFixedRateStrategyRevision() public { + assertEq(FIXED_RATE_STRATEGY_FACTORY.REVISION(), FIXED_RATE_STRATEGY_FACTORY_REVISION); + } +} diff --git a/src/test/TestGhoBase.t.sol b/src/test/TestGhoBase.t.sol index fa3c3ea8..8711f15c 100644 --- a/src/test/TestGhoBase.t.sol +++ b/src/test/TestGhoBase.t.sol @@ -41,11 +41,13 @@ 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'; import {ERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/ERC20.sol'; import {StakedAaveV3} from 'aave-stk-v1-5/src/contracts/StakedAaveV3.sol'; +import {ReserveConfiguration} from '@aave/core-v3/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; // GHO contracts import {GhoAToken} from '../contracts/facilitators/aave/tokens/GhoAToken.sol'; @@ -54,10 +56,13 @@ import {GhoFlashMinter} from '../contracts/facilitators/flashMinter/GhoFlashMint 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 {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 {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 import {IGsm} from '../contracts/facilitators/gsm/interfaces/IGsm.sol'; @@ -65,6 +70,7 @@ import {Gsm} from '../contracts/facilitators/gsm/Gsm.sol'; import {Gsm4626} from '../contracts/facilitators/gsm/Gsm4626.sol'; import {FixedPriceStrategy} from '../contracts/facilitators/gsm/priceStrategy/FixedPriceStrategy.sol'; import {FixedPriceStrategy4626} from '../contracts/facilitators/gsm/priceStrategy/FixedPriceStrategy4626.sol'; +import {IGsmFeeStrategy} from '../contracts/facilitators/gsm/feeStrategy/interfaces/IGsmFeeStrategy.sol'; import {FixedFeeStrategy} from '../contracts/facilitators/gsm/feeStrategy/FixedFeeStrategy.sol'; import {SampleLiquidator} from '../contracts/facilitators/gsm/misc/SampleLiquidator.sol'; import {SampleSwapFreezer} from '../contracts/facilitators/gsm/misc/SampleSwapFreezer.sol'; @@ -116,6 +122,8 @@ contract TestGhoBase is Test, Constants, Events { GsmRegistry GHO_GSM_REGISTRY; GhoOracle GHO_ORACLE; GhoSteward GHO_STEWARD; + GhoStewardV2 GHO_STEWARD_V2; + FixedRateStrategyFactory FIXED_RATE_STRATEGY_FACTORY; constructor() { setupGho(); @@ -288,6 +296,21 @@ contract TestGhoBase is Test, Constants, Events { 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), + 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); } function ghoFaucet(address to, uint256 amount) public { @@ -626,4 +649,13 @@ contract TestGhoBase is Test, Constants, Events { token.transfer(address(1), amount); } } + + function _contains(address[] memory list, address item) internal returns (bool) { + for (uint256 i = 0; i < list.length; i++) { + if (list[i] == item) { + return true; + } + } + return false; + } } diff --git a/src/test/TestGhoStewardV2.t.sol b/src/test/TestGhoStewardV2.t.sol new file mode 100644 index 00000000..3eefe5eb --- /dev/null +++ b/src/test/TestGhoStewardV2.t.sol @@ -0,0 +1,767 @@ +// 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 testRevertUpdateFacilitatorBucketCapacityIfValueLowerThanCurrent() 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) - 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 testUpdateGhoBorrowCapMaxValue() 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 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 testRevertUpdateGhoBorrowCapIfValueLowerThanCurrent() public { + uint256 oldBorrowCap = 1e6; + _setGhoBorrowCapViaConfigurator(oldBorrowCap); + vm.prank(RISK_COUNCIL); + vm.expectRevert('INVALID_BORROW_CAP_UPDATE'); + GHO_STEWARD_V2.updateGhoBorrowCap(oldBorrowCap - 1); + } + + 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 testUpdateGhoBorrowRate() 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 testUpdateGsmExposureCap() 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 testUpdateGsmExposureCapMaxValue() 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 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 testRevertUpdateGsmExposureCapIfValueLowerThanCurrent() public { + uint128 oldExposureCap = GHO_GSM.getExposureCap(); + vm.prank(RISK_COUNCIL); + vm.expectRevert('INVALID_EXPOSURE_CAP_UPDATE'); + GHO_STEWARD_V2.updateGsmExposureCap(address(GHO_GSM), oldExposureCap - 1); + } + + 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 testUpdateGsmBuySellFeesBuyFee() 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 testUpdateGsmBuySellFeesSellFee() 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 testUpdateGsmBuySellFeesBothFees() 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 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 testRevertUpdateGsmBuySellFeesIfBuyFeeDecrement() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + 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 - 1, sellFee); + } + + function testRevertUpdateGsmBuySellFeesIfSellFeeDecrement() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + 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 - 1); + } + + function testRevertUpdateGsmBuySellFeesIfBothDecrement() public { + address feeStrategy = GHO_GSM.getFeeStrategy(); + 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 - 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 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 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 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('Ownable: caller is not the 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/helpers/Constants.sol b/src/test/helpers/Constants.sol index 864090fa..605ccfbb 100644 --- a/src/test/helpers/Constants.sol +++ b/src/test/helpers/Constants.sol @@ -52,6 +52,13 @@ contract Constants { uint256 constant BORROW_RATE_CHANGE_MAX = 0.01e4; uint40 constant STEWARD_LIFESPAN = 90 days; + // GhoStewardV2 + uint256 constant GHO_BORROW_RATE_CHANGE_MAX = 0.0500e27; + uint256 constant GSM_FEE_RATE_CHANGE_MAX = 0.0050e4; + uint256 constant GHO_BORROW_RATE_MAX = 0.2500e27; + uint256 constant MINIMUM_DELAY_V2 = 2 days; + uint256 constant FIXED_RATE_STRATEGY_FACTORY_REVISION = 1; + // sample users used across unit tests address constant ALICE = address(0x1111); address constant BOB = address(0x1112); diff --git a/src/test/helpers/Events.sol b/src/test/helpers/Events.sol index 62ed314e..23488186 100644 --- a/src/test/helpers/Events.sol +++ b/src/test/helpers/Events.sol @@ -114,6 +114,9 @@ interface Events { // GhoSteward event StewardExpirationUpdated(uint40 oldStewardExpiration, uint40 newStewardExpiration); + // FixedRateStrategyFactory + event RateStrategyCreated(address indexed strategy, uint256 indexed rate); + // IGsmRegistry events event GsmAdded(address indexed gsmAddress); event GsmRemoved(address indexed gsmAddress); diff --git a/src/test/mocks/MockConfigurator.sol b/src/test/mocks/MockConfigurator.sol index f9c17d08..c7f6d2e1 100644 --- a/src/test/mocks/MockConfigurator.sol +++ b/src/test/mocks/MockConfigurator.sol @@ -3,8 +3,11 @@ 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'; contract MockConfigurator { + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + IPool internal _pool; event ReserveInterestRateStrategyChanged( @@ -13,6 +16,8 @@ contract MockConfigurator { address newStrategy ); + event BorrowCapChanged(address indexed asset, uint256 oldBorrowCap, uint256 newBorrowCap); + constructor(IPool pool) { _pool = pool; } @@ -31,4 +36,12 @@ contract MockConfigurator { _pool.setReserveInterestRateStrategyAddress(asset, newRateStrategyAddress); emit ReserveInterestRateStrategyChanged(asset, oldRateStrategyAddress, newRateStrategyAddress); } + + function setBorrowCap(address asset, uint256 newBorrowCap) external { + DataTypes.ReserveConfigurationMap memory currentConfig = _pool.getConfiguration(asset); + uint256 oldBorrowCap = currentConfig.getBorrowCap(); + currentConfig.setBorrowCap(newBorrowCap); + _pool.setConfiguration(asset, currentConfig); + emit BorrowCapChanged(asset, oldBorrowCap, newBorrowCap); + } } diff --git a/src/test/mocks/MockPool.sol b/src/test/mocks/MockPool.sol index 4a964511..e166775a 100644 --- a/src/test/mocks/MockPool.sol +++ b/src/test/mocks/MockPool.sol @@ -114,4 +114,12 @@ contract MockPool is Pool { function getReserveInterestRateStrategyAddress(address asset) public view returns (address) { return _reserves[asset].interestRateStrategyAddress; } + + function setConfiguration( + address asset, + DataTypes.ReserveConfigurationMap calldata configuration + ) external override { + require(asset != address(0), Errors.ZERO_ADDRESS_NOT_VALID); + _reserves[asset].configuration = configuration; + } }