Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
No configuration changes detected.
2 changes: 1 addition & 1 deletion lib/aave-helpers
5 changes: 5 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,10 @@ solidity-utils/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-origin/lib/so
aave-v3-origin/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-origin/src/
aave-v3-origin-tests/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-origin/tests/
aave-address-book/=lib/aave-helpers/lib/aave-address-book/src/
aave-v4/=lib/aave-helpers/lib/aave-address-book/lib/aave-v4/src/
lib/aave-address-book/lib/aave-v4/:src=lib/aave-helpers/lib/aave-address-book/lib/aave-v4/src
lib/aave-helpers/lib/aave-address-book/lib/aave-v4/:src=lib/aave-helpers/lib/aave-address-book/lib/aave-v4/src
aave-helpers/=lib/aave-helpers/
lib/aave-helpers/:src=lib/aave-helpers/src
openzeppelin-contracts/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/
aave-umbrella/=lib/aave-umbrella/src/contracts/
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';
Copy link
Copy Markdown
Contributor

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


/**
* @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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefer not to use ether units for non ethereum assets (just imo)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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 FeeSharesMinter is fully based on amounts now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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/).
Loading
Loading