Skip to content

Commit 01e3f45

Browse files
feat: aToken Vault Revenue-Splitter Owner (#101)
* feat: aToken Vault Revenut-Splitter Owner - Initial impl * misc: Some TODO comments removed * misc: Natspec added * misc: Little comment ammend * test: Some ATokenVaultRevenueSplitterOwner tests added - still some core tests missing * feat: Do not allow empty recipients for the revenue splitter * test: Constructor params and recipients setup tested * test: ERC-20 split revenue tested * test: Native currency split revenue tested * test: Fuzzed BPS shares test added * test: Test that some basic reentrancy fails * misc: RevenueSplit event renamed to RevenueSplitTransferred * feat: Native currency revenue split removed * feat: Revoke support for native currency * misc: Comments added and improved * feat: Do not accept address(0) as recipient * feat: Call withdrawFees only when getClaimableFees > 0 * feat: Calculate amount to split based on prev accumulated balance & outflows * feat: Leave unit undistributed to avoid aToken transfer issues * misc: Comment punctuation & author correction * test: ATokenVaultRevenueSplitterOwnerTest tests fixed after changes * test: Rounding error is not accumulated after many splits * feat: Do not allow to split assets with 0 balance * feat: Disallow duplicated recipients * test: Tests added for duplicated recipients check * misc: Comment related to undistributed dust simplified * chore: Audit report added for aToken Vault Revenue Splitter
1 parent 5a1a25c commit 01e3f45

File tree

4 files changed

+961
-0
lines changed

4 files changed

+961
-0
lines changed
Binary file not shown.
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
// All Rights Reserved © AaveCo
3+
4+
pragma solidity ^0.8.10;
5+
6+
import {Ownable} from "@openzeppelin/access/Ownable.sol";
7+
import {IATokenVault} from "./interfaces/IATokenVault.sol";
8+
import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol";
9+
import {SafeERC20} from "@openzeppelin/token/ERC20/utils/SafeERC20.sol";
10+
11+
/**
12+
* @title ATokenVaultRevenueSplitterOwner
13+
* @author Aave Labs
14+
* @notice ATokenVault owner with revenue split capabilities.
15+
*/
16+
contract ATokenVaultRevenueSplitterOwner is Ownable {
17+
using SafeERC20 for IERC20;
18+
19+
/**
20+
* @dev Emitted at construction time for each recipient set.
21+
* @param recipient The address of the recipient set.
22+
* @param shareInBps The recipient's share of the revenue in basis points.
23+
*/
24+
event RecipientSet(address indexed recipient, uint16 shareInBps);
25+
26+
/**
27+
* @dev Emitted when revenue is split for each recipient and asset.
28+
* @param recipient The address of the recipient receiving the revenue.
29+
* @param asset The asset being split.
30+
* @param amount The amount of revenue sent to the recipient in the split asset.
31+
*/
32+
event RevenueSplitTransferred(address indexed recipient, address indexed asset, uint256 amount);
33+
34+
/**
35+
* @dev The sum of all recipients' shares in basis points, represents 100.00%. Each basis point is 0.01%.
36+
*/
37+
uint256 public constant TOTAL_SHARE_IN_BPS = 10_000;
38+
39+
/**
40+
* @dev The aToken Vault to own, whose revenue is split.
41+
*/
42+
IATokenVault public immutable VAULT;
43+
44+
/**
45+
* @dev A struct to represent a recipient and its share of the revenue in basis points.
46+
* @param addr The address of the recipient.
47+
* @param shareInBps The recipient's share of the revenue in basis points.
48+
*/
49+
struct Recipient {
50+
address addr;
51+
uint16 shareInBps;
52+
}
53+
54+
/**
55+
* @dev The recipients set for this revenue splitter. Set at construction time only, cannot be modified afterwards.
56+
*/
57+
Recipient[] internal _recipients;
58+
59+
/**
60+
* @dev Total historical amount held for a given asset in this contract.
61+
*/
62+
mapping(address => uint256) internal _previousAccumulatedBalance;
63+
64+
/**
65+
* @dev Amount already transferred for a given asset to a given recipient.
66+
*/
67+
mapping(address => mapping(address => uint256)) internal _amountAlreadyTransferred;
68+
69+
/**
70+
* @dev Constructor.
71+
* @param vault The address of the aToken Vault to own, whose revenue is split.
72+
* @param owner The address owning this contract, the effective owner of the vault.
73+
* @param recipients The recipients to set for the revenue split. Duplicates are not allowed. The recipients
74+
* configuration cannot be modified afterwards.
75+
*/
76+
constructor(address vault, address owner, Recipient[] memory recipients) {
77+
VAULT = IATokenVault(vault);
78+
require(recipients.length > 0, "MISSING_RECIPIENTS");
79+
_setRecipients(recipients);
80+
_transferOwnership(owner);
81+
}
82+
83+
/**
84+
* @dev Rejects native currency transfers.
85+
*/
86+
receive() external payable {
87+
revert("NATIVE_CURRENCY_NOT_SUPPORTED");
88+
}
89+
90+
/**
91+
* @dev Transfers the ownership of the aToken vault to a new owner. Claims all fees and rewards prior to transfer,
92+
* to secure already accrued fees and rewards for the configured split recipients.
93+
* @dev Only callable by the owner of this contract.
94+
* @dev DO NOT confuse with `transferOwnership` which transfers the ownership of this contract instead.
95+
* @param newVaultOwner The address of the new aToken vault owner.
96+
*/
97+
function transferVaultOwnership(address newVaultOwner) public onlyOwner {
98+
_claimRewards();
99+
_withdrawFees();
100+
Ownable(address(VAULT)).transferOwnership(newVaultOwner);
101+
}
102+
103+
/**
104+
* @dev Withdraws all vault fees to this contract, so they can be split among the configured recipients.
105+
*/
106+
function withdrawFees() public {
107+
_withdrawFees();
108+
}
109+
110+
/**
111+
* @dev Claims all vault rewards to this contract, so they can be split among the configured recipients.
112+
*/
113+
function claimRewards() external {
114+
_claimRewards();
115+
}
116+
117+
/**
118+
* @dev Splits the revenue from the given assets among the configured recipients. Assets must follow the ERC-20
119+
* standard and be held by this contract.
120+
* @param assets The assets to split the revenue from.
121+
*/
122+
function splitRevenue(address[] calldata assets) public {
123+
Recipient[] memory recipients = _recipients;
124+
for (uint256 i = 0; i < assets.length; i++) {
125+
uint256 assetBalance = IERC20(assets[i]).balanceOf(address(this));
126+
require(assetBalance > 0, "ASSET_NOT_HELD_BY_SPLITTER");
127+
// Decrease balance by one unit to ensure aToken transfers will not fail due to scaled balance rounding.
128+
assetBalance--;
129+
uint256 accumulatedAssetBalance = _previousAccumulatedBalance[assets[i]] + assetBalance;
130+
_previousAccumulatedBalance[assets[i]] = accumulatedAssetBalance;
131+
uint256 undistributedAmount = assetBalance;
132+
for (uint256 j = 0; j < recipients.length; j++) {
133+
/**
134+
* The `assetBalance` adjustment previously done by decrementing one unit will leave that unit of the
135+
* asset undistributed in this contract's balance.
136+
* However, due to floor-rounding in integer division, the sum of the amounts transferred may be less
137+
* than the intended total amount to split, leaving a few more units of the asset undistributed.
138+
* These units (also known as 'dust') may be distributed in the next `splitRevenue` call.
139+
*/
140+
uint256 amountForRecipient = accumulatedAssetBalance * recipients[j].shareInBps / TOTAL_SHARE_IN_BPS
141+
- _amountAlreadyTransferred[assets[i]][recipients[j].addr];
142+
if (amountForRecipient > 0) {
143+
_amountAlreadyTransferred[assets[i]][recipients[j].addr] += amountForRecipient;
144+
IERC20(assets[i]).safeTransfer(recipients[j].addr, amountForRecipient);
145+
undistributedAmount -= amountForRecipient;
146+
}
147+
emit RevenueSplitTransferred(recipients[j].addr, assets[i], amountForRecipient);
148+
}
149+
if (undistributedAmount > 0) {
150+
_previousAccumulatedBalance[assets[i]] -= undistributedAmount;
151+
}
152+
}
153+
}
154+
155+
/**
156+
* @dev Rescues assets that may have accidentally been transferred to the vault.
157+
* @dev Only callable by the owner of this contract.
158+
* @dev The asset to rescue cannot be the vault's aToken.
159+
* @dev Fees cannot be "rescued" as they are accrued in the vault's aToken. Rewards cannot be "rescued" as they are
160+
* not held by the vault contract. Thus, already accrued fees and rewards cannot be taken from split recipients.
161+
* @param asset The asset to rescue from the vault.
162+
* @param to The address to send the rescued assets to.
163+
* @param amount The amount of assets to rescue from the vault.
164+
*/
165+
function emergencyRescue(address asset, address to, uint256 amount) public onlyOwner {
166+
VAULT.emergencyRescue(asset, to, amount);
167+
}
168+
169+
/**
170+
* @dev Sets the fee for the vault.
171+
* @dev Only callable by the owner of this contract.
172+
* @param newFee The new fee for the vault.
173+
*/
174+
function setFee(uint256 newFee) public onlyOwner {
175+
VAULT.setFee(newFee);
176+
}
177+
178+
/**
179+
* @dev Getter for the revenue split configured recipients.
180+
* @return The configured recipients with their corresponding share in basis points.
181+
*/
182+
function getRecipients() public view returns (Recipient[] memory) {
183+
return _recipients;
184+
}
185+
186+
function _claimRewards() internal {
187+
VAULT.claimRewards(address(this));
188+
}
189+
190+
function _withdrawFees() internal {
191+
uint256 feesToWithdraw = VAULT.getClaimableFees();
192+
if (feesToWithdraw > 0) {
193+
VAULT.withdrawFees(address(this), feesToWithdraw);
194+
}
195+
}
196+
197+
/**
198+
* @dev Sum of shares must represent 100.00% in basis points.
199+
*/
200+
function _setRecipients(Recipient[] memory recipients) internal {
201+
uint256 accumulatedShareInBps = 0;
202+
for (uint256 i = 0; i < recipients.length; i++) {
203+
require(recipients[i].addr != address(0), "RECIPIENT_CANNOT_BE_ZERO_ADDRESS");
204+
require(recipients[i].shareInBps > 0, "BPS_SHARE_CANNOT_BE_ZERO");
205+
accumulatedShareInBps += recipients[i].shareInBps;
206+
_recipients.push(recipients[i]);
207+
emit RecipientSet(recipients[i].addr, recipients[i].shareInBps);
208+
for (uint256 j = 0; j < i; j++) {
209+
require(recipients[i].addr != recipients[j].addr, "DUPLICATED_RECIPIENT");
210+
}
211+
}
212+
require(accumulatedShareInBps == TOTAL_SHARE_IN_BPS, "WRONG_BPS_SUM");
213+
}
214+
}

0 commit comments

Comments
 (0)