-
Notifications
You must be signed in to change notification settings - Fork 40
Add Aave V4 Fee Minter Keeper #1059
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| No configuration changes detected. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.0; | ||
|
|
||
| import {SafeERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol'; | ||
| import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; | ||
| import {Roles} from 'aave-v4/deployments/utils/libraries/Roles.sol'; | ||
| import {AaveV3Ethereum, AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; | ||
| import {AaveV4Ethereum, AaveV4EthereumHubs} from 'aave-address-book/AaveV4Ethereum.sol'; | ||
| import {IHub} from 'aave-address-book/AaveV4.sol'; | ||
| import {GovernanceV3Ethereum} from 'aave-address-book/GovernanceV3Ethereum.sol'; | ||
| import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; | ||
| import {IProposalGenericExecutor} from 'aave-helpers/src/interfaces/IProposalGenericExecutor.sol'; | ||
| import {CollectorUtils, ICollector} from 'aave-helpers/src/CollectorUtils.sol'; | ||
|
|
||
| import {IAaveCLRobotOperator} from 'src/interfaces/IAaveCLRobotOperator.sol'; | ||
| import {FeeSharesMinterBase} from './dependencies/FeeSharesMinterBase.sol'; | ||
|
|
||
| /** | ||
| * @title Register FeeSharesMinter Keeper | ||
| * @author Aave Labs | ||
| * - Snapshot: TODO | ||
| * - Discussion: TODO | ||
| */ | ||
| contract AaveV4Ethereum_RegisterFeeSharesMinterKeeper_20260409 is IProposalGenericExecutor { | ||
| using SafeERC20 for IERC20; | ||
| using CollectorUtils for ICollector; | ||
|
|
||
| uint96 public constant LINK_AMOUNT = 200 ether; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. prefer not to use ether units for non ethereum assets (just imo)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also wondering on general costs, how long 200e18 worth of LINK will last here
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the amount should be enough to start with, should last quite a while, as the actions are not that gas expensive. But would be good to have a real estimate ofc, depending on the number of reserves to cover. |
||
| uint256 public constant TOTAL_KEEPERS = 31; | ||
| uint32 public constant KEEPER_GAS_LIMIT = 100_000; | ||
| uint16 public constant MIN_ACCRUED_FEES_PERCENT = 5_00; // 5% // TODO: Verify initial config | ||
|
|
||
| function execute() external { | ||
| // TODO: Replace this deployment with the pre-deployed address once available. | ||
| address feeSharesMinter = address(new FeeSharesMinterBase(GovernanceV3Ethereum.EXECUTOR_LVL_1)); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rm please, let's have it deployed with the Deploy Engine in aave-v4 and set as a constant here.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. todo: add this to deploy engine for me |
||
|
|
||
| AaveV4Ethereum.ACCESS_MANAGER.grantRole(Roles.HUB_FEE_MINTER_ROLE, feeSharesMinter, 0); | ||
|
|
||
| _configureHubAssets(AaveV4EthereumHubs.CORE_HUB, feeSharesMinter); | ||
| _configureHubAssets(AaveV4EthereumHubs.PLUS_HUB, feeSharesMinter); | ||
| _configureHubAssets(AaveV4EthereumHubs.PRIME_HUB, feeSharesMinter); | ||
|
|
||
| uint256 withdrawnBalance = AaveV3Ethereum.COLLECTOR.withdrawFromV3( | ||
| CollectorUtils.IOInput({ | ||
| pool: address(AaveV3Ethereum.POOL), | ||
| underlying: AaveV3EthereumAssets.LINK_UNDERLYING, | ||
| amount: LINK_AMOUNT | ||
| }), | ||
| address(this) | ||
| ); | ||
| IERC20(AaveV3EthereumAssets.LINK_UNDERLYING).forceApprove( | ||
| MiscEthereum.AAVE_CL_ROBOT_OPERATOR, | ||
| withdrawnBalance | ||
| ); | ||
|
Comment on lines
+43
to
+54
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's put this in the pre hook |
||
|
|
||
| uint96 linkPerKeeper = uint96(withdrawnBalance / TOTAL_KEEPERS); | ||
| uint256 keepersRegistered; | ||
|
|
||
| keepersRegistered = _registerHubKeepers( | ||
| AaveV4EthereumHubs.CORE_HUB, | ||
| feeSharesMinter, | ||
| linkPerKeeper, | ||
| keepersRegistered, | ||
| withdrawnBalance | ||
| ); | ||
| keepersRegistered = _registerHubKeepers( | ||
| AaveV4EthereumHubs.PLUS_HUB, | ||
| feeSharesMinter, | ||
| linkPerKeeper, | ||
| keepersRegistered, | ||
| withdrawnBalance | ||
| ); | ||
| keepersRegistered = _registerHubKeepers( | ||
| AaveV4EthereumHubs.PRIME_HUB, | ||
| feeSharesMinter, | ||
| linkPerKeeper, | ||
| keepersRegistered, | ||
| withdrawnBalance | ||
| ); | ||
| } | ||
|
|
||
| function _registerHubKeepers( | ||
| IHub hub, | ||
| address feeSharesMinter, | ||
| uint96 linkPerKeeper, | ||
| uint256 keepersRegistered, | ||
| uint256 withdrawnBalance | ||
| ) internal returns (uint256) { | ||
| uint256 assetCount = hub.getAssetCount(); | ||
| for (uint256 assetId; assetId < assetCount; ++assetId) { | ||
| bool isLast = keepersRegistered == TOTAL_KEEPERS - 1; | ||
|
|
||
| IAaveCLRobotOperator(MiscEthereum.AAVE_CL_ROBOT_OPERATOR).register( | ||
| 'FeeSharesMinter', | ||
| feeSharesMinter, | ||
| abi.encode(address(hub), assetId), | ||
| KEEPER_GAS_LIMIT, | ||
| isLast | ||
| ? uint96(withdrawnBalance) - linkPerKeeper * uint96(keepersRegistered) | ||
| : linkPerKeeper, | ||
| 0, | ||
| '' | ||
| ); | ||
|
|
||
| ++keepersRegistered; | ||
| } | ||
|
|
||
| return keepersRegistered; | ||
| } | ||
|
|
||
| function _configureHubAssets(IHub hub, address feeSharesMinter) internal { | ||
| FeeSharesMinterBase minter = FeeSharesMinterBase(feeSharesMinter); | ||
| uint256 assetCount = hub.getAssetCount(); | ||
| for (uint256 assetId; assetId < assetCount; ++assetId) { | ||
| minter.setConfig(address(hub), assetId, MIN_ACCRUED_FEES_PERCENT); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.0; | ||
|
|
||
| import {Vm} from 'forge-std/Vm.sol'; | ||
| import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; | ||
| import {Roles} from 'aave-v4/deployments/utils/libraries/Roles.sol'; | ||
| import {AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; | ||
| import {AaveV4Ethereum, AaveV4EthereumHubs} from 'aave-address-book/AaveV4Ethereum.sol'; | ||
| import {IHub} from 'aave-address-book/AaveV4.sol'; | ||
| import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; | ||
| import {ProtocolV4TestBase} from 'src/helpers/v4/tests/utils/ProtocolV4TestBase.sol'; | ||
| import {AaveV4EthereumSpokes, AaveV4EthereumTokenizationSpokes} from 'src/20260319_AaveV4Ethereum_ActivateV4Ethereum/AaveV4EthereumAddresses.sol'; | ||
|
|
||
| import {IAaveCLRobotOperator} from 'src/interfaces/IAaveCLRobotOperator.sol'; | ||
| import {IFeeSharesMinterBase} from 'src/interfaces/IFeeSharesMinterBase.sol'; | ||
| import {AaveV4Ethereum_RegisterFeeSharesMinterKeeper_20260409} from './AaveV4Ethereum_RegisterFeeSharesMinterKeeper_20260409.sol'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. consistent imports |
||
|
|
||
| /** | ||
| * @dev Test for AaveV4Ethereum_RegisterFeeSharesMinterKeeper_20260409 | ||
| * command: FOUNDRY_PROFILE=test forge test --match-path=src/20260409_AaveV4Ethereum_RegisterFeeSharesMinterKeeper/AaveV4Ethereum_RegisterFeeSharesMinterKeeper_20260409.t.sol -vv | ||
| */ | ||
| contract AaveV4Ethereum_RegisterFeeSharesMinterKeeper_20260409_Test is ProtocolV4TestBase { | ||
| AaveV4Ethereum_RegisterFeeSharesMinterKeeper_20260409 internal proposal; | ||
|
|
||
| function setUp() public { | ||
| vm.createSelectFork(vm.rpcUrl('mainnet'), 24845913); | ||
| proposal = new AaveV4Ethereum_RegisterFeeSharesMinterKeeper_20260409(); | ||
| } | ||
|
|
||
| /** | ||
| * @dev executes the generic test suite with config snapshots (e2e disabled to stay within gas limits) | ||
| */ | ||
| function test_defaultProposalExecution() public { | ||
| defaultTest( | ||
| 'AaveV4Ethereum_RegisterFeeSharesMinterKeeper_20260409', | ||
| AaveV4EthereumSpokes.getUserSpokes(), | ||
| AaveV4EthereumTokenizationSpokes.getTokenizationSpokes(), | ||
| address(proposal), | ||
| false | ||
| ); | ||
| } | ||
|
|
||
| function test_hubAssetCounts() public view { | ||
| assertEq(AaveV4EthereumHubs.CORE_HUB.getAssetCount(), 17, 'Core Hub asset count'); | ||
| assertEq(AaveV4EthereumHubs.PLUS_HUB.getAssetCount(), 7, 'Plus Hub asset count'); | ||
| assertEq(AaveV4EthereumHubs.PRIME_HUB.getAssetCount(), 7, 'Prime Hub asset count'); | ||
|
|
||
| uint256 totalAssets = AaveV4EthereumHubs.CORE_HUB.getAssetCount() + | ||
| AaveV4EthereumHubs.PLUS_HUB.getAssetCount() + | ||
| AaveV4EthereumHubs.PRIME_HUB.getAssetCount(); | ||
| assertEq(totalAssets, proposal.TOTAL_KEEPERS(), 'Total assets across all hubs'); | ||
| } | ||
|
|
||
| function test_feeMinterRoleGranted() public { | ||
| uint64 roleId = Roles.HUB_FEE_MINTER_ROLE; | ||
|
|
||
| vm.recordLogs(); | ||
| executePayload(vm, address(proposal)); | ||
| Vm.Log[] memory logs = vm.getRecordedLogs(); | ||
|
|
||
| address minterAddress = _getMinterAddressFromLogs(logs); | ||
|
|
||
| (bool hasRole, ) = AaveV4Ethereum.ACCESS_MANAGER.hasRole(roleId, minterAddress); | ||
| assertTrue(hasRole, 'FeeSharesMinter should have HUB_FEE_MINTER_ROLE'); | ||
| } | ||
|
|
||
| function test_keepersRegisteredForAllHubAssets() public { | ||
| bytes32 keeperRegisteredSelector = IAaveCLRobotOperator.KeeperRegistered.selector; | ||
|
|
||
| vm.recordLogs(); | ||
| executePayload(vm, address(proposal)); | ||
| Vm.Log[] memory logs = vm.getRecordedLogs(); | ||
|
|
||
| uint256 keeperCount; | ||
| uint256 totalLinkFunded; | ||
| address upkeepAddress; | ||
|
|
||
| for (uint256 i; i < logs.length; ++i) { | ||
| if ( | ||
| logs[i].emitter == MiscEthereum.AAVE_CL_ROBOT_OPERATOR && | ||
| logs[i].topics[0] == keeperRegisteredSelector | ||
| ) { | ||
| keeperCount++; | ||
| address upkeep = address(uint160(uint256(logs[i].topics[2]))); | ||
| uint96 amount = uint96(uint256(logs[i].topics[3])); | ||
|
|
||
| if (upkeepAddress == address(0)) { | ||
| upkeepAddress = upkeep; | ||
| } | ||
| assertEq(upkeep, upkeepAddress, 'All keepers should use the same upkeep contract'); | ||
| assertGt(amount, 0, 'Each keeper should be funded with LINK'); | ||
| totalLinkFunded += amount; | ||
| } | ||
| } | ||
|
|
||
| assertEq(keeperCount, proposal.TOTAL_KEEPERS(), 'Wrong number of keepers registered'); | ||
| assertEq( | ||
| totalLinkFunded, | ||
| proposal.LINK_AMOUNT(), | ||
| 'Total LINK funded should match withdrawn amount' | ||
| ); | ||
| } | ||
|
|
||
| function test_configSetOnAllAssets() public { | ||
| vm.recordLogs(); | ||
| executePayload(vm, address(proposal)); | ||
| Vm.Log[] memory logs = vm.getRecordedLogs(); | ||
|
|
||
| address minterAddress = _getMinterAddressFromLogs(logs); | ||
| IFeeSharesMinterBase minter = IFeeSharesMinterBase(minterAddress); | ||
|
|
||
| IHub[3] memory hubs = [ | ||
| AaveV4EthereumHubs.CORE_HUB, | ||
| AaveV4EthereumHubs.PLUS_HUB, | ||
| AaveV4EthereumHubs.PRIME_HUB | ||
| ]; | ||
| for (uint256 i; i < hubs.length; ++i) { | ||
| uint256 assetCount = hubs[i].getAssetCount(); | ||
| for (uint256 assetId; assetId < assetCount; ++assetId) { | ||
| assertEq( | ||
| minter.getConfig(address(hubs[i]), assetId), | ||
| proposal.MIN_ACCRUED_FEES_PERCENT(), | ||
| 'Config should be set to 5%' | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function test_performUpkeepMintsFeeShares() public { | ||
| vm.recordLogs(); | ||
| executePayload(vm, address(proposal)); | ||
| Vm.Log[] memory logs = vm.getRecordedLogs(); | ||
|
|
||
| address minterAddress = _getMinterAddressFromLogs(logs); | ||
| IFeeSharesMinterBase minter = IFeeSharesMinterBase(minterAddress); | ||
| IHub hub = AaveV4EthereumHubs.CORE_HUB; | ||
| uint256 assetId = 0; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know if I'm comfortable with us only testing 1 specific asset one Core Hub, I'd prefer we have multiple scenarios, especially since the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. agree, esp with high/low decimals and value |
||
|
|
||
| skip(20000 days); | ||
|
|
||
| uint256 accruedFeesBefore = hub.getAssetAccruedFees(assetId); | ||
| uint256 addedSharesBefore = hub.getAddedShares(assetId); | ||
|
|
||
| assertGt(accruedFeesBefore, 0, 'Accrued fees should be non-zero before upkeep'); | ||
|
|
||
| (bool upkeepNeeded, bytes memory performData) = minter.checkUpkeep( | ||
| abi.encode(address(hub), assetId) | ||
| ); | ||
|
|
||
| assertTrue(upkeepNeeded, 'Upkeep should be needed after sufficient time'); | ||
| minter.performUpkeep(performData); | ||
| uint256 gasUsed = vm.snapshotGasLastCall('performUpkeep'); | ||
| assertLt(gasUsed, 100_000, 'performUpkeep should use less than 100k gas'); | ||
|
|
||
| uint256 accruedFeesAfter = hub.getAssetAccruedFees(assetId); | ||
| uint256 addedSharesAfter = hub.getAddedShares(assetId); | ||
|
|
||
| assertEq(accruedFeesAfter, 0, 'Accrued fees should be zero after minting fee shares'); | ||
| assertGt(addedSharesAfter, addedSharesBefore, 'Total added shares should increase'); | ||
| } | ||
|
|
||
| function test_noLinkRemainsOnProposal() public { | ||
| executePayload(vm, address(proposal)); | ||
|
|
||
| assertEq( | ||
| IERC20(AaveV3EthereumAssets.LINK_UNDERLYING).balanceOf(address(proposal)), | ||
| 0, | ||
| 'No LINK should remain on proposal contract' | ||
| ); | ||
| } | ||
|
|
||
| function _getMinterAddressFromLogs(Vm.Log[] memory logs) internal pure returns (address) { | ||
| bytes32 keeperRegisteredSelector = IAaveCLRobotOperator.KeeperRegistered.selector; | ||
| for (uint256 i; i < logs.length; ++i) { | ||
| if ( | ||
| logs[i].emitter == MiscEthereum.AAVE_CL_ROBOT_OPERATOR && | ||
| logs[i].topics[0] == keeperRegisteredSelector | ||
| ) { | ||
| return address(uint160(uint256(logs[i].topics[2]))); | ||
| } | ||
| } | ||
| revert('No KeeperRegistered event found'); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| --- | ||
| title: "Register FeeSharesMinter Keeper" | ||
| author: "Aave Labs" | ||
| discussions: "TODO" | ||
| snapshot: "TODO" | ||
| --- | ||
|
|
||
| ## Simple Summary | ||
|
|
||
| Register the FeeSharesMinter contract as Chainlink Automation keepers via the Aave CL Robot Operator for all assets across all three V4 Ethereum hubs (Core, Plus, Prime), funded with 200 LINK from the Aave Collector. | ||
|
|
||
| ## Motivation | ||
|
|
||
| TODO | ||
|
|
||
| ## Specification | ||
|
|
||
| - Deploy the FeeSharesMinterBase contract owned by the governance executor | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. above it is mentioned as being pre-deployed prior to this aip? |
||
| - Grant the `HUB_FEE_MINTER_ROLE` to the FeeSharesMinterBase via the V4 AccessManager | ||
| - Configure all hub assets with a 5% minimum accrued fees threshold | ||
| - Withdraw 200 LINK from the Aave V3 Collector | ||
| - Register a Chainlink keeper for each asset on each V4 hub (Core, Plus, Prime) — 31 keepers total | ||
| - LINK is distributed evenly across all keepers, each configured with a 100,000 gas limit | ||
|
|
||
| ## References | ||
|
|
||
| - Implementation: [AaveV4Ethereum](https://github.com/aave-dao/aave-proposals-v3/blob/main/src/20260409_AaveV4Ethereum_RegisterFeeSharesMinterKeeper/AaveV4Ethereum_RegisterFeeSharesMinterKeeper_20260409.sol) | ||
| - Tests: [AaveV4Ethereum](https://github.com/aave-dao/aave-proposals-v3/blob/main/src/20260409_AaveV4Ethereum_RegisterFeeSharesMinterKeeper/AaveV4Ethereum_RegisterFeeSharesMinterKeeper_20260409.t.sol) | ||
| - [Snapshot](TODO) | ||
| - [Discussion](TODO) | ||
|
|
||
| ## Copyright | ||
|
|
||
| Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's have consistent import patterns