Skip to content
This repository was archived by the owner on Jul 15, 2024. It is now read-only.
Open
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
11 changes: 11 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/aave-v3-periphery"]
path = lib/aave-v3-periphery
url = https://github.com/aave/aave-v3-periphery
[submodule "lib/chainlink"]
path = lib/chainlink
url = https://github.com/smartcontractkit/chainlink
branch = v1.13.0
[submodule "lib/aave-helpers"]
path = lib/aave-helpers
url = https://github.com/bgd-labs/aave-helpers
[submodule "lib/aave-governance-v3-robot"]
path = lib/aave-governance-v3-robot
url = https://github.com/bgd-labs/aave-governance-v3-robot
branch = fix/operator
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ An up to date address can be fetched from the respective [address-book pool libr

The `stataToken` is not natively integrated into the aave protocol and therefore cannot hook into the emissionManager.
This means a `reward` added **after** `statToken` creation needs to be registered manually on the token via the permissionless `refreshRewardTokens()` method.
As this process is not currently automated users might be missing out on rewards until the method is called.

This process is currently automated using [RefreshRewardRobot](./src/robots/RefreshRewardsRobot.sol).

## Security procedures

Expand Down
1 change: 1 addition & 0 deletions lib/aave-governance-v3-robot
1 change: 1 addition & 0 deletions lib/chainlink
Submodule chainlink added at 594387
2 changes: 2 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ aave-helpers/=lib/aave-helpers/src/
aave-v3-core/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-core/
aave-v3-periphery/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-periphery/
@aave/periphery-v3/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-periphery/
chainlink/=lib/chainlink/contracts/
aave-governance-v3-robot/=lib/aave-governance-v3-robot/src/
108 changes: 108 additions & 0 deletions scripts/Robots.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {EthereumScript, PolygonScript, AvalancheScript, ArbitrumScript, OptimismScript, MetisScript, BaseScript, BNBScript, GnosisScript} from 'aave-helpers/ScriptUtils.sol';
import {AaveV3Ethereum, IPool} from 'aave-address-book/AaveV3Ethereum.sol';
import {AaveV3Polygon} from 'aave-address-book/AaveV3Polygon.sol';
import {AaveV3Avalanche} from 'aave-address-book/AaveV3Avalanche.sol';
import {AaveV3Optimism} from 'aave-address-book/AaveV3Optimism.sol';
import {AaveV3Arbitrum} from 'aave-address-book/AaveV3Arbitrum.sol';
import {AaveV3Metis} from 'aave-address-book/AaveV3Metis.sol';
import {AaveV3Base} from 'aave-address-book/AaveV3Base.sol';
import {AaveV3BNB} from 'aave-address-book/AaveV3BNB.sol';
import {AaveV3Gnosis} from 'aave-address-book/AaveV3Gnosis.sol';
import {GasCappedRefreshRewardsRobot} from '../src/robots/GasCappedRefreshRewardsRobot.sol';
import {GelatoGasCappedRefreshRewardsRobot} from '../src/robots/GelatoGasCappedRefreshRewardsRobot.sol';
import {RefreshRewardsRobot} from '../src/robots/RefreshRewardsRobot.sol';

// make deploy-ledger contract=scripts/Robots.s.sol:DeployMainnet chain=mainnet
contract DeployMainnet is EthereumScript {
function run() external broadcast {
GasCappedRefreshRewardsRobot robot = new GasCappedRefreshRewardsRobot(
AaveV3Ethereum.STATIC_A_TOKEN_FACTORY,
AaveV3Ethereum.DEFAULT_INCENTIVES_CONTROLLER,
0x169E633A2D1E6c10dD91238Ba11c4A708dfEF37C // chainlink fast gas feed
);
robot.setMaxGasPrice(150 gwei);
}
}

// make deploy-ledger contract=scripts/Robots.s.sol:DeployPolygon chain=polygon
contract DeployPolygon is PolygonScript {
function run() external broadcast {
new RefreshRewardsRobot(
AaveV3Polygon.STATIC_A_TOKEN_FACTORY,
AaveV3Polygon.DEFAULT_INCENTIVES_CONTROLLER
);
}
}

// make deploy-ledger contract=scripts/Robots.s.sol:DeployAvalanche chain=avalanche
contract DeployAvalanche is AvalancheScript {
function run() external broadcast {
new RefreshRewardsRobot(
AaveV3Avalanche.STATIC_A_TOKEN_FACTORY,
AaveV3Avalanche.DEFAULT_INCENTIVES_CONTROLLER
);
}
}

// make deploy-ledger contract=scripts/Robots.s.sol:DeployOptimism chain=optimism
contract DeployOptimism is OptimismScript {
function run() external broadcast {
new RefreshRewardsRobot(
AaveV3Optimism.STATIC_A_TOKEN_FACTORY,
AaveV3Optimism.DEFAULT_INCENTIVES_CONTROLLER
);
}
}

// make deploy-ledger contract=scripts/Robots.s.sol:DeployArbitrum chain=arbitrum
contract DeployArbitrum is ArbitrumScript {
function run() external broadcast {
new RefreshRewardsRobot(
AaveV3Arbitrum.STATIC_A_TOKEN_FACTORY,
AaveV3Arbitrum.DEFAULT_INCENTIVES_CONTROLLER
);
}
}

// make deploy-ledger contract=scripts/Robots.s.sol:DeployBase chain=base
contract DeployBase is BaseScript {
function run() external broadcast {
new RefreshRewardsRobot(
AaveV3Base.STATIC_A_TOKEN_FACTORY,
AaveV3Base.DEFAULT_INCENTIVES_CONTROLLER
);
}
}

// make deploy-ledger contract=scripts/Robots.s.sol:DeployBNB chain=bnb
contract DeployBNB is BNBScript {
function run() external broadcast {
new RefreshRewardsRobot(
AaveV3BNB.STATIC_A_TOKEN_FACTORY,
AaveV3BNB.DEFAULT_INCENTIVES_CONTROLLER
);
}
}

// make deploy-ledger contract=scripts/Robots.s.sol:DeployMetis chain=metis
contract DeployMetis is MetisScript {
function run() external broadcast {
new GelatoGasCappedRefreshRewardsRobot(
AaveV3Metis.STATIC_A_TOKEN_FACTORY,
AaveV3Metis.DEFAULT_INCENTIVES_CONTROLLER
);
}
}

// make deploy-ledger contract=scripts/Robots.s.sol:DeployGnosis chain=gnosis
contract DeployGnosis is GnosisScript {
function run() external broadcast {
new GelatoGasCappedRefreshRewardsRobot(
AaveV3Gnosis.STATIC_A_TOKEN_FACTORY,
AaveV3Gnosis.DEFAULT_INCENTIVES_CONTROLLER
);
}
}
50 changes: 50 additions & 0 deletions src/interfaces/IRefreshRewardsRobot.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {AutomationCompatibleInterface} from 'chainlink/src/v0.8/interfaces/automation/AutomationCompatibleInterface.sol';
import {IStaticATokenFactory} from './IStaticATokenFactory.sol';
import {IRewardsController} from 'aave-v3-periphery/contracts/rewards/interfaces/IRewardsController.sol';

/**
* @title IRefreshRewardsRobot
* @author BGD Labs
* @notice Defines the interface for the contract to automate refresh rewards for staticATokens.
**/
interface IRefreshRewardsRobot is AutomationCompatibleInterface {
/**
* @notice emitted when rewards are refreshed for a staticAToken.
* @param staticAToken address of the staticAToken for which rewards have been refreshed.
*/
event RefreshSucceeded(address indexed staticAToken);

/**
* @notice method to check if the staticAToken is disabled for automation.
* @param staticAToken staticAToken to check if disabled for refresh rewards automation.
**/
function isDisabled(address staticAToken) external view returns (bool);

/**
* @notice method to disable automation for the staticAToken.
* @param staticAToken staticAToken to disable automation for refresh rewards.
* @param disable bool true to disable automation, false to enable it back.
**/
function disableAutomation(address staticAToken, bool disable) external;

/**
* @notice method to get the maximum number of actions that can be performed by the robot in one performUpkeep.
* @return max number of actions.
*/
function MAX_ACTIONS() external returns (uint256);

/**
* @notice method to get the rewards controller of the protocol.
* @return address of the aave rewards controller.
*/
function REWARDS_CONTROLLER() external returns (IRewardsController);

/**
* @notice method to get the static a token factory.
* @return address of the static a token factory contract.
*/
function STATIC_A_TOKEN_FACTORY() external returns (IStaticATokenFactory);
}
41 changes: 41 additions & 0 deletions src/robots/GasCappedRefreshRewardsRobot.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import {AutomationCompatibleInterface} from 'chainlink/src/v0.8/interfaces/automation/AutomationCompatibleInterface.sol';
import {GasCappedRobotBase} from 'aave-governance-v3-robot/contracts/gasprice-capped-robots/GasCappedRobotBase.sol';
import {RefreshRewardsRobot} from './RefreshRewardsRobot.sol';

/**
* @title GasCappedRefreshRewardsRobot
* @author BGD Labs
* @notice Automation contract to automate refresh rewards for staticATokens if a reward
* is added after staticAToken creation to register the missing rewards.
* The difference from RefreshRewardsRobot is that automation is only
* performed when the network gas price in within the maximum configured range.
*/
contract GasCappedRefreshRewardsRobot is RefreshRewardsRobot, GasCappedRobotBase {
/**
* @param staticATokenFactory address of the static a token factory contract.
* @param rewardsController address of the rewards controller of the protocol.
* @param gasPriceOracle address of the gas price oracle contract.
*/
constructor(
address staticATokenFactory,
address rewardsController,
address gasPriceOracle
)
RefreshRewardsRobot(staticATokenFactory, rewardsController)
GasCappedRobotBase(gasPriceOracle)
{}

/**
* @inheritdoc AutomationCompatibleInterface
* @dev runs off-chain, checks if there is a reward added after statToken creation which needs to be registered.
* also checks that the gas price of the network in within range to perform actions.
*/
function checkUpkeep(bytes memory) public view virtual override returns (bool, bytes memory) {
if (!isGasPriceInRange()) return (false, '');

return super.checkUpkeep('');
}
}
40 changes: 40 additions & 0 deletions src/robots/GelatoGasCappedRefreshRewardsRobot.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import {GasCappedRefreshRewardsRobot} from './GasCappedRefreshRewardsRobot.sol';
import {IGasPriceCappedRobot} from 'aave-governance-v3-robot/interfaces/IGasPriceCappedRobot.sol';

/**
* @title GelatoGasCappedRefreshRewardsRobot
* @author BGD Labs
* @notice Automation contract to automate refresh rewards for staticATokens.
* The difference from GasCappedRefreshRewardsRobot is that we use tx.gasprice
* instead of gas price oracle in order to limit the execution of the robot.
*/
contract GelatoGasCappedRefreshRewardsRobot is GasCappedRefreshRewardsRobot {
/**
* @param staticATokenFactory address of the static a token factory contract.
* @param rewardsController address of the rewards controller of the protocol.
*/
constructor(
address staticATokenFactory,
address rewardsController
) GasCappedRefreshRewardsRobot(staticATokenFactory, rewardsController, address(0)) {}

/**
* @inheritdoc GasCappedRefreshRewardsRobot
* @dev the returned bytes is specific to gelato and is encoded with the function selector.
*/
function checkUpkeep(bytes memory) public view override returns (bool, bytes memory) {
(bool upkeepNeeded, bytes memory encodedPayloadIdsToExecute) = super.checkUpkeep('');
return (upkeepNeeded, abi.encodeCall(this.performUpkeep, encodedPayloadIdsToExecute));
}

/// @inheritdoc IGasPriceCappedRobot
function isGasPriceInRange() public view virtual override returns (bool) {
if (tx.gasprice > _maxGasPrice) {
return false;
}
return true;
}
}
97 changes: 97 additions & 0 deletions src/robots/RefreshRewardsRobot.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import {IStaticATokenFactory} from '../interfaces/IStaticATokenFactory.sol';
import {IRefreshRewardsRobot, AutomationCompatibleInterface} from '../interfaces/IRefreshRewardsRobot.sol';
import {IStaticATokenLM} from '../interfaces/IStaticATokenLM.sol';
import {IRewardsController} from 'aave-v3-periphery/contracts/rewards/interfaces/IRewardsController.sol';
import {Ownable} from 'solidity-utils/contracts/oz-common/Ownable.sol';

/**
* @title RefreshRewardsRobot
* @author BGD Labs
* @notice Automation contract to automate refresh rewards for staticATokens if a reward
* is added after staticAToken creation to register the missing rewards.
*/
contract RefreshRewardsRobot is Ownable, IRefreshRewardsRobot {
mapping(address => bool) internal disabledStaticATokens;

/// @inheritdoc IRefreshRewardsRobot
IStaticATokenFactory public immutable STATIC_A_TOKEN_FACTORY;

/// @inheritdoc IRefreshRewardsRobot
IRewardsController public immutable REWARDS_CONTROLLER;

/// @inheritdoc IRefreshRewardsRobot
uint256 public constant MAX_ACTIONS = 10;
Comment thread
sakulstra marked this conversation as resolved.

/**
* @param staticATokenFactory address of the static a token factory contract.
* @param rewardsController address of the rewards controller of the protocol.
*/
constructor(address staticATokenFactory, address rewardsController) {
STATIC_A_TOKEN_FACTORY = IStaticATokenFactory(staticATokenFactory);
REWARDS_CONTROLLER = IRewardsController(rewardsController);
}

/**
* @inheritdoc AutomationCompatibleInterface
* @dev runs off-chain, checks if there is a reward added after statToken creation which needs to be registered.
*/
function checkUpkeep(bytes memory) public view virtual override returns (bool, bytes memory) {
address[] memory staticATokensToRefresh = new address[](MAX_ACTIONS);
address[] memory staticATokens = STATIC_A_TOKEN_FACTORY.getStaticATokens();
uint256 actionsCount = 0;

for (uint i = 0; i < staticATokens.length; i++) {
address aTokenAddress = address(IStaticATokenLM(staticATokens[i]).aToken());
address[] memory rewards = REWARDS_CONTROLLER.getRewardsByAsset(aTokenAddress);

for (uint256 j = 0; j < rewards.length; j++) {
bool isRegisteredReward = IStaticATokenLM(staticATokens[i]).isRegisteredRewardToken(
rewards[j]
);
if (!isRegisteredReward && actionsCount < MAX_ACTIONS && !isDisabled(staticATokens[i])) {
staticATokensToRefresh[actionsCount] = staticATokens[i];
actionsCount++;
Comment thread
brotherlymite marked this conversation as resolved.
break;
}
}
}

if (actionsCount > 0) {
// we do not know the length in advance, so we init arrays with MAX_ACTIONS
// and then squeeze the array using mstore
assembly {
mstore(staticATokensToRefresh, actionsCount)
}
bytes memory performData = abi.encode(staticATokensToRefresh);
return (true, performData);
}

return (false, '');
}

/**
* @dev executes refreshRewardTokens() on the staticAToken to register the missing rewards
* @param performData array of staticATokens for which refresh needs to be performed
*/
function performUpkeep(bytes calldata performData) external override {
address[] memory staticATokensToRefresh = abi.decode(performData, (address[]));

for (uint256 i = 0; i < staticATokensToRefresh.length; i++) {
IStaticATokenLM(staticATokensToRefresh[i]).refreshRewardTokens();
emit RefreshSucceeded(staticATokensToRefresh[i]);
}
}

/// @inheritdoc IRefreshRewardsRobot
function isDisabled(address staticAToken) public view returns (bool) {
return disabledStaticATokens[staticAToken];
}

/// @inheritdoc IRefreshRewardsRobot
function disableAutomation(address staticAToken, bool disable) external onlyOwner {
disabledStaticATokens[staticAToken] = disable;
}
}
Loading