diff --git a/.gitmodules b/.gitmodules index 888d42d..71126eb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "lib/forge-std"] - path = lib/forge-std - url = https://github.com/foundry-rs/forge-std +[submodule "lib/protocol-v3.1-upgrade"] + path = lib/protocol-v3.1-upgrade + url = https://github.com/bgd-labs/protocol-v3.1-upgrade diff --git a/LICENSE b/LICENSE index 07f396a..447c806 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,117 @@ -Copyright 2022 BGD Labs +Business Source License 1.1 -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved. +“Business Source License” is a trademark of MariaDB Corporation Ab. -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +----------------------------------------------------------------------------- -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Parameters + +Licensor: Aave DAO, represented by its governance smart contracts + + +Licensed Work: Aave v3.1 + The Licensed Work is (c) 2024 Aave DAO, represented by its governance smart contracts + +Additional Use Grant: You are permitted to use, copy, and modify the Licensed Work, subject to + the following conditions: + - Your use of the Licensed Work shall not, directly or indirectly, enable, facilitate, + or assist in any way with the migration of users and/or funds from the Aave ecosystem. + The "Aave ecosystem" is defined in the context of this License as the collection of + software protocols and applications approved by the Aave governance, including all + those produced within compensated service provider engagements with the Aave DAO. + The Aave DAO is able to waive this requirement for one or more third-parties, if and + only if explicitly indicating it on a record 'authorizations' on govv3.aavelicense.eth. + - You are neither an individual nor a direct or indirect participant in any incorporated + organization, DAO, or identifiable group, that has deployed in production any original + or derived software ("fork") of the Aave ecosystem for purposes competitive to Aave, + within the preceding four years. + The Aave DAO is able to waive this requirement for one or more third-parties, if and + only if explicitly indicating it on a record 'authorizations' on v31.aavelicense.eth. + - You must ensure that the usage of the Licensed Work does not result in any direct or + indirect harm to the Aave ecosystem or the Aave brand. This encompasses, but is not limited to, + reputational damage, omission of proper credit/attribution, or utilization for any malicious + intent. + +Change Date: The earlier of: + - 2027-03-06 + - If specified, the date in the 'change-date' record on v31.aavelicense.eth + +Change License: MIT + +----------------------------------------------------------------------------- + +Notice + +The Business Source License (this document, or the “License”) is not an Open +Source license. However, the Licensed Work will eventually be made available +under an Open Source License, as stated in this License. + +----------------------------------------------------------------------------- + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative +works, redistribute, and make non-production use of the Licensed Work. The +Licensor may make an Additional Use Grant, above, permitting limited +production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly +available distribution of a specific version of the Licensed Work under this +License, whichever comes first, the Licensor hereby grants you rights under +the terms of the Change License, and the rights granted in the paragraph +above terminate. + +If your use of the Licensed Work does not comply with the requirements +currently in effect as described in this License, you must purchase a +commercial license from the Licensor, its affiliated entities, or authorized +resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works +of the Licensed Work, are subject to this License. This License applies +separately for each version of the Licensed Work and the Change Date may vary +for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy +of the Licensed Work. If you receive the Licensed Work in original or +modified form from a third party, the terms and conditions set forth in this +License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically +terminate your rights under this License for the current and all other +versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of +Licensor or its affiliates (provided that you may use a trademark or logo of +Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +TITLE. + +MariaDB hereby grants you permission to use this License’s text to license +your works, and to refer to it using the trademark “Business Source License”, +as long as you comply with the Covenants of Licensor below. + +Covenants of Licensor + +In consideration of the right to use this License’s text and the “Business +Source License” name and trademark, Licensor covenants to MariaDB, and to all +other recipients of the licensed work to be provided by Licensor: + +1. To specify as the Change License the GPL Version 2.0 or any later version, + or a license that is compatible with GPL Version 2.0 or a later version, + where “compatible” means that software provided under the Change License can + be included in a program with software provided under GPL Version 2.0 or a + later version. Licensor may specify additional Change Licenses without + limitation. + +2. To either: (a) specify an additional grant of rights to use that does not + impose any additional restriction on the right granted in this License, as + the Additional Use Grant; or (b) insert the text “None”. + +3. To specify a Change Date. + +4. Not to modify this License in any other way. diff --git a/README.md b/README.md index 6815a70..f006dba 100644 --- a/README.md +++ b/README.md @@ -1 +1,69 @@ # Aave Risk Stewards Phase 2 + +Expanding from the scope of from CapsPlusRiskSteward, we now introduce the new RiskSteward, allowing hardly constrained risk parameter updates by risk service providers and reducing governance overhead. + +## Specification + +The new RiskSteward we propose follows the same design as the CapsPlusRiskSteward: a smart contract to which the Aave Governance gives `POOL_ADMIN` the role over all v3 instances, controlled by a 2-of-2 multi-sig of the risk providers, and heavily constrained on what can do and how by its own logic. + +_Note: The Risk Stewards 2 will only be available for Aave V3 instances and not Aave V2 due to missing admin roles on Aave V2 instances._ + +The following risk params could be changed by the RiskStewards: + +- Supply Caps +- Borrow Caps + +- LTV +- Liquidation Threshold +- Liquidation Bonus +- Debt Ceiling + +- Base variable borrow rate +- Slope 1 +- Slope 2 +- Optimal point + +#### Min Delay: + +For each risk param, `minDelay` can be configured, which is the minimum amount of delay (denominated in seconds) required before pushing another update for the risk param. Please note that this is specific for a risk param and includes both in upwards and downwards direction. Ex. after increasing LTV by 5%, we must wait by `minDelay` before either increasing it again or decreasing it. + +#### Max Percent Change: + +For each risk param, `maxPercentChange` which is the maximum percent change allowed (both upwards and downwards) for the risk param using the RiskStewards. + +- Supply cap, Borrow cap and Debt ceiling: The `maxPercentChange` is relative and is denominated in BPS. (Ex. `50_00` for +-50% relative change). +For example, for a current supply cap of an asset at 1_000_000 and `maxPercentChange` configured for supply cap at `50_00`, the max supply cap that can be configured is 1_500_000 and the minimum 500_000 via the steward. + +- LTV, LT, LB: The `maxPercentChange` is in absolute values and is also denominated in BPS. (Ex. `5_00` for +-5% change in LTV). +For example, for a current LTV of an asset configured at 70_00 (70%) and `maxPercentChange` configured for ltv at `10_00`, the max ltv that can be configured is 77_00 (77%) and the minimum 63_00 (63%) via the steward. + +- Interest rates params: For Base Variable Borrow Rate, Slope 1, Slope 2, uOptimal the `maxPercentChange` is in absolute values and is denominated in BPS. +For example, for a current uOptimal of an asset configured at 50_00 (50%) and `maxPercentChange` configured for uOptimal at `10_00`, the max ltv that can be configured is 55_00 (55%) and the minimum 45_00 (45%) via the steward. + +After the activation proposal, these params could only be changed by the governance by calling the `setRiskConfig()` method. + +_Note: The Risk Stewards will not allow setting the values to 0 for supply cap, borrow cap, debt ceiling, LTV, Liquidation Threshold, Liquidation Bonus no matter if the maxPercentChange has been configured to 100%. The Risk Stewards will however allow setting the value to 0 for interest rate param updates._ + +#### Restricted Assets: + +Some assets can also be restricted on the RiskStewards by calling the `setAssetRestricted()` method. This prevents the RiskStewards to make any updates on the specific asset. One example of the restricted asset could be GHO. + +### Setup + +```sh +cp .env.example .env +forge install +``` + +### Test + +```sh +forge test +``` + +## License + +Copyright © 2024, Aave DAO, represented by its governance smart contracts. + +The [BUSL1.1](./LICENSE) license of this repository allows for any usage of the software, if respecting the Additional Use Grant limitations, forbidding any use case damaging anyhow the Aave DAO's interests. +Interfaces and other components required for integrations are explicitly MIT licensed. diff --git a/foundry.toml b/foundry.toml index 1292908..bc67e04 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,6 +3,7 @@ src = 'src' test = 'tests' script = 'scripts' out = 'out' +solc = '0.8.19' libs = ['lib'] remappings = [ ] diff --git a/lib/forge-std b/lib/forge-std deleted file mode 160000 index 066ff16..0000000 --- a/lib/forge-std +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 066ff16c5c03e6f931cd041fd366bc4be1fae82a diff --git a/lib/protocol-v3.1-upgrade b/lib/protocol-v3.1-upgrade new file mode 160000 index 0000000..a80a0fb --- /dev/null +++ b/lib/protocol-v3.1-upgrade @@ -0,0 +1 @@ +Subproject commit a80a0fb843e4aef524bad5acd8185a470d5d712f diff --git a/package.json b/package.json index e6e6d05..2108deb 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ }, "keywords": [], "author": "BGD labs", - "license": "MIT", + "license": "BUSL-1.1", "bugs": { "url": "https://github.com/bgd-labs/aave-risk-stewards-2/issues" }, diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..c622814 --- /dev/null +++ b/remappings.txt @@ -0,0 +1,10 @@ +ds-test/=lib/protocol-v3.1-upgrade/lib/aave-helpers/lib/forge-std/lib/ds-test/src/ +forge-std/=lib/protocol-v3.1-upgrade/lib/aave-helpers/lib/forge-std/src/ +aave-v3-origin/=lib/protocol-v3.1-upgrade/lib/aave-v3-origin/src +aave-helpers/=lib/protocol-v3.1-upgrade/lib/aave-helpers/src +aave-address-book/=lib/protocol-v3.1-upgrade/lib/aave-helpers/lib/aave-address-book/src/ +solidity-utils/=lib/protocol-v3.1-upgrade/lib/aave-helpers/lib/solidity-utils/src +lib/aave-v3-origin:aave-v3-core/=lib/aave-v3-origin/src/core +lib/aave-v3-origin:aave-v3-periphery/=lib/aave-v3-origin/src/periphery/ +lib/protocol-v3.1-upgrade/lib/aave-helpers/lib/aave-address-book:aave-v3-core/=lib/protocol-v3.1-upgrade/lib/aave-helpers/lib/aave-address-book/lib/aave-v3-core/ +lib/aave-helpers:aave-v3-core/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-core/ diff --git a/src/contracts/RiskSteward.sol b/src/contracts/RiskSteward.sol new file mode 100644 index 0000000..5aeb417 --- /dev/null +++ b/src/contracts/RiskSteward.sol @@ -0,0 +1,439 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IPoolDataProvider} from 'aave-address-book/AaveV3.sol'; +import {Address} from 'solidity-utils/contracts/oz-common/Address.sol'; +import {EngineFlags} from 'aave-helpers/v3-config-engine/EngineFlags.sol'; +import {Ownable} from 'solidity-utils/contracts/oz-common/Ownable.sol'; +import {IAaveV3ConfigEngine as IEngine} from 'aave-v3-origin/periphery/contracts/v3-config-engine/AaveV3ConfigEngine.sol'; +import {IRiskSteward} from '../interfaces/IRiskSteward.sol'; +import {IDefaultInterestRateStrategyV2} from 'aave-v3-origin/core/contracts/interfaces/IDefaultInterestRateStrategyV2.sol'; + +/** + * @title RiskSteward + * @author BGD labs + * @notice Contract to manage the risk params within configured bound on aave v3 pool: + * This contract can update the following risk params: caps, ltv, liqThreshold, liqBonus, debtCeiling, interest rates params. + */ +contract RiskSteward is Ownable, IRiskSteward { + using Address for address; + + /// @inheritdoc IRiskSteward + IEngine public immutable CONFIG_ENGINE; + + /// @inheritdoc IRiskSteward + IPoolDataProvider public immutable POOL_DATA_PROVIDER; + + /// @inheritdoc IRiskSteward + address public immutable RISK_COUNCIL; + + uint256 internal constant BPS_MAX = 100_00; + + Config internal _riskConfig; + + mapping(address => Debounce) internal _timelocks; + + mapping(address => bool) internal _restrictedAssets; + + /** + * @dev Modifier preventing anyone, but the council to update risk params. + */ + modifier onlyRiskCouncil() { + if (RISK_COUNCIL != msg.sender) revert InvalidCaller(); + _; + } + + /** + * @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 + ) { + POOL_DATA_PROVIDER = poolDataProvider; + CONFIG_ENGINE = engine; + RISK_COUNCIL = riskCouncil; + _riskConfig = riskConfig; + } + + /// @inheritdoc IRiskSteward + function updateCaps(IEngine.CapsUpdate[] calldata capsUpdate) external onlyRiskCouncil { + _validateCapsUpdate(capsUpdate); + _updateCaps(capsUpdate); + } + + /// @inheritdoc IRiskSteward + function updateRates(IEngine.RateStrategyUpdate[] calldata ratesUpdate) external onlyRiskCouncil { + _validateRatesUpdate(ratesUpdate); + _updateRates(ratesUpdate); + } + + /// @inheritdoc IRiskSteward + function updateCollateralSide( + IEngine.CollateralUpdate[] calldata collateralUpdates + ) external onlyRiskCouncil { + _validateCollateralsUpdate(collateralUpdates); + _updateCollateralSide(collateralUpdates); + } + + /// @inheritdoc IRiskSteward + function getTimelock(address asset) external view returns (Debounce memory) { + return _timelocks[asset]; + } + + /// @inheritdoc IRiskSteward + function setRiskConfig(Config calldata riskConfig) external onlyOwner { + _riskConfig = riskConfig; + emit RiskConfigSet(riskConfig); + } + + /// @inheritdoc IRiskSteward + function getRiskConfig() external view returns (Config memory) { + return _riskConfig; + } + + /// @inheritdoc IRiskSteward + function isAssetRestricted(address asset) external view returns (bool) { + return _restrictedAssets[asset]; + } + + /// @inheritdoc IRiskSteward + function setAssetRestricted(address asset, bool isRestricted) external onlyOwner { + _restrictedAssets[asset] = isRestricted; + emit AssetRestricted(asset, isRestricted); + } + + /** + * @notice method to validate the caps update + * @param capsUpdate list containing the new supply, borrow caps of the assets + */ + function _validateCapsUpdate(IEngine.CapsUpdate[] calldata capsUpdate) internal view { + if (capsUpdate.length == 0) revert NoZeroUpdates(); + + for (uint256 i = 0; i < capsUpdate.length; i++) { + address asset = capsUpdate[i].asset; + + if (_restrictedAssets[asset]) revert AssetIsRestricted(); + if (capsUpdate[i].supplyCap == 0 || capsUpdate[i].borrowCap == 0) revert InvalidUpdateToZero(); + + (uint256 currentBorrowCap, uint256 currentSupplyCap) = POOL_DATA_PROVIDER.getReserveCaps( + capsUpdate[i].asset + ); + + _validateParamUpdate( + ParamUpdateValidationInput({ + currentValue: currentSupplyCap, + newValue: capsUpdate[i].supplyCap, + lastUpdated: _timelocks[asset].supplyCapLastUpdated, + riskConfig: _riskConfig.supplyCap, + isChangeRelative: true + }) + ); + _validateParamUpdate( + ParamUpdateValidationInput({ + currentValue: currentBorrowCap, + newValue: capsUpdate[i].borrowCap, + lastUpdated: _timelocks[asset].borrowCapLastUpdated, + riskConfig: _riskConfig.borrowCap, + isChangeRelative: true + }) + ); + } + } + + /** + * @notice method to validate the interest rates update + * @param ratesUpdate list containing the new interest rates params of the assets + */ + function _validateRatesUpdate(IEngine.RateStrategyUpdate[] calldata ratesUpdate) internal view { + if (ratesUpdate.length == 0) revert NoZeroUpdates(); + + for (uint256 i = 0; i < ratesUpdate.length; i++) { + address asset = ratesUpdate[i].asset; + if (_restrictedAssets[asset]) revert AssetIsRestricted(); + + ( + uint256 currentOptimalUsageRatio, + uint256 currentBaseVariableBorrowRate, + uint256 currentVariableRateSlope1, + uint256 currentVariableRateSlope2 + ) = _getInterestRatesForAsset(asset); + + _validateParamUpdate( + ParamUpdateValidationInput({ + currentValue: currentOptimalUsageRatio, + newValue: ratesUpdate[i].params.optimalUsageRatio, + lastUpdated: _timelocks[asset].optimalUsageRatioLastUpdated, + riskConfig: _riskConfig.optimalUsageRatio, + isChangeRelative: false + }) + ); + _validateParamUpdate( + ParamUpdateValidationInput({ + currentValue: currentBaseVariableBorrowRate, + newValue: ratesUpdate[i].params.baseVariableBorrowRate, + lastUpdated: _timelocks[asset].baseVariableRateLastUpdated, + riskConfig: _riskConfig.baseVariableBorrowRate, + isChangeRelative: false + }) + ); + _validateParamUpdate( + ParamUpdateValidationInput({ + currentValue: currentVariableRateSlope1, + newValue: ratesUpdate[i].params.variableRateSlope1, + lastUpdated: _timelocks[asset].variableRateSlope1LastUpdated, + riskConfig: _riskConfig.variableRateSlope1, + isChangeRelative: false + }) + ); + _validateParamUpdate( + ParamUpdateValidationInput({ + currentValue: currentVariableRateSlope2, + newValue: ratesUpdate[i].params.variableRateSlope2, + lastUpdated: _timelocks[asset].variableRateSlope2LastUpdated, + riskConfig: _riskConfig.variableRateSlope2, + isChangeRelative: false + }) + ); + } + } + + /** + * @notice method to validate the collaterals update + * @param collateralUpdates list containing the new collateral updates of the assets + */ + function _validateCollateralsUpdate( + IEngine.CollateralUpdate[] calldata collateralUpdates + ) internal view { + if (collateralUpdates.length == 0) revert NoZeroUpdates(); + + for (uint256 i = 0; i < collateralUpdates.length; i++) { + address asset = collateralUpdates[i].asset; + + if (_restrictedAssets[asset]) revert AssetIsRestricted(); + if (collateralUpdates[i].liqProtocolFee != EngineFlags.KEEP_CURRENT) revert ParamChangeNotAllowed(); + if ( + collateralUpdates[i].ltv == 0 || + collateralUpdates[i].liqThreshold == 0 || + collateralUpdates[i].liqBonus == 0 || + collateralUpdates[i].debtCeiling == 0 + ) revert InvalidUpdateToZero(); + + ( + , + uint256 currentLtv, + uint256 currentLiquidationThreshold, + uint256 currentLiquidationBonus, + , + , + , + , + , + + ) = POOL_DATA_PROVIDER.getReserveConfigurationData(asset); + uint256 currentDebtCeiling = POOL_DATA_PROVIDER.getDebtCeiling(asset); + + _validateParamUpdate( + ParamUpdateValidationInput({ + currentValue: currentLtv, + newValue: collateralUpdates[i].ltv, + lastUpdated: _timelocks[asset].ltvLastUpdated, + riskConfig: _riskConfig.ltv, + isChangeRelative: false + }) + ); + _validateParamUpdate( + ParamUpdateValidationInput({ + currentValue: currentLiquidationThreshold, + newValue: collateralUpdates[i].liqThreshold, + lastUpdated: _timelocks[asset].liquidationThresholdLastUpdated, + riskConfig: _riskConfig.liquidationThreshold, + isChangeRelative: false + }) + ); + _validateParamUpdate( + ParamUpdateValidationInput({ + currentValue: currentLiquidationBonus - 100_00, // as the definition is 100% + x%, and config engine takes into account x% for simplicity. + newValue: collateralUpdates[i].liqBonus, + lastUpdated: _timelocks[asset].liquidationBonusLastUpdated, + riskConfig: _riskConfig.liquidationBonus, + isChangeRelative: false + }) + ); + _validateParamUpdate( + ParamUpdateValidationInput({ + currentValue: currentDebtCeiling / 100, // as the definition is with 2 decimals, and config engine does not take the decimals into account. + newValue: collateralUpdates[i].debtCeiling, + lastUpdated: _timelocks[asset].debtCeilingLastUpdated, + riskConfig: _riskConfig.debtCeiling, + isChangeRelative: true + }) + ); + } + } + + /** + * @notice method to validate the risk param update is within the allowed bound and the debounce is respected + * @param validationParam struct containing values used for validation of the risk param update + */ + function _validateParamUpdate( + ParamUpdateValidationInput memory validationParam + ) internal view { + if (validationParam.newValue == EngineFlags.KEEP_CURRENT) return; + + if (block.timestamp - validationParam.lastUpdated < validationParam.riskConfig.minDelay) revert DebounceNotRespected(); + if ( + !_updateWithinAllowedRange( + validationParam.currentValue, + validationParam.newValue, + validationParam.riskConfig.maxPercentChange, + validationParam.isChangeRelative + ) + ) revert UpdateNotInRange(); + } + + /** + * @notice method to update the borrow / supply caps using the config engine and updates the debounce + * @param capsUpdate list containing the new supply, borrow caps of the assets + */ + function _updateCaps(IEngine.CapsUpdate[] calldata capsUpdate) internal { + for (uint256 i = 0; i < capsUpdate.length; i++) { + address asset = capsUpdate[i].asset; + + if (capsUpdate[i].supplyCap != EngineFlags.KEEP_CURRENT) { + _timelocks[asset].supplyCapLastUpdated = uint40(block.timestamp); + } + + if (capsUpdate[i].borrowCap != EngineFlags.KEEP_CURRENT) { + _timelocks[asset].borrowCapLastUpdated = uint40(block.timestamp); + } + } + + address(CONFIG_ENGINE).functionDelegateCall( + abi.encodeWithSelector(CONFIG_ENGINE.updateCaps.selector, capsUpdate) + ); + } + + /** + * @notice method to update the interest rates params using the config engine and updates the debounce + * @param ratesUpdate list containing the new interest rates params of the assets + */ + function _updateRates(IEngine.RateStrategyUpdate[] calldata ratesUpdate) internal { + for (uint256 i = 0; i < ratesUpdate.length; i++) { + address asset = ratesUpdate[i].asset; + + if (ratesUpdate[i].params.optimalUsageRatio != EngineFlags.KEEP_CURRENT) { + _timelocks[asset].optimalUsageRatioLastUpdated = uint40(block.timestamp); + } + + if (ratesUpdate[i].params.baseVariableBorrowRate != EngineFlags.KEEP_CURRENT) { + _timelocks[asset].baseVariableRateLastUpdated = uint40(block.timestamp); + } + + if (ratesUpdate[i].params.variableRateSlope1 != EngineFlags.KEEP_CURRENT) { + _timelocks[asset].variableRateSlope1LastUpdated = uint40(block.timestamp); + } + + if (ratesUpdate[i].params.variableRateSlope2 != EngineFlags.KEEP_CURRENT) { + _timelocks[asset].variableRateSlope2LastUpdated = uint40(block.timestamp); + } + } + + address(CONFIG_ENGINE).functionDelegateCall( + abi.encodeWithSelector(CONFIG_ENGINE.updateRateStrategies.selector, ratesUpdate) + ); + } + + /** + * @notice method to update the collateral side params using the config engine and updates the debounce + * @param collateralUpdates list containing the new collateral updates of the assets + */ + function _updateCollateralSide(IEngine.CollateralUpdate[] calldata collateralUpdates) internal { + for (uint256 i = 0; i < collateralUpdates.length; i++) { + address asset = collateralUpdates[i].asset; + + if (collateralUpdates[i].ltv != EngineFlags.KEEP_CURRENT) { + _timelocks[asset].ltvLastUpdated = uint40(block.timestamp); + } + + if (collateralUpdates[i].liqThreshold != EngineFlags.KEEP_CURRENT) { + _timelocks[asset].liquidationThresholdLastUpdated = uint40(block.timestamp); + } + + if (collateralUpdates[i].liqBonus != EngineFlags.KEEP_CURRENT) { + _timelocks[asset].liquidationBonusLastUpdated = uint40(block.timestamp); + } + + if (collateralUpdates[i].debtCeiling != EngineFlags.KEEP_CURRENT) { + _timelocks[asset].debtCeilingLastUpdated = uint40(block.timestamp); + } + } + + address(CONFIG_ENGINE).functionDelegateCall( + abi.encodeWithSelector(CONFIG_ENGINE.updateCollateralSide.selector, collateralUpdates) + ); + } + + /** + * @notice method to fetch the current interest rate params of the asset + * @param asset the address of the underlying asset + * @return optimalUsageRatio the current optimal usage ratio of the asset + * @return baseVariableBorrowRate the current base variable borrow rate of the asset + * @return variableRateSlope1 the current variable rate slope 1 of the asset + * @return variableRateSlope2 the current variable rate slope 2 of the asset + */ + function _getInterestRatesForAsset( + address asset + ) + internal + view + returns ( + uint256 optimalUsageRatio, + uint256 baseVariableBorrowRate, + uint256 variableRateSlope1, + uint256 variableRateSlope2 + ) + { + address rateStrategyAddress = POOL_DATA_PROVIDER.getInterestRateStrategyAddress(asset); + IDefaultInterestRateStrategyV2.InterestRateData + memory interestRateData = IDefaultInterestRateStrategyV2(rateStrategyAddress) + .getInterestRateDataBps(asset); + return ( + interestRateData.optimalUsageRatio, + interestRateData.baseVariableBorrowRate, + interestRateData.variableRateSlope1, + interestRateData.variableRateSlope2 + ); + } + + /** + * @notice Ensures the risk param update is within the allowed range + * @param from current risk param value + * @param to new updated risk param value + * @param maxPercentChange the max percent change allowed + * @param isChangeRelative true, if maxPercentChange is relative in value, false if maxPercentChange + * is absolute in value. + * @return bool true, if difference is within the maxPercentChange + */ + function _updateWithinAllowedRange( + uint256 from, + uint256 to, + uint256 maxPercentChange, + bool isChangeRelative + ) internal pure returns (bool) { + // diff denotes the difference between the from and to values, ensuring it is a positive value always + uint256 diff = from > to ? from - to : to - from; + + // maxDiff denotes the max permitted difference on both the upper and lower bounds, if the maxPercentChange is relative in value + // we calculate the max permitted difference using the maxPercentChange and the from value, otherwise if the maxPercentChange is absolute in value + // the max permitted difference is the maxPercentChange itself + uint256 maxDiff = isChangeRelative ? (maxPercentChange * from) / BPS_MAX : maxPercentChange; + if (diff > maxDiff) return false; + return true; + } +} diff --git a/src/interfaces/IRiskSteward.sol b/src/interfaces/IRiskSteward.sol new file mode 100644 index 0000000..5503516 --- /dev/null +++ b/src/interfaces/IRiskSteward.sol @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IPoolDataProvider} from 'aave-address-book/AaveV3.sol'; +import {EngineFlags} from 'aave-helpers/v3-config-engine/EngineFlags.sol'; +import {IAaveV3ConfigEngine as IEngine} from 'aave-v3-origin/periphery/contracts/v3-config-engine/AaveV3ConfigEngine.sol'; + +/** + * @title IRiskSteward + * @author BGD labs + * @notice Defines the interface for the contract to manage the risk params updates on aave v3 pool + */ +interface IRiskSteward { + /** + * @notice Only the permissioned council is allowed to call methods on the steward + */ + error InvalidCaller(); + + /** + * @notice A single risk param update can only be changed after the minimum delay configured has passed + */ + error DebounceNotRespected(); + + /** + * @notice A single risk param update must not be increased / decreased by maxPercentChange configured + */ + error UpdateNotInRange(); + + /** + * @notice There must be at least one risk param update per execution + */ + error NoZeroUpdates(); + + /** + * @notice The steward does not allow the risk param change for the param given + */ + error ParamChangeNotAllowed(); + + /** + * @notice The steward does not allow updates of risk param of a restricted asset + */ + error AssetIsRestricted(); + + /** + * @notice Setting the risk parameter value to zero is not allowed + */ + error InvalidUpdateToZero(); + + /** + * @notice Emitted when the owner configures an asset as restricted to be used by steward + * @param asset address of the underlying asset + * @param isRestricted true if asset is set as restricted, false otherwise + */ + event AssetRestricted(address indexed asset, bool indexed isRestricted); + + /** + * @notice Emitted when the risk configuration for the risk params has been set + * @param riskConfig struct containing the risk configurations + */ + event RiskConfigSet(Config indexed riskConfig); + + /** + * @notice Struct storing the last update by the steward of each risk param + */ + struct Debounce { + uint40 supplyCapLastUpdated; + uint40 borrowCapLastUpdated; + uint40 ltvLastUpdated; + uint40 liquidationBonusLastUpdated; + uint40 liquidationThresholdLastUpdated; + uint40 debtCeilingLastUpdated; + uint40 baseVariableRateLastUpdated; + uint40 variableRateSlope1LastUpdated; + uint40 variableRateSlope2LastUpdated; + uint40 optimalUsageRatioLastUpdated; + } + + /** + * @notice Struct storing the params used for validation of the risk param update + * @param currentValue the current value of the risk param + * @param newValue the new value of the risk param + * @param lastUpdated timestamp when the risk param was last updated by the steward + * @param riskConfig the risk configuration containing the minimum delay and the max percent change allowed for the risk param + * @param isChangeRelative true, if risk param change is relative in value, false if risk param change is absolute in value + */ + struct ParamUpdateValidationInput { + uint256 currentValue; + uint256 newValue; + uint40 lastUpdated; + RiskParamConfig riskConfig; + bool isChangeRelative; + } + + /** + * @notice Struct storing the minimum delay and maximum percent change for a risk param + */ + struct RiskParamConfig { + uint40 minDelay; + uint256 maxPercentChange; + } + + /** + * @notice Struct storing the risk configuration for all the risk param + */ + struct Config { + RiskParamConfig ltv; + RiskParamConfig liquidationThreshold; + RiskParamConfig liquidationBonus; + RiskParamConfig supplyCap; + RiskParamConfig borrowCap; + RiskParamConfig debtCeiling; + RiskParamConfig baseVariableBorrowRate; + RiskParamConfig variableRateSlope1; + RiskParamConfig variableRateSlope2; + RiskParamConfig optimalUsageRatio; + } + + /** + * @notice The config engine used to perform the cap update via delegatecall + */ + function CONFIG_ENGINE() external view returns (IEngine); + + /** + * @notice The pool data provider of the POOL the steward controls + */ + function POOL_DATA_PROVIDER() external view returns (IPoolDataProvider); + + /** + * @notice The safe controlling the steward + */ + function RISK_COUNCIL() external view returns (address); + + /** + * @notice Allows increasing borrow and supply caps across multiple assets + * @dev A cap update is only possible after minDelay has passed after last update + * @dev A cap increase / decrease is only allowed by a magnitude of maxPercentChange + * @param capUpdates struct containing new caps to be updated + */ + function updateCaps(IEngine.CapsUpdate[] calldata capUpdates) external; + + /** + * @notice Allows updating interest rates params across multiple assets + * @dev A rate update is only possible after minDelay has passed after last update + * @dev A rate increase / decrease is only allowed by a magnitude of maxPercentChange + * @param rateUpdates struct containing new interest rate params to be updated + */ + function updateRates(IEngine.RateStrategyUpdate[] calldata rateUpdates) external; + + /** + * @notice Allows updating collateral params across multiple assets + * @dev A collateral update is only possible after minDelay has passed after last update + * @dev A collateral increase / decrease is only allowed by a magnitude of maxPercentChange + * @param collateralUpdates struct containing new collateral rate params to be updated + */ + function updateCollateralSide(IEngine.CollateralUpdate[] calldata collateralUpdates) external; + + /** + * @notice method to check if an asset is restricted to be used by the risk stewards + * @param asset address of the underlying asset + * @return bool if asset is restricted or not + */ + function isAssetRestricted(address asset) external view returns (bool); + + /** + * @notice method called by the owner to set an asset as restricted + * @param asset address of the underlying asset + * @param isRestricted true if asset needs to be restricted, false otherwise + */ + function setAssetRestricted(address asset, bool isRestricted) external; + + /** + * @notice Returns the timelock for a specific asset i.e the last updated timestamp + * @param asset for which to fetch the timelock + * @return struct containing the latest updated timestamps of all the risk params by the steward + */ + function getTimelock(address asset) external view returns (Debounce memory); + + /** + * @notice method to get the risk configuration set for all the risk params + * @return struct containing the risk configurations + */ + function getRiskConfig() external view returns (Config memory); + + /** + * @notice method called by the owner to set the risk configuration for the risk params + * @param riskConfig struct containing the risk configurations + */ + function setRiskConfig(Config memory riskConfig) external; +} diff --git a/tests/RiskSteward.t.sol b/tests/RiskSteward.t.sol new file mode 100644 index 0000000..40e0cb7 --- /dev/null +++ b/tests/RiskSteward.t.sol @@ -0,0 +1,813 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {IACLManager, IPoolConfigurator, IPoolDataProvider} from 'aave-address-book/AaveV3.sol'; +import {IDefaultInterestRateStrategyV2} from 'aave-v3-origin/core/contracts/interfaces/IDefaultInterestRateStrategyV2.sol'; +import {GovernanceV3Ethereum} from 'aave-address-book/GovernanceV3Ethereum.sol'; +import {AaveV3Ethereum, AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; +import {RiskSteward, IRiskSteward, IEngine, EngineFlags} from 'src/contracts/RiskSteward.sol'; +import {DeploymentLibrary, UpgradePayload} from 'protocol-v3.1-upgrade/scripts/Deploy.s.sol'; +import {IAaveV3ConfigEngine as IEngine} from 'aave-v3-origin/periphery/contracts/v3-config-engine/AaveV3ConfigEngine.sol'; +import {GovV3Helpers} from 'aave-helpers/GovV3Helpers.sol'; +import {ConfigEngineDeployer} from './utils/ConfigEngineDeployer.sol'; + +contract RiskSteward_Test is Test { + address public constant riskCouncil = address(42); + RiskSteward public steward; + IRiskSteward.Config public riskConfig; + + event AssetRestricted(address indexed asset, bool indexed isRestricted); + + event RiskConfigSet(IRiskSteward.Config indexed riskConfig); + + function setUp() public { + vm.createSelectFork(vm.rpcUrl('mainnet'), 19055256); + + // update protocol to v3.1 + address v3_1_updatePayload = DeploymentLibrary._deployEthereum(); + GovV3Helpers.executePayload(vm, v3_1_updatePayload); + + // deploy new config engine + address configEngine = ConfigEngineDeployer.deployEngine( + address(UpgradePayload(v3_1_updatePayload).DEFAULT_IR()) + ); + + IRiskSteward.RiskParamConfig memory defaultRiskParamConfig = IRiskSteward.RiskParamConfig({ + minDelay: 5 days, + maxPercentChange: 10_00 // 10% + }); + IRiskSteward.RiskParamConfig memory liquidationBonusParamConfig = IRiskSteward.RiskParamConfig({ + minDelay: 5 days, + maxPercentChange: 2_00 // 2% + }); + + riskConfig = IRiskSteward.Config({ + ltv: defaultRiskParamConfig, + liquidationThreshold: defaultRiskParamConfig, + liquidationBonus: liquidationBonusParamConfig, + supplyCap: defaultRiskParamConfig, + borrowCap: defaultRiskParamConfig, + debtCeiling: defaultRiskParamConfig, + baseVariableBorrowRate: defaultRiskParamConfig, + variableRateSlope1: defaultRiskParamConfig, + variableRateSlope2: defaultRiskParamConfig, + optimalUsageRatio: defaultRiskParamConfig + }); + + vm.startPrank(GovernanceV3Ethereum.EXECUTOR_LVL_1); + steward = new RiskSteward( + AaveV3Ethereum.AAVE_PROTOCOL_DATA_PROVIDER, + IEngine(configEngine), + riskCouncil, + riskConfig + ); + AaveV3Ethereum.ACL_MANAGER.addRiskAdmin(address(steward)); + vm.stopPrank(); + } + + /* ----------------------------- Caps Tests ----------------------------- */ + + function test_updateCaps() public { + (uint256 daiBorrowCapBefore, uint256 daiSupplyCapBefore) = AaveV3Ethereum + .AAVE_PROTOCOL_DATA_PROVIDER + .getReserveCaps(AaveV3EthereumAssets.DAI_UNDERLYING); + + IEngine.CapsUpdate[] memory capUpdates = new IEngine.CapsUpdate[](1); + capUpdates[0] = IEngine.CapsUpdate( + AaveV3EthereumAssets.DAI_UNDERLYING, + (daiSupplyCapBefore * 110) / 100, // 10% relative increase + (daiBorrowCapBefore * 110) / 100 // 10% relative increase + ); + + vm.startPrank(riskCouncil); + steward.updateCaps(capUpdates); + + (uint256 daiBorrowCapAfter, uint256 daiSupplyCapAfter) = AaveV3Ethereum + .AAVE_PROTOCOL_DATA_PROVIDER + .getReserveCaps(AaveV3EthereumAssets.DAI_UNDERLYING); + + RiskSteward.Debounce memory lastUpdated = steward.getTimelock( + AaveV3EthereumAssets.DAI_UNDERLYING + ); + assertEq(daiBorrowCapAfter, capUpdates[0].borrowCap); + assertEq(daiSupplyCapAfter, capUpdates[0].supplyCap); + assertEq(lastUpdated.supplyCapLastUpdated, block.timestamp); + assertEq(lastUpdated.borrowCapLastUpdated, block.timestamp); + + // after min time passed test caps decrease + vm.warp(block.timestamp + 5 days + 1); + (daiBorrowCapBefore, daiSupplyCapBefore) = AaveV3Ethereum + .AAVE_PROTOCOL_DATA_PROVIDER + .getReserveCaps(AaveV3EthereumAssets.DAI_UNDERLYING); + + capUpdates[0] = IEngine.CapsUpdate( + AaveV3EthereumAssets.DAI_UNDERLYING, + (daiSupplyCapBefore * 90) / 100, // 10% relative decrease + (daiBorrowCapBefore * 90) / 100 // 10% relative decrease + ); + steward.updateCaps(capUpdates); + vm.stopPrank(); + + (daiBorrowCapAfter, daiSupplyCapAfter) = AaveV3Ethereum + .AAVE_PROTOCOL_DATA_PROVIDER + .getReserveCaps(AaveV3EthereumAssets.DAI_UNDERLYING); + + assertEq(daiBorrowCapAfter, capUpdates[0].borrowCap); + assertEq(daiSupplyCapAfter, capUpdates[0].supplyCap); + } + + function test_updateCaps_outOfRange() public { + (uint256 daiBorrowCapBefore, uint256 daiSupplyCapBefore) = AaveV3Ethereum + .AAVE_PROTOCOL_DATA_PROVIDER + .getReserveCaps(AaveV3EthereumAssets.DAI_UNDERLYING); + + IEngine.CapsUpdate[] memory capUpdates = new IEngine.CapsUpdate[](1); + capUpdates[0] = IEngine.CapsUpdate( + AaveV3EthereumAssets.DAI_UNDERLYING, + (daiSupplyCapBefore * 120) / 100, // 20% relative increase (current maxChangePercent configured is 10%) + (daiBorrowCapBefore * 120) / 100 // 20% relative increase + ); + + vm.startPrank(riskCouncil); + vm.expectRevert(IRiskSteward.UpdateNotInRange.selector); + steward.updateCaps(capUpdates); + + capUpdates[0] = IEngine.CapsUpdate( + AaveV3EthereumAssets.DAI_UNDERLYING, + (daiSupplyCapBefore * 80) / 100, // 20% relative decrease + (daiBorrowCapBefore * 80) / 100 // 20% relative decrease + ); + vm.expectRevert(IRiskSteward.UpdateNotInRange.selector); + steward.updateCaps(capUpdates); + + vm.stopPrank(); + } + + function test_updateCaps_debounceNotRespected() public { + (uint256 daiBorrowCapBefore, uint256 daiSupplyCapBefore) = AaveV3Ethereum + .AAVE_PROTOCOL_DATA_PROVIDER + .getReserveCaps(AaveV3EthereumAssets.DAI_UNDERLYING); + + IEngine.CapsUpdate[] memory capUpdates = new IEngine.CapsUpdate[](1); + capUpdates[0] = IEngine.CapsUpdate( + AaveV3EthereumAssets.DAI_UNDERLYING, + (daiSupplyCapBefore * 110) / 100, // 10% relative increase + (daiBorrowCapBefore * 110) / 100 // 10% relative increase + ); + + vm.startPrank(riskCouncil); + steward.updateCaps(capUpdates); + + // expect revert as minimum time has not passed for next update + vm.expectRevert(IRiskSteward.DebounceNotRespected.selector); + steward.updateCaps(capUpdates); + vm.stopPrank(); + } + + function test_updateCaps_allKeepCurrent() public { + (uint256 daiBorrowCapBefore, uint256 daiSupplyCapBefore) = AaveV3Ethereum + .AAVE_PROTOCOL_DATA_PROVIDER + .getReserveCaps(AaveV3EthereumAssets.DAI_UNDERLYING); + + IEngine.CapsUpdate[] memory capUpdates = new IEngine.CapsUpdate[](1); + capUpdates[0] = IEngine.CapsUpdate( + AaveV3EthereumAssets.DAI_UNDERLYING, + EngineFlags.KEEP_CURRENT, + EngineFlags.KEEP_CURRENT + ); + + vm.startPrank(riskCouncil); + steward.updateCaps(capUpdates); + + (uint256 daiBorrowCapAfter, uint256 daiSupplyCapAfter) = AaveV3Ethereum + .AAVE_PROTOCOL_DATA_PROVIDER + .getReserveCaps(AaveV3EthereumAssets.DAI_UNDERLYING); + + RiskSteward.Debounce memory lastUpdated = steward.getTimelock( + AaveV3EthereumAssets.DAI_UNDERLYING + ); + assertEq(daiBorrowCapAfter, daiBorrowCapBefore); + assertEq(daiSupplyCapAfter, daiSupplyCapBefore); + assertEq(lastUpdated.supplyCapLastUpdated, 0); + assertEq(lastUpdated.borrowCapLastUpdated, 0); + } + + function test_updateCaps_assetUnlisted() public { + address unlistedAsset = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; // stETH + + IEngine.CapsUpdate[] memory capUpdates = new IEngine.CapsUpdate[](1); + capUpdates[0] = IEngine.CapsUpdate(unlistedAsset, 100, 100); + + vm.prank(riskCouncil); + // as the update is from value 0 + vm.expectRevert(IRiskSteward.UpdateNotInRange.selector); + steward.updateCaps(capUpdates); + } + + function test_updateCaps_assetRestricted() public { + vm.startPrank(GovernanceV3Ethereum.EXECUTOR_LVL_1); + steward.setAssetRestricted(AaveV3EthereumAssets.GHO_UNDERLYING, true); + vm.stopPrank(); + + IEngine.CapsUpdate[] memory capUpdates = new IEngine.CapsUpdate[](1); + capUpdates[0] = IEngine.CapsUpdate(AaveV3EthereumAssets.GHO_UNDERLYING, 100, 100); + + vm.startPrank(riskCouncil); + vm.expectRevert(IRiskSteward.AssetIsRestricted.selector); + steward.updateCaps(capUpdates); + vm.stopPrank(); + } + + function test_updateCaps_toValueZeroNotAllowed() public { + // set risk config to allow 100% cap change to 0 + IRiskSteward.RiskParamConfig memory capsParamConfig = IRiskSteward.RiskParamConfig({ + minDelay: 5 days, + maxPercentChange: 100_00 // 100% relative change + }); + + riskConfig.supplyCap = capsParamConfig; + riskConfig.borrowCap = capsParamConfig; + + vm.prank(GovernanceV3Ethereum.EXECUTOR_LVL_1); + steward.setRiskConfig(riskConfig); + + IEngine.CapsUpdate[] memory capUpdates = new IEngine.CapsUpdate[](1); + capUpdates[0] = IEngine.CapsUpdate( + AaveV3EthereumAssets.DAI_UNDERLYING, + 0, // 100% relative decrease to 0 + 0 // 100% relative decrease to 0 + ); + + vm.startPrank(riskCouncil); + vm.expectRevert(IRiskSteward.InvalidUpdateToZero.selector); + steward.updateCaps(capUpdates); + } + + /* ----------------------------- Rates Tests ----------------------------- */ + + function test_updateRates() public { + ( + 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 + 10_00, // 10% 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); + steward.updateRates(rateUpdates); + + ( + uint256 afterOptimalUsageRatio, + uint256 afterBaseVariableBorrowRate, + uint256 afterVariableRateSlope1, + uint256 afterVariableRateSlope2 + ) = _getInterestRatesForAsset(AaveV3EthereumAssets.WETH_UNDERLYING); + + RiskSteward.Debounce memory lastUpdated = steward.getTimelock( + AaveV3EthereumAssets.WETH_UNDERLYING + ); + assertEq(afterOptimalUsageRatio, rateUpdates[0].params.optimalUsageRatio); + assertEq(afterBaseVariableBorrowRate, rateUpdates[0].params.baseVariableBorrowRate); + assertEq(afterVariableRateSlope1, rateUpdates[0].params.variableRateSlope1); + assertEq(afterVariableRateSlope2, rateUpdates[0].params.variableRateSlope2); + + assertEq(lastUpdated.optimalUsageRatioLastUpdated, block.timestamp); + assertEq(lastUpdated.baseVariableRateLastUpdated, block.timestamp); + assertEq(lastUpdated.variableRateSlope1LastUpdated, block.timestamp); + assertEq(lastUpdated.variableRateSlope2LastUpdated, block.timestamp); + + // after min time passed test rates decrease + vm.warp(block.timestamp + 5 days + 1); + + ( + beforeOptimalUsageRatio, + beforeBaseVariableBorrowRate, + beforeVariableRateSlope1, + beforeVariableRateSlope2 + ) = _getInterestRatesForAsset(AaveV3EthereumAssets.WETH_UNDERLYING); + + rateUpdates[0] = IEngine.RateStrategyUpdate({ + asset: AaveV3EthereumAssets.WETH_UNDERLYING, + params: IEngine.InterestRateInputData({ + optimalUsageRatio: beforeOptimalUsageRatio - 10_00, // 10% decrease + baseVariableBorrowRate: beforeBaseVariableBorrowRate - 1_00, // 1% decrease + variableRateSlope1: beforeVariableRateSlope1 - 1_00, // 1% decrease + variableRateSlope2: beforeVariableRateSlope2 - 10_00 // 10% absolute decrease + }) + }); + steward.updateRates(rateUpdates); + vm.stopPrank(); + + ( + afterOptimalUsageRatio, + afterBaseVariableBorrowRate, + afterVariableRateSlope1, + afterVariableRateSlope2 + ) = _getInterestRatesForAsset(AaveV3EthereumAssets.WETH_UNDERLYING); + lastUpdated = steward.getTimelock(AaveV3EthereumAssets.WETH_UNDERLYING); + + assertEq(afterOptimalUsageRatio, rateUpdates[0].params.optimalUsageRatio); + assertEq(afterBaseVariableBorrowRate, rateUpdates[0].params.baseVariableBorrowRate); + assertEq(afterVariableRateSlope1, rateUpdates[0].params.variableRateSlope1); + assertEq(afterVariableRateSlope2, rateUpdates[0].params.variableRateSlope2); + + assertEq(lastUpdated.optimalUsageRatioLastUpdated, block.timestamp); + assertEq(lastUpdated.baseVariableRateLastUpdated, block.timestamp); + assertEq(lastUpdated.variableRateSlope1LastUpdated, block.timestamp); + assertEq(lastUpdated.variableRateSlope2LastUpdated, block.timestamp); + } + + function test_updateRates_outOfRange() public { + ( + 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 + 12_00, // 12% absolute increase + baseVariableBorrowRate: beforeBaseVariableBorrowRate + 12_00, // 12% absolute increase + variableRateSlope1: beforeVariableRateSlope1 + 12_00, // 12% absolute increase + variableRateSlope2: beforeVariableRateSlope2 + 12_00 // 12% absolute increase + }) + }); + + vm.startPrank(riskCouncil); + vm.expectRevert(IRiskSteward.UpdateNotInRange.selector); + steward.updateRates(rateUpdates); + vm.stopPrank(); + } + + function test_updateRates_debounceNotRespected() public { + ( + 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 + 10_00, // 10% 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); + steward.updateRates(rateUpdates); + + // expect revert as minimum time has not passed for next update + vm.expectRevert(IRiskSteward.DebounceNotRespected.selector); + steward.updateRates(rateUpdates); + vm.stopPrank(); + } + + function test_updateRates_assetUnlisted() public { + IEngine.RateStrategyUpdate[] memory rateUpdates = new IEngine.RateStrategyUpdate[](1); + rateUpdates[0] = IEngine.RateStrategyUpdate({ + asset: 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84, // stETH + params: IEngine.InterestRateInputData({ + optimalUsageRatio: 40_00, + baseVariableBorrowRate: 0, + variableRateSlope1: 2_00, + variableRateSlope2: 50_00 + }) + }); + + vm.prank(riskCouncil); + vm.expectRevert(); + steward.updateRates(rateUpdates); + } + + function test_updateRates_assetRestricted() public { + vm.startPrank(GovernanceV3Ethereum.EXECUTOR_LVL_1); + steward.setAssetRestricted(AaveV3EthereumAssets.GHO_UNDERLYING, true); + vm.stopPrank(); + + IEngine.RateStrategyUpdate[] memory rateUpdates = new IEngine.RateStrategyUpdate[](1); + rateUpdates[0] = IEngine.RateStrategyUpdate({ + asset: AaveV3EthereumAssets.GHO_UNDERLYING, + params: IEngine.InterestRateInputData({ + optimalUsageRatio: 40_00, + baseVariableBorrowRate: 0, + variableRateSlope1: 2_00, + variableRateSlope2: 50_00 + }) + }); + + vm.prank(riskCouncil); + vm.expectRevert(IRiskSteward.AssetIsRestricted.selector); + steward.updateRates(rateUpdates); + } + + /* ----------------------------- Collateral Tests ----------------------------- */ + + function test_updateCollateralSide() public { + (, 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); + steward.updateCollateralSide(collateralUpdates); + + RiskSteward.Debounce memory lastUpdated = steward.getTimelock( + AaveV3EthereumAssets.UNI_UNDERLYING + ); + + (, uint256 ltvAfter, uint256 ltAfter, uint256 lbAfter, , , , , , ) = AaveV3Ethereum + .AAVE_PROTOCOL_DATA_PROVIDER + .getReserveConfigurationData(AaveV3EthereumAssets.UNI_UNDERLYING); + + uint256 debtCeilingAfter = AaveV3Ethereum.AAVE_PROTOCOL_DATA_PROVIDER.getDebtCeiling( + AaveV3EthereumAssets.UNI_UNDERLYING + ) / 100; + + assertEq(ltvAfter, collateralUpdates[0].ltv); + assertEq(ltAfter, collateralUpdates[0].liqThreshold); + assertEq(lbAfter - 100_00, collateralUpdates[0].liqBonus); + assertEq(debtCeilingAfter, collateralUpdates[0].debtCeiling); + + assertEq(lastUpdated.ltvLastUpdated, block.timestamp); + assertEq(lastUpdated.liquidationThresholdLastUpdated, block.timestamp); + assertEq(lastUpdated.liquidationBonusLastUpdated, block.timestamp); + assertEq(lastUpdated.debtCeilingLastUpdated, block.timestamp); + + // after min time passed test collateral update decrease + vm.warp(block.timestamp + 5 days + 1); + + (, ltvBefore, ltBefore, lbBefore, , , , , , ) = AaveV3Ethereum + .AAVE_PROTOCOL_DATA_PROVIDER + .getReserveConfigurationData(AaveV3EthereumAssets.UNI_UNDERLYING); + + debtCeilingBefore = + AaveV3Ethereum.AAVE_PROTOCOL_DATA_PROVIDER.getDebtCeiling( + AaveV3EthereumAssets.UNI_UNDERLYING + ) / + 100; + + collateralUpdates[0] = IEngine.CollateralUpdate({ + asset: AaveV3EthereumAssets.UNI_UNDERLYING, + ltv: ltvBefore - 10_00, // 10% absolute decrease + liqThreshold: ltBefore - 10_00, // 10% absolute decrease + liqBonus: (lbBefore - 100_00) - 2_00, // 2% absolute decrease + debtCeiling: (debtCeilingBefore * 90) / 100, // 10% relative decrease + liqProtocolFee: EngineFlags.KEEP_CURRENT + }); + steward.updateCollateralSide(collateralUpdates); + vm.stopPrank(); + + (, ltvAfter, ltAfter, lbAfter, , , , , , ) = AaveV3Ethereum + .AAVE_PROTOCOL_DATA_PROVIDER + .getReserveConfigurationData(AaveV3EthereumAssets.UNI_UNDERLYING); + debtCeilingAfter = + AaveV3Ethereum.AAVE_PROTOCOL_DATA_PROVIDER.getDebtCeiling( + AaveV3EthereumAssets.UNI_UNDERLYING + ) / + 100; + + assertEq(ltvAfter, collateralUpdates[0].ltv); + assertEq(ltAfter, collateralUpdates[0].liqThreshold); + assertEq(lbAfter - 100_00, collateralUpdates[0].liqBonus); + + lastUpdated = steward.getTimelock(AaveV3EthereumAssets.UNI_UNDERLYING); + + assertEq(lastUpdated.ltvLastUpdated, block.timestamp); + assertEq(lastUpdated.liquidationThresholdLastUpdated, block.timestamp); + assertEq(lastUpdated.liquidationBonusLastUpdated, block.timestamp); + } + + function test_updateCollateralSide_outOfRange() public { + (, 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 + 12_00, // 12% absolute increase + liqThreshold: ltBefore + 11_00, // 11% absolute increase + liqBonus: (lbBefore - 100_00) + 3_00, // 3% absolute increase + debtCeiling: (debtCeilingBefore * 112) / 100, // 12% relative increase + liqProtocolFee: EngineFlags.KEEP_CURRENT + }); + + vm.startPrank(riskCouncil); + vm.expectRevert(); + steward.updateCollateralSide(collateralUpdates); + + // after min time passed test collateral update decrease + vm.warp(block.timestamp + 5 days + 1); + + collateralUpdates[0] = IEngine.CollateralUpdate({ + asset: AaveV3EthereumAssets.UNI_UNDERLYING, + ltv: ltvBefore - 11_00, // 11% absolute decrease + liqThreshold: ltBefore - 11_00, // 11% absolute decrease + liqBonus: (lbBefore - 100_00) - 2_50, // 2.5% absolute decrease + debtCeiling: (debtCeilingBefore * 85) / 100, // 15% relative decrease + liqProtocolFee: EngineFlags.KEEP_CURRENT + }); + + vm.expectRevert(IRiskSteward.UpdateNotInRange.selector); + steward.updateCollateralSide(collateralUpdates); + vm.stopPrank(); + } + + function test_updateCollateralSide_debounceNotRespected() public { + // 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: EngineFlags.KEEP_CURRENT, + liqThreshold: EngineFlags.KEEP_CURRENT, + liqBonus: EngineFlags.KEEP_CURRENT, + debtCeiling: (debtCeilingBefore * 110) / 100, // 10% relative increase + liqProtocolFee: EngineFlags.KEEP_CURRENT + }); + + vm.startPrank(riskCouncil); + steward.updateCollateralSide(collateralUpdates); + + vm.warp(block.timestamp + 1 days); + + // expect revert as minimum time has not passed for next update + vm.expectRevert(IRiskSteward.DebounceNotRespected.selector); + steward.updateCollateralSide(collateralUpdates); + vm.stopPrank(); + } + + function test_updateCollateralSide_liqProtocolFeeNotAllowed() public { + IEngine.CollateralUpdate[] memory collateralUpdates = new IEngine.CollateralUpdate[](1); + collateralUpdates[0] = IEngine.CollateralUpdate({ + asset: AaveV3EthereumAssets.UNI_UNDERLYING, + ltv: EngineFlags.KEEP_CURRENT, + liqThreshold: EngineFlags.KEEP_CURRENT, + liqBonus: EngineFlags.KEEP_CURRENT, + debtCeiling: EngineFlags.KEEP_CURRENT, + liqProtocolFee: 10_00 + }); + + vm.startPrank(riskCouncil); + vm.expectRevert(IRiskSteward.ParamChangeNotAllowed.selector); + steward.updateCollateralSide(collateralUpdates); + vm.stopPrank(); + } + + function test_updateCollateralSide_assetUnlisted() public { + IEngine.CollateralUpdate[] memory collateralUpdates = new IEngine.CollateralUpdate[](1); + collateralUpdates[0] = IEngine.CollateralUpdate({ + asset: 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84, // stETH + ltv: 80_00, + liqThreshold: 83_00, + liqBonus: 5_00, + debtCeiling: 1_000_000, + liqProtocolFee: EngineFlags.KEEP_CURRENT + }); + + vm.prank(riskCouncil); + vm.expectRevert(); + steward.updateCollateralSide(collateralUpdates); + } + + function test_updateCollateralSide_assetRestricted() public { + vm.startPrank(GovernanceV3Ethereum.EXECUTOR_LVL_1); + steward.setAssetRestricted(AaveV3EthereumAssets.UNI_UNDERLYING, true); + vm.stopPrank(); + + IEngine.CollateralUpdate[] memory collateralUpdates = new IEngine.CollateralUpdate[](1); + collateralUpdates[0] = IEngine.CollateralUpdate({ + asset: AaveV3EthereumAssets.UNI_UNDERLYING, + ltv: 90_00, + liqThreshold: 83_00, + liqBonus: 1_00, + debtCeiling: 100_000_000, + liqProtocolFee: EngineFlags.KEEP_CURRENT + }); + + vm.prank(riskCouncil); + vm.expectRevert(IRiskSteward.AssetIsRestricted.selector); + steward.updateCollateralSide(collateralUpdates); + } + + function test_updateCollateralSide_toValueZeroNotAllowed() public { + // set risk config to allow 100% collateral param change to 0 + IRiskSteward.RiskParamConfig memory collateralParamConfig = IRiskSteward.RiskParamConfig({ + minDelay: 5 days, + maxPercentChange: 100_00 // 100% relative change + }); + + riskConfig.ltv = collateralParamConfig; + riskConfig.liquidationThreshold = collateralParamConfig; + riskConfig.liquidationBonus = collateralParamConfig; + riskConfig.debtCeiling = collateralParamConfig; + + vm.prank(GovernanceV3Ethereum.EXECUTOR_LVL_1); + steward.setRiskConfig(riskConfig); + + IEngine.CollateralUpdate[] memory collateralUpdates = new IEngine.CollateralUpdate[](1); + collateralUpdates[0] = IEngine.CollateralUpdate({ + asset: AaveV3EthereumAssets.UNI_UNDERLYING, + ltv: 0, + liqThreshold: 0, + liqBonus: 0, + debtCeiling: 0, // 100% relative decrease to value 0 + liqProtocolFee: EngineFlags.KEEP_CURRENT + }); + + vm.startPrank(riskCouncil); + vm.expectRevert(IRiskSteward.InvalidUpdateToZero.selector); + steward.updateCollateralSide(collateralUpdates); + } + + function test_invalidCaller(address caller) public { + vm.assume(caller != riskCouncil); + + IEngine.CapsUpdate[] memory capUpdates = new IEngine.CapsUpdate[](1); + capUpdates[0] = IEngine.CapsUpdate( + AaveV3EthereumAssets.DAI_UNDERLYING, + EngineFlags.KEEP_CURRENT, + EngineFlags.KEEP_CURRENT + ); + + IEngine.CollateralUpdate[] memory collateralUpdates = new IEngine.CollateralUpdate[](1); + collateralUpdates[0] = IEngine.CollateralUpdate({ + asset: AaveV3EthereumAssets.DAI_UNDERLYING, + ltv: EngineFlags.KEEP_CURRENT, + liqThreshold: EngineFlags.KEEP_CURRENT, + liqBonus: EngineFlags.KEEP_CURRENT, + debtCeiling: EngineFlags.KEEP_CURRENT, + liqProtocolFee: EngineFlags.KEEP_CURRENT + }); + + IEngine.RateStrategyUpdate[] memory rateStrategyUpdate = new IEngine.RateStrategyUpdate[](1); + rateStrategyUpdate[0] = IEngine.RateStrategyUpdate({ + asset: AaveV3EthereumAssets.DAI_UNDERLYING, + params: IEngine.InterestRateInputData({ + optimalUsageRatio: _bpsToRay(90_00), + baseVariableBorrowRate: EngineFlags.KEEP_CURRENT, + variableRateSlope1: EngineFlags.KEEP_CURRENT, + variableRateSlope2: EngineFlags.KEEP_CURRENT + }) + }); + + vm.startPrank(caller); + + vm.expectRevert(IRiskSteward.InvalidCaller.selector); + steward.updateCaps(capUpdates); + + vm.expectRevert(IRiskSteward.InvalidCaller.selector); + steward.updateCollateralSide(collateralUpdates); + + vm.expectRevert(IRiskSteward.InvalidCaller.selector); + steward.updateRates(rateStrategyUpdate); + + vm.expectRevert('Ownable: caller is not the owner'); + steward.setRiskConfig(riskConfig); + + vm.expectRevert('Ownable: caller is not the owner'); + steward.setAssetRestricted(AaveV3EthereumAssets.GHO_UNDERLYING, true); + + vm.stopPrank(); + } + + function test_assetRestricted() public { + vm.expectEmit(); + emit AssetRestricted(AaveV3EthereumAssets.GHO_UNDERLYING, true); + + vm.prank(GovernanceV3Ethereum.EXECUTOR_LVL_1); + steward.setAssetRestricted(AaveV3EthereumAssets.GHO_UNDERLYING, true); + + assertTrue(steward.isAssetRestricted(AaveV3EthereumAssets.GHO_UNDERLYING)); + + vm.expectEmit(); + emit AssetRestricted(AaveV3EthereumAssets.GHO_UNDERLYING, false); + + vm.prank(GovernanceV3Ethereum.EXECUTOR_LVL_1); + steward.setAssetRestricted(AaveV3EthereumAssets.GHO_UNDERLYING, false); + + assertFalse(steward.isAssetRestricted(AaveV3EthereumAssets.GHO_UNDERLYING)); + } + + function test_setRiskConfig() public { + IRiskSteward.RiskParamConfig memory newRiskParamConfig = IRiskSteward.RiskParamConfig({ + minDelay: 10 days, + maxPercentChange: 20_00 // 20% + }); + + IRiskSteward.Config memory newRiskConfig = IRiskSteward.Config({ + ltv: newRiskParamConfig, + liquidationThreshold: newRiskParamConfig, + liquidationBonus: newRiskParamConfig, + supplyCap: newRiskParamConfig, + borrowCap: newRiskParamConfig, + debtCeiling: newRiskParamConfig, + baseVariableBorrowRate: newRiskParamConfig, + variableRateSlope1: newRiskParamConfig, + variableRateSlope2: newRiskParamConfig, + optimalUsageRatio: newRiskParamConfig + }); + + vm.expectEmit(); + emit RiskConfigSet(newRiskConfig); + + vm.prank(GovernanceV3Ethereum.EXECUTOR_LVL_1); + steward.setRiskConfig(newRiskConfig); + + IRiskSteward.Config memory updatedRiskConfig = steward.getRiskConfig(); + + assertEq(newRiskConfig.ltv.minDelay, updatedRiskConfig.ltv.minDelay); + assertEq(newRiskConfig.ltv.maxPercentChange, updatedRiskConfig.ltv.maxPercentChange); + assertEq(newRiskConfig.liquidationThreshold.minDelay, updatedRiskConfig.liquidationThreshold.minDelay); + assertEq(newRiskConfig.liquidationThreshold.maxPercentChange, updatedRiskConfig.liquidationThreshold.maxPercentChange); + assertEq(newRiskConfig.liquidationBonus.minDelay, updatedRiskConfig.liquidationBonus.minDelay); + assertEq(newRiskConfig.liquidationBonus.maxPercentChange, updatedRiskConfig.liquidationBonus.maxPercentChange); + assertEq(newRiskConfig.supplyCap.minDelay, updatedRiskConfig.supplyCap.minDelay); + assertEq(newRiskConfig.supplyCap.maxPercentChange, updatedRiskConfig.supplyCap.maxPercentChange); + assertEq(newRiskConfig.borrowCap.minDelay, updatedRiskConfig.borrowCap.minDelay); + assertEq(newRiskConfig.borrowCap.maxPercentChange, updatedRiskConfig.borrowCap.maxPercentChange); + assertEq(newRiskConfig.debtCeiling.minDelay, updatedRiskConfig.debtCeiling.minDelay); + assertEq(newRiskConfig.debtCeiling.maxPercentChange, updatedRiskConfig.debtCeiling.maxPercentChange); + assertEq(newRiskConfig.baseVariableBorrowRate.minDelay, updatedRiskConfig.baseVariableBorrowRate.minDelay); + assertEq(newRiskConfig.baseVariableBorrowRate.maxPercentChange, updatedRiskConfig.baseVariableBorrowRate.maxPercentChange); + assertEq(newRiskConfig.variableRateSlope1.minDelay, updatedRiskConfig.variableRateSlope1.minDelay); + assertEq(newRiskConfig.variableRateSlope1.maxPercentChange, updatedRiskConfig.variableRateSlope1.maxPercentChange); + assertEq(newRiskConfig.variableRateSlope2.minDelay, updatedRiskConfig.variableRateSlope2.minDelay); + assertEq(newRiskConfig.variableRateSlope2.maxPercentChange, updatedRiskConfig.variableRateSlope2.maxPercentChange); + assertEq(newRiskConfig.optimalUsageRatio.minDelay, updatedRiskConfig.optimalUsageRatio.minDelay); + assertEq(newRiskConfig.optimalUsageRatio.maxPercentChange, updatedRiskConfig.optimalUsageRatio.maxPercentChange); + } + + function _bpsToRay(uint256 amount) internal pure returns (uint256) { + return (amount * 1e27) / 10_000; + } + + function _getInterestRatesForAsset( + address asset + ) + internal + view + returns ( + uint256 optimalUsageRatio, + uint256 baseVariableBorrowRate, + uint256 variableRateSlope1, + uint256 variableRateSlope2 + ) + { + address rateStrategyAddress = AaveV3Ethereum + .AAVE_PROTOCOL_DATA_PROVIDER + .getInterestRateStrategyAddress(asset); + + IDefaultInterestRateStrategyV2.InterestRateData + memory interestRateData = IDefaultInterestRateStrategyV2(rateStrategyAddress) + .getInterestRateDataBps(asset); + return ( + interestRateData.optimalUsageRatio, + interestRateData.baseVariableBorrowRate, + interestRateData.variableRateSlope1, + interestRateData.variableRateSlope2 + ); + } +} diff --git a/tests/utils/ConfigEngineDeployer.sol b/tests/utils/ConfigEngineDeployer.sol new file mode 100644 index 0000000..f2b7856 --- /dev/null +++ b/tests/utils/ConfigEngineDeployer.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {Vm} from 'forge-std/Vm.sol'; +import {Create2Utils} from 'aave-v3-origin/deployments/contracts/utilities/Create2Utils.sol'; +import {AaveV3ConfigEngine, IAaveV3ConfigEngine} from 'aave-v3-origin/periphery/contracts/v3-config-engine/AaveV3ConfigEngine.sol'; +import {IPoolAddressesProvider} from 'aave-v3-origin/core/contracts/interfaces/IPoolAddressesProvider.sol'; +import {IPool} from 'aave-v3-origin/core/contracts/interfaces/IPool.sol'; +import {IPoolConfigurator} from 'aave-v3-origin/core/contracts/interfaces/IPoolConfigurator.sol'; +import {IAaveOracle} from 'aave-v3-origin/core/contracts/interfaces/IAaveOracle.sol'; +import {CapsEngine} from 'aave-v3-origin/periphery/contracts/v3-config-engine/libraries/CapsEngine.sol'; +import {BorrowEngine} from 'aave-v3-origin/periphery/contracts/v3-config-engine/libraries/BorrowEngine.sol'; +import {CollateralEngine} from 'aave-v3-origin/periphery/contracts/v3-config-engine/libraries/CollateralEngine.sol'; +import {RateEngine} from 'aave-v3-origin/periphery/contracts/v3-config-engine/libraries/RateEngine.sol'; +import {PriceFeedEngine} from 'aave-v3-origin/periphery/contracts/v3-config-engine/libraries/PriceFeedEngine.sol'; +import {EModeEngine} from 'aave-v3-origin/periphery/contracts/v3-config-engine/libraries/EModeEngine.sol'; +import {ListingEngine} from 'aave-v3-origin/periphery/contracts/v3-config-engine/libraries/ListingEngine.sol'; +import {AaveV3Ethereum} from 'aave-address-book/AaveV3Ethereum.sol'; + +library ConfigEngineDeployer { + function deployEngine(address interestRateStrategy) internal returns (address) { + IAaveV3ConfigEngine.EngineLibraries memory engineLibraries = IAaveV3ConfigEngine + .EngineLibraries({ + listingEngine: Create2Utils._create2Deploy('v1', type(ListingEngine).creationCode), + eModeEngine: Create2Utils._create2Deploy('v1', type(EModeEngine).creationCode), + borrowEngine: Create2Utils._create2Deploy('v1', type(BorrowEngine).creationCode), + collateralEngine: Create2Utils._create2Deploy('v1', type(CollateralEngine).creationCode), + priceFeedEngine: Create2Utils._create2Deploy('v1', type(PriceFeedEngine).creationCode), + rateEngine: Create2Utils._create2Deploy('v1', type(RateEngine).creationCode), + capsEngine: Create2Utils._create2Deploy('v1', type(CapsEngine).creationCode) + }); + + IAaveV3ConfigEngine.EngineConstants memory engineConstants = IAaveV3ConfigEngine + .EngineConstants({ + pool: IPool(address(AaveV3Ethereum.POOL)), + poolConfigurator: IPoolConfigurator(address(AaveV3Ethereum.POOL_CONFIGURATOR)), + defaultInterestRateStrategy: interestRateStrategy, + oracle: IAaveOracle(address(AaveV3Ethereum.ORACLE)), + rewardsController: AaveV3Ethereum.DEFAULT_INCENTIVES_CONTROLLER, + collector: address(AaveV3Ethereum.COLLECTOR) + }); + + return + address( + new AaveV3ConfigEngine( + AaveV3Ethereum.DEFAULT_A_TOKEN_IMPL_REV_1, + AaveV3Ethereum.DEFAULT_VARIABLE_DEBT_TOKEN_IMPL_REV_1, + AaveV3Ethereum.DEFAULT_STABLE_DEBT_TOKEN_IMPL_REV_1, + engineConstants, + engineLibraries + ) + ); + } +}