Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: steward injector robot #12

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ update:; forge update
coverage :; forge coverage --report lcov && \
lcov --remove ./lcov.info -o ./lcov.info.p \
'tests/*' \
'src/contracts/dependencies/*' \
'src/contracts/examples/*' \
'src/contracts/updates/*' \
'scripts/*' \
&& genhtml ./lcov.info.p -o report --branch-coverage \
&& coverage=$$(awk -F '[<>]' '/headerCovTableEntryHi/{print $3}' ./report/index.html | sed 's/[^0-9.]//g' | head -n 1); \
wget -O ./report/coverage.svg "https://img.shields.io/badge/coverage-$${coverage}%25-brightgreen"
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,30 @@ Some assets/oracles can also be restricted on the RiskStewards by calling the `s

<br>

## AGRS + Edge Infrastructure (Risk Oracles)

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 Risk Steward, using a middleware contract `AaveStewardsInjector`.

The Risk Steward contract used for automated updates, called [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 following is a simple diagram on how the system works as a whole:

<img src="./docs/agrs-edge.png" alt="AGRS and Risk Oracle Diagram Flow" width="100%" height="100%">

### AaveStewardsInjector

The [AaveStewardsInjector](./src/contracts//AaveStewardInjector.sol) is a chainlink automation compatible contract which checks if updates from the Edge Risk Oracle could be pushed to the Risk Steward, and if so it injects the update from the Edge Risk Oracle to the Risk Stewards. The `AaveStewardsInjector` should be set as the `riskCouncil` on the Risk Steward contract so it can inject updates.

The `AaveStewardsInjector` has the following major functions:

- `checkUpkeep()`: This method is called off-chain by chainlink nodes every block to check if any updates could be injected, and if so, calls `performUpKeep()`. It fetches the latest update counter on the Risk Oracle, and starting from the latest update, checks if the update can be injected into the Risk Steward if not already executed before. If the latest update is executed or disabled it checks the previous update and so on until `MAX_SKIP` iterations. If conditions for the injections are met, the method returns true with the encoded `payloadId`.

- `performUpkeep()`: The `performUpkeep()` is the method called by the chainlink automation nodes when the `checkUpkeep()` method returns true, the method is called with the `payloadId` to be injected encoded in bytes. The `performUpkeep()` call ensures that the latest update gets executed first, unless that update has been disabled by the steward injector guardian and if all the conditions for injection are satisfied, it calls the EdgeRiskSteward with the params from the Risk Oracle. After an update has been injected on the Risk Steward, we mark the updateId as executed on storage mapping `_isUpdateIdExecuted[id]`. The `performUpkeep()` method is permissionless on purpose, so as to allow injections from the Risk Oracle to the Risk Steward even in case of some downtime on the automation infra via a manual trigger.

The Stewards Injector Guardian is an entity, which is the owner of the `AaveStewardsInjector` contract and has access to whitelist / blacklist assets on which update can be performed upon using the `whitelistAddress()` method. In case of any emergencies, the `AaveStewardsInjector` also has access to disable updates for the specific `updateId` using the `disableUpdateById()` method. The guardian can also change / add update types using the `addUpdateType()` method, to whitelist the type of updates which can be injected via the Stewards Injector.

The `AaveStewardsInjector` contract also introduces an `EXPIRATION_PERIOD` to disallow outdated risk param updates to be injected. The `EXPIRATION_PERIOD` is set to 6 hours, which means after an update pushed on the Edge Risk Oracle, the `AaveStewardsInjector` has a maximum of 6 hours to inject the update onto the Risk Steward otherwise the update expires.

## Security

- Certora security review: [2024-07-10](./audits/10-07-2024_Certora_AaveV3-Risk-Steward.pdf)
Expand Down
Binary file added docs/agrs-edge.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ src = 'src'
test = 'tests'
script = 'scripts'
out = 'out'
solc = '0.8.19'
solc = '0.8.20'
libs = ['lib']
remappings = [
]
Expand All @@ -16,7 +16,7 @@ src = 'zksync'
test = 'zksync'
script = 'scripts'
libs = ['lib']
solc = '0.8.19'
solc = '0.8.20'
fs_permissions = [{ access = "write", path = "./reports" }]
ffi = true
evm_version = 'paris'
Expand Down
4 changes: 3 additions & 1 deletion remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ aave-helpers/=lib/aave-helpers/
forge-std/=lib/aave-helpers/lib/forge-std/src/
aave-address-book/=lib/aave-helpers/lib/aave-address-book/src/
solidity-utils/=lib/aave-helpers/lib/solidity-utils/src/
aave-v3-origin/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-origin/src/
lib/aave-helpers/lib/aave-address-book/lib/aave-v3-origin:solidity-utils/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-origin/lib/solidity-utils/src
lib/aave-helpers:aave-v3-origin/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-origin/src
aave-v3-origin/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-origin/
aave-v3-core/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-origin/src/core
aave-v3-periphery/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-origin/src/periphery
aave-capo/=lib/aave-capo/src
Expand Down
88 changes: 88 additions & 0 deletions scripts/deploy/DeployInjector.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// 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 = address(32); // TODO

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
) internal returns (address) {
address stewardInjector = ICreate3Factory(MiscEthereum.CREATE_3_FACTORY).create(
salt,
abi.encodePacked(
type(AaveStewardInjector).creationCode,
abi.encode(
EDGE_RISK_ORACLE,
riskSteward,
guardian
)
)
);
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 {
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
);

address stewardsInjector = DeployStewardContracts._deployStewardsInjector(salt, riskSteward, msg.sender);
IAaveStewardInjector(stewardsInjector).whitelistAddress(AaveV3EthereumLidoAssets.WETH_UNDERLYING, true);
IAaveStewardInjector(stewardsInjector).addUpdateType('RateStrategyUpdate', true);
vm.stopBroadcast();
}
}
20 changes: 10 additions & 10 deletions scripts/deploy/DeployStewards.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,18 @@ library DeployRiskStewards {

function _getRiskConfig() internal pure returns (IRiskSteward.Config memory) {
return IRiskSteward.Config({
ltv: IRiskSteward.RiskParamConfig({minDelay: 3 days, maxPercentChange: 3_00}),
liquidationThreshold: IRiskSteward.RiskParamConfig({minDelay: 3 days, maxPercentChange: 3_00}),
liquidationBonus: IRiskSteward.RiskParamConfig({minDelay: 3 days, maxPercentChange: 2_00}),
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: 100_00}),
borrowCap: IRiskSteward.RiskParamConfig({minDelay: 3 days, maxPercentChange: 100_00}),
debtCeiling: IRiskSteward.RiskParamConfig({minDelay: 3 days, maxPercentChange: 100_00}),
baseVariableBorrowRate: IRiskSteward.RiskParamConfig({minDelay: 3 days, maxPercentChange: 2_00}),
variableRateSlope1: IRiskSteward.RiskParamConfig({minDelay: 3 days, maxPercentChange: 2_00}),
variableRateSlope2: IRiskSteward.RiskParamConfig({minDelay: 3 days, maxPercentChange: 20_00}),
optimalUsageRatio: IRiskSteward.RiskParamConfig({minDelay: 3 days, maxPercentChange: 10_00}),
priceCapLst: IRiskSteward.RiskParamConfig({minDelay: 3 days, maxPercentChange: 15_00}),
priceCapStable: IRiskSteward.RiskParamConfig({minDelay: 3 days, maxPercentChange: 2_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})
});
}
}
Expand Down
175 changes: 175 additions & 0 deletions src/contracts/AaveStewardInjector.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// SPDX-License-Identifier: BUSL-1.1
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 {IAaveV3ConfigEngine as IEngine} from 'aave-v3-origin/src/periphery/contracts/v3-config-engine/AaveV3ConfigEngine.sol';
import {Ownable} from 'solidity-utils/contracts/oz-common/Ownable.sol';

/**
* @title AaveStewardInjector
* @author BGD Labs
* @notice 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.
* - injectes risk updates on the risk steward if all conditions are met.
*/
contract AaveStewardInjector is Ownable, IAaveStewardInjector {
/// @inheritdoc IAaveStewardInjector
address public immutable RISK_ORACLE;

/// @inheritdoc IAaveStewardInjector
address public immutable RISK_STEWARD;

/**
* @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;

/**
* @inheritdoc IAaveStewardInjector
* @dev maximum number of updateIds to check before the latest updateCounter, if they could be injected.
* from the latest updateId we check 10 more updateIds to be sure that no update is being unchecked.
*/
uint256 public constant MAX_SKIP = 10;

mapping(uint256 => bool) internal _isUpdateIdExecuted;
mapping(uint256 => bool) internal _disabledUpdates;
mapping(address => bool) internal _isWhitelistedAddress;
mapping(string => bool) internal _isValidUpdateType;

/**
* @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.
*/
constructor(address riskOracle, address riskSteward, address guardian) {
RISK_ORACLE = riskOracle;
RISK_STEWARD = riskSteward;
_transferOwnership(guardian);
}

/**
* @inheritdoc AutomationCompatibleInterface
* @dev run off-chain, checks if updates from risk oracle should be injected on risk steward
*/
function checkUpkeep(bytes memory) public view virtual override returns (bool, bytes memory) {
uint256 latestUpdateId = IRiskOracle(RISK_ORACLE).updateCounter();
uint256 updateIdLowerBound = latestUpdateId <= MAX_SKIP ? 1 : latestUpdateId - MAX_SKIP;

for (uint256 i = latestUpdateId; i >= updateIdLowerBound; i--) {
if (_canUpdateBeInjected(i, false)) return (true, abi.encode(i));
}

return (false, '');
}

/**
* @inheritdoc AutomationCompatibleInterface
* @dev executes injection of an update from the risk oracle into the risk steward.
* @param performData encoded updateId to inject into the risk steward.
*/
function performUpkeep(bytes calldata performData) external override {
uint256 updateIdToExecute = abi.decode(performData, (uint256));

if (!_canUpdateBeInjected(updateIdToExecute, true)) {
revert UpdateCannotBeInjected();
}

IRiskOracle.RiskParameterUpdate memory riskParams = IRiskOracle(RISK_ORACLE).getUpdateById(updateIdToExecute);
IRiskSteward(RISK_STEWARD).updateRates(_repackRateUpdate(riskParams));
_isUpdateIdExecuted[updateIdToExecute] = true;

emit ActionSucceeded(updateIdToExecute);
}

/// @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 addUpdateType(string memory updateType, bool isValid) external onlyOwner {
_isValidUpdateType[updateType] = isValid;
emit UpdateTypeChanged(updateType, isValid);
}

/// @inheritdoc IAaveStewardInjector
function isValidUpdateType(string memory updateType) public view returns (bool) {
return _isValidUpdateType[updateType];
}

/// @inheritdoc IAaveStewardInjector
function whitelistAddress(address contractAddress, bool isWhitelisted) external onlyOwner {
_isWhitelistedAddress[contractAddress] = isWhitelisted;
emit AddressWhitelisted(contractAddress, isWhitelisted);
}

/// @inheritdoc IAaveStewardInjector
function isWhitelistedAddress(address contractAddress) public view returns (bool) {
return _isWhitelistedAddress[contractAddress];
}

/// @inheritdoc IAaveStewardInjector
function isUpdateIdExecuted(uint256 updateid) public view returns (bool) {
return _isUpdateIdExecuted[updateid];
}

/**
* @notice method to check if the updateId from risk oracle could be injected into the risk steward.
* @param updateId the id from the risk oralce to check if it can be injected.
* @param validateIfNewUpdatesExecuted if true, we validate that all updates after current are executed, false otherwise.
* @return true if the update could be injected to the risk steward, false otherwise.
*/
function _canUpdateBeInjected(
uint256 updateId,
bool validateIfNewUpdatesExecuted
) internal view returns (bool) {
IRiskOracle.RiskParameterUpdate memory riskParams = IRiskOracle(RISK_ORACLE).getUpdateById(
updateId
);

if (validateIfNewUpdatesExecuted) {
uint256 latestUpdateId = IRiskOracle(RISK_ORACLE).updateCounter();
if (updateId > latestUpdateId) return false;

// validate that the latest updates are executed before exeucting the current update
if (updateId != latestUpdateId) {
for (uint256 i = updateId + 1; i <= latestUpdateId; i++) {
if (!isUpdateIdExecuted(i) && !isDisabled(i)) return false;
}
}
}

return (
!isUpdateIdExecuted(updateId) &&
(riskParams.timestamp + EXPIRATION_PERIOD > block.timestamp) &&
isWhitelistedAddress(riskParams.market) &&
isValidUpdateType(riskParams.updateType) &&
!isDisabled(updateId)
);
}

/**
* @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 the repacked risk update in the format of the risk steward.
*/
function _repackRateUpdate(
IRiskOracle.RiskParameterUpdate memory riskParams
) internal pure returns (IEngine.RateStrategyUpdate[] memory) {
IEngine.RateStrategyUpdate[] memory rateUpdate = new IEngine.RateStrategyUpdate[](1);
rateUpdate[0].asset = riskParams.market;
rateUpdate[0].params = abi.decode(riskParams.newValue, (IEngine.InterestRateInputData));
return rateUpdate;
}
}
Loading
Loading