diff --git a/README.md b/README.md index 891d316..b38cc15 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Some assets/oracles can also be restricted on the RiskStewards by calling the `s With the introduction of Edge Risk Oracles by Chaos Labs, which leverages advanced off-chain infrastructure to deliver real-time risk updates to the Aave protocol via the Risk Oracle, the risk updates for the Aave protocol can be automated in a constrained manner. This can be done by combining the Edge Risk Oracle with the Aave Risk Steward, using a middleware contract `AaveStewardsInjector`. -The Aave Risk Steward contract used for automated updates (called now [EdgeRiskSteward](./src/contracts/EdgeRiskSteward.sol)), has been slightly modified to only allow Interest Rates Updates on the protocol initially as a matter of extra security considerations. +The Aave Risk Steward contract used for automated updates (called now [EdgeRiskStewardRates](./src/contracts/EdgeRiskStewardRates.sol)), has been slightly modified to only allow Interest Rates Updates on the protocol initially as a matter of extra security considerations. The following is a simple diagram of how the system works as a whole: diff --git a/scripts/RiskStewardsBase.s.sol b/scripts/RiskStewardsBase.s.sol index fabf42f..9bc0c9b 100644 --- a/scripts/RiskStewardsBase.s.sol +++ b/scripts/RiskStewardsBase.s.sol @@ -125,8 +125,7 @@ abstract contract RiskStewardsBase is ProtocolV3TestBase { vm.warp(block.timestamp + uint256(maxTimelock) + 1); } - if (generateDiffReport) - createConfigurationSnapshot(pre, POOL, true, true, false, false); + if (generateDiffReport) createConfigurationSnapshot(pre, POOL, true, true, false, false); if (capUpdates.length != 0) { callDatas[txCount] = abi.encodeWithSelector(IRiskSteward.updateCaps.selector, capUpdates); diff --git a/scripts/deploy/DeployCapInjector.s.sol b/scripts/deploy/DeployCapInjector.s.sol new file mode 100644 index 0000000..5cbce81 --- /dev/null +++ b/scripts/deploy/DeployCapInjector.s.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'solidity-utils/contracts/utils/ScriptUtils.sol'; +import {MiscArbitrum} from 'aave-address-book/MiscArbitrum.sol'; +import {AaveV3Arbitrum, AaveV3ArbitrumAssets} from 'aave-address-book/AaveV3Arbitrum.sol'; +import {GovernanceV3Arbitrum} from 'aave-address-book/GovernanceV3Arbitrum.sol'; +import {ICreate3Factory} from 'solidity-utils/contracts/create3/interfaces/ICreate3Factory.sol'; +import {IOwnable} from 'aave-address-book/common/IOwnable.sol'; +import {EdgeRiskStewardCaps, IRiskSteward, IPoolDataProvider, IEngine} from '../../src/contracts/EdgeRiskStewardCaps.sol'; +import {AaveStewardInjectorCaps} from '../../src/contracts/AaveStewardInjectorCaps.sol'; + +library DeployStewardContracts { + address constant EDGE_RISK_ORACLE = 0x861eeAdB55E41f161F31Acb1BFD4c70E3a964Aed; + + function _deployRiskStewards( + address poolDataProvider, + address configEngine, + address riskCouncil, + address governance + ) internal returns (address) { + address riskSteward = address( + new EdgeRiskStewardCaps( + IPoolDataProvider(poolDataProvider), + IEngine(configEngine), + riskCouncil, + _getRiskConfig() + ) + ); + IOwnable(riskSteward).transferOwnership(governance); + return riskSteward; + } + + function _deployCapsStewardInjector( + bytes32 salt, + address riskSteward, + address owner, + address guardian, + address[] memory whitelistedMarkets + ) internal returns (address) { + address stewardInjector = ICreate3Factory(MiscArbitrum.CREATE_3_FACTORY).create( + salt, + abi.encodePacked( + type(AaveStewardInjectorCaps).creationCode, + abi.encode(EDGE_RISK_ORACLE, riskSteward, msg.sender, guardian) + ) + ); + AaveStewardInjectorCaps(stewardInjector).addMarkets(whitelistedMarkets); + AaveStewardInjectorCaps(stewardInjector).transferOwnership(owner); + return stewardInjector; + } + + function _getRiskConfig() internal pure returns (IRiskSteward.Config memory) { + return + IRiskSteward.Config({ + ltv: IRiskSteward.RiskParamConfig({minDelay: 3 days, maxPercentChange: 25}), + liquidationThreshold: IRiskSteward.RiskParamConfig({ + minDelay: 3 days, + maxPercentChange: 25 + }), + liquidationBonus: IRiskSteward.RiskParamConfig({minDelay: 3 days, maxPercentChange: 50}), + supplyCap: IRiskSteward.RiskParamConfig({minDelay: 3 days, maxPercentChange: 30_00}), + borrowCap: IRiskSteward.RiskParamConfig({minDelay: 3 days, maxPercentChange: 30_00}), + debtCeiling: IRiskSteward.RiskParamConfig({minDelay: 3 days, maxPercentChange: 20_00}), + baseVariableBorrowRate: IRiskSteward.RiskParamConfig({ + minDelay: 3 days, + maxPercentChange: 50 + }), + variableRateSlope1: IRiskSteward.RiskParamConfig({minDelay: 3 days, maxPercentChange: 50}), + variableRateSlope2: IRiskSteward.RiskParamConfig({ + minDelay: 3 days, + maxPercentChange: 5_00 + }), + optimalUsageRatio: IRiskSteward.RiskParamConfig({minDelay: 3 days, maxPercentChange: 3_00}), + priceCapLst: IRiskSteward.RiskParamConfig({minDelay: 3 days, maxPercentChange: 5_00}), + priceCapStable: IRiskSteward.RiskParamConfig({minDelay: 3 days, maxPercentChange: 50}) + }); + } +} + +// make deploy-ledger contract=scripts/deploy/DeployCapInjector.s.sol:DeployArbitrum chain=arbitrum +contract DeployArbitrum is ArbitrumScript { + address constant GUARDIAN = 0x87dFb794364f2B117C8dbaE29EA622938b3Ce465; + + function run() external { + vm.startBroadcast(); + bytes32 salt = 'CapsStewardInjector'; + address predictedStewardsInjector = ICreate3Factory(MiscArbitrum.CREATE_3_FACTORY) + .predictAddress(msg.sender, salt); + + address riskSteward = DeployStewardContracts._deployRiskStewards( + address(AaveV3Arbitrum.AAVE_PROTOCOL_DATA_PROVIDER), + AaveV3Arbitrum.CONFIG_ENGINE, + predictedStewardsInjector, + GovernanceV3Arbitrum.EXECUTOR_LVL_1 + ); + + address[] memory whitelistedMarkets = new address[](15); + whitelistedMarkets[0] = AaveV3ArbitrumAssets.WETH_A_TOKEN; + whitelistedMarkets[1] = AaveV3ArbitrumAssets.USDC_A_TOKEN; + whitelistedMarkets[2] = AaveV3ArbitrumAssets.USDT_A_TOKEN; + whitelistedMarkets[3] = AaveV3ArbitrumAssets.WBTC_A_TOKEN; + whitelistedMarkets[4] = AaveV3ArbitrumAssets.DAI_A_TOKEN; + whitelistedMarkets[5] = AaveV3ArbitrumAssets.weETH_A_TOKEN; + whitelistedMarkets[6] = AaveV3ArbitrumAssets.ARB_A_TOKEN; + whitelistedMarkets[7] = AaveV3ArbitrumAssets.USDCn_A_TOKEN; + whitelistedMarkets[8] = AaveV3ArbitrumAssets.GHO_A_TOKEN; + whitelistedMarkets[9] = AaveV3ArbitrumAssets.LINK_A_TOKEN; + whitelistedMarkets[10] = AaveV3ArbitrumAssets.wstETH_A_TOKEN; + whitelistedMarkets[11] = AaveV3ArbitrumAssets.LUSD_A_TOKEN; + whitelistedMarkets[12] = AaveV3ArbitrumAssets.FRAX_A_TOKEN; + whitelistedMarkets[13] = AaveV3ArbitrumAssets.rETH_A_TOKEN; + whitelistedMarkets[14] = AaveV3ArbitrumAssets.AAVE_A_TOKEN; + + DeployStewardContracts._deployCapsStewardInjector( + salt, + riskSteward, + GovernanceV3Arbitrum.EXECUTOR_LVL_1, + GUARDIAN, + whitelistedMarkets + ); + vm.stopBroadcast(); + } +} diff --git a/scripts/deploy/DeployInjector.s.sol b/scripts/deploy/DeployInjector.s.sol deleted file mode 100644 index 594e324..0000000 --- a/scripts/deploy/DeployInjector.s.sol +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import 'solidity-utils/contracts/utils/ScriptUtils.sol'; -import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; -import {AaveV3EthereumLido, AaveV3EthereumLidoAssets} from 'aave-address-book/AaveV3EthereumLido.sol'; -import {GovernanceV3Ethereum} from 'aave-address-book/GovernanceV3Ethereum.sol'; -import {ICreate3Factory} from 'solidity-utils/contracts/create3/interfaces/ICreate3Factory.sol'; -import {IOwnable} from 'aave-address-book/common/IOwnable.sol'; -import {EdgeRiskSteward, IRiskSteward, IPoolDataProvider, IEngine} from '../../src/contracts/EdgeRiskSteward.sol'; -import {AaveStewardInjector, IAaveStewardInjector} from '../../src/contracts/AaveStewardInjector.sol'; - -library DeployStewardContracts { - address constant EDGE_RISK_ORACLE = 0x7ABB46C690C52E919687D19ebF89C81A6136C1F2; - - function _deployRiskStewards( - address poolDataProvider, - address configEngine, - address riskCouncil, - address governance - ) internal returns (address) { - address riskSteward = address(new EdgeRiskSteward( - IPoolDataProvider(poolDataProvider), - IEngine(configEngine), - riskCouncil, - _getRiskConfig() - )); - IOwnable(riskSteward).transferOwnership(governance); - return riskSteward; - } - - function _deployStewardsInjector( - bytes32 salt, - address riskSteward, - address guardian, - address whitelistedAsset - ) internal returns (address) { - address stewardInjector = ICreate3Factory(MiscEthereum.CREATE_3_FACTORY).create( - salt, - abi.encodePacked( - type(AaveStewardInjector).creationCode, - abi.encode( - EDGE_RISK_ORACLE, - riskSteward, - guardian, - whitelistedAsset - ) - ) - ); - return stewardInjector; - } - - function _getRiskConfig() internal pure returns (IRiskSteward.Config memory) { - return IRiskSteward.Config({ - ltv: IRiskSteward.RiskParamConfig({minDelay: 1 days, maxPercentChange: 25}), - liquidationThreshold: IRiskSteward.RiskParamConfig({minDelay: 1 days, maxPercentChange: 25}), - liquidationBonus: IRiskSteward.RiskParamConfig({minDelay: 1 days, maxPercentChange: 50}), - supplyCap: IRiskSteward.RiskParamConfig({minDelay: 1 days, maxPercentChange: 100_00}), - borrowCap: IRiskSteward.RiskParamConfig({minDelay: 1 days, maxPercentChange: 100_00}), - debtCeiling: IRiskSteward.RiskParamConfig({minDelay: 1 days, maxPercentChange: 20_00}), - baseVariableBorrowRate: IRiskSteward.RiskParamConfig({minDelay: 1 days, maxPercentChange: 50}), - variableRateSlope1: IRiskSteward.RiskParamConfig({minDelay: 1 days, maxPercentChange: 50}), - variableRateSlope2: IRiskSteward.RiskParamConfig({minDelay: 1 days, maxPercentChange: 5_00}), - optimalUsageRatio: IRiskSteward.RiskParamConfig({minDelay: 1 days, maxPercentChange: 3_00}), - priceCapLst: IRiskSteward.RiskParamConfig({minDelay: 1 days, maxPercentChange: 5_00}), - priceCapStable: IRiskSteward.RiskParamConfig({minDelay: 1 days, maxPercentChange: 50}) - }); - } -} - -// make deploy-ledger contract=scripts/deploy/DeployInjector.s.sol:DeployEthereumLido chain=mainnet -contract DeployEthereumLido is EthereumScript { - address constant GUARDIAN = 0xff37939808EcF199A2D599ef91D699Fb13dab7F7; - - function run() external { - vm.startBroadcast(); - bytes32 salt = 'StewardInjector'; - address predictedStewardsInjector = ICreate3Factory(MiscEthereum.CREATE_3_FACTORY).predictAddress(msg.sender, salt); - - address riskSteward = DeployStewardContracts._deployRiskStewards( - address(AaveV3EthereumLido.AAVE_PROTOCOL_DATA_PROVIDER), - AaveV3EthereumLido.CONFIG_ENGINE, - predictedStewardsInjector, - GovernanceV3Ethereum.EXECUTOR_LVL_1 - ); - - DeployStewardContracts._deployStewardsInjector(salt, riskSteward, GUARDIAN, AaveV3EthereumLidoAssets.WETH_UNDERLYING); - vm.stopBroadcast(); - } -} diff --git a/scripts/deploy/DeployRateInjector.s.sol b/scripts/deploy/DeployRateInjector.s.sol new file mode 100644 index 0000000..0fe52d1 --- /dev/null +++ b/scripts/deploy/DeployRateInjector.s.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'solidity-utils/contracts/utils/ScriptUtils.sol'; +import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; +import {AaveV3EthereumLido, AaveV3EthereumLidoAssets} from 'aave-address-book/AaveV3EthereumLido.sol'; +import {GovernanceV3Ethereum} from 'aave-address-book/GovernanceV3Ethereum.sol'; +import {ICreate3Factory} from 'solidity-utils/contracts/create3/interfaces/ICreate3Factory.sol'; +import {IOwnable} from 'aave-address-book/common/IOwnable.sol'; +import {EdgeRiskStewardRates, IRiskSteward, IPoolDataProvider, IEngine} from '../../src/contracts/EdgeRiskStewardRates.sol'; +import {AaveStewardInjectorRates} from '../../src/contracts/AaveStewardInjectorRates.sol'; + +library DeployStewardContracts { + address constant EDGE_RISK_ORACLE = 0x7ABB46C690C52E919687D19ebF89C81A6136C1F2; + + function _deployRiskStewards( + address poolDataProvider, + address configEngine, + address riskCouncil, + address governance + ) internal returns (address) { + address riskSteward = address( + new EdgeRiskStewardRates( + IPoolDataProvider(poolDataProvider), + IEngine(configEngine), + riskCouncil, + _getRiskConfig() + ) + ); + IOwnable(riskSteward).transferOwnership(governance); + return riskSteward; + } + + function _deployRatesStewardInjector( + bytes32 salt, + address riskSteward, + address owner, + address guardian, + address whitelistedAsset + ) internal returns (address) { + address stewardInjector = ICreate3Factory(MiscEthereum.CREATE_3_FACTORY).create( + salt, + abi.encodePacked( + type(AaveStewardInjectorRates).creationCode, + abi.encode(EDGE_RISK_ORACLE, riskSteward, owner, guardian, whitelistedAsset) + ) + ); + return stewardInjector; + } + + function _getRiskConfig() internal pure returns (IRiskSteward.Config memory) { + return + IRiskSteward.Config({ + ltv: IRiskSteward.RiskParamConfig({minDelay: 1 days, maxPercentChange: 25}), + liquidationThreshold: IRiskSteward.RiskParamConfig({ + minDelay: 1 days, + maxPercentChange: 25 + }), + liquidationBonus: IRiskSteward.RiskParamConfig({minDelay: 1 days, maxPercentChange: 50}), + supplyCap: IRiskSteward.RiskParamConfig({minDelay: 1 days, maxPercentChange: 100_00}), + borrowCap: IRiskSteward.RiskParamConfig({minDelay: 1 days, maxPercentChange: 100_00}), + debtCeiling: IRiskSteward.RiskParamConfig({minDelay: 1 days, maxPercentChange: 20_00}), + baseVariableBorrowRate: IRiskSteward.RiskParamConfig({ + minDelay: 1 days, + maxPercentChange: 50 + }), + variableRateSlope1: IRiskSteward.RiskParamConfig({minDelay: 1 days, maxPercentChange: 50}), + variableRateSlope2: IRiskSteward.RiskParamConfig({ + minDelay: 1 days, + maxPercentChange: 5_00 + }), + optimalUsageRatio: IRiskSteward.RiskParamConfig({minDelay: 1 days, maxPercentChange: 3_00}), + priceCapLst: IRiskSteward.RiskParamConfig({minDelay: 1 days, maxPercentChange: 5_00}), + priceCapStable: IRiskSteward.RiskParamConfig({minDelay: 1 days, maxPercentChange: 50}) + }); + } +} + +// make deploy-ledger contract=scripts/deploy/DeployRateInjector.s.sol:DeployEthereumLido chain=mainnet +contract DeployEthereumLido is EthereumScript { + address constant GUARDIAN = 0xff37939808EcF199A2D599ef91D699Fb13dab7F7; + + function run() external { + vm.startBroadcast(); + bytes32 salt = 'StewardInjector'; + address predictedStewardsInjector = ICreate3Factory(MiscEthereum.CREATE_3_FACTORY) + .predictAddress(msg.sender, salt); + + address riskSteward = DeployStewardContracts._deployRiskStewards( + address(AaveV3EthereumLido.AAVE_PROTOCOL_DATA_PROVIDER), + AaveV3EthereumLido.CONFIG_ENGINE, + predictedStewardsInjector, + GovernanceV3Ethereum.EXECUTOR_LVL_1 + ); + + DeployStewardContracts._deployRatesStewardInjector( + salt, + riskSteward, + GovernanceV3Ethereum.EXECUTOR_LVL_1, + GUARDIAN, + AaveV3EthereumLidoAssets.WETH_UNDERLYING + ); + vm.stopBroadcast(); + } +} diff --git a/src/contracts/AaveStewardInjectorBase.sol b/src/contracts/AaveStewardInjectorBase.sol new file mode 100644 index 0000000..613b044 --- /dev/null +++ b/src/contracts/AaveStewardInjectorBase.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IRiskOracle} from './dependencies/IRiskOracle.sol'; +import {IRiskSteward} from '../interfaces/IRiskSteward.sol'; +import {IAaveStewardInjectorBase} from '../interfaces/IAaveStewardInjectorBase.sol'; +import {AutomationCompatibleInterface} from './dependencies/AutomationCompatibleInterface.sol'; +import {OwnableWithGuardian} from 'solidity-utils/contracts/access-control/OwnableWithGuardian.sol'; + +/** + * @title AaveStewardInjectorBase + * @author BGD Labs + * @notice Base Injector contract to perform automation on risk steward using the edge risk oracle. + * @dev Aave chainlink automation-keeper-compatible contract to: + * - check if updates from edge risk oracles can be injected into risk steward. + * - injects updates on the risk steward if all conditions are met. + */ +abstract contract AaveStewardInjectorBase is + OwnableWithGuardian, + AutomationCompatibleInterface, + IAaveStewardInjectorBase +{ + /// @inheritdoc IAaveStewardInjectorBase + address public immutable RISK_ORACLE; + + /// @inheritdoc IAaveStewardInjectorBase + address public immutable RISK_STEWARD; + + /** + * @inheritdoc IAaveStewardInjectorBase + * @dev after an update is added on the risk oracle, the update is only valid from the timestamp it was added + * on the risk oracle plus the expiration time, after which the update cannot be injected into the risk steward. + */ + uint256 public constant EXPIRATION_PERIOD = 6 hours; + + mapping(uint256 => bool) internal _isUpdateIdExecuted; + mapping(uint256 => bool) internal _disabledUpdates; + bool internal _isPaused; + + /** + * @param riskOracle address of the edge risk oracle contract. + * @param riskSteward address of the risk steward contract. + * @param owner address of the owner of the stewards injector. + * @param guardian address of the guardian of the stewards injector. + */ + constructor( + address riskOracle, + address riskSteward, + address owner, + address guardian + ) OwnableWithGuardian(owner, guardian) { + RISK_ORACLE = riskOracle; + RISK_STEWARD = riskSteward; + } + + /** + * @inheritdoc AutomationCompatibleInterface + * @dev run off-chain, checks if the update from risk oracle should be injected on risk steward + */ + function checkUpkeep(bytes memory) public view virtual returns (bool, bytes memory); + + /** + * @inheritdoc AutomationCompatibleInterface + * @dev executes injection of update from the risk oracle into the risk steward. + */ + function performUpkeep(bytes calldata) external virtual; + + /// @inheritdoc IAaveStewardInjectorBase + function isDisabled(uint256 updateId) public view returns (bool) { + return _disabledUpdates[updateId]; + } + + /// @inheritdoc IAaveStewardInjectorBase + function disableUpdateById(uint256 updateId, bool disabled) external onlyOwnerOrGuardian { + _disabledUpdates[updateId] = disabled; + emit UpdateDisabled(updateId, disabled); + } + + /// @inheritdoc IAaveStewardInjectorBase + function pauseInjector(bool isPaused) external onlyOwnerOrGuardian { + _isPaused = isPaused; + emit InjectorPaused(isPaused); + } + + /// @inheritdoc IAaveStewardInjectorBase + function isInjectorPaused() public view returns (bool) { + return _isPaused; + } + + /// @inheritdoc IAaveStewardInjectorBase + function isUpdateIdExecuted(uint256 updateId) public view returns (bool) { + return _isUpdateIdExecuted[updateId]; + } +} diff --git a/src/contracts/AaveStewardInjectorCaps.sol b/src/contracts/AaveStewardInjectorCaps.sol new file mode 100644 index 0000000..7554ff4 --- /dev/null +++ b/src/contracts/AaveStewardInjectorCaps.sol @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IRiskOracle} from './dependencies/IRiskOracle.sol'; +import {IRiskSteward} from '../interfaces/IRiskSteward.sol'; +import {IAaveStewardInjectorCaps} from '../interfaces/IAaveStewardInjectorCaps.sol'; +import {AaveStewardInjectorBase} from './AaveStewardInjectorBase.sol'; +import {IAaveV3ConfigEngine as IEngine} from 'aave-v3-origin/src/contracts/extensions/v3-config-engine/IAaveV3ConfigEngine.sol'; +import {EngineFlags} from 'aave-v3-origin/src/contracts/extensions/v3-config-engine/EngineFlags.sol'; +import {EnumerableSet} from 'openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sol'; +import {Strings} from 'openzeppelin-contracts/contracts/utils/Strings.sol'; +import {IAToken} from 'aave-v3-origin/src/contracts/interfaces/IAToken.sol'; +import {IERC20Metadata} from 'openzeppelin-contracts/contracts/interfaces/IERC20Metadata.sol'; + +/** + * @title AaveStewardInjectorCaps + * @author BGD Labs + * @notice Aave chainlink automation-keeper-compatible contract to perform caps update injection + * on risk steward using the edge risk oracle. + */ +contract AaveStewardInjectorCaps is AaveStewardInjectorBase, IAaveStewardInjectorCaps { + using Strings for string; + using EnumerableSet for EnumerableSet.AddressSet; + + EnumerableSet.AddressSet internal _markets; + + /** + * @param riskOracle address of the edge risk oracle contract. + * @param riskSteward address of the risk steward contract. + * @param owner address of the owner of the stewards injector. + * @param guardian address of the guardian of the stewards injector. + */ + constructor( + address riskOracle, + address riskSteward, + address owner, + address guardian + ) AaveStewardInjectorBase(riskOracle, riskSteward, owner, guardian) {} + + /// @inheritdoc AaveStewardInjectorBase + function checkUpkeep(bytes memory) public view virtual override returns (bool, bytes memory) { + address[] memory markets = getMarkets(); + string[] memory updateTypes = getUpdateTypes(); + + ActionData[] memory actions = new ActionData[](markets.length * updateTypes.length); + uint256 actionCount; + + for (uint256 i = 0; i < markets.length; i++) { + for (uint256 j = 0; j < updateTypes.length; j++) { + address market = markets[i]; + string memory updateType = updateTypes[j]; + + try + IRiskOracle(RISK_ORACLE).getLatestUpdateByParameterAndMarket(updateType, market) + returns (IRiskOracle.RiskParameterUpdate memory updateRiskParams) { + if (_canUpdateBeInjected(updateRiskParams)) { + actions[actionCount] = ActionData({market: market, updateType: updateType}); + actionCount++; + } + } catch {} + } + } + + if (actionCount > 0) return (true, abi.encode(_getRandomizedAction(actions, actionCount))); + return (false, ''); + } + + /// @inheritdoc AaveStewardInjectorBase + function performUpkeep(bytes calldata performData) external override { + ActionData memory actionData = abi.decode(performData, (ActionData)); + + IRiskOracle.RiskParameterUpdate memory updateRiskParams = IRiskOracle(RISK_ORACLE) + .getLatestUpdateByParameterAndMarket(actionData.updateType, actionData.market); + + if (!_canUpdateBeInjected(updateRiskParams)) { + revert UpdateCannotBeInjected(); + } + + IRiskSteward(RISK_STEWARD).updateCaps(_repackCapUpdate(updateRiskParams)); + _isUpdateIdExecuted[updateRiskParams.updateId] = true; + + emit ActionSucceeded(updateRiskParams.updateId); + } + + /// @inheritdoc IAaveStewardInjectorCaps + function getMarkets() public view returns (address[] memory) { + return _markets.values(); + } + + /// @inheritdoc IAaveStewardInjectorCaps + function addMarkets(address[] calldata markets) external onlyOwner { + for (uint256 i = 0; i < markets.length; i++) { + _markets.add(markets[i]); + emit MarketAdded(markets[i]); + } + } + + /// @inheritdoc IAaveStewardInjectorCaps + function removeMarkets(address[] calldata markets) external onlyOwner { + for (uint256 i = 0; i < markets.length; i++) { + _markets.remove(markets[i]); + emit MarketRemoved(markets[i]); + } + } + + /// @inheritdoc IAaveStewardInjectorCaps + function getUpdateTypes() public pure returns (string[] memory updateTypes) { + updateTypes = new string[](2); + updateTypes[0] = 'supplyCap'; + updateTypes[1] = 'borrowCap'; + } + + /** + * @notice method to check if the update from risk oracle could be injected into the risk steward. + * @dev only allow injecting cap updates for the configured assets i.e aToken addresses. + * @param updateRiskParams struct containing the risk param update from the risk oracle to check if it can be injected. + * @return true if the update could be injected to the risk steward, false otherwise. + */ + function _canUpdateBeInjected( + IRiskOracle.RiskParameterUpdate memory updateRiskParams + ) internal view returns (bool) { + return (!isUpdateIdExecuted(updateRiskParams.updateId) && + (updateRiskParams.timestamp + EXPIRATION_PERIOD > block.timestamp) && + _markets.contains(updateRiskParams.market) && + (updateRiskParams.updateType.equal('supplyCap') || + updateRiskParams.updateType.equal('borrowCap')) && + !isDisabled(updateRiskParams.updateId) && + !isInjectorPaused()); + } + + /** + * @notice method to repack update params from the risk oracle to the format of risk steward. + * @param riskParams the risk update param from the edge risk oracle. + * @return capUpdate the repacked caps update in the format of the risk steward. + */ + function _repackCapUpdate( + IRiskOracle.RiskParameterUpdate memory riskParams + ) internal view returns (IEngine.CapsUpdate[] memory capUpdate) { + address underlyingAddress = IAToken(riskParams.market).UNDERLYING_ASSET_ADDRESS(); + uint256 capValue = abi.decode( + abi.encodePacked(new bytes(32 - riskParams.newValue.length), riskParams.newValue), + (uint256) + ) / (10 ** IERC20Metadata(riskParams.market).decimals()); + + capUpdate = new IEngine.CapsUpdate[](1); + if (riskParams.updateType.equal('supplyCap')) { + capUpdate[0] = IEngine.CapsUpdate({ + asset: underlyingAddress, + supplyCap: capValue, + borrowCap: EngineFlags.KEEP_CURRENT + }); + } else { + capUpdate[0] = IEngine.CapsUpdate({ + asset: underlyingAddress, + supplyCap: EngineFlags.KEEP_CURRENT, + borrowCap: capValue + }); + } + } + + /** + * @notice method to select a randomized action from a list of actions. + * @param actions the list of actions from where we select a randomized action. + * @param actionCount the count of actions. + * @return action the randomized action from the actions list. + */ + function _getRandomizedAction( + ActionData[] memory actions, + uint256 actionCount + ) internal view returns (ActionData memory action) { + uint256 randomNumber = uint256( + keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)) + ); + action = actions[randomNumber % actionCount]; + } +} diff --git a/src/contracts/AaveStewardInjector.sol b/src/contracts/AaveStewardInjectorRates.sol similarity index 55% rename from src/contracts/AaveStewardInjector.sol rename to src/contracts/AaveStewardInjectorRates.sol index a4ae67f..ba1b61c 100644 --- a/src/contracts/AaveStewardInjector.sol +++ b/src/contracts/AaveStewardInjectorRates.sol @@ -3,63 +3,51 @@ pragma solidity ^0.8.0; import {IRiskOracle} from './dependencies/IRiskOracle.sol'; import {IRiskSteward} from '../interfaces/IRiskSteward.sol'; -import {IAaveStewardInjector, AutomationCompatibleInterface} from '../interfaces/IAaveStewardInjector.sol'; +import {IAaveStewardInjectorRates} from '../interfaces/IAaveStewardInjectorRates.sol'; +import {AaveStewardInjectorBase} from './AaveStewardInjectorBase.sol'; import {IAaveV3ConfigEngine as IEngine} from 'aave-v3-origin/src/contracts/extensions/v3-config-engine/IAaveV3ConfigEngine.sol'; -import {Ownable} from 'openzeppelin-contracts/contracts/access/Ownable.sol'; +import {Strings} from 'openzeppelin-contracts/contracts/utils/Strings.sol'; +import {OwnableWithGuardian} from 'solidity-utils/contracts/access-control/OwnableWithGuardian.sol'; /** - * @title AaveStewardInjector + * @title AaveStewardInjectorRates * @author BGD Labs - * @notice Contract to perform automation on risk steward using the edge risk oracle. - * The contract only permits for injecting rate updates for the whitelisted asset. - * @dev Aave chainlink automation-keeper-compatible contract to: - * - check if updates from edge risk oracles can be injected into risk steward. - * - injectes risk updates on the risk steward if all conditions are met. + * @notice Aave chainlink automation-keeper-compatible contract to perform interest rate update injection + * on risk steward using the edge risk oracle. */ -contract AaveStewardInjector is Ownable, IAaveStewardInjector { - /// @inheritdoc IAaveStewardInjector - address public immutable RISK_ORACLE; - - /// @inheritdoc IAaveStewardInjector - address public immutable RISK_STEWARD; - - /// @inheritdoc IAaveStewardInjector +contract AaveStewardInjectorRates is + OwnableWithGuardian, + AaveStewardInjectorBase, + IAaveStewardInjectorRates +{ + using Strings for string; + + /// @inheritdoc IAaveStewardInjectorRates address public immutable WHITELISTED_ASSET; - /// @inheritdoc IAaveStewardInjector + /// @inheritdoc IAaveStewardInjectorRates string public constant WHITELISTED_UPDATE_TYPE = 'RateStrategyUpdate'; - /** - * @inheritdoc IAaveStewardInjector - * @dev after an update is added on the risk oracle, the update is only valid from the timestamp it was added - * on the risk oracle plus the expiration time, after which the update cannot be injected into the risk steward. - */ - uint256 public constant EXPIRATION_PERIOD = 6 hours; - - mapping(uint256 => bool) internal _isUpdateIdExecuted; - mapping(uint256 => bool) internal _disabledUpdates; - /** * @param riskOracle address of the edge risk oracle contract. * @param riskSteward address of the risk steward contract. - * @param guardian address of the guardian / owner of the stewards injector. + * @param owner address of the owner of the stewards injector. + * @param guardian address of the guardian of the stewards injector. * @param whitelistedAsset address of the whitelisted asset for which update can be injected. */ constructor( address riskOracle, address riskSteward, + address owner, address guardian, address whitelistedAsset - ) Ownable(guardian) { + ) AaveStewardInjectorBase(riskOracle, riskSteward, owner, guardian) { RISK_ORACLE = riskOracle; RISK_STEWARD = riskSteward; WHITELISTED_ASSET = whitelistedAsset; } - /** - * @inheritdoc AutomationCompatibleInterface - * @dev run off-chain, checks if the latest update from risk oracle should be injected on risk steward - */ + /// @inheritdoc AaveStewardInjectorBase function checkUpkeep(bytes memory) public view virtual override returns (bool, bytes memory) { IRiskOracle.RiskParameterUpdate memory updateRiskParams = IRiskOracle(RISK_ORACLE) .getLatestUpdateByParameterAndMarket(WHITELISTED_UPDATE_TYPE, WHITELISTED_ASSET); @@ -69,10 +57,7 @@ contract AaveStewardInjector is Ownable, IAaveStewardInjector { return (false, ''); } - /** - * @inheritdoc AutomationCompatibleInterface - * @dev executes injection of the latest update from the risk oracle into the risk steward. - */ + /// @inheritdoc AaveStewardInjectorBase function performUpkeep(bytes calldata) external override { IRiskOracle.RiskParameterUpdate memory updateRiskParams = IRiskOracle(RISK_ORACLE) .getLatestUpdateByParameterAndMarket(WHITELISTED_UPDATE_TYPE, WHITELISTED_ASSET); @@ -87,26 +72,10 @@ contract AaveStewardInjector is Ownable, IAaveStewardInjector { emit ActionSucceeded(updateRiskParams.updateId); } - /// @inheritdoc IAaveStewardInjector - function isDisabled(uint256 updateId) public view returns (bool) { - return _disabledUpdates[updateId]; - } - - /// @inheritdoc IAaveStewardInjector - function disableUpdateById(uint256 updateId, bool disabled) external onlyOwner { - _disabledUpdates[updateId] = disabled; - emit UpdateDisabled(updateId, disabled); - } - - /// @inheritdoc IAaveStewardInjector - function isUpdateIdExecuted(uint256 updateid) public view returns (bool) { - return _isUpdateIdExecuted[updateid]; - } - /** * @notice method to check if the update from risk oracle could be injected into the risk steward. * @dev only allow injecting interest rate updates for the whitelisted asset. - * @param updateRiskParams struct containing the risk param update from the risk oralce to check if it can be injected. + * @param updateRiskParams struct containing the risk param update from the risk oracle to check if it can be injected. * @return true if the update could be injected to the risk steward, false otherwise. */ function _canUpdateBeInjected( @@ -115,8 +84,9 @@ contract AaveStewardInjector is Ownable, IAaveStewardInjector { return (!isUpdateIdExecuted(updateRiskParams.updateId) && (updateRiskParams.timestamp + EXPIRATION_PERIOD > block.timestamp) && updateRiskParams.market == WHITELISTED_ASSET && - keccak256(bytes(updateRiskParams.updateType)) == keccak256(bytes(WHITELISTED_UPDATE_TYPE)) && - !isDisabled(updateRiskParams.updateId)); + updateRiskParams.updateType.equal(WHITELISTED_UPDATE_TYPE) && + !isDisabled(updateRiskParams.updateId) && + !isInjectorPaused()); } /** diff --git a/src/contracts/EdgeRiskStewardCaps.sol b/src/contracts/EdgeRiskStewardCaps.sol new file mode 100644 index 0000000..9a17487 --- /dev/null +++ b/src/contracts/EdgeRiskStewardCaps.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import './RiskSteward.sol'; + +/** + * @title EdgeRiskStewardCaps + * @author BGD labs + * @notice Contract to manage the supply / borrow caps within configured bound on aave v3 pool. + * To be triggered by the Aave Steward Injector Contract in a automated way via the Edge Risk Oracle. + */ +contract EdgeRiskStewardCaps is RiskSteward { + /** + * @param poolDataProvider The pool data provider of the pool to be controlled by the steward + * @param engine the config engine to be used by the steward + * @param riskCouncil the safe address of the council being able to interact with the steward + * @param riskConfig the risk configuration to setup for each individual risk param + */ + constructor( + IPoolDataProvider poolDataProvider, + IEngine engine, + address riskCouncil, + Config memory riskConfig + ) RiskSteward(poolDataProvider, engine, riskCouncil, riskConfig) {} + + /// @inheritdoc IRiskSteward + function updateRates( + IEngine.RateStrategyUpdate[] calldata + ) external virtual override onlyRiskCouncil { + revert UpdateNotAllowed(); + } + + /// @inheritdoc IRiskSteward + function updateCollateralSide( + IEngine.CollateralUpdate[] calldata + ) external virtual override onlyRiskCouncil { + revert UpdateNotAllowed(); + } + + /// @inheritdoc IRiskSteward + function updateLstPriceCaps( + PriceCapLstUpdate[] calldata + ) external virtual override onlyRiskCouncil { + revert UpdateNotAllowed(); + } + + /// @inheritdoc IRiskSteward + function updateStablePriceCaps( + PriceCapStableUpdate[] calldata + ) external virtual override onlyRiskCouncil { + revert UpdateNotAllowed(); + } +} diff --git a/src/contracts/EdgeRiskSteward.sol b/src/contracts/EdgeRiskStewardRates.sol similarity index 95% rename from src/contracts/EdgeRiskSteward.sol rename to src/contracts/EdgeRiskStewardRates.sol index a63f65b..924f220 100644 --- a/src/contracts/EdgeRiskSteward.sol +++ b/src/contracts/EdgeRiskStewardRates.sol @@ -4,12 +4,12 @@ pragma solidity ^0.8.0; import './RiskSteward.sol'; /** - * @title EdgeRiskSteward + * @title EdgeRiskStewardRates * @author BGD labs * @notice Contract to manage the interest rates params within configured bound on aave v3 pool. * To be triggered by the Aave Steward Injector Contract in a automated way via the Edge Risk Oracle. */ -contract EdgeRiskSteward is RiskSteward { +contract EdgeRiskStewardRates is RiskSteward { /** * @param poolDataProvider The pool data provider of the pool to be controlled by the steward * @param engine the config engine to be used by the steward diff --git a/src/interfaces/IAaveStewardInjector.sol b/src/interfaces/IAaveStewardInjectorBase.sol similarity index 71% rename from src/interfaces/IAaveStewardInjector.sol rename to src/interfaces/IAaveStewardInjectorBase.sol index bbd683c..30d46b6 100644 --- a/src/interfaces/IAaveStewardInjector.sol +++ b/src/interfaces/IAaveStewardInjectorBase.sol @@ -1,14 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {AutomationCompatibleInterface} from '../contracts/dependencies/AutomationCompatibleInterface.sol'; - /** - * @title IAaveStewardInjector + * @title IAaveStewardInjectorBase * @author BGD Labs - * @notice Defines the interface for the injector contract to automate actions for Risk Steward. + * @notice Defines the interface for the base injector contract to automate actions for Risk Steward. **/ -interface IAaveStewardInjector is AutomationCompatibleInterface { +interface IAaveStewardInjectorBase { /** * @notice Emitted when performUpkeep is called and an update is injected into the risk steward. * @param updateId the risk oracle update id injected into the risk steward. @@ -22,6 +20,12 @@ interface IAaveStewardInjector is AutomationCompatibleInterface { */ event UpdateDisabled(uint256 indexed updateId, bool indexed disabled); + /** + * @notice Emitted when the injector is paused/unpaused. + * @param isPaused true if the injector is being paused, false otherwise. + */ + event InjectorPaused(bool indexed isPaused); + /** * @notice The following update cannot be injected in the steward injector because the conditions are not met. */ @@ -43,10 +47,22 @@ interface IAaveStewardInjector is AutomationCompatibleInterface { /** * @notice method to check if the updateId from the risk oracle has been executed/injected into the risk steward. - * @param updateid the updateId from the risk oracle to check if already executed/injected. + * @param updateId the updateId from the risk oracle to check if already executed/injected. * @return bool true if the updateId is executed/injected, false otherwise. */ - function isUpdateIdExecuted(uint256 updateid) external view returns (bool); + function isUpdateIdExecuted(uint256 updateId) external view returns (bool); + + /** + * @notice method called by owner to pause/unpause the injector. + * @param isPaused true if the injector is being paused, false otherwise. + */ + function pauseInjector(bool isPaused) external; + + /** + * @notice method to check if the injector is paused. + * @return true if the injector is paused, false otherwise. + */ + function isInjectorPaused() external view returns (bool); /** * @notice method to get the address of the edge risk oracle contract. @@ -65,16 +81,4 @@ interface IAaveStewardInjector is AutomationCompatibleInterface { * @return time in seconds of the expiration time. */ function EXPIRATION_PERIOD() external view returns (uint256); - - /** - * @notice method to get the whitelisted update type for which injection is allowed from the risk oracle into the stewards. - * @return string for the whitelisted update type - interest rate update. - */ - function WHITELISTED_UPDATE_TYPE() external view returns (string memory); - - /** - * @notice method to get the whitelisted asset for which injection is allowed from the risk oracle into the stewards. - * @return address for the whitelisted asset. - */ - function WHITELISTED_ASSET() external view returns (address); } diff --git a/src/interfaces/IAaveStewardInjectorCaps.sol b/src/interfaces/IAaveStewardInjectorCaps.sol new file mode 100644 index 0000000..4c02376 --- /dev/null +++ b/src/interfaces/IAaveStewardInjectorCaps.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IAaveStewardInjectorBase} from './IAaveStewardInjectorBase.sol'; + +/** + * @title IAaveStewardInjectorCaps + * @author BGD Labs + * @notice Defines the interface for the injector contract to automate caps updates on Risk Steward. + **/ +interface IAaveStewardInjectorCaps is IAaveStewardInjectorBase { + /** + * @notice Emitted when a market is whitelisted. + * @param market the address of the market i.e aToken address. + */ + event MarketAdded(address indexed market); + + /** + * @notice Emitted when a market is removed from the whitelist. + * @param market the address of the market i.e aToken address. + */ + event MarketRemoved(address indexed market); + + /** + * @notice struct holding action for which update can be performed. + * @param market aToken address for which action needs to be performed. + * @param updateType updateType for which action needs to be performed. + */ + struct ActionData { + address market; + string updateType; + } + + /** + * @notice method to get the whitelisted markets / aToken addresses on the injector. + * @return array of whitelisted markets / aToken addresses. + */ + function getMarkets() external view returns (address[] memory); + + /** + * @notice method called by the owner to whitelist markets on the injector. + * @param markets array of aToken addresses to whitelist. + */ + function addMarkets(address[] calldata markets) external; + + /** + * @notice method called by the owner to remove whitelisted markets on the injector. + * @param markets array of aToken addresses to remove from whitelist. + */ + function removeMarkets(address[] calldata markets) external; + + /** + * @notice method to get all the valid update types. + * @param updateTypes array of updateTypes. + */ + function getUpdateTypes() external pure returns (string[] memory updateTypes); +} diff --git a/src/interfaces/IAaveStewardInjectorRates.sol b/src/interfaces/IAaveStewardInjectorRates.sol new file mode 100644 index 0000000..50261a6 --- /dev/null +++ b/src/interfaces/IAaveStewardInjectorRates.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IAaveStewardInjectorBase} from './IAaveStewardInjectorBase.sol'; + +/** + * @title IAaveStewardInjectorRates + * @author BGD Labs + * @notice Defines the interface for the injector contract to automate rate updates on Risk Steward. + **/ +interface IAaveStewardInjectorRates is IAaveStewardInjectorBase { + /** + * @notice method to get the whitelisted update type for which injection is allowed from the risk oracle into the stewards. + * @return string for the whitelisted update type - interest rate update. + */ + function WHITELISTED_UPDATE_TYPE() external view returns (string memory); + + /** + * @notice method to get the whitelisted asset for which injection is allowed from the risk oracle into the stewards. + * @return address for the whitelisted asset. + */ + function WHITELISTED_ASSET() external view returns (address); +} diff --git a/tests/AaveStewardsInjector.t.sol b/tests/AaveStewardsInjector.t.sol deleted file mode 100644 index 9d8bfa3..0000000 --- a/tests/AaveStewardsInjector.t.sol +++ /dev/null @@ -1,245 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -import {RiskSteward, IRiskSteward, IEngine, EngineFlags} from 'src/contracts/RiskSteward.sol'; -import {TestnetProcedures} from 'aave-v3-origin/tests/utils/TestnetProcedures.sol'; -import {RiskOracle} from '../src/contracts/dependencies/RiskOracle.sol'; -import {AaveStewardInjector, IAaveStewardInjector} from '../src/contracts/AaveStewardInjector.sol'; -import {AaveV3EthereumLidoAssets} from 'aave-address-book/AaveV3EthereumLido.sol'; -import {Ownable} from 'openzeppelin-contracts/contracts/access/Ownable.sol'; - -contract AaveStewardsInjector_Test is TestnetProcedures { - RiskSteward _riskSteward; - RiskOracle _riskOracle; - AaveStewardInjector _stewardInjector; - - address _riskOracleOwner = address(20); - address _stewardsInjectorOwner = address(25); - - event ActionSucceeded(uint256 indexed updateId); - event AddressWhitelisted(address indexed contractAddress, bool indexed isWhitelisted); - event UpdateDisabled(uint256 indexed updateId, bool indexed disabled); - event UpdateTypeChanged(string indexed updateType, bool indexed isValid); - - function setUp() public { - initTestEnvironment(); - - IRiskSteward.RiskParamConfig memory defaultRiskParamConfig = IRiskSteward.RiskParamConfig({ - minDelay: 3 days, - maxPercentChange: 5_00 // 5% - }); - - IRiskSteward.Config memory riskConfig = IRiskSteward.Config({ - ltv: defaultRiskParamConfig, - liquidationThreshold: defaultRiskParamConfig, - liquidationBonus: defaultRiskParamConfig, - supplyCap: defaultRiskParamConfig, - borrowCap: defaultRiskParamConfig, - debtCeiling: defaultRiskParamConfig, - baseVariableBorrowRate: defaultRiskParamConfig, - variableRateSlope1: defaultRiskParamConfig, - variableRateSlope2: defaultRiskParamConfig, - optimalUsageRatio: defaultRiskParamConfig, - priceCapLst: defaultRiskParamConfig, - priceCapStable: defaultRiskParamConfig - }); - - // setup risk oracle - vm.startPrank(_riskOracleOwner); - address[] memory initialSenders = new address[](1); - initialSenders[0] = _riskOracleOwner; - string[] memory initialUpdateTypes = new string[](1); - initialUpdateTypes[0] = 'RateStrategyUpdate'; - - _riskOracle = new RiskOracle('RiskOracle', initialSenders, initialUpdateTypes); - vm.stopPrank(); - - // setup steward injector - vm.startPrank(_stewardsInjectorOwner); - - address computedRiskStewardAddress = vm.computeCreateAddress( - _stewardsInjectorOwner, - vm.getNonce(_stewardsInjectorOwner) + 1 - ); - _stewardInjector = new AaveStewardInjector( - address(_riskOracle), - address(computedRiskStewardAddress), - _stewardsInjectorOwner, - address(weth) - ); - - // setup risk steward - _riskSteward = new RiskSteward( - contracts.protocolDataProvider, - IEngine(report.configEngine), - address(_stewardInjector), - riskConfig - ); - - vm.assertEq(computedRiskStewardAddress, address(_riskSteward)); - vm.stopPrank(); - - vm.startPrank(poolAdmin); - contracts.aclManager.addRiskAdmin(address(_riskSteward)); - vm.stopPrank(); - - vm.warp(5 days); - } - - function test_rateInjection() public { - // add rate update to risk oracle - _addUpdateToRiskOracle(); - - vm.expectEmit(address(_stewardInjector)); - emit ActionSucceeded(1); - - bool isAutomationPerformed = _checkAndPerformAutomation(); - assertTrue(isAutomationPerformed); - } - - function test_disableUpdate() public { - // add rate update to risk oracle - _addUpdateToRiskOracle(); - - vm.prank(address(1)); - vm.expectRevert( - abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(1)) - ); - _stewardInjector.disableUpdateById(1, true); - - assertFalse(_stewardInjector.isDisabled(1)); - - vm.expectEmit(address(_stewardInjector)); - emit UpdateDisabled(1, true); - - vm.prank(_stewardsInjectorOwner); - _stewardInjector.disableUpdateById(1, true); - - assertTrue(_stewardInjector.isDisabled(1)); - - bool isAutomationPerformed = _checkAndPerformAutomation(); - assertFalse(isAutomationPerformed); - - vm.expectEmit(address(_stewardInjector)); - emit UpdateDisabled(1, false); - - vm.prank(_stewardsInjectorOwner); - _stewardInjector.disableUpdateById(1, false); - - assertFalse(_stewardInjector.isDisabled(1)); - - isAutomationPerformed = _checkAndPerformAutomation(); - assertTrue(isAutomationPerformed); - } - - function test_isUpdatedIdExecuted() public { - // add rate update to risk oracle - _addUpdateToRiskOracle(); - - assertFalse(_stewardInjector.isUpdateIdExecuted(1)); - - bool isAutomationPerformed = _checkAndPerformAutomation(); - assertTrue(isAutomationPerformed); - assertTrue(_stewardInjector.isUpdateIdExecuted(1)); - - isAutomationPerformed = _checkAndPerformAutomation(); - assertFalse(isAutomationPerformed); - } - - function test_expiredUpdate() public { - // add rate update to risk oracle - _addUpdateToRiskOracle(); - - uint256 initialTs = block.timestamp; - vm.warp(initialTs + _stewardInjector.EXPIRATION_PERIOD()); - - bool isAutomationPerformed = _checkAndPerformAutomation(); - assertFalse(isAutomationPerformed); - - vm.warp(initialTs); - isAutomationPerformed = _checkAndPerformAutomation(); - assertTrue(isAutomationPerformed); - } - - function test_reverts_sameUpdateInjectedTwice() public { - _addUpdateToRiskOracle( - EngineFlags.KEEP_CURRENT, - 5_00, - EngineFlags.KEEP_CURRENT, - EngineFlags.KEEP_CURRENT, - block.timestamp - 100 - ); // updateId 1 - - vm.expectEmit(address(_stewardInjector)); - emit ActionSucceeded(1); - - bool isAutomationPerformed = _checkAndPerformAutomation(); - assertTrue(isAutomationPerformed); - - vm.expectRevert(IAaveStewardInjector.UpdateCannotBeInjected.selector); - _stewardInjector.performUpkeep(''); - } - - function test_reverts_ifUpdateDoesNotExist() public { - vm.expectRevert(bytes('No update found for the specified parameter and market.')); - _stewardInjector.checkUpkeep(''); - - vm.expectRevert(bytes('No update found for the specified parameter and market.')); - _stewardInjector.performUpkeep(''); - } - - function _addUpdateToRiskOracle( - uint256 optimalUsageRatio, - uint256 baseVariableBorrowRate, - uint256 variableRateSlope1, - uint256 variableRateSlope2, - uint256 updateTimestamp - ) internal { - uint256 currentTs = block.timestamp; - vm.startPrank(_riskOracleOwner); - vm.warp(updateTimestamp); - - IEngine.InterestRateInputData memory rate = IEngine.InterestRateInputData({ - optimalUsageRatio: optimalUsageRatio, - baseVariableBorrowRate: baseVariableBorrowRate, - variableRateSlope1: variableRateSlope1, - variableRateSlope2: variableRateSlope2 - }); - _riskOracle.publishRiskParameterUpdate( - 'referenceId', - abi.encode(rate), - 'RateStrategyUpdate', - address(weth), - 'additionalData' - ); - vm.warp(currentTs); - vm.stopPrank(); - } - - function _addUpdateToRiskOracle() internal { - vm.startPrank(_riskOracleOwner); - - IEngine.InterestRateInputData memory rate = IEngine.InterestRateInputData({ - optimalUsageRatio: EngineFlags.KEEP_CURRENT, - baseVariableBorrowRate: 5_00, - variableRateSlope1: EngineFlags.KEEP_CURRENT, - variableRateSlope2: EngineFlags.KEEP_CURRENT - }); - _riskOracle.publishRiskParameterUpdate( - 'referenceId', - abi.encode(rate), - 'RateStrategyUpdate', - address(weth), - 'additionalData' - ); - vm.stopPrank(); - } - - function _checkAndPerformAutomation() internal virtual returns (bool) { - (bool shouldRunKeeper, bytes memory performData) = _stewardInjector.checkUpkeep(''); - if (shouldRunKeeper) { - _stewardInjector.performUpkeep(performData); - } - return shouldRunKeeper; - } -} diff --git a/tests/AaveStewardsInjectorBase.t.sol b/tests/AaveStewardsInjectorBase.t.sol new file mode 100644 index 0000000..51a57e9 --- /dev/null +++ b/tests/AaveStewardsInjectorBase.t.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {RiskSteward, IRiskSteward, IEngine, EngineFlags} from 'src/contracts/RiskSteward.sol'; +import {TestnetProcedures} from 'aave-v3-origin/tests/utils/TestnetProcedures.sol'; +import {RiskOracle} from '../src/contracts/dependencies/RiskOracle.sol'; +import {AaveStewardInjectorBase, OwnableWithGuardian, IAaveStewardInjectorBase} from '../src/contracts/AaveStewardInjectorBase.sol'; +import {IWithGuardian} from 'solidity-utils/contracts/access-control/interfaces/IWithGuardian.sol'; +import {Ownable} from 'openzeppelin-contracts/contracts/access/Ownable.sol'; + +abstract contract AaveStewardsInjectorBaseTest is TestnetProcedures { + RiskSteward _riskSteward; + RiskOracle _riskOracle; + AaveStewardInjectorBase _stewardInjector; + + address _riskOracleOwner = address(20); + address _stewardsInjectorOwner = address(25); + address _stewardsInjectorGuardian = address(30); + + event ActionSucceeded(uint256 indexed updateId); + event AddressWhitelisted(address indexed contractAddress, bool indexed isWhitelisted); + event UpdateDisabled(uint256 indexed updateId, bool indexed disabled); + event UpdateTypeChanged(string indexed updateType, bool indexed isValid); + event InjectorPaused(bool indexed isPaused); + + function setUp() public virtual { + initTestEnvironment(); + + vm.warp(5 days); + } + + function test_injection() public { + // add rate update to risk oracle + _addUpdateToRiskOracle(); + + vm.expectEmit(address(_stewardInjector)); + emit ActionSucceeded(1); + + bool isAutomationPerformed = _checkAndPerformAutomation(); + assertTrue(isAutomationPerformed); + } + + function test_disableUpdate() public { + // add rate update to risk oracle + _addUpdateToRiskOracle(); + + vm.prank(address(1)); + vm.expectRevert( + abi.encodeWithSelector(IWithGuardian.OnlyGuardianOrOwnerInvalidCaller.selector, address(1)) + ); + _stewardInjector.disableUpdateById(1, true); + + assertFalse(_stewardInjector.isDisabled(1)); + + vm.expectEmit(address(_stewardInjector)); + emit UpdateDisabled(1, true); + + vm.prank(_stewardsInjectorOwner); + _stewardInjector.disableUpdateById(1, true); + + assertTrue(_stewardInjector.isDisabled(1)); + + bool isAutomationPerformed = _checkAndPerformAutomation(); + assertFalse(isAutomationPerformed); + + vm.expectEmit(address(_stewardInjector)); + emit UpdateDisabled(1, false); + + vm.prank(_stewardsInjectorOwner); + _stewardInjector.disableUpdateById(1, false); + + assertFalse(_stewardInjector.isDisabled(1)); + + isAutomationPerformed = _checkAndPerformAutomation(); + assertTrue(isAutomationPerformed); + } + + function test_injectorPaused() public { + // add rate update to risk oracle + _addUpdateToRiskOracle(); + + vm.prank(address(1)); + vm.expectRevert( + abi.encodeWithSelector(IWithGuardian.OnlyGuardianOrOwnerInvalidCaller.selector, address(1)) + ); + _stewardInjector.pauseInjector(true); + + assertFalse(_stewardInjector.isInjectorPaused()); + + vm.expectEmit(address(_stewardInjector)); + emit InjectorPaused(true); + + vm.prank(_stewardsInjectorOwner); + _stewardInjector.pauseInjector(true); + + assertTrue(_stewardInjector.isInjectorPaused()); + + bool isAutomationPerformed = _checkAndPerformAutomation(); + assertFalse(isAutomationPerformed); + + vm.expectEmit(address(_stewardInjector)); + emit InjectorPaused(false); + + vm.prank(_stewardsInjectorOwner); + _stewardInjector.pauseInjector(false); + + assertFalse(_stewardInjector.isInjectorPaused()); + + isAutomationPerformed = _checkAndPerformAutomation(); + assertTrue(isAutomationPerformed); + } + + function test_isUpdatedIdExecuted() public { + // add rate update to risk oracle + _addUpdateToRiskOracle(); + + assertFalse(_stewardInjector.isUpdateIdExecuted(1)); + + bool isAutomationPerformed = _checkAndPerformAutomation(); + assertTrue(isAutomationPerformed); + assertTrue(_stewardInjector.isUpdateIdExecuted(1)); + + isAutomationPerformed = _checkAndPerformAutomation(); + assertFalse(isAutomationPerformed); + } + + function test_expiredUpdate() public { + // add rate update to risk oracle + _addUpdateToRiskOracle(); + + uint256 initialTs = block.timestamp; + vm.warp(initialTs + _stewardInjector.EXPIRATION_PERIOD()); + + bool isAutomationPerformed = _checkAndPerformAutomation(); + assertFalse(isAutomationPerformed); + + vm.warp(initialTs); + isAutomationPerformed = _checkAndPerformAutomation(); + assertTrue(isAutomationPerformed); + } + + function test_reverts_sameUpdateInjectedTwice() public { + _addUpdateToRiskOracle(); // updateId 1 + + vm.expectEmit(address(_stewardInjector)); + emit ActionSucceeded(1); + + (bool shouldRunKeeper, bytes memory performData) = _stewardInjector.checkUpkeep(''); + _stewardInjector.performUpkeep(performData); + assertTrue(shouldRunKeeper); + + vm.expectRevert(IAaveStewardInjectorBase.UpdateCannotBeInjected.selector); + _stewardInjector.performUpkeep(performData); + } + + function _addUpdateToRiskOracle() internal virtual; + + function _checkAndPerformAutomation() internal virtual returns (bool) { + (bool shouldRunKeeper, bytes memory performData) = _stewardInjector.checkUpkeep(''); + if (shouldRunKeeper) { + _stewardInjector.performUpkeep(performData); + } + return shouldRunKeeper; + } +} diff --git a/tests/AaveStewardsInjectorCaps.t.sol b/tests/AaveStewardsInjectorCaps.t.sol new file mode 100644 index 0000000..a2a273b --- /dev/null +++ b/tests/AaveStewardsInjectorCaps.t.sol @@ -0,0 +1,339 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {AaveStewardInjectorCaps, IAaveStewardInjectorCaps} from '../src/contracts/AaveStewardInjectorCaps.sol'; +import './AaveStewardsInjectorBase.t.sol'; + +contract AaveStewardsInjectorCaps_Test is AaveStewardsInjectorBaseTest { + event MarketAdded(address indexed market); + event MarketRemoved(address indexed market); + + address internal _aWETH; + address internal _aWBTC; + + function setUp() public override { + super.setUp(); + + IRiskSteward.RiskParamConfig memory defaultRiskParamConfig = IRiskSteward.RiskParamConfig({ + minDelay: 3 days, + maxPercentChange: 100_00 + }); + IRiskSteward.Config memory riskConfig = IRiskSteward.Config({ + ltv: defaultRiskParamConfig, + liquidationThreshold: defaultRiskParamConfig, + liquidationBonus: defaultRiskParamConfig, + supplyCap: defaultRiskParamConfig, + borrowCap: defaultRiskParamConfig, + debtCeiling: defaultRiskParamConfig, + baseVariableBorrowRate: defaultRiskParamConfig, + variableRateSlope1: defaultRiskParamConfig, + variableRateSlope2: defaultRiskParamConfig, + optimalUsageRatio: defaultRiskParamConfig, + priceCapLst: defaultRiskParamConfig, + priceCapStable: defaultRiskParamConfig + }); + + // setup risk oracle + vm.startPrank(_riskOracleOwner); + address[] memory initialSenders = new address[](1); + initialSenders[0] = _riskOracleOwner; + string[] memory initialUpdateTypes = new string[](3); + initialUpdateTypes[0] = 'supplyCap'; + initialUpdateTypes[1] = 'borrowCap'; + initialUpdateTypes[2] = 'wrongUpdateType'; + + _riskOracle = new RiskOracle('RiskOracle', initialSenders, initialUpdateTypes); + vm.stopPrank(); + + // setup steward injector + vm.startPrank(_stewardsInjectorOwner); + + address computedRiskStewardAddress = vm.computeCreateAddress( + _stewardsInjectorOwner, + vm.getNonce(_stewardsInjectorOwner) + 1 + ); + _stewardInjector = new AaveStewardInjectorCaps( + address(_riskOracle), + address(computedRiskStewardAddress), + _stewardsInjectorOwner, + _stewardsInjectorGuardian + ); + + // setup risk steward + _riskSteward = new RiskSteward( + contracts.protocolDataProvider, + IEngine(report.configEngine), + address(_stewardInjector), + riskConfig + ); + vm.assertEq(computedRiskStewardAddress, address(_riskSteward)); + vm.stopPrank(); + + _aWETH = _getAToken(address(weth)); + _aWBTC = _getAToken(address(wbtc)); + + _addMarket(_aWETH); + + vm.startPrank(poolAdmin); + contracts.aclManager.addRiskAdmin(address(_riskSteward)); + + // as initial caps are at 0, which the steward cannot update from + contracts.poolConfiguratorProxy.setSupplyCap(address(weth), 100); + contracts.poolConfiguratorProxy.setBorrowCap(address(weth), 50); + contracts.poolConfiguratorProxy.setSupplyCap(address(wbtc), 100); + contracts.poolConfiguratorProxy.setBorrowCap(address(wbtc), 50); + vm.stopPrank(); + } + + function test_addMarkets() public { + address[] memory markets = new address[](1); + markets[0] = _aWETH; + + vm.prank(address(1)); + vm.expectRevert( + abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(1)) + ); + AaveStewardInjectorCaps(address(_stewardInjector)).addMarkets(markets); + + vm.expectEmit(address(_stewardInjector)); + emit MarketAdded(_aWETH); + + vm.prank(_stewardsInjectorOwner); + AaveStewardInjectorCaps(address(_stewardInjector)).addMarkets(markets); + + markets = AaveStewardInjectorCaps(address(_stewardInjector)).getMarkets(); + assertEq(markets.length, 1); + assertEq(markets[0], _aWETH); + } + + function test_removeMarkets() public { + address[] memory markets = AaveStewardInjectorCaps(address(_stewardInjector)).getMarkets(); + assertEq(markets.length, 1); + assertEq(markets[0], _aWETH); + + vm.prank(address(1)); + vm.expectRevert( + abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(1)) + ); + AaveStewardInjectorCaps(address(_stewardInjector)).removeMarkets(markets); + + vm.expectEmit(address(_stewardInjector)); + emit MarketRemoved(_aWETH); + + vm.prank(_stewardsInjectorOwner); + AaveStewardInjectorCaps(address(_stewardInjector)).removeMarkets(markets); + + markets = AaveStewardInjectorCaps(address(_stewardInjector)).getMarkets(); + assertEq(markets.length, 0); + + // removing already removed market does nothing + markets = new address[](1); + markets[0] = _aWETH; + vm.prank(_stewardsInjectorOwner); + AaveStewardInjectorCaps(address(_stewardInjector)).removeMarkets(markets); + markets = AaveStewardInjectorCaps(address(_stewardInjector)).getMarkets(); + assertEq(markets.length, 0); + } + + function test_perform_invalidMarketPassed() public { + _addUpdateToRiskOracle(_aWBTC, 'supplyCap', _encode(105e8)); + + IAaveStewardInjectorCaps.ActionData memory action = IAaveStewardInjectorCaps.ActionData({ + market: _aWBTC, + updateType: 'supplyCap' + }); + + vm.expectRevert(IAaveStewardInjectorBase.UpdateCannotBeInjected.selector); + _stewardInjector.performUpkeep(abi.encode(action)); + + address[] memory markets = new address[](1); + markets[0] = _aWBTC; + vm.prank(_stewardsInjectorOwner); + AaveStewardInjectorCaps(address(_stewardInjector)).addMarkets(markets); + + vm.expectEmit(address(_stewardInjector)); + emit ActionSucceeded(1); + _stewardInjector.performUpkeep(abi.encode(action)); + } + + function test_perform_invalidUpdateTypePassed() public { + _addUpdateToRiskOracle(_aWETH, 'wrongUpdateType', _encode(105e18)); + + IAaveStewardInjectorCaps.ActionData memory action = IAaveStewardInjectorCaps.ActionData({ + market: _aWETH, + updateType: 'wrongUpdateType' + }); + + vm.expectRevert(IAaveStewardInjectorBase.UpdateCannotBeInjected.selector); + _stewardInjector.performUpkeep(abi.encode(action)); + + _addUpdateToRiskOracle(_aWETH, 'supplyCap', _encode(105e18)); + action = IAaveStewardInjectorCaps.ActionData({market: _aWETH, updateType: 'supplyCap'}); + vm.expectEmit(address(_stewardInjector)); + emit ActionSucceeded(2); + _stewardInjector.performUpkeep(abi.encode(action)); + } + + function test_multipleMarketInjection() public { + _addMarket(_aWBTC); + _addUpdateToRiskOracle(_aWETH, 'supplyCap', _encode(105e18)); + _addUpdateToRiskOracle(_aWBTC, 'supplyCap', _encode(105e8)); + + vm.expectEmit(address(_stewardInjector)); + emit ActionSucceeded(1); + assertTrue(_checkAndPerformAutomation()); + + vm.expectEmit(address(_stewardInjector)); + emit ActionSucceeded(2); + assertTrue(_checkAndPerformAutomation()); + } + + function test_multipleUpdateTypeInjection() public { + _addUpdateToRiskOracle(_aWETH, 'supplyCap', _encode(105e18)); + _addUpdateToRiskOracle(_aWETH, 'borrowCap', _encode(55e18)); + + vm.expectEmit(address(_stewardInjector)); + emit ActionSucceeded(1); + assertTrue(_checkAndPerformAutomation()); + + vm.expectEmit(address(_stewardInjector)); + emit ActionSucceeded(2); + assertTrue(_checkAndPerformAutomation()); + } + + function test_randomized_multipleMarketInjection() public { + _addMarket(_aWBTC); + _addUpdateToRiskOracle(_aWETH, 'supplyCap', _encode(105e18)); + _addUpdateToRiskOracle(_aWETH, 'borrowCap', _encode(55e18)); + _addUpdateToRiskOracle(_aWBTC, 'supplyCap', _encode(105e8)); + _addUpdateToRiskOracle(_aWBTC, 'borrowCap', _encode(55e8)); + + uint256 snapshot = vm.snapshotState(); + + vm.expectEmit(address(_stewardInjector)); + emit ActionSucceeded(1); + assertTrue(_checkAndPerformAutomation()); + + vm.expectEmit(address(_stewardInjector)); + emit ActionSucceeded(3); + assertTrue(_checkAndPerformAutomation()); + + vm.expectEmit(address(_stewardInjector)); + emit ActionSucceeded(2); + assertTrue(_checkAndPerformAutomation()); + + vm.expectEmit(address(_stewardInjector)); + emit ActionSucceeded(4); + assertTrue(_checkAndPerformAutomation()); + + assertTrue(vm.revertToState(snapshot)); + vm.warp(block.timestamp + 3); + + // previous updateId order of execution: 1, 3, 2, 4 + // updateId order of execution: 4, 1, 3, 2 + // we can see with block.timestamp changing the order of execution of action changes as well + + vm.expectEmit(address(_stewardInjector)); + emit ActionSucceeded(4); + assertTrue(_checkAndPerformAutomation()); + + vm.expectEmit(address(_stewardInjector)); + emit ActionSucceeded(1); + assertTrue(_checkAndPerformAutomation()); + + vm.expectEmit(address(_stewardInjector)); + emit ActionSucceeded(3); + assertTrue(_checkAndPerformAutomation()); + + vm.expectEmit(address(_stewardInjector)); + emit ActionSucceeded(2); + assertTrue(_checkAndPerformAutomation()); + } + + function test_noInjection_ifUpdateDoesNotExist() public { + assertFalse(_checkAndPerformAutomation()); + } + + function test_checkUpkeepGasLimit() public { + _addMultipleUpdatesToRiskOracleOfDifferentMarkets(40); + + uint256 startGas = gasleft(); + _stewardInjector.checkUpkeep(''); + uint256 gasUsed = startGas - gasleft(); + + // for 40 markets added, the checkUpkeep gas consumed is less than 5m + // which is within the bounds of automation infra + assertLt(gasUsed, 5_000_000); + } + + function _addUpdateToRiskOracle( + address market, + string memory updateType, + bytes memory value + ) internal { + vm.startPrank(_riskOracleOwner); + + _riskOracle.publishRiskParameterUpdate( + 'referenceId', + value, + updateType, + market, + 'additionalData' + ); + vm.stopPrank(); + } + + function _addUpdateToRiskOracle() internal override { + vm.startPrank(_riskOracleOwner); + + _riskOracle.publishRiskParameterUpdate( + 'referenceId', + _encode(105e18), + 'supplyCap', + _aWETH, + 'additionalData' + ); + vm.stopPrank(); + } + + function _addMarket(address market) internal { + address[] memory markets = new address[](1); + markets[0] = market; + + vm.prank(_stewardsInjectorOwner); + AaveStewardInjectorCaps(address(_stewardInjector)).addMarkets(markets); + } + + function _addMultipleUpdatesToRiskOracleOfDifferentMarkets(uint160 count) internal { + for (uint160 i = 0; i < count; i++) { + vm.startPrank(_riskOracleOwner); + + address market = address(i); + _riskOracle.publishRiskParameterUpdate( + 'referenceId', + _encode(105e18), + 'supplyCap', + market, + 'additionalData' + ); + _riskOracle.publishRiskParameterUpdate( + 'referenceId', + _encode(55e18), + 'borrowCap', + market, + 'additionalData' + ); + vm.stopPrank(); + + _addMarket(market); + } + } + + function _encode(uint256 input) internal pure returns (bytes memory encodedData) { + encodedData = abi.encodePacked(uint256(input)); + } + + function _getAToken(address underlying) internal view returns (address aToken) { + (aToken, , ) = contracts.protocolDataProvider.getReserveTokensAddresses(underlying); + } +} diff --git a/tests/AaveStewardsInjectorRates.t.sol b/tests/AaveStewardsInjectorRates.t.sol new file mode 100644 index 0000000..dd6e921 --- /dev/null +++ b/tests/AaveStewardsInjectorRates.t.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {AaveStewardInjectorRates} from '../src/contracts/AaveStewardInjectorRates.sol'; +import './AaveStewardsInjectorBase.t.sol'; + +contract AaveStewardsInjectorRates_Test is AaveStewardsInjectorBaseTest { + function setUp() public override { + super.setUp(); + + IRiskSteward.RiskParamConfig memory defaultRiskParamConfig = IRiskSteward.RiskParamConfig({ + minDelay: 3 days, + maxPercentChange: 5_00 // 5% + }); + IRiskSteward.Config memory riskConfig = IRiskSteward.Config({ + ltv: defaultRiskParamConfig, + liquidationThreshold: defaultRiskParamConfig, + liquidationBonus: defaultRiskParamConfig, + supplyCap: defaultRiskParamConfig, + borrowCap: defaultRiskParamConfig, + debtCeiling: defaultRiskParamConfig, + baseVariableBorrowRate: defaultRiskParamConfig, + variableRateSlope1: defaultRiskParamConfig, + variableRateSlope2: defaultRiskParamConfig, + optimalUsageRatio: defaultRiskParamConfig, + priceCapLst: defaultRiskParamConfig, + priceCapStable: defaultRiskParamConfig + }); + + // setup risk oracle + vm.startPrank(_riskOracleOwner); + address[] memory initialSenders = new address[](1); + initialSenders[0] = _riskOracleOwner; + string[] memory initialUpdateTypes = new string[](1); + initialUpdateTypes[0] = 'RateStrategyUpdate'; + + _riskOracle = new RiskOracle('RiskOracle', initialSenders, initialUpdateTypes); + vm.stopPrank(); + + // setup steward injector + vm.startPrank(_stewardsInjectorOwner); + + address computedRiskStewardAddress = vm.computeCreateAddress( + _stewardsInjectorOwner, + vm.getNonce(_stewardsInjectorOwner) + 1 + ); + _stewardInjector = new AaveStewardInjectorRates( + address(_riskOracle), + address(computedRiskStewardAddress), + _stewardsInjectorOwner, + _stewardsInjectorGuardian, + address(weth) + ); + + // setup risk steward + _riskSteward = new RiskSteward( + contracts.protocolDataProvider, + IEngine(report.configEngine), + address(_stewardInjector), + riskConfig + ); + + vm.assertEq(computedRiskStewardAddress, address(_riskSteward)); + vm.stopPrank(); + + vm.startPrank(poolAdmin); + contracts.aclManager.addRiskAdmin(address(_riskSteward)); + vm.stopPrank(); + } + + function test_reverts_ifUpdateDoesNotExist() public { + vm.expectRevert(bytes('No update found for the specified parameter and market.')); + (, bytes memory performData) = _stewardInjector.checkUpkeep(''); + + vm.expectRevert(bytes('No update found for the specified parameter and market.')); + _stewardInjector.performUpkeep(performData); + } + + function _addUpdateToRiskOracle() internal override { + vm.startPrank(_riskOracleOwner); + + IEngine.InterestRateInputData memory rate = IEngine.InterestRateInputData({ + optimalUsageRatio: EngineFlags.KEEP_CURRENT, + baseVariableBorrowRate: 5_00, + variableRateSlope1: EngineFlags.KEEP_CURRENT, + variableRateSlope2: EngineFlags.KEEP_CURRENT + }); + _riskOracle.publishRiskParameterUpdate( + 'referenceId', + abi.encode(rate), + 'RateStrategyUpdate', + address(weth), + 'additionalData' + ); + vm.stopPrank(); + } +} diff --git a/tests/EdgeRiskStewardCaps.t.sol b/tests/EdgeRiskStewardCaps.t.sol new file mode 100644 index 0000000..52458be --- /dev/null +++ b/tests/EdgeRiskStewardCaps.t.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {EdgeRiskStewardCaps} from 'src/contracts/EdgeRiskStewardCaps.sol'; +import {IPriceCapAdapter} from 'aave-capo/interfaces/IPriceCapAdapter.sol'; +import './RiskSteward.t.sol'; + +contract EdgeRiskStewardCaps_Test is RiskSteward_Test { + function setUp() public override { + super.setUp(); + + vm.startPrank(GovernanceV3Ethereum.EXECUTOR_LVL_1); + steward = new EdgeRiskStewardCaps( + AaveV3Ethereum.AAVE_PROTOCOL_DATA_PROVIDER, + IEngine(configEngine), + riskCouncil, + riskConfig + ); + AaveV3Ethereum.ACL_MANAGER.addRiskAdmin(address(steward)); + vm.stopPrank(); + } + + /* ----------------------------- Rates Tests ----------------------------- */ + + function test_updateRates() public override { + ( + uint256 beforeOptimalUsageRatio, + uint256 beforeBaseVariableBorrowRate, + uint256 beforeVariableRateSlope1, + uint256 beforeVariableRateSlope2 + ) = _getInterestRatesForAsset(AaveV3EthereumAssets.WETH_UNDERLYING); + + IEngine.RateStrategyUpdate[] memory rateUpdates = new IEngine.RateStrategyUpdate[](1); + rateUpdates[0] = IEngine.RateStrategyUpdate({ + asset: AaveV3EthereumAssets.WETH_UNDERLYING, + params: IEngine.InterestRateInputData({ + optimalUsageRatio: beforeOptimalUsageRatio + 5_00, // 5% absolute increase + baseVariableBorrowRate: beforeBaseVariableBorrowRate + 10_00, // 10% absolute increase + variableRateSlope1: beforeVariableRateSlope1 + 10_00, // 10% absolute increase + variableRateSlope2: beforeVariableRateSlope2 + 10_00 // 10% absolute increase + }) + }); + + vm.startPrank(riskCouncil); + vm.expectRevert(IRiskSteward.UpdateNotAllowed.selector); + steward.updateRates(rateUpdates); + } + + function test_updateRates_outOfRange() public override {} + + function test_updateRates_debounceNotRespected() public override {} + + function test_updateRates_assetUnlisted() public override {} + + function test_updateRates_assetRestricted() public override {} + + function test_updateRates_allKeepCurrent() public override {} + + function test_updateRate_sameUpdate() public override {} + + /* ----------------------------- Collateral Tests ----------------------------- */ + + function test_updateCollateralSide() public override { + (, uint256 ltvBefore, uint256 ltBefore, uint256 lbBefore, , , , , , ) = AaveV3Ethereum + .AAVE_PROTOCOL_DATA_PROVIDER + .getReserveConfigurationData(AaveV3EthereumAssets.UNI_UNDERLYING); + + // as the definition is with 2 decimals, and config engine does not take the decimals into account, so we divide by 100. + uint256 debtCeilingBefore = AaveV3Ethereum.AAVE_PROTOCOL_DATA_PROVIDER.getDebtCeiling( + AaveV3EthereumAssets.UNI_UNDERLYING + ) / 100; + + IEngine.CollateralUpdate[] memory collateralUpdates = new IEngine.CollateralUpdate[](1); + collateralUpdates[0] = IEngine.CollateralUpdate({ + asset: AaveV3EthereumAssets.UNI_UNDERLYING, + ltv: ltvBefore + 10_00, // 10% absolute increase + liqThreshold: ltBefore + 5_00, // 5% absolute increase + liqBonus: (lbBefore - 100_00) + 2_00, // 2% absolute increase + debtCeiling: (debtCeilingBefore * 110) / 100, // 10% relative increase + liqProtocolFee: EngineFlags.KEEP_CURRENT + }); + + vm.startPrank(riskCouncil); + vm.expectRevert(IRiskSteward.UpdateNotAllowed.selector); + steward.updateCollateralSide(collateralUpdates); + } + + function test_updateCollateralSide_outOfRange() public override {} + + function test_updateCollateralSide_debounceNotRespected() public override {} + + function test_updateCollateralSide_liqProtocolFeeNotAllowed() public override {} + + function test_updateCollateralSide_assetUnlisted() public override {} + + function test_updateCollateralSide_assetRestricted() public override {} + + function test_updateCollateralSide_toValueZeroNotAllowed() public override {} + + function test_updateCollaterals_allKeepCurrent() public override {} + + function test_updateCollaterals_sameUpdate() public override {} + + /* ----------------------------- LST Price Cap Tests ----------------------------- */ + + function test_updateLstPriceCap() public { + IRiskSteward.PriceCapLstUpdate[] memory priceCapUpdates = new IRiskSteward.PriceCapLstUpdate[]( + 1 + ); + priceCapUpdates[0] = IRiskSteward.PriceCapLstUpdate({ + oracle: AaveV3EthereumAssets.wstETH_ORACLE, + priceCapUpdateParams: IPriceCapAdapter.PriceCapUpdateParams({ + snapshotTimestamp: uint48(block.timestamp - 2), + snapshotRatio: 1.1e18, + maxYearlyRatioGrowthPercent: 9_68 + }) + }); + + vm.startPrank(riskCouncil); + vm.expectRevert(IRiskSteward.UpdateNotAllowed.selector); + steward.updateLstPriceCaps(priceCapUpdates); + } + + /* ----------------------------- Stable Price Cap Test ----------------------------- */ + + function test_updateStablePriceCap() public { + IRiskSteward.PriceCapStableUpdate[] + memory priceCapUpdates = new IRiskSteward.PriceCapStableUpdate[](1); + + priceCapUpdates[0] = IRiskSteward.PriceCapStableUpdate({ + oracle: AaveV3EthereumAssets.USDT_ORACLE, + priceCap: 1060000 + }); + + vm.startPrank(riskCouncil); + vm.expectRevert(IRiskSteward.UpdateNotAllowed.selector); + steward.updateStablePriceCaps(priceCapUpdates); + } +} diff --git a/tests/EdgeRiskSteward.t.sol b/tests/EdgeRiskStewardRates.t.sol similarity index 96% rename from tests/EdgeRiskSteward.t.sol rename to tests/EdgeRiskStewardRates.t.sol index a35c5b9..dc69453 100644 --- a/tests/EdgeRiskSteward.t.sol +++ b/tests/EdgeRiskStewardRates.t.sol @@ -1,16 +1,16 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; -import {EdgeRiskSteward} from 'src/contracts/EdgeRiskSteward.sol'; +import {EdgeRiskStewardRates} from 'src/contracts/EdgeRiskStewardRates.sol'; import {IPriceCapAdapter} from 'aave-capo/interfaces/IPriceCapAdapter.sol'; import './RiskSteward.t.sol'; -contract EdgeRiskSteward_Test is RiskSteward_Test { +contract EdgeRiskStewardRates_Test is RiskSteward_Test { function setUp() public override { super.setUp(); vm.startPrank(GovernanceV3Ethereum.EXECUTOR_LVL_1); - steward = new EdgeRiskSteward( + steward = new EdgeRiskStewardRates( AaveV3Ethereum.AAVE_PROTOCOL_DATA_PROVIDER, IEngine(configEngine), riskCouncil, diff --git a/tests/RiskSteward.t.sol b/tests/RiskSteward.t.sol index 4e9028a..88b6b15 100644 --- a/tests/RiskSteward.t.sol +++ b/tests/RiskSteward.t.sol @@ -272,7 +272,7 @@ contract RiskSteward_Test is Test { /* ----------------------------- Rates Tests ----------------------------- */ - function test_updateRates() public { + function test_updateRates() public virtual { ( uint256 beforeOptimalUsageRatio, uint256 beforeBaseVariableBorrowRate, @@ -355,7 +355,7 @@ contract RiskSteward_Test is Test { assertEq(lastUpdated.variableRateSlope2LastUpdated, block.timestamp); } - function test_updateRates_outOfRange() public { + function test_updateRates_outOfRange() public virtual { ( uint256 beforeOptimalUsageRatio, uint256 beforeBaseVariableBorrowRate, @@ -380,7 +380,7 @@ contract RiskSteward_Test is Test { vm.stopPrank(); } - function test_updateRates_debounceNotRespected() public { + function test_updateRates_debounceNotRespected() public virtual { ( uint256 beforeOptimalUsageRatio, uint256 beforeBaseVariableBorrowRate, @@ -425,7 +425,7 @@ contract RiskSteward_Test is Test { vm.stopPrank(); } - function test_updateRates_assetUnlisted() public { + function test_updateRates_assetUnlisted() public virtual { IEngine.RateStrategyUpdate[] memory rateUpdates = new IEngine.RateStrategyUpdate[](1); rateUpdates[0] = IEngine.RateStrategyUpdate({ asset: 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84, // stETH @@ -442,7 +442,7 @@ contract RiskSteward_Test is Test { steward.updateRates(rateUpdates); } - function test_updateRates_assetRestricted() public { + function test_updateRates_assetRestricted() public virtual { vm.startPrank(GovernanceV3Ethereum.EXECUTOR_LVL_1); steward.setAddressRestricted(AaveV3EthereumAssets.GHO_UNDERLYING, true); vm.stopPrank(); @@ -463,7 +463,7 @@ contract RiskSteward_Test is Test { steward.updateRates(rateUpdates); } - function test_updateRates_allKeepCurrent() public { + function test_updateRates_allKeepCurrent() public virtual { ( uint256 beforeOptimalUsageRatio, uint256 beforeBaseVariableBorrowRate, @@ -498,7 +498,7 @@ contract RiskSteward_Test is Test { assertEq(beforeVariableRateSlope2, afterVariableRateSlope2); } - function test_updateRate_sameUpdate() public { + function test_updateRate_sameUpdate() public virtual { ( uint256 beforeOptimalUsageRatio, uint256 beforeBaseVariableBorrowRate,