Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Gho steward v2 #388

Merged
merged 29 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6763340
feat: basic gho steward
JoaquinBattilana Feb 9, 2024
45361a1
feat: init tests for gho steward
JoaquinBattilana Feb 9, 2024
426e39c
feat:updateBorrowCap tests
JoaquinBattilana Feb 9, 2024
7dc49c4
feat: polish tests for GhoStewardV2
JoaquinBattilana Feb 12, 2024
1f23190
feat: new parameters requeriments and natspec for GhoStewardV2
JoaquinBattilana Feb 16, 2024
1373dae
feat: initial test suit for GhoStewardV2
JoaquinBattilana Feb 16, 2024
7000996
feat: refactor to updateFacilitator instead of GHO and GSM by separate
JoaquinBattilana Feb 16, 2024
54ecaff
feat: natspec polish
JoaquinBattilana Feb 16, 2024
7c40a3a
feat: fixed tests
JoaquinBattilana Feb 16, 2024
ba70de3
feat: PR feedback + new tests for GhoStewardV2
JoaquinBattilana Feb 19, 2024
e40f895
Merge branch 'main' into feat/gho-steward-v2
JoaquinBattilana Feb 19, 2024
d548248
feat: deleted not used libraries and added setFacilitators tests
JoaquinBattilana Feb 19, 2024
28face8
feat: fixed PR feedback
JoaquinBattilana Feb 22, 2024
79829a5
Merge branch 'main' into feat/gho-steward-v2
JoaquinBattilana Feb 22, 2024
45c7dc5
feat: format fix
JoaquinBattilana Feb 22, 2024
c160ced
feat: PR reviews 3
JoaquinBattilana Feb 22, 2024
6fe257b
feat: added updateGhoBorrowCap
JoaquinBattilana Feb 22, 2024
9b4e8cb
feat: pr reviews and added test for roles removed
JoaquinBattilana Feb 23, 2024
e2b3e4b
feat: added fixed rate strategy factory and deleted requeriment for b…
JoaquinBattilana Mar 1, 2024
7f42dfc
feat: fixed natspec
JoaquinBattilana Mar 1, 2024
967025c
feat: more natspec cleaning
JoaquinBattilana Mar 1, 2024
7d33cbb
feat: deleted old test
JoaquinBattilana Mar 1, 2024
c23dd42
feat: added tests for decrease gho borrow rate
JoaquinBattilana Mar 1, 2024
2de50ff
feat: added test for missing constructor
JoaquinBattilana Mar 1, 2024
4f67e2e
feat: added FixedRateStrategyFactory tests
JoaquinBattilana Mar 1, 2024
1962c0e
feat: made strategy factory initializable and moved it to correct dir…
JoaquinBattilana Mar 5, 2024
1e9e2c9
feat: PR feedback
JoaquinBattilana Mar 7, 2024
dfabf24
feat: moved event test to common cases instead of separate test
JoaquinBattilana Mar 7, 2024
32f4ff2
fix: Update values for input validation
miguelmtzinf Mar 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
253 changes: 253 additions & 0 deletions src/contracts/misc/GhoStewardV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
// 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 {GhoInterestRateStrategy} from '../facilitators/aave/interestStrategy/GhoInterestRateStrategy.sol';
import {FixedFeeStrategy} from '../facilitators/gsm/feeStrategy/FixedFeeStrategy.sol';
import {IGhoToken} from '../gho/interfaces/IGhoToken.sol';
import {IGhoStewardV2} from './interfaces/IGhoStewardV2.sol';
import {IGsm} from '../facilitators/gsm/interfaces/IGsm.sol';
import {IGsmFeeStrategy} from '../facilitators/gsm/feeStrategy/interfaces/IGsmFeeStrategy.sol';
import {EnumerableSet} from '@openzeppelin/contracts/utils/structs/EnumerableSet.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 that will be managed by the risk council.
* @dev Only the Risk Council is able to action contract's functions.
* @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 GhoInterestRateStrategy is used when creating a new borrow rate strategy for GHO
*/
contract GhoStewardV2 is Ownable, IGhoStewardV2 {
using EnumerableSet for EnumerableSet.AddressSet;

/// @inheritdoc IGhoStewardV2
uint256 public constant GHO_BORROW_RATE_CHANGE_MAX = 0.0050e27; // 0.5%

/// @inheritdoc IGhoStewardV2
uint256 public constant GSM_FEE_RATE_CHANGE_MAX = 0.0050e4; // 0.5%

/// @inheritdoc IGhoStewardV2
uint256 public constant GHO_BORROW_RATE_MAX = 0.095e27; // 9.5%

/// @inheritdoc IGhoStewardV2
uint256 public constant MINIMUM_DELAY = 7 days;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
uint256 public constant MINIMUM_DELAY = 7 days;
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 RISK_COUNCIL;

uint40 internal _ghoBorrowRateLastUpdated;
mapping(address => uint40) _facilitatorsBucketCapacityTimelocks;
mapping(address => GsmDebounce) internal _gsmTimelocksByAddress;

mapping(address => bool) internal _controlledFacilitatorsByAddress;
EnumerableSet.AddressSet internal _controlledFacilitators;

mapping(uint256 => address) internal _ghoBorrowRateStrategiesByRate;
EnumerableSet.AddressSet internal _ghoBorrowRateStrategies;
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 notLocked(uint40 timelock) {
require(block.timestamp - timelock > MINIMUM_DELAY, 'DEBOUNCE_NOT_RESPECTED');
_;
}

/**
* @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 risk council
*/
constructor(address addressesProvider, address ghoToken, address riskCouncil, address executor) {
require(addressesProvider != address(0), 'INVALID_ADDRESSES_PROVIDER');
require(ghoToken != address(0), 'INVALID_GHO_TOKEN');
require(riskCouncil != address(0), 'INVALID_RISK_COUNCIL');
require(executor != address(0), 'INVALID_EXECUTOR');
POOL_ADDRESSES_PROVIDER = addressesProvider;
GHO_TOKEN = ghoToken;
RISK_COUNCIL = riskCouncil;
_transferOwnership(executor);
}

/// @inheritdoc IGhoStewardV2
function updateFacilitatorBucketCapacity(
address facilitator,
uint128 newBucketCapacity
) external onlyRiskCouncil notLocked(_facilitatorsBucketCapacityTimelocks[facilitator]) {
require(_controlledFacilitatorsByAddress[facilitator], 'FACILITATOR_NOT_IN_CONTROL');
(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 updateGhoBorrowRate(
uint256 newBorrowRate
) external onlyRiskCouncil notLocked(_ghoBorrowRateLastUpdated) {
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(
_isIncreaseLowerThanMax(currentBorrowRate, newBorrowRate, GHO_BORROW_RATE_CHANGE_MAX),
'INVALID_BORROW_RATE_UPDATE'
);
require(newBorrowRate <= GHO_BORROW_RATE_MAX, 'INVALID_BORROW_RATE_UPDATE');
address cachedStrategyAddress = _ghoBorrowRateStrategiesByRate[newBorrowRate];

if (cachedStrategyAddress == address(0)) {
GhoInterestRateStrategy newRateStrategy = new GhoInterestRateStrategy(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as i understand there's nothing really "GHO" specific about this ir. It's just a constant ir. Wouldn't it make more sense to externalize the ir registry/factory in a similar pattern as https://github.com/bgd-labs/aave-helpers/blob/master/src/v3-config-engine/V3RateStrategyFactory.sol ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GhoInterestRateStrategy offers a constant borrow rate. While the DefaultReserveInterestRateStrategy could also provide a constant rate, it adds unnecessary complexity in a less efficient way. We also prefer to keep this implementation as it is, to speed up the its deployment.

Copy link

@sakulstra sakulstra Feb 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My point was not about using DefaultReserveInterestRateStrategy but using a factory/registry that is detached from the GhoSteward. A ConstantInterestStrategyFactory of sorts.

This way it would be:

  • easier to upgrade the steward without having to redeploy the registry.
  • easier to reuse the constant IR for non GHO-assets if a use-case ever emerges.

The config engine has been upgraded 3 or 4 times by now and risk steward also has a pending v2.
As the IR-factory was externalized, there has never been an update needed for this part.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the implementation with a new contract FixedRateStrategyFactory which is a simplified version of the one you shared, using GhoInterestRateStrategy. I've used Fixed instead of Constant because i think it't more consistent with how we name in this repo.
Let me know what you think about this.

POOL_ADDRESSES_PROVIDER,
newBorrowRate
);
cachedStrategyAddress = address(newRateStrategy);

_ghoBorrowRateStrategiesByRate[newBorrowRate] = cachedStrategyAddress;
_ghoBorrowRateStrategies.add(cachedStrategyAddress);
}

_ghoBorrowRateLastUpdated = uint40(block.timestamp);

IPoolConfigurator(IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER).getPoolConfigurator())
.setReserveInterestRateStrategyAddress(GHO_TOKEN, cachedStrategyAddress);
}

/// @inheritdoc IGhoStewardV2
function updateGsmExposureCap(
address gsm,
uint128 newExposureCap
) external onlyRiskCouncil notLocked(_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 updateGsmFeeStrategy(
address gsm,
uint256 buyFee,
uint256 sellFee
) external onlyRiskCouncil notLocked(_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) &&
_isIncreaseLowerThanMax(currentSellFee, sellFee, GSM_FEE_RATE_CHANGE_MAX),
'INVALID_FEE_STRATEGY_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 getGhoBorrowRateTimelock() external view returns (uint40) {
return _ghoBorrowRateLastUpdated;
}

/// @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();
}

/// @inheritdoc IGhoStewardV2
function getGhoBorrowRateStrategies() external view returns (address[] memory) {
return _ghoBorrowRateStrategies.values();
}

/**
* @notice 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 if change is possitive and lower than max
*/
function _isIncreaseLowerThanMax(
uint256 from,
uint256 to,
uint256 max
) internal pure returns (bool) {
return to >= from && to - from <= max;
}
}
Loading
Loading