Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/ATokenVaultFactory.sol": {
"content": "// SPDX-License-Identifier: UNLICENSED
// All Rights Reserved © AaveCo
pragma solidity ^0.8.10;
import {IERC20} from "@openzeppelin/interfaces/IERC20.sol";
import {IPoolAddressesProvider} from "@aave-v3-core/interfaces/IPoolAddressesProvider.sol";
import {TransparentUpgradeableProxy} from "@openzeppelin/proxy/transparent/TransparentUpgradeableProxy.sol";
import {ATokenVault} from "./ATokenVault.sol";
import {SafeERC20} from "@openzeppelin/token/ERC20/utils/SafeERC20.sol";
import {ProxyAdmin} from "@openzeppelin/proxy/transparent/ProxyAdmin.sol";
import {ATokenVaultRevenueSplitterOwner} from "./ATokenVaultRevenueSplitterOwner.sol";
/**
* @title ATokenVaultImplDeploymentLib
* @author Aave Labs
* @notice Library that handles the deployment of the ATokenVault implementation contract
* @dev This library is a helper to avoid holding the ATokenVault bytecode in the factory contract avoiding exceeding
* the contract size limit.
*/
library ATokenVaultImplDeploymentLib {
function deployVaultImpl(
address underlying,
uint16 referralCode,
IPoolAddressesProvider poolAddressesProvider
) external returns (address vault) {
return address(new ATokenVault(
underlying,
referralCode,
poolAddressesProvider
));
}
}
/**
* @title ATokenVaultFactory
* @author Aave Labs
* @notice Factory contract for deploying ATokenVault instances
*/
contract ATokenVaultFactory {
using SafeERC20 for IERC20;
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
/**
* @dev Emitted when a new vault is deployed
* @param vault The address of the deployed vault proxy
* @param implementation The address of the vault implementation
* @param underlying The underlying asset address
* @param deployer The address that deployed the vault
* @param params The parameters used to deploy the vault
*/
event VaultDeployed(
address indexed vault,
address indexed implementation,
address indexed underlying,
address deployer,
VaultParams params
);
/**
* @dev Emitted when a new revenue splitter owner is deployed
* @param revenueSplitterOwner The address of the deployed revenue splitter owner
* @param vault The address of the vault to split the revenue from
* @param owner The address of the owner of the revenue splitter, effective owner of the vault
* @param revenueRecipients The recipients of the revenue
*/
event RevenueSplitterOwnerDeployed(
address indexed revenueSplitterOwner,
address indexed vault,
address indexed owner,
ATokenVaultRevenueSplitterOwner.Recipient[] revenueRecipients
);
/*//////////////////////////////////////////////////////////////
CONSTANTS
//////////////////////////////////////////////////////////////*/
/// @notice Proxy admin address for all deployed vaults, with renounced ownership.
/// @dev Future version will deploy a plain immutable vault without proxy.
address internal immutable RENOUNCED_PROXY_ADMIN;
/*//////////////////////////////////////////////////////////////
STRUCTS
//////////////////////////////////////////////////////////////*/
/**
* @dev Struct containing constructor parameters for vault deployment
*/
struct VaultParams {
address underlying;
uint16 referralCode;
IPoolAddressesProvider poolAddressesProvider;
address owner;
uint256 initialFee;
string shareName;
string shareSymbol;
uint256 initialLockDeposit;
ATokenVaultRevenueSplitterOwner.Recipient[] revenueRecipients;
}
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
/**
* @dev Constructor
* @param proxyAdmin The address that will be the admin of all deployed proxies. Must have renounced ownership.
*/
constructor(address proxyAdmin) {
RENOUNCED_PROXY_ADMIN = proxyAdmin;
require(ProxyAdmin(proxyAdmin).owner() == address(0), "PROXY_ADMIN_OWNERSHIP_NOT_RENOUNCED");
}
/*//////////////////////////////////////////////////////////////
EXTERNAL FUNCTIONS
//////////////////////////////////////////////////////////////*/
/**
* @notice Deploy a new ATokenVault with the given parameters
* @param params All parameters needed for vault deployment and initialization
* @return vault The address of the deployed vault proxy
*/
function deployVault(VaultParams memory params) public returns (address vault) {
require(params.underlying != address(0), "ZERO_ADDRESS_NOT_VALID");
require(address(params.poolAddressesProvider) != address(0), "ZERO_ADDRESS_NOT_VALID");
require(params.owner != address(0), "ZERO_ADDRESS_NOT_VALID");
require(params.initialLockDeposit > 0, "ZERO_INITIAL_LOCK_DEPOSIT");
require(bytes(params.shareName).length > 0, "EMPTY_SHARE_NAME");
require(bytes(params.shareSymbol).length > 0, "EMPTY_SHARE_SYMBOL");
IERC20(params.underlying).safeTransferFrom(
msg.sender,
address(this),
params.initialLockDeposit
);
address implementation = ATokenVaultImplDeploymentLib.deployVaultImpl(
params.underlying,
params.referralCode,
params.poolAddressesProvider
);
vault = address(new TransparentUpgradeableProxy(
implementation,
RENOUNCED_PROXY_ADMIN,
""
));
address vaultOwner = params.owner;
if (params.revenueRecipients.length > 0) {
vaultOwner = _deployRevenueSplitterOwner(
vault,
params.owner,
params.revenueRecipients
);
}
IERC20(params.underlying).safeApprove(vault, params.initialLockDeposit);
ATokenVault(vault).initialize(
vaultOwner,
params.initialFee,
params.shareName,
params.shareSymbol,
params.initialLockDeposit
);
emit VaultDeployed(
vault,
implementation,
params.underlying,
msg.sender,
params
);
}
/**
* @notice Deploys a new ATokenVaultRevenueSplitterOwner with the given parameters
* @param vaultAddress The address of the vault to split the revenue from
* @param owner The address of the owner of the revenue splitter, effective owner of the vault
* @param revenueRecipients The recipients of the revenue
* @return revenueSplitter The address of the deployed revenue splitter
*/
function deployRevenueSplitterOwner(
address vaultAddress,
address owner,
ATokenVaultRevenueSplitterOwner.Recipient[] memory revenueRecipients
) external returns (address) {
return _deployRevenueSplitterOwner(vaultAddress, owner, revenueRecipients);
}
function _deployRevenueSplitterOwner(
address vaultAddress,
address owner,
ATokenVaultRevenueSplitterOwner.Recipient[] memory revenueRecipients
) internal returns (address) {
address revenueSplitter = address(new ATokenVaultRevenueSplitterOwner(vaultAddress, owner, revenueRecipients));
emit RevenueSplitterOwnerDeployed(revenueSplitter, vaultAddress, owner, revenueRecipients);
return revenueSplitter;
}
}
"
},
"lib/openzeppelin-contracts/contracts/interfaces/IERC20.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (interfaces/IERC20.sol)
pragma solidity ^0.8.0;
import "../token/ERC20/IERC20.sol";
"
},
"lib/aave-v3-core/contracts/interfaces/IPoolAddressesProvider.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.0;
/**
* @title IPoolAddressesProvider
* @author Aave
* @notice Defines the basic interface for a Pool Addresses Provider.
*/
interface IPoolAddressesProvider {
/**
* @dev Emitted when the market identifier is updated.
* @param oldMarketId The old id of the market
* @param newMarketId The new id of the market
*/
event MarketIdSet(string indexed oldMarketId, string indexed newMarketId);
/**
* @dev Emitted when the pool is updated.
* @param oldAddress The old address of the Pool
* @param newAddress The new address of the Pool
*/
event PoolUpdated(address indexed oldAddress, address indexed newAddress);
/**
* @dev Emitted when the pool configurator is updated.
* @param oldAddress The old address of the PoolConfigurator
* @param newAddress The new address of the PoolConfigurator
*/
event PoolConfiguratorUpdated(address indexed oldAddress, address indexed newAddress);
/**
* @dev Emitted when the price oracle is updated.
* @param oldAddress The old address of the PriceOracle
* @param newAddress The new address of the PriceOracle
*/
event PriceOracleUpdated(address indexed oldAddress, address indexed newAddress);
/**
* @dev Emitted when the ACL manager is updated.
* @param oldAddress The old address of the ACLManager
* @param newAddress The new address of the ACLManager
*/
event ACLManagerUpdated(address indexed oldAddress, address indexed newAddress);
/**
* @dev Emitted when the ACL admin is updated.
* @param oldAddress The old address of the ACLAdmin
* @param newAddress The new address of the ACLAdmin
*/
event ACLAdminUpdated(address indexed oldAddress, address indexed newAddress);
/**
* @dev Emitted when the price oracle sentinel is updated.
* @param oldAddress The old address of the PriceOracleSentinel
* @param newAddress The new address of the PriceOracleSentinel
*/
event PriceOracleSentinelUpdated(address indexed oldAddress, address indexed newAddress);
/**
* @dev Emitted when the pool data provider is updated.
* @param oldAddress The old address of the PoolDataProvider
* @param newAddress The new address of the PoolDataProvider
*/
event PoolDataProviderUpdated(address indexed oldAddress, address indexed newAddress);
/**
* @dev Emitted when a new proxy is created.
* @param id The identifier of the proxy
* @param proxyAddress The address of the created proxy contract
* @param implementationAddress The address of the implementation contract
*/
event ProxyCreated(
bytes32 indexed id,
address indexed proxyAddress,
address indexed implementationAddress
);
/**
* @dev Emitted when a new non-proxied contract address is registered.
* @param id The identifier of the contract
* @param oldAddress The address of the old contract
* @param newAddress The address of the new contract
*/
event AddressSet(bytes32 indexed id, address indexed oldAddress, address indexed newAddress);
/**
* @dev Emitted when the implementation of the proxy registered with id is updated
* @param id The identifier of the contract
* @param proxyAddress The address of the proxy contract
* @param oldImplementationAddress The address of the old implementation contract
* @param newImplementationAddress The address of the new implementation contract
*/
event AddressSetAsProxy(
bytes32 indexed id,
address indexed proxyAddress,
address oldImplementationAddress,
address indexed newImplementationAddress
);
/**
* @notice Returns the id of the Aave market to which this contract points to.
* @return The market id
*/
function getMarketId() external view returns (string memory);
/**
* @notice Associates an id with a specific PoolAddressesProvider.
* @dev This can be used to create an onchain registry of PoolAddressesProviders to
* identify and validate multiple Aave markets.
* @param newMarketId The market id
*/
function setMarketId(string calldata newMarketId) external;
/**
* @notice Returns an address by its identifier.
* @dev The returned address might be an EOA or a contract, potentially proxied
* @dev It returns ZERO if there is no registered address with the given id
* @param id The id
* @return The address of the registered for the specified id
*/
function getAddress(bytes32 id) external view returns (address);
/**
* @notice General function to update the implementation of a proxy registered with
* certain `id`. If there is no proxy registered, it will instantiate one and
* set as implementation the `newImplementationAddress`.
* @dev IMPORTANT Use this function carefully, only for ids that don't have an explicit
* setter function, in order to avoid unexpected consequences
* @param id The id
* @param newImplementationAddress The address of the new implementation
*/
function setAddressAsProxy(bytes32 id, address newImplementationAddress) external;
/**
* @notice Sets an address for an id replacing the address saved in the addresses map.
* @dev IMPORTANT Use this function carefully, as it will do a hard replacement
* @param id The id
* @param newAddress The address to set
*/
function setAddress(bytes32 id, address newAddress) external;
/**
* @notice Returns the address of the Pool proxy.
* @return The Pool proxy address
*/
function getPool() external view returns (address);
/**
* @notice Updates the implementation of the Pool, or creates a proxy
* setting the new `pool` implementation when the function is called for the first time.
* @param newPoolImpl The new Pool implementation
*/
function setPoolImpl(address newPoolImpl) external;
/**
* @notice Returns the address of the PoolConfigurator proxy.
* @return The PoolConfigurator proxy address
*/
function getPoolConfigurator() external view returns (address);
/**
* @notice Updates the implementation of the PoolConfigurator, or creates a proxy
* setting the new `PoolConfigurator` implementation when the function is called for the first time.
* @param newPoolConfiguratorImpl The new PoolConfigurator implementation
*/
function setPoolConfiguratorImpl(address newPoolConfiguratorImpl) external;
/**
* @notice Returns the address of the price oracle.
* @return The address of the PriceOracle
*/
function getPriceOracle() external view returns (address);
/**
* @notice Updates the address of the price oracle.
* @param newPriceOracle The address of the new PriceOracle
*/
function setPriceOracle(address newPriceOracle) external;
/**
* @notice Returns the address of the ACL manager.
* @return The address of the ACLManager
*/
function getACLManager() external view returns (address);
/**
* @notice Updates the address of the ACL manager.
* @param newAclManager The address of the new ACLManager
*/
function setACLManager(address newAclManager) external;
/**
* @notice Returns the address of the ACL admin.
* @return The address of the ACL admin
*/
function getACLAdmin() external view returns (address);
/**
* @notice Updates the address of the ACL admin.
* @param newAclAdmin The address of the new ACL admin
*/
function setACLAdmin(address newAclAdmin) external;
/**
* @notice Returns the address of the price oracle sentinel.
* @return The address of the PriceOracleSentinel
*/
function getPriceOracleSentinel() external view returns (address);
/**
* @notice Updates the address of the price oracle sentinel.
* @param newPriceOracleSentinel The address of the new PriceOracleSentinel
*/
function setPriceOracleSentinel(address newPriceOracleSentinel) external;
/**
* @notice Returns the address of the data provider.
* @return The address of the DataProvider
*/
function getPoolDataProvider() external view returns (address);
/**
* @notice Updates the address of the data provider.
* @param newDataProvider The address of the new DataProvider
*/
function setPoolDataProvider(address newDataProvider) external;
}
"
},
"lib/openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (proxy/transparent/TransparentUpgradeableProxy.sol)
pragma solidity ^0.8.0;
import "../ERC1967/ERC1967Proxy.sol";
/**
* @dev This contract implements a proxy that is upgradeable by an admin.
*
* To avoid https://medium.com/nomic-labs-blog/malicious-backdoors-in-ethereum-proxies-62629adf3357[proxy selector
* clashing], which can potentially be used in an attack, this contract uses the
* https://blog.openzeppelin.com/the-transparent-proxy-pattern/[transparent proxy pattern]. This pattern implies two
* things that go hand in hand:
*
* 1. If any account other than the admin calls the proxy, the call will be forwarded to the implementation, even if
* that call matches one of the admin functions exposed by the proxy itself.
* 2. If the admin calls the proxy, it can access the admin functions, but its calls will never be forwarded to the
* implementation. If the admin tries to call a function on the implementation it will fail with an error that says
* "admin cannot fallback to proxy target".
*
* These properties mean that the admin account can only be used for admin actions like upgrading the proxy or changing
* the admin, so it's best if it's a dedicated account that is not used for anything else. This will avoid headaches due
* to sudden errors when trying to call a function from the proxy implementation.
*
* Our recommendation is for the dedicated account to be an instance of the {ProxyAdmin} contract. If set up this way,
* you should think of the `ProxyAdmin` instance as the real administrative interface of your proxy.
*/
contract TransparentUpgradeableProxy is ERC1967Proxy {
/**
* @dev Initializes an upgradeable proxy managed by `_admin`, backed by the implementation at `_logic`, and
* optionally initialized with `_data` as explained in {ERC1967Proxy-constructor}.
*/
constructor(
address _logic,
address admin_,
bytes memory _data
) payable ERC1967Proxy(_logic, _data) {
_changeAdmin(admin_);
}
/**
* @dev Modifier used internally that will delegate the call to the implementation unless the sender is the admin.
*/
modifier ifAdmin() {
if (msg.sender == _getAdmin()) {
_;
} else {
_fallback();
}
}
/**
* @dev Returns the current admin.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-getProxyAdmin}.
*
* TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the
* https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call.
* `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103`
*/
function admin() external ifAdmin returns (address admin_) {
admin_ = _getAdmin();
}
/**
* @dev Returns the current implementation.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-getProxyImplementation}.
*
* TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the
* https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call.
* `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc`
*/
function implementation() external ifAdmin returns (address implementation_) {
implementation_ = _implementation();
}
/**
* @dev Changes the admin of the proxy.
*
* Emits an {AdminChanged} event.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-changeProxyAdmin}.
*/
function changeAdmin(address newAdmin) external virtual ifAdmin {
_changeAdmin(newAdmin);
}
/**
* @dev Upgrade the implementation of the proxy.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-upgrade}.
*/
function upgradeTo(address newImplementation) external ifAdmin {
_upgradeToAndCall(newImplementation, bytes(""), false);
}
/**
* @dev Upgrade the implementation of the proxy, and then call a function from the new implementation as specified
* by `data`, which should be an encoded function call. This is useful to initialize new storage variables in the
* proxied contract.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-upgradeAndCall}.
*/
function upgradeToAndCall(address newImplementation, bytes calldata data) external payable ifAdmin {
_upgradeToAndCall(newImplementation, data, true);
}
/**
* @dev Returns the current admin.
*/
function _admin() internal view virtual returns (address) {
return _getAdmin();
}
/**
* @dev Makes sure the admin cannot access the fallback function. See {Proxy-_beforeFallback}.
*/
function _beforeFallback() internal virtual override {
require(msg.sender != _getAdmin(), "TransparentUpgradeableProxy: admin cannot fallback to proxy target");
super._beforeFallback();
}
}
"
},
"src/ATokenVault.sol": {
"content": "// SPDX-License-Identifier: UNLICENSED
// All Rights Reserved © AaveCo
pragma solidity ^0.8.10;
import {ERC4626Upgradeable} from "@openzeppelin-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";
import {OwnableUpgradeable} from "@openzeppelin-upgradeable/access/OwnableUpgradeable.sol";
import {SafeERC20Upgradeable} from "@openzeppelin-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
import {IERC20Upgradeable} from "@openzeppelin-upgradeable/interfaces/IERC20Upgradeable.sol";
import {EIP712Upgradeable} from "@openzeppelin-upgradeable/utils/cryptography/EIP712Upgradeable.sol";
import {MathUpgradeable} from "@openzeppelin-upgradeable/utils/math/MathUpgradeable.sol";
import {IncentivizedERC20} from "@aave-v3-core/protocol/tokenization/base/IncentivizedERC20.sol";
import {IPoolAddressesProvider} from "@aave-v3-core/interfaces/IPoolAddressesProvider.sol";
import {IPool} from "@aave-v3-core/interfaces/IPool.sol";
import {IAToken} from "@aave-v3-core/interfaces/IAToken.sol";
import {DataTypes as AaveDataTypes} from "@aave-v3-core/protocol/libraries/types/DataTypes.sol";
import {WadRayMath} from "@aave-v3-core/protocol/libraries/math/WadRayMath.sol";
import {IRewardsController} from "@aave-v3-periphery/rewards/interfaces/IRewardsController.sol";
import {IATokenVault} from "./interfaces/IATokenVault.sol";
import {MetaTxHelpers} from "./libraries/MetaTxHelpers.sol";
import "./libraries/Constants.sol";
import {ATokenVaultStorage} from "./ATokenVaultStorage.sol";
/**
* @title ATokenVault
* @author Aave Protocol
* @notice An ERC-4626 vault for Aave V3, with support to add a fee on yield earned.
*/
contract ATokenVault is ERC4626Upgradeable, OwnableUpgradeable, EIP712Upgradeable, ATokenVaultStorage, IATokenVault {
using SafeERC20Upgradeable for IERC20Upgradeable;
using MathUpgradeable for uint256;
/// @inheritdoc IATokenVault
IPoolAddressesProvider public immutable POOL_ADDRESSES_PROVIDER;
/// @inheritdoc IATokenVault
IPool public immutable AAVE_POOL;
/// @inheritdoc IATokenVault
IAToken public immutable ATOKEN;
/// @inheritdoc IATokenVault
IERC20Upgradeable public immutable UNDERLYING;
/// @inheritdoc IATokenVault
uint16 public immutable REFERRAL_CODE;
/**
* @dev Constructor.
* @param underlying The underlying ERC20 asset which can be supplied to Aave
* @param referralCode The Aave referral code to use for deposits from this vault
* @param poolAddressesProvider The address of the Aave v3 Pool Addresses Provider
*/
constructor(address underlying, uint16 referralCode, IPoolAddressesProvider poolAddressesProvider) {
_disableInitializers();
POOL_ADDRESSES_PROVIDER = poolAddressesProvider;
AAVE_POOL = IPool(poolAddressesProvider.getPool());
REFERRAL_CODE = referralCode;
UNDERLYING = IERC20Upgradeable(underlying);
address aTokenAddress = AAVE_POOL.getReserveData(address(underlying)).aTokenAddress;
require(aTokenAddress != address(0), "ASSET_NOT_SUPPORTED");
ATOKEN = IAToken(aTokenAddress);
}
/**
* @notice Initializes the vault, setting the initial parameters and initializing inherited contracts.
* @dev It requires an initial non-zero deposit to prevent a frontrunning attack (in underlying tokens). Note
* that care should be taken to provide a non-trivial amount, but this depends on the underlying asset's decimals.
* @dev It does not initialize the OwnableUpgradeable contract to avoid setting the proxy admin as the owner.
* @param owner The owner to set
* @param initialFee The initial fee to set, expressed in wad, where 1e18 is 100%
* @param shareName The name to set for this vault
* @param shareSymbol The symbol to set for this vault
* @param initialLockDeposit The initial amount of underlying assets to deposit
*/
function initialize(
address owner,
uint256 initialFee,
string memory shareName,
string memory shareSymbol,
uint256 initialLockDeposit
) external initializer {
require(owner != address(0), "ZERO_ADDRESS_NOT_VALID");
require(initialLockDeposit != 0, "ZERO_INITIAL_LOCK_DEPOSIT");
_transferOwnership(owner);
__ERC4626_init(UNDERLYING);
__ERC20_init(shareName, shareSymbol);
__EIP712_init(shareName, "1");
_setFee(initialFee);
UNDERLYING.safeApprove(address(AAVE_POOL), type(uint256).max);
_handleDeposit(initialLockDeposit, address(this), msg.sender, false);
}
/*//////////////////////////////////////////////////////////////
DEPOSIT/WITHDRAWAL LOGIC
//////////////////////////////////////////////////////////////*/
/// @inheritdoc IATokenVault
function deposit(uint256 assets, address receiver) public override(ERC4626Upgradeable, IATokenVault) returns (uint256) {
return _handleDeposit(assets, receiver, msg.sender, false);
}
/// @inheritdoc IATokenVault
function depositATokens(uint256 assets, address receiver) public override returns (uint256) {
return _handleDeposit(assets, receiver, msg.sender, true);
}
/// @inheritdoc IATokenVault
function depositWithSig(
uint256 assets,
address receiver,
address depositor,
EIP712Signature calldata sig
) public override returns (uint256) {
unchecked {
MetaTxHelpers._validateRecoveredAddress(
MetaTxHelpers._calculateDigest(
keccak256(
abi.encode(
DEPOSIT_WITH_SIG_TYPEHASH,
assets,
receiver,
depositor,
_sigNonces[depositor]++,
sig.deadline
)
),
_domainSeparatorV4()
),
depositor,
sig
);
}
return _handleDeposit(assets, receiver, depositor, false);
}
/// @inheritdoc IATokenVault
function depositATokensWithSig(
uint256 assets,
address receiver,
address depositor,
EIP712Signature calldata sig
) public override returns (uint256) {
unchecked {
MetaTxHelpers._validateRecoveredAddress(
MetaTxHelpers._calculateDigest(
keccak256(
abi.encode(
DEPOSIT_ATOKENS_WITH_SIG_TYPEHASH,
assets,
receiver,
depositor,
_sigNonces[depositor]++,
sig.deadline
)
),
_domainSeparatorV4()
),
depositor,
sig
);
}
return _handleDeposit(assets, receiver, depositor, true);
}
/// @inheritdoc IATokenVault
function mint(uint256 shares, address receiver) public override(ERC4626Upgradeable, IATokenVault) returns (uint256) {
return _handleMint(shares, receiver, msg.sender, false);
}
/// @inheritdoc IATokenVault
function mintWithATokens(uint256 shares, address receiver) public override returns (uint256) {
return _handleMint(shares, receiver, msg.sender, true);
}
/// @inheritdoc IATokenVault
function mintWithSig(
uint256 shares,
address receiver,
address depositor,
EIP712Signature calldata sig
) public override returns (uint256) {
unchecked {
MetaTxHelpers._validateRecoveredAddress(
MetaTxHelpers._calculateDigest(
keccak256(
abi.encode(MINT_WITH_SIG_TYPEHASH, shares, receiver, depositor, _sigNonces[depositor]++, sig.deadline)
),
_domainSeparatorV4()
),
depositor,
sig
);
}
return _handleMint(shares, receiver, depositor, false);
}
/// @inheritdoc IATokenVault
function mintWithATokensWithSig(
uint256 shares,
address receiver,
address depositor,
EIP712Signature calldata sig
) public override returns (uint256) {
unchecked {
MetaTxHelpers._validateRecoveredAddress(
MetaTxHelpers._calculateDigest(
keccak256(
abi.encode(
MINT_WITH_ATOKENS_WITH_SIG_TYPEHASH,
shares,
receiver,
depositor,
_sigNonces[depositor]++,
sig.deadline
)
),
_domainSeparatorV4()
),
depositor,
sig
);
}
return _handleMint(shares, receiver, depositor, true);
}
/// @inheritdoc IATokenVault
function withdraw(
uint256 assets,
address receiver,
address owner
) public override(ERC4626Upgradeable, IATokenVault) returns (uint256) {
return _handleWithdraw(assets, receiver, owner, msg.sender, false);
}
/// @inheritdoc IATokenVault
function withdrawATokens(uint256 assets, address receiver, address owner) public override returns (uint256) {
return _handleWithdraw(assets, receiver, owner, msg.sender, true);
}
/// @inheritdoc IATokenVault
function withdrawWithSig(
uint256 assets,
address receiver,
address owner,
EIP712Signature calldata sig
) public override returns (uint256) {
unchecked {
MetaTxHelpers._validateRecoveredAddress(
MetaTxHelpers._calculateDigest(
keccak256(
abi.encode(WITHDRAW_WITH_SIG_TYPEHASH, assets, receiver, owner, _sigNonces[owner]++, sig.deadline)
),
_domainSeparatorV4()
),
owner,
sig
);
}
return _handleWithdraw(assets, receiver, owner, owner, false);
}
/// @inheritdoc IATokenVault
function withdrawATokensWithSig(
uint256 assets,
address receiver,
address owner,
EIP712Signature calldata sig
) public override returns (uint256) {
unchecked {
MetaTxHelpers._validateRecoveredAddress(
MetaTxHelpers._calculateDigest(
keccak256(
abi.encode(
WITHDRAW_ATOKENS_WITH_SIG_TYPEHASH,
assets,
receiver,
owner,
_sigNonces[owner]++,
sig.deadline
)
),
_domainSeparatorV4()
),
owner,
sig
);
}
return _handleWithdraw(assets, receiver, owner, owner, true);
}
/// @inheritdoc IATokenVault
function redeem(
uint256 shares,
address receiver,
address owner
) public override(ERC4626Upgradeable, IATokenVault) returns (uint256) {
return _handleRedeem(shares, receiver, owner, msg.sender, false);
}
/// @inheritdoc IATokenVault
function redeemAsATokens(uint256 shares, address receiver, address owner) public override returns (uint256) {
return _handleRedeem(shares, receiver, owner, msg.sender, true);
}
/// @inheritdoc IATokenVault
function redeemWithSig(
uint256 shares,
address receiver,
address owner,
EIP712Signature calldata sig
) public override returns (uint256) {
unchecked {
MetaTxHelpers._validateRecoveredAddress(
MetaTxHelpers._calculateDigest(
keccak256(abi.encode(REDEEM_WITH_SIG_TYPEHASH, shares, receiver, owner, _sigNonces[owner]++, sig.deadline)),
_domainSeparatorV4()
),
owner,
sig
);
}
return _handleRedeem(shares, receiver, owner, owner, false);
}
/// @inheritdoc IATokenVault
function redeemWithATokensWithSig(
uint256 shares,
address receiver,
address owner,
EIP712Signature calldata sig
) public override returns (uint256) {
unchecked {
MetaTxHelpers._validateRecoveredAddress(
MetaTxHelpers._calculateDigest(
keccak256(
abi.encode(
REDEEM_WITH_ATOKENS_WITH_SIG_TYPEHASH,
shares,
receiver,
owner,
_sigNonces[owner]++,
sig.deadline
)
),
_domainSeparatorV4()
),
owner,
sig
);
}
return _handleRedeem(shares, receiver, owner, owner, true);
}
/// @inheritdoc IATokenVault
function maxDeposit(address) public view override(ERC4626Upgradeable, IATokenVault) returns (uint256) {
return _maxAssetsSuppliableToAave();
}
/// @inheritdoc IATokenVault
function maxMint(address) public view override(ERC4626Upgradeable, IATokenVault) returns (uint256) {
return _convertToShares(_maxAssetsSuppliableToAave(), MathUpgradeable.Rounding.Down);
}
/// @inheritdoc IATokenVault
function maxWithdraw(address owner) public view override(ERC4626Upgradeable, IATokenVault) returns (uint256) {
uint256 maxWithdrawable = _maxAssetsWithdrawableFromAave();
return
maxWithdrawable == 0 ? 0 : maxWithdrawable.min(_convertToAssets(balanceOf(owner), MathUpgradeable.Rounding.Down));
}
/// @inheritdoc IATokenVault
function maxRedeem(address owner) public view override(ERC4626Upgradeable, IATokenVault) returns (uint256) {
uint256 maxWithdrawable = _maxAssetsWithdrawableFromAave();
return
maxWithdrawable == 0 ? 0 : _convertToShares(maxWithdrawable, MathUpgradeable.Rounding.Down).min(balanceOf(owner));
}
/// @inheritdoc IATokenVault
function previewDeposit(uint256 assets) public view override(ERC4626Upgradeable, IATokenVault) returns (uint256) {
return _convertToShares(_maxAssetsSuppliableToAave().min(assets), MathUpgradeable.Rounding.Down);
}
/// @inheritdoc IATokenVault
function previewMint(uint256 shares) public view override(ERC4626Upgradeable, IATokenVault) returns (uint256) {
return _convertToAssets(shares, MathUpgradeable.Rounding.Up).min(_maxAssetsSuppliableToAave());
}
/// @inheritdoc IATokenVault
function previewWithdraw(uint256 assets) public view override(ERC4626Upgradeable, IATokenVault) returns (uint256) {
uint256 maxWithdrawable = _maxAssetsWithdrawableFromAave();
return maxWithdrawable == 0 ? 0 : _convertToShares(maxWithdrawable.min(assets), MathUpgradeable.Rounding.Up);
}
/// @inheritdoc IATokenVault
function previewRedeem(uint256 shares) public view override(ERC4626Upgradeable, IATokenVault) returns (uint256) {
uint256 maxWithdrawable = _maxAssetsWithdrawableFromAave();
return maxWithdrawable == 0 ? 0 : _convertToAssets(shares, MathUpgradeable.Rounding.Down).min(maxWithdrawable);
}
/// @inheritdoc IATokenVault
function domainSeparator() public view override returns (bytes32) {
return _domainSeparatorV4();
}
/*//////////////////////////////////////////////////////////////
ONLY OWNER FUNCTIONS
//////////////////////////////////////////////////////////////*/
/// @inheritdoc IATokenVault
function setFee(uint256 newFee) public override onlyOwner {
_accrueYield();
_setFee(newFee);
}
/// @inheritdoc IATokenVault
function withdrawFees(address to, uint256 amount) public override onlyOwner {
_accrueYield();
require(amount <= _s.accumulatedFees, "INSUFFICIENT_FEES"); // will underflow below anyway, error msg for clarity
_s.accumulatedFees -= uint128(amount);
ATOKEN.transfer(to, amount);
_s.lastVaultBalance = uint128(ATOKEN.balanceOf(address(this)));
emit FeesWithdrawn(to, amount, _s.lastVaultBalance, _s.accumulatedFees);
}
/// @inheritdoc IATokenVault
function claimRewards(address to) public override onlyOwner {
require(to != address(0), "CANNOT_CLAIM_TO_ZERO_ADDRESS");
address[] memory assets = new address[](1);
assets[0] = address(ATOKEN);
(address[] memory rewardsList, uint256[] memory claimedAmounts) = IRewardsController(
address(IncentivizedERC20(address(ATOKEN)).getIncentivesController())
).claimAllRewards(assets, to);
emit RewardsClaimed(to, rewardsList, claimedAmounts);
}
/// @inheritdoc IATokenVault
function emergencyRescue(address token, address to, uint256 amount) public override onlyOwner {
require(token != address(ATOKEN), "CANNOT_RESCUE_ATOKEN");
IERC20Upgradeable(token).safeTransfer(to, amount);
emit EmergencyRescue(token, to, amount);
}
/*//////////////////////////////////////////////////////////////
VIEW FUNCTIONS
//////////////////////////////////////////////////////////////*/
/// @inheritdoc IATokenVault
function totalAssets() public view override(ERC4626Upgradeable, IATokenVault) returns (uint256) {
// Report only the total assets net of fees, for vault share logic
return ATOKEN.balanceOf(address(this)) - getClaimableFees();
}
/// @inheritdoc IATokenVault
function getClaimableFees() public view override returns (uint256) {
uint256 newVaultBalance = ATOKEN.balanceOf(address(this));
// Skip computation if there is no yield
if (newVaultBalance <= _s.lastVaultBalance) {
return _s.accumulatedFees;
}
uint256 newYield = newVaultBalance - _s.lastVaultBalance;
uint256 newFees = newYield.mulDiv(_s.fee, SCALE, MathUpgradeable.Rounding.Down);
return _s.accumulatedFees + newFees;
}
/// @inheritdoc IATokenVault
function getSigNonce(address signer) public view override returns (uint256) {
return _sigNonces[signer];
}
/// @inheritdoc IATokenVault
function getLastVaultBalance() public view override returns (uint256) {
return _s.lastVaultBalance;
}
/// @inheritdoc IATokenVault
function getFee() public view override returns (uint256) {
return _s.fee;
}
/*//////////////////////////////////////////////////////////////
INTERNAL FUNCTIONS
//////////////////////////////////////////////////////////////*/
function _setFee(uint256 newFee) internal {
require(newFee <= SCALE, "FEE_TOO_HIGH");
uint256 oldFee = _s.fee;
_s.fee = uint64(newFee);
emit FeeUpdated(oldFee, newFee);
}
function _accrueYield() internal {
uint256 newVaultBalance = ATOKEN.balanceOf(address(this));
// Skip computation if there is no yield
if (newVaultBalance <= _s.lastVaultBalance) {
return;
}
uint256 newYield = newVaultBalance - _s.lastVaultBalance;
uint256 newFeesEarned = newYield.mulDiv(_s.fee, SCALE, MathUpgradeable.Rounding.Down);
_s.accumulatedFees += uint128(newFeesEarned);
_s.lastVaultBalance = uint128(newVaultBalance);
emit YieldAccrued(newYield, newFeesEarned, newVaultBalance);
}
function _handleDeposit(uint256 assets, address receiver, address depositor, bool asAToken) internal returns (uint256) {
if (!asAToken) require(assets <= maxDeposit(receiver), "DEPOSIT_EXCEEDS_MAX");
_accrueYield();
uint256 shares = super.previewDeposit(assets);
require(shares != 0, "ZERO_SHARES"); // Check for rounding error since we round down in previewDeposit.
_baseDeposit(_convertToAssets(shares, MathUpgradeable.Rounding.Up), shares, depositor, receiver, asAToken);
return shares;
}
function _handleMint(uint256 shares, address receiver, address depositor, bool asAToken) internal returns (uint256) {
if (!asAToken) require(shares <= maxMint(receiver), "MINT_EXCEEDS_MAX");
_accrueYield();
uint256 assets = super.previewMint(shares); // No need to check for rounding error, previewMint rounds up.
_baseDeposit(assets, shares, depositor, receiver, asAToken);
return assets;
}
function _handleWithdraw(
uint256 assets,
address receiver,
address owner,
address allowanceTarget,
bool asAToken
) internal returns (uint256) {
_accrueYield();
require(assets <= maxWithdraw(owner), "WITHDRAW_EXCEEDS_MAX");
uint256 shares = super.previewWithdraw(assets); // No need to check for rounding error, previewWithdraw rounds up.
_baseWithdraw(assets, shares, owner, receiver, allowanceTarget, asAToken);
return shares;
}
function _handleRedeem(
uint256 shares,
address receiver,
address owner,
address allowanceTarget,
bool asAToken
) internal returns (uint256) {
_accrueYield();
require(shares <= maxRedeem(owner), "REDEEM_EXCEEDS_MAX");
uint256 assets = super.previewRedeem(shares);
require(assets != 0, "ZERO_ASSETS"); // Check for rounding error since we round down in previewRedeem.
_baseWithdraw(assets, shares, owner, receiver, allowanceTarget, asAToken);
return assets;
}
function _maxAssetsSuppliableToAave() internal view returns (uint256) {
// returns 0 if reserve is not active, frozen, or paused
// returns max uint256 value if supply cap is 0 (not capped)
// returns supply cap - current amount supplied as max suppliable if there is a supply cap for this reserve
AaveDataTypes.ReserveData memory reserveData = AAVE_POOL.getReserveData(address(UNDERLYING));
uint256 reserveConfigMap = reserveData.configuration.data;
uint256 supplyCap = (reserveConfigMap & ~AAVE_SUPPLY_CAP_MASK) >> AAVE_SUPPLY_CAP_BIT_POSITION;
if (
(reserveConfigMap & ~AAVE_ACTIVE_MASK == 0) ||
(reserveConfigMap & ~AAVE_FROZEN_MASK != 0) ||
(reserveConfigMap & ~AAVE_PAUSED_MASK != 0)
) {
return 0;
} else if (supplyCap == 0) {
return type(uint256).max;
} else {
// Reserve's supply cap - current amount supplied
// See similar logic in Aave v3 ValidationLogic library, in the validateSupply function
// https://github.com/aave/aave-v3-core/blob/a00f28e3ad7c0e4a369d8e06e0ac9fd0acabcab7/contracts/protocol/libraries/logic/ValidationLogic.sol#L71-L78
uint256 currentSupply = WadRayMath.rayMul(
(ATOKEN.scaledTotalSupply() + uint256(reserveData.accruedToTreasury)),
reserveData.liquidityIndex
);
uint256 supplyCapWithDecimals = supplyCap * 10 ** decimals();
return supplyCapWithDecimals > currentSupply ? supplyCapWithDecimals - currentSupply : 0;
}
}
function _maxAssetsWithdrawableFromAave() internal view returns (uint256) {
// returns 0 if reserve is not active, or paused
// otherwise, returns available liquidity
AaveDataTypes.ReserveData memory reserveData = AAVE_POOL.getReserveData(address(UNDERLYING));
uint256 reserveConfigMap = reserveData.configuration.data;
if ((reserveConfigMap & ~AAVE_ACTIVE_MASK == 0) || (reserveConfigMap & ~AAVE_PAUSED_MASK != 0)) {
return 0;
} else {
return UNDERLYING.balanceOf(address(ATOKEN));
}
}
function _baseDeposit(uint256 assets, uint256 shares, address depositor, address receiver, bool asAToken) private {
// Need to transfer before minting or ERC777s could reenter.
if (asAToken) {
ATOKEN.transferFrom(depositor, address(this), assets);
} else {
UNDERLYING.safeTransferFrom(depositor, address(this), assets);
AAVE_POOL.supply(address(UNDERLYING), assets, address(this), REFERRAL_CODE);
}
_s.lastVaultBalance = uint128(ATOKEN.balanceOf(address(this)));
_mint(receiver, shares);
emit Deposit(depositor, receiver, assets, shares);
}
function _baseWithdraw(
uint256 assets,
uint256 shares,
address owner,
address receiver,
address allowanceTarget,
bool asAToken
) private {
if (allowanceTarget != owner) {
_spendAllowance(owner, allowanceTarget, shares);
}
_burn(owner, shares);
// Withdraw assets from Aave v3 and send to receiver
if (asAToken) {
ATOKEN.transfer(receiver, assets);
} else {
AAVE_POOL.withdraw(address(UNDERLYING), assets, receiver);
}
_s.lastVaultBalance = uint128(ATOKEN.balanceOf(address(this)));
emit Withdraw(allowanceTarget, receiver, owner, assets, shares);
}
}
"
},
"lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC20/utils/SafeERC20.sol)
pragma solidity ^0.8.0;
import "../IERC20.sol";
import "../extensions/draft-IERC20Permit.sol";
import "../../../utils/Address.sol";
/**
* @title SafeERC20
* @dev Wrappers around ERC20 operations that throw on failure (when the token
* contract returns false). Tokens that return no value (and instead revert or
* throw on failure) are also supported, non-reverting calls are assumed to be
* successful.
* To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
* which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
*/
library SafeERC20 {
using Address for address;
function safeTransfer(
IERC20 token,
address to,
uint256 value
) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
}
function safeTransferFrom(
IERC20 token,
address from,
address to,
uint256 value
) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
}
/**
* @dev Deprecated. This function has issues similar to the ones found in
* {IERC20-approve}, and its usage is discouraged.
*
* Whenever possible, use {safeIncreaseAllowance} and
* {safeDecreaseAllowance} instead.
*/
function safeApprove(
IERC20 token,
address spender,
uint256 value
) internal {
// safeApprove should only be called when setting an initial allowance,
// or when resetting it to zero. To increase and decrease it, use
// 'safeIncreaseAllowance' and 'safeDecreaseAllowance'
require(
(value == 0) || (token.allowance(address(this), spender) == 0),
"SafeERC20: approve from non-zero to non-zero allowance"
);
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
}
function safeIncreaseAllowance(
IERC20 token,
address spender,
uint256 value
) internal {
uint256 newAllowance = token.allowance(address(this), spender) + value;
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
}
function safeDecreaseAllowance(
IERC20 token,
address spender,
uint256 value
) internal {
unchecked {
uint256 oldAllowance = token.allowance(address(this), spender);
require(oldAllowance >= value, "SafeERC20: decreased allowance below zero");
uint256 newAllowance = oldAllowance - value;
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
}
}
function safePermit(
IERC20Permit token,
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) internal {
uint256 nonceBefore = token.nonces(owner);
token.permit(owner, spender, value, deadline, v, r, s);
uint256 nonceAfter = token.nonces(owner);
require(nonceAfter == nonceBefore + 1, "SafeERC20: permit did not succeed");
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*/
function _callOptionalReturn(IERC20 token, bytes memory data) private {
// We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
// we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that
// the target address contains contract code and also asserts for success in the low-level call.
bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed");
if (returndata.length > 0) {
// Return data is optional
require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
}
}
}
"
},
"lib/openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (proxy/transparent/ProxyAdmin.sol)
pragma solidity ^0.8.0;
import "./TransparentUpgradeableProxy.sol";
import "../../access/Ownable.sol";
/**
* @dev This is an auxiliary contract meant to be assigned as the admin of a {TransparentUpgradeableProxy}. For an
* explanation of why you would want to use this see the documentation for {TransparentUpgradeableProxy}.
*/
contract ProxyAdmin is Ownable {
/**
* @dev Returns the current implementation of `proxy`.
*
* Requirements:
*
* - This contract must be the admin of `proxy`.
*/
function getProxyImplementation(TransparentUpgradeableProxy proxy) public view virtual returns (address) {
// We need to manually run the static call since the getter cannot be flagged as view
// bytes4(keccak256("implementation()")) == 0x5c60da1b
(bool success, bytes memory returndata) = address(proxy).staticcall(hex"5c60da1b");
require(success);
return abi.decode(returndata, (address));
}
/**
* @dev Returns the current admin of `proxy`.
*
* Requirements:
*
* - This contract must be the admin of `proxy`.
*/
function getProxyAdmin(TransparentUpgradeableProxy proxy) public view virtual returns (address) {
// We need to manually run the static call since the getter cannot be flagged as view
// bytes4(keccak256("admin()")) == 0xf851a440
(bool success, bytes memory returndata) = address(proxy).staticcall(hex"f851a440");
require(success);
return abi.decode(returndata, (address));
}
/**
* @dev Changes the admin of `proxy` to `newAdmin`.
*
* Requirements:
*
* - This contract must be the current admin of `proxy`.
*/
function changeProxyAdmin(TransparentUpgradeableProxy proxy, address newAdmin) public virtual onlyOwner {
proxy.changeAdmin(newAdmin);
}
/**
* @dev Upgrades `proxy` to `implementation`. See {TransparentUpgradeableProxy-upgradeTo}.
*
* Requirements:
*
* - This contract must be the admin of `proxy`.
*/
function upgrade(TransparentUpgradeableProxy proxy, address implementation) public virtual onlyOwner {
proxy.upgradeTo(implementation);
}
/**
* @dev Upgrades `proxy` to `implementation` and calls a function on the new implementation. See
* {TransparentUpgradeableProxy-upgradeToAndCall}.
*
* Requirements:
*
* - This contract must be the admin of `proxy`.
*/
function upgradeAndCall(
TransparentUpgradeableProxy proxy,
address implementation,
bytes memory data
) public payable virtual onlyOwner {
proxy.upgradeToAndCall{value: msg.value}(implementation, data);
}
}
"
},
"src/ATokenVaultRevenueSplitterOwner.sol": {
"content": "// 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 Labs
* @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 RevenueSplitTransferred(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 Total historical amount held for a given asset in this contract.
*/
mapping(address => uint256) internal _previousAccumulatedBalance;
/**
* @dev Amount already transferred for a given asset to a given recipient.
*/
mapping(address => mapping(address => uint256)) internal _amountAlreadyTransferred;
/**
* @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. Duplicates are not allowed. The recipients
* configuration 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 Rejects native currency transfers.
*/
receive() external payable {
revert("NATIVE_CURRENCY_NOT_SUPPORTED");
}
/**
* @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
* standard 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 assetBalance = IERC20(assets[i]).balanceOf(address(this));
require(assetBalance > 0, "ASSET_NOT_HELD_BY_SPLITTER");
// Decrease balance by one unit to ensure aToken transfers will not fail due to scaled balance rounding.
assetBalance--;
uint256 accumulatedAssetBalance = _previousAccumulatedBalance[assets[i]] + assetBalance;
_previousAccumulatedBalance[assets[i]] = accumulatedAssetBalance;
uint256 undistributedAmount = assetBalance;
for (uint256 j = 0; j < recipients.length; j++) {
/**
* The `assetBalance` adjustment previously done by decrementing one unit will leave that unit of the
* asset undistributed in this contract's balance.
* However, due to floor-rounding in integer division, the sum of the amounts transferred may be less
* than the intended total amount to split, leaving a few more units of the asset undistributed.
* These units (also known as 'dust') may be distributed in the next `splitRevenue` call.
*/
uint256 amountForRecipient = accumulatedAssetBalance * recipients[j].shareInBps / TOTAL_SHARE_IN_BPS
- _amountAlreadyTransferred[assets[i]][recipients[j].addr];
if (amountForRecipient > 0) {
_amountAlreadyTransferred[assets[i]][recipients[j].addr] += amountForRecipient;
IERC20(assets[i]).safeTransfer(recipients[j].addr, amountForRecipient);
undistributedAmount -= amountForRecipient;
}
emit RevenueSplitTransferred(recipients[j].addr, assets[i], amountForRecipient);
}
if (undistributedAmount > 0) {
_previousAccumulatedBalance[assets[i]] -= undistributedAmount;
}
}
}
/**
* @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();
if (feesToWithdraw > 0) {
VAULT.withdrawFees(address(this), feesToWithdraw);
}
}
/**
* @dev 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].addr != address(0), "RECIPIENT_CANNOT_BE_ZERO_ADDRESS");
require(recipients[i].shareInBps > 0, "BPS_SHARE_CANNOT_BE_ZERO");
accumulatedShareInBps += recipients[i].share
Submitted on: 2025-09-30 10:19:57
Comments
Log in to comment.
No comments yet.