|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | +pragma solidity ^0.8.10; |
| 3 | + |
| 4 | +import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; |
| 5 | +import {IPoolAddressesProvider} from '@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol'; |
| 6 | +import {IPoolConfigurator} from '@aave/core-v3/contracts/interfaces/IPoolConfigurator.sol'; |
| 7 | +import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; |
| 8 | +import {DataTypes} from '@aave/core-v3/contracts/protocol/libraries/types/DataTypes.sol'; |
| 9 | +import {PercentageMath} from '@aave/core-v3/contracts/protocol/libraries/math/PercentageMath.sol'; |
| 10 | +import {GhoInterestRateStrategy} from '../facilitators/aave/interestStrategy/GhoInterestRateStrategy.sol'; |
| 11 | +import {IGhoToken} from '../gho/interfaces/IGhoToken.sol'; |
| 12 | +import {IGhoSteward} from './interfaces/IGhoSteward.sol'; |
| 13 | + |
| 14 | +/** |
| 15 | + * @title GhoSteward |
| 16 | + * @author Aave |
| 17 | + * @notice Helper contract for managing risk parameters of the GHO reserve within the Aave Facilitator |
| 18 | + * @dev This contract must be granted `PoolAdmin` in the Aave V3 Ethereum Pool and `BucketManager` in GHO Token |
| 19 | + * @dev Only the Risk Council is able to action contract's functions. |
| 20 | + * @dev Only the Aave DAO is able to extend the steward's lifespan. |
| 21 | + */ |
| 22 | +contract GhoSteward is Ownable, IGhoSteward { |
| 23 | + using PercentageMath for uint256; |
| 24 | + |
| 25 | + /// @inheritdoc IGhoSteward |
| 26 | + uint256 public constant MINIMUM_DELAY = 5 days; |
| 27 | + |
| 28 | + /// @inheritdoc IGhoSteward |
| 29 | + uint256 public constant BORROW_RATE_CHANGE_MAX = 0.0050e4; |
| 30 | + |
| 31 | + /// @inheritdoc IGhoSteward |
| 32 | + uint40 public constant STEWARD_LIFESPAN = 60 days; |
| 33 | + |
| 34 | + /// @inheritdoc IGhoSteward |
| 35 | + address public immutable POOL_ADDRESSES_PROVIDER; |
| 36 | + |
| 37 | + /// @inheritdoc IGhoSteward |
| 38 | + address public immutable GHO_TOKEN; |
| 39 | + |
| 40 | + /// @inheritdoc IGhoSteward |
| 41 | + address public immutable RISK_COUNCIL; |
| 42 | + |
| 43 | + Debounce internal _timelocks; |
| 44 | + uint40 internal _stewardExpiration; |
| 45 | + mapping(uint256 => address) internal strategiesByRate; |
| 46 | + address[] internal strategies; |
| 47 | + |
| 48 | + /** |
| 49 | + * @dev Only Risk Council can call functions marked by this modifier. |
| 50 | + */ |
| 51 | + modifier onlyRiskCouncil() { |
| 52 | + require(RISK_COUNCIL == msg.sender, 'INVALID_CALLER'); |
| 53 | + _; |
| 54 | + } |
| 55 | + |
| 56 | + /** |
| 57 | + * @dev Constructor |
| 58 | + * @param addressesProvider The address of the PoolAddressesProvider of Aave V3 Ethereum Pool |
| 59 | + * @param ghoToken The address of the GhoToken |
| 60 | + * @param riskCouncil The address of the RiskCouncil |
| 61 | + * @param shortExecutor The address of the Aave Short Executor |
| 62 | + */ |
| 63 | + constructor( |
| 64 | + address addressesProvider, |
| 65 | + address ghoToken, |
| 66 | + address riskCouncil, |
| 67 | + address shortExecutor |
| 68 | + ) { |
| 69 | + require(addressesProvider != address(0), 'INVALID_ADDRESSES_PROVIDER'); |
| 70 | + require(ghoToken != address(0), 'INVALID_GHO_TOKEN'); |
| 71 | + require(riskCouncil != address(0), 'INVALID_RISK_COUNCIL'); |
| 72 | + require(shortExecutor != address(0), 'INVALID_SHORT_EXECUTOR'); |
| 73 | + POOL_ADDRESSES_PROVIDER = addressesProvider; |
| 74 | + GHO_TOKEN = ghoToken; |
| 75 | + RISK_COUNCIL = riskCouncil; |
| 76 | + _stewardExpiration = uint40(block.timestamp + STEWARD_LIFESPAN); |
| 77 | + |
| 78 | + _transferOwnership(shortExecutor); |
| 79 | + } |
| 80 | + |
| 81 | + /// @inheritdoc IGhoSteward |
| 82 | + function updateBorrowRate(uint256 newBorrowRate) external onlyRiskCouncil { |
| 83 | + require(block.timestamp < _stewardExpiration, 'STEWARD_EXPIRED'); |
| 84 | + require( |
| 85 | + block.timestamp - _timelocks.borrowRateLastUpdated > MINIMUM_DELAY, |
| 86 | + 'DEBOUNCE_NOT_RESPECTED' |
| 87 | + ); |
| 88 | + |
| 89 | + DataTypes.ReserveData memory ghoReserveData = IPool( |
| 90 | + IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER).getPool() |
| 91 | + ).getReserveData(GHO_TOKEN); |
| 92 | + require( |
| 93 | + ghoReserveData.interestRateStrategyAddress != address(0), |
| 94 | + 'GHO_INTEREST_RATE_STRATEGY_NOT_FOUND' |
| 95 | + ); |
| 96 | + |
| 97 | + uint256 oldBorrowRate = GhoInterestRateStrategy(ghoReserveData.interestRateStrategyAddress) |
| 98 | + .getBaseVariableBorrowRate(); |
| 99 | + require(_borrowRateChangeAllowed(oldBorrowRate, newBorrowRate), 'INVALID_BORROW_RATE_UPDATE'); |
| 100 | + |
| 101 | + _timelocks.borrowRateLastUpdated = uint40(block.timestamp); |
| 102 | + |
| 103 | + address cachedStrategyAddress = strategiesByRate[newBorrowRate]; |
| 104 | + // Deploy a new one if does not exist |
| 105 | + if (cachedStrategyAddress == address(0)) { |
| 106 | + GhoInterestRateStrategy newRateStrategy = new GhoInterestRateStrategy( |
| 107 | + POOL_ADDRESSES_PROVIDER, |
| 108 | + newBorrowRate |
| 109 | + ); |
| 110 | + cachedStrategyAddress = address(newRateStrategy); |
| 111 | + |
| 112 | + strategiesByRate[newBorrowRate] = address(newRateStrategy); |
| 113 | + strategies.push(address(newRateStrategy)); |
| 114 | + } |
| 115 | + |
| 116 | + IPoolConfigurator(IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER).getPoolConfigurator()) |
| 117 | + .setReserveInterestRateStrategyAddress(GHO_TOKEN, cachedStrategyAddress); |
| 118 | + } |
| 119 | + |
| 120 | + /// @inheritdoc IGhoSteward |
| 121 | + function updateBucketCapacity(uint128 newBucketCapacity) external onlyRiskCouncil { |
| 122 | + require(block.timestamp < _stewardExpiration, 'STEWARD_EXPIRED'); |
| 123 | + require( |
| 124 | + block.timestamp - _timelocks.bucketCapacityLastUpdated > MINIMUM_DELAY, |
| 125 | + 'DEBOUNCE_NOT_RESPECTED' |
| 126 | + ); |
| 127 | + |
| 128 | + DataTypes.ReserveData memory ghoReserveData = IPool( |
| 129 | + IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER).getPool() |
| 130 | + ).getReserveData(GHO_TOKEN); |
| 131 | + require(ghoReserveData.aTokenAddress != address(0), 'GHO_ATOKEN_NOT_FOUND'); |
| 132 | + |
| 133 | + (uint256 oldBucketCapacity, ) = IGhoToken(GHO_TOKEN).getFacilitatorBucket( |
| 134 | + ghoReserveData.aTokenAddress |
| 135 | + ); |
| 136 | + require( |
| 137 | + _bucketCapacityIncreaseAllowed(oldBucketCapacity, newBucketCapacity), |
| 138 | + 'INVALID_BUCKET_CAPACITY_UPDATE' |
| 139 | + ); |
| 140 | + |
| 141 | + _timelocks.bucketCapacityLastUpdated = uint40(block.timestamp); |
| 142 | + |
| 143 | + IGhoToken(GHO_TOKEN).setFacilitatorBucketCapacity( |
| 144 | + ghoReserveData.aTokenAddress, |
| 145 | + newBucketCapacity |
| 146 | + ); |
| 147 | + } |
| 148 | + |
| 149 | + /// @inheritdoc IGhoSteward |
| 150 | + function extendStewardExpiration() external onlyOwner { |
| 151 | + uint40 oldStewardExpiration = _stewardExpiration; |
| 152 | + _stewardExpiration += uint40(STEWARD_LIFESPAN); |
| 153 | + emit StewardExpirationUpdated(oldStewardExpiration, _stewardExpiration); |
| 154 | + } |
| 155 | + |
| 156 | + /// @inheritdoc IGhoSteward |
| 157 | + function getTimelock() external view returns (Debounce memory) { |
| 158 | + return _timelocks; |
| 159 | + } |
| 160 | + |
| 161 | + /// @inheritdoc IGhoSteward |
| 162 | + function getStewardExpiration() external view returns (uint40) { |
| 163 | + return _stewardExpiration; |
| 164 | + } |
| 165 | + |
| 166 | + /// @inheritdoc IGhoSteward |
| 167 | + function getAllStrategies() external view returns (address[] memory) { |
| 168 | + return strategies; |
| 169 | + } |
| 170 | + |
| 171 | + /** |
| 172 | + * @notice Ensures the borrow rate change is within the allowed range. |
| 173 | + * @param from current borrow rate (in ray) |
| 174 | + * @param to new borrow rate (in ray) |
| 175 | + * @return bool true, if difference is within the max 0.5% change window |
| 176 | + */ |
| 177 | + function _borrowRateChangeAllowed(uint256 from, uint256 to) internal pure returns (bool) { |
| 178 | + return |
| 179 | + from < to |
| 180 | + ? to - from <= from.percentMul(BORROW_RATE_CHANGE_MAX) |
| 181 | + : from - to <= from.percentMul(BORROW_RATE_CHANGE_MAX); |
| 182 | + } |
| 183 | + |
| 184 | + /** |
| 185 | + * @notice Ensures the bucket capacity increase is within the allowed range. |
| 186 | + * @param from current bucket capacity |
| 187 | + * @param to new bucket capacity |
| 188 | + * @return bool true, if difference is within the max 100% increase window |
| 189 | + */ |
| 190 | + function _bucketCapacityIncreaseAllowed(uint256 from, uint256 to) internal pure returns (bool) { |
| 191 | + return to >= from && to - from <= from; |
| 192 | + } |
| 193 | +} |
0 commit comments