Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c33f294
feat: aToken Vault Revenut-Splitter Owner - Initial impl
donosonaumczuk Jul 17, 2025
3db0ba1
misc: Some TODO comments removed
donosonaumczuk Jul 18, 2025
0260658
misc: Natspec added
donosonaumczuk Jul 18, 2025
4a21714
misc: Little comment ammend
donosonaumczuk Jul 18, 2025
af9ec4e
test: Some ATokenVaultRevenueSplitterOwner tests added - still some c…
donosonaumczuk Jul 18, 2025
15be1cb
feat: Do not allow empty recipients for the revenue splitter
donosonaumczuk Jul 21, 2025
894b3cf
test: Constructor params and recipients setup tested
donosonaumczuk Jul 21, 2025
0d52af1
test: ERC-20 split revenue tested
donosonaumczuk Jul 21, 2025
54ef64b
test: Native currency split revenue tested
donosonaumczuk Jul 21, 2025
df20ef9
test: Fuzzed BPS shares test added
donosonaumczuk Jul 21, 2025
0f33b6e
test: Test that some basic reentrancy fails
donosonaumczuk Jul 21, 2025
badb787
misc: RevenueSplit event renamed to RevenueSplitTransferred
donosonaumczuk Jul 21, 2025
1eedf5f
feat: Native currency revenue split removed
donosonaumczuk Jul 21, 2025
c02d423
feat: Revoke support for native currency
donosonaumczuk Jul 21, 2025
efd4dff
misc: Comments added and improved
donosonaumczuk Jul 21, 2025
cc16333
feat: Do not accept address(0) as recipient
donosonaumczuk Jul 31, 2025
460fe18
feat: Call withdrawFees only when getClaimableFees > 0
donosonaumczuk Jul 31, 2025
acb49b1
feat: Calculate amount to split based on prev accumulated balance & o…
donosonaumczuk Jul 31, 2025
3922643
feat: Leave unit undistributed to avoid aToken transfer issues
donosonaumczuk Aug 1, 2025
193eea4
misc: Comment punctuation & author correction
donosonaumczuk Aug 1, 2025
c68956b
test: ATokenVaultRevenueSplitterOwnerTest tests fixed after changes
donosonaumczuk Aug 1, 2025
730483b
test: Rounding error is not accumulated after many splits
donosonaumczuk Aug 1, 2025
657db34
feat: Do not allow to split assets with 0 balance
donosonaumczuk Aug 1, 2025
ddca6ca
feat: Disallow duplicated recipients
donosonaumczuk Aug 14, 2025
8243213
test: Tests added for duplicated recipients check
donosonaumczuk Aug 15, 2025
05ae204
misc: Comment related to undistributed dust simplified
donosonaumczuk Aug 15, 2025
080865b
feat: Merge changes after audit round
donosonaumczuk Aug 15, 2025
817a624
chore: Audit report added for aToken Vault Revenue Splitter
donosonaumczuk Aug 21, 2025
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
189 changes: 189 additions & 0 deletions src/ATokenVaultRevenueSplitterOwner.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// SPDX-License-Identifier: UNLICENSED
// All Rights Reserved © AaveCo

pragma solidity ^0.8.10;

import {Ownable} from "@openzeppelin/access/Ownable.sol";
import {IATokenVault} from "./interfaces/IATokenVault.sol";
import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/token/ERC20/utils/SafeERC20.sol";

/**
* @title ATokenVaultRevenueSplitterOwner
* @author Aave Protocol
* @notice ATokenVault owner with revenue split capabilities.
*/
contract ATokenVaultRevenueSplitterOwner is Ownable {
using SafeERC20 for IERC20;

/**
* @dev Emitted at construction time for each recipient set
* @param recipient The address of the recipient set
* @param shareInBps The recipient's share of the revenue in basis points
*/
event RecipientSet(address indexed recipient, uint16 shareInBps);

/**
* @dev Emitted when revenue is split for each recipient and asset
* @param recipient The address of the recipient receiving the revenue
* @param asset The asset being split
* @param amount The amount of revenue sent to the recipient in the split asset
*/
event RevenueSplit(address indexed recipient, address indexed asset, uint256 amount);

/**
* @dev The sum of all recipients' shares in basis points, represents 100.00%. Each basis point is 0.01%.
*/
uint256 public constant TOTAL_SHARE_IN_BPS = 10_000;

/**
* @dev The aToken Vault to own, whose revenue is split.
*/
IATokenVault public immutable VAULT;

/**
* @dev A struct to represent a recipient and its share of the revenue in basis points.
* @param addr The address of the recipient
* @param shareInBps The recipient's share of the revenue in basis points
*/
struct Recipient {
address addr;
uint16 shareInBps;
}

/**
* @dev The recipients set for this revenue splitter. Set at construction time only, cannot be modified afterwards.
*/
Recipient[] internal _recipients;

/**
* @dev Constructor.
* @param vault The address of the aToken Vault to own, whose revenue is split.
* @param owner The address owning this contract, the effective owner of the vault.
* @param recipients The recipients to set for the revenue split. Cannot be modified afterwards.
*/
constructor(address vault, address owner, Recipient[] memory recipients) {
VAULT = IATokenVault(vault);
require(recipients.length > 0, "MISSING_RECIPIENTS");
_setRecipients(recipients);
_transferOwnership(owner);
}

/**
* @dev Transfers the ownership of the aToken vault to a new owner. Claims all fees and rewards prior to transfer,
* to secure already accrued fees and rewards for the configured split recipients.
* @dev Only callable by the owner of this contract.
* @dev DO NOT confuse with `transferOwnership` which transfers the ownership of this contract instead.
* @param newVaultOwner The address of the new aToken vault owner.
*/
function transferVaultOwnership(address newVaultOwner) public onlyOwner {
_claimRewards();
_withdrawFees();
Ownable(address(VAULT)).transferOwnership(newVaultOwner);
}

/**
* @dev Withdraws all vault fees to this contract, so they can be split among the configured recipients.
*/
function withdrawFees() public {
_withdrawFees();
}

/**
* @dev Claims all vault rewards to this contract, so they can be split among the configured recipients.
*/
function claimRewards() external {
_claimRewards();
}

/**
* @dev Splits the revenue from the given assets among the configured recipients. Assets must follow the ERC-20
* interface and be held by this contract.
* @param assets The assets to split the revenue from.
*/
function splitRevenue(address[] calldata assets) public {
Recipient[] memory recipients = _recipients;
for (uint256 i = 0; i < assets.length; i++) {
uint256 amountToSplit = IERC20(assets[i]).balanceOf(address(this));
for (uint256 j = 0; j < recipients.length; j++) {
uint256 amountForRecipient = amountToSplit * recipients[j].shareInBps / TOTAL_SHARE_IN_BPS;
if (amountForRecipient > 0) {
IERC20(assets[i]).safeTransfer(recipients[j].addr, amountForRecipient);
}
emit RevenueSplit(recipients[j].addr, assets[i], amountForRecipient);
}
}
}

// TODO: Should we support splitting native currency revenue or only ERC20s?
// TODO: address(0) used in the event instead of ad-hoc event for native revenue?
// TODO: Should we assume recipients will succeed at receiving native? If one fails, the whole call fails.
/**
* @dev Splits the native currency revenue among the configured recipients.
*/
function splitRevenue() public {
uint256 amountToSplit = address(this).balance;
for (uint256 j = 0; j < _recipients.length; j++) {
uint256 amountForRecipient = amountToSplit * _recipients[j].shareInBps / TOTAL_SHARE_IN_BPS;
if (amountForRecipient > 0) {
(bool transferSucceeded, ) = _recipients[j].addr.call{value: amountForRecipient}("");
require(transferSucceeded, "NATIVE_TRANSFER_FAILED");
}
emit RevenueSplit(_recipients[j].addr, address(0), amountForRecipient);
}
}

/**
* @dev Rescues assets that may have accidentally been transferred to the vault.
* @dev Only callable by the owner of this contract.
* @dev The asset to rescue cannot be the vault's aToken.
* @dev Fees cannot be "rescued" as they are accrued in the vault's aToken. Rewards cannot be "rescued" as they are
* not held by the vault contract. Thus, already accrued fees and rewards cannot be taken from split recipients.
* @param asset The asset to rescue from the vault.
* @param to The address to send the rescued assets to.
* @param amount The amount of assets to rescue from the vault.
*/
function emergencyRescue(address asset, address to, uint256 amount) public onlyOwner {
VAULT.emergencyRescue(asset, to, amount);
}

/**
* @dev Sets the fee for the vault.
* @dev Only callable by the owner of this contract.
* @param newFee The new fee for the vault.
*/
function setFee(uint256 newFee) public onlyOwner {
VAULT.setFee(newFee);
}

/**
* @dev Getter for the revenue split configured recipients.
* @return The configured recipients with their corresponding share in basis points.
*/
function getRecipients() public view returns (Recipient[] memory) {
return _recipients;
}

function _claimRewards() internal {
VAULT.claimRewards(address(this));
}

function _withdrawFees() internal {
uint256 feesToWithdraw = VAULT.getClaimableFees();
VAULT.withdrawFees(address(this), feesToWithdraw);
}

/**
* @dev Does not check for duplicates in the recipients array. Sum of shares must represent 100.00% in basis points.
*/
function _setRecipients(Recipient[] memory recipients) internal {
uint256 accumulatedShareInBps = 0;
for (uint256 i = 0; i < recipients.length; i++) {
require(recipients[i].shareInBps > 0, "BPS_SHARE_CANNOT_BE_ZERO");
accumulatedShareInBps += recipients[i].shareInBps;
_recipients.push(recipients[i]);
emit RecipientSet(recipients[i].addr, recipients[i].shareInBps);
}
require(accumulatedShareInBps == TOTAL_SHARE_IN_BPS, "WRONG_BPS_SUM");
}
}
Loading
Loading