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