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/allocator-vaults/AllocatorVaultV3.sol": {
"content": "// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.25;
import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import { ERC4626Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
/**
* @title AllocatorVaultV3
* @dev Implementation of an ERC4626 compliant Allocator Vault with fee management.
* The vault delegates asset management to a strategy contract while handling deposits,
* withdrawals, and fee calculations.
*/
contract AllocatorVaultV3 is ERC4626Upgradeable, AccessControlUpgradeable, ReentrancyGuardUpgradeable {
using SafeERC20 for IERC20;
/// @notice Emitted when a harvest operation is executed
/// @param caller The address that initiated the harvest
/// @param sharesMinted The number of fee shares minted during the harvest
/// @param previousTotalUnderlying The total underlying assets before harvest
/// @param currentTotalUnderlying The total underlying assets after harvest
event Harvested(
address indexed caller, uint256 sharesMinted, uint256 previousTotalUnderlying, uint256 currentTotalUnderlying
);
/// @notice Emitted when the vault configuration is updated
/// @param config The new configuration applied to the vault
event VaultConfigured(AllocatorVaultConfig config);
/// @notice Emitted when the vault is paused or unpaused
/// @param paused The new paused state (true if paused)
event PausedSet(bool paused);
/// @notice Emitted when a target and selector are added to or removed from the whitelist
/// @param target The address of the contract target
/// @param selector The 4-byte function selector affected
/// @param added True if added to whitelist, false if removed
event Whitelisted(address indexed target, bytes4 selector, bool added);
/// @notice Emitted when additional reward tokens are claimed and transferred
/// @param target The address of the contract call used to claim rewards
/// @param asset The address of the reward token claimed
/// @param amount The amount of tokens successfully claimed
event RewardsClaimed(address indexed target, address indexed asset, uint256 amount);
/// @notice Emitted when miscellaneous ERC20 tokens are swept to the fee recipient
/// @param asset The address of the token being swept
/// @param amount The amount of tokens transferred
event MiscellaneousRewardClaimed(address indexed asset, uint256 amount);
/**
* @dev Configuration for the vault's fee structure and behavior
* @param depositFee Fee charged when depositing assets (in basis points)
* @param performanceFee Fee charged on performance gains (in basis points)
* @param managementFee Ongoing fee for management (in basis points)
* @param feeRecipient Address that receives collected fees
* @param hasCooldown Whether withdrawals are subject to a cooldown period
*/
struct AllocatorVaultConfig {
uint256 depositFee;
uint256 performanceFee;
uint256 managementFee;
address feeRecipient;
bool hasCooldown;
}
// Constants
uint256 private constant MAX_BPS = 10_000_000_000;
uint256 private constant MAX_PERF_FEE = 5_000_000_000;
uint256 private constant MAX_MGMT_FEE = 1_000_000_000;
uint256 private constant MAX_DEP_FEE = 500_000_000;
uint256 private constant SECONDS_IN_YEAR = 365 * 24 * 60 * 60;
uint256 private constant VIRTUAL_ASSETS = 1;
uint256 private constant VIRTUAL_SHARES = 10 ** 6;
bytes32 public constant HARVESTER_ROLE = keccak256("HARVESTER_ROLE");
// Variables
bool public paused;
address public underlying;
AllocatorVaultConfig public config;
IERC4626 public strategy;
uint256 public lastStamp;
uint256 public totalUnderlyingStamp;
mapping(address target => mapping(bytes4 selector => bool isWhitelisted)) public whitelistedTargets;
// modifiers
/**
* @dev Ensures the vault is not paused when executing function
*/
modifier notPaused() {
require(!paused, "Paused");
_;
}
/**
* @dev Initializes the vault with the specified parameters
* @param _underlying Address of the underlying token
* @param _strategy Strategy contract that will manage the assets
* @param vaultTokenName Name for the vault's share token
* @param vaultTokenSymbol Symbol for the vault's share token
*/
function initialize(
address _underlying,
IERC4626 _strategy,
string memory vaultTokenName,
string memory vaultTokenSymbol
)
public
initializer
{
__ERC4626_init(IERC20(address(_strategy)));
__ERC20_init(vaultTokenName, vaultTokenSymbol);
__AccessControl_init();
__ReentrancyGuard_init();
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(HARVESTER_ROLE, msg.sender);
require(address(_strategy) != address(0), "Invalid strategy");
require(_underlying != address(0), "Invalid underlying token");
require(_underlying == _strategy.asset(), "Underlying token mismatch");
require(address(_strategy) == asset(), "Strategy token mismatch");
strategy = _strategy;
underlying = _underlying;
lastStamp = block.timestamp;
}
/**
* @dev Sets the configuration for the vault
* @param _config New configuration to apply
*/
function configureVault(AllocatorVaultConfig memory _config) public onlyRole(DEFAULT_ADMIN_ROLE) {
require(_config.depositFee <= MAX_DEP_FEE, "Deposit fee too high");
require(_config.performanceFee <= MAX_PERF_FEE, "Performance fee too high");
require(_config.managementFee <= MAX_MGMT_FEE, "Management fee too high");
require(_config.feeRecipient != address(0), "Invalid fee recipient");
config = _config;
emit VaultConfigured(_config);
}
/**
* @dev Pauses or unpauses the vault
* @param _paused New paused state
*/
function setPaused(bool _paused) external onlyRole(DEFAULT_ADMIN_ROLE) {
paused = _paused;
emit PausedSet(_paused);
}
/**
* @dev Returns the total amount of assets in the vault
* @notice the contract's assets are actually the strategy's shares. This function returns
* the amount of strategy shares that this vault holds, not the underlying tokens.
* @return Total strategy shares held by the vault
*/
function totalAssets() public view override returns (uint256) {
return strategy.balanceOf(address(this));
}
/**
* @dev Converts an amount of assets to an equivalent amount of shares, with optional ceiling rounding
* @param assets Amount of assets to convert
* @param ceil If true, rounds up to ensure sufficient shares; if false, performs standard rounding
* @return Amount of shares corresponding to the assets
*/
function _convertToShares(uint256 assets, bool ceil) internal view returns (uint256) {
if (ceil) {
uint256 numerator = assets * (totalSupply() + VIRTUAL_SHARES);
uint256 denominator = totalAssets() + VIRTUAL_ASSETS;
uint256 shares = (numerator + denominator - 1) / denominator;
return shares;
} else {
return (assets * (totalSupply() + VIRTUAL_SHARES)) / (totalAssets() + VIRTUAL_ASSETS);
}
}
/**
* @dev Converts an amount of assets to an equivalent amount of shares
* @param assets Amount of assets to convert
* @return Amount of shares
*/
function convertToShares(uint256 assets) public view override returns (uint256) {
return _convertToShares(assets, false);
}
/**
* @dev Converts an amount of shares to an equivalent amount of assets, with optional ceiling rounding
* @param shares Amount of shares to convert
* @param ceil If true, rounds up to ensure sufficient assets; if false, performs standard rounding
* @return Amount of assets corresponding to the shares
*/
function _convertToAssets(uint256 shares, bool ceil) internal view returns (uint256) {
if (ceil) {
uint256 numerator = shares * (totalAssets() + VIRTUAL_ASSETS);
uint256 denominator = totalSupply() + VIRTUAL_SHARES;
uint256 assets = (numerator + denominator - 1) / denominator;
return assets;
} else {
return (shares * (totalAssets() + VIRTUAL_ASSETS)) / (totalSupply() + VIRTUAL_SHARES);
}
}
/**
* @dev Converts an amount of shares to an equivalent amount of assets
* @param shares Amount of shares to convert
* @return Amount of assets
*/
function convertToAssets(uint256 shares) public view override returns (uint256) {
return _convertToAssets(shares, false);
}
/**
* @dev Simulates the effects of a deposit at the current block, returning the expected shares
* @param _underlying Amount of underlying tokens to deposit
* @return Expected amount of shares to be received
*/
function previewDeposit(uint256 _underlying) public view override returns (uint256) {
uint256 assets = strategy.previewDeposit(_underlying);
uint256 newTotalShares = previewHarvest();
uint256 shares = (assets * (newTotalShares + VIRTUAL_SHARES)) / (totalAssets() + VIRTUAL_ASSETS);
return shares * (MAX_BPS - config.depositFee) / MAX_BPS;
}
/**
* @dev Deposits underlying tokens into the vault
* @param _underlying Amount of underlying tokens to deposit
* @param receiver Address that will receive the shares
* @notice Harvest is called before the deposit to ensure all accrued fees are
* properly collected before changing the deposit state, maintaining accurate
* accounting and fee distribution
* @return Amount of shares minted
*/
function deposit(uint256 _underlying, address receiver) public override notPaused nonReentrant returns (uint256) {
uint256 maxUnderlying = maxDeposit(receiver);
require(_underlying <= maxUnderlying, "Exceeded max deposit");
uint256 expectedAssets = strategy.previewDeposit(_underlying);
require(expectedAssets > 0, "Insufficient deposit amount");
_harvest();
IERC20(underlying).safeTransferFrom(msg.sender, address(this), _underlying);
uint256 previousTotalAssets = totalAssets();
IERC20(underlying).approve(address(strategy), _underlying);
uint256 receivedAssets = strategy.deposit(_underlying, address(this));
uint256 newShares = receivedAssets * (totalSupply() + VIRTUAL_SHARES) / (previousTotalAssets + VIRTUAL_ASSETS);
uint256 userShares = (newShares * (MAX_BPS - config.depositFee)) / MAX_BPS;
uint256 feeShares = newShares - userShares;
totalUnderlyingStamp = strategy.convertToAssets(totalAssets());
lastStamp = block.timestamp;
_mint(receiver, userShares);
if (feeShares > 0) {
_mint(config.feeRecipient, feeShares);
}
emit Deposit(msg.sender, receiver, _underlying, userShares);
return userShares;
}
/**
* @dev Simulates the effects of a mint at the current block, returning the expected amount of underlying tokens
* @param shares Amount of shares to mint
* @return Expected amount of underlying tokens needed
*/
function previewMint(uint256 shares) public view override returns (uint256) {
uint256 newTotalShares = previewHarvest();
uint256 actualShares = (shares * MAX_BPS) / (MAX_BPS - config.depositFee);
// round up
uint256 numerator = actualShares * (totalAssets() + VIRTUAL_ASSETS);
uint256 denominator = newTotalShares + VIRTUAL_SHARES;
uint256 assets = (numerator + denominator - 1) / denominator;
return strategy.previewMint(assets);
}
/**
* @dev Mints the specified amount of shares to the receiver by depositing underlying tokens
* @param shares Amount of shares to mint
* @param receiver Address that will receive the shares
* @return Amount of underlying tokens deposited
*/
function mint(uint256 shares, address receiver) public override notPaused nonReentrant returns (uint256) {
uint256 maxShares = maxMint(receiver);
require(shares > 0, "Cannot mint 0");
require(shares <= maxShares, "Exceeded max mint");
uint256 _underlying = previewMint(shares);
_harvest();
IERC20(underlying).safeTransferFrom(msg.sender, address(this), _underlying);
IERC20(underlying).approve(address(strategy), _underlying);
strategy.deposit(_underlying, address(this));
uint256 newShares = (shares * MAX_BPS) / (MAX_BPS - config.depositFee);
uint256 feeShares = newShares - shares;
totalUnderlyingStamp = strategy.convertToAssets(totalAssets());
lastStamp = block.timestamp;
_mint(receiver, shares);
if (feeShares > 0) {
_mint(config.feeRecipient, feeShares);
}
emit Deposit(msg.sender, receiver, _underlying, shares);
return _underlying;
}
/**
* @dev Returns the maximum amount of underlying tokens that can be withdrawn by the receiver
* @param receiver Address that would receive the underlying tokens
* @return Maximum amount of underlying tokens
*/
function maxWithdraw(address receiver) public view override returns (uint256) {
uint256 shares = balanceOf(receiver);
uint256 _underlying = previewRedeem(shares);
return _underlying;
}
/**
* @dev Simulates the effects of a withdrawal at the current block, returning the expected shares needed
* @param _underlying Amount of underlying tokens to withdraw
* @return Expected amount of shares to be burned
*/
function previewWithdraw(uint256 _underlying) public view override returns (uint256) {
uint256 assets = strategy.previewWithdraw(_underlying);
uint256 newTotalShares = previewHarvest();
if (newTotalShares == 0) {
return 0;
}
uint256 numerator = assets * (newTotalShares + VIRTUAL_SHARES);
uint256 denominator = totalAssets() + VIRTUAL_ASSETS;
uint256 shares = (numerator + denominator - 1) / denominator;
return shares;
}
/**
* @dev Withdraws the specified amount of underlying tokens to the receiver
* @param _underlying Amount of underlying tokens to withdraw
* @param receiver Address that will receive the underlying tokens
* @param owner Address that owns the shares to be burned
* @return Amount of shares burned
*/
function withdraw(
uint256 _underlying,
address receiver,
address owner
)
public
override
nonReentrant
returns (uint256)
{
uint256 maxUnderlying = maxWithdraw(owner);
require(_underlying > 0, "Cannot withdraw 0");
require(_underlying <= maxUnderlying, "Exceeded max withdraw");
_harvest();
uint256 assets = strategy.previewWithdraw(_underlying);
uint256 shares = _convertToShares(assets, true);
if (msg.sender != owner) {
_spendAllowance(owner, msg.sender, shares);
}
_burn(owner, shares);
if (config.hasCooldown) {
IERC20(address(strategy)).safeTransfer(receiver, assets);
} else {
strategy.withdraw(_underlying, receiver, address(this));
}
totalUnderlyingStamp = strategy.convertToAssets(totalAssets());
lastStamp = block.timestamp;
emit Withdraw(msg.sender, receiver, owner, _underlying, shares);
return shares;
}
/**
* @dev Returns the maximum amount of shares that can be redeemed by the owner
* @param owner Address that owns the shares
* @return Maximum amount of shares that can be redeemed
*/
function maxRedeem(address owner) public view override returns (uint256) {
return balanceOf(owner);
}
/**
* @dev Simulates the effects of a redemption at the current block, returning the expected amount of underlying
* tokens
* @param shares Amount of shares to redeem
* @return Expected amount of underlying tokens to be received
*/
function previewRedeem(uint256 shares) public view override returns (uint256) {
uint256 newTotalShares = previewHarvest();
if (newTotalShares == 0) {
return 0;
}
uint256 strategyShares = shares * (totalAssets() + VIRTUAL_ASSETS) / (newTotalShares + VIRTUAL_SHARES);
return strategy.convertToAssets(strategyShares);
}
/**
* @dev Redeems the specified amount of shares for underlying tokens
* @param shares Amount of shares to redeem
* @param receiver Address that will receive the underlying tokens
* @param owner Address that owns the shares to be burned
* @return Amount of underlying tokens received
*/
function redeem(uint256 shares, address receiver, address owner) public override nonReentrant returns (uint256) {
uint256 maxShares = maxRedeem(owner);
require(shares > 0, "Cannot redeem 0");
require(shares <= maxShares, "Exceeded max redeem");
_harvest();
uint256 assets = _convertToAssets(shares, false);
uint256 _underlying = strategy.convertToAssets(assets);
if (msg.sender != owner) {
_spendAllowance(owner, msg.sender, shares);
}
_burn(owner, shares);
if (config.hasCooldown) {
IERC20(address(strategy)).safeTransfer(receiver, assets);
} else {
strategy.redeem(assets, receiver, address(this));
}
totalUnderlyingStamp = strategy.convertToAssets(totalAssets());
lastStamp = block.timestamp;
emit Withdraw(msg.sender, receiver, owner, _underlying, shares);
return _underlying;
}
/**
* @dev Previews the effect of a harvest operation without executing it
* @return Expected total supply after harvest fees are applied
*/
function previewHarvest() internal view returns (uint256) {
uint256 fee = computeHarvestFee();
uint256 newShares = (totalSupply() + VIRTUAL_SHARES) * fee / (MAX_BPS - fee);
return totalSupply() + newShares;
}
/**
* @dev Internal harvest implementation that mints fee shares to the fee recipient
* @return Amount of new shares minted as fees
*/
function _harvest() internal returns (uint256) {
uint256 previousTotalUnderlying = totalUnderlyingStamp;
uint256 fee = computeHarvestFee();
uint256 newShares = (totalSupply() + VIRTUAL_SHARES) * fee / (MAX_BPS - fee);
if (newShares != 0) {
_mint(config.feeRecipient, newShares);
}
totalUnderlyingStamp = strategy.convertToAssets(totalAssets());
lastStamp = block.timestamp;
emit Harvested(msg.sender, newShares, previousTotalUnderlying, totalUnderlyingStamp);
return newShares;
}
/**
* @dev Executes a harvest operation, minting fee shares to the fee recipient
* @return Amount of new shares minted as fees
*/
function harvest() public onlyRole(HARVESTER_ROLE) returns (uint256) {
return _harvest();
}
/**
* @dev Calculates the harvest fee based on management and performance parameters
* @return Fee amount in basis points
*/
function computeHarvestFee() internal view returns (uint256) {
uint256 timeElapsed = block.timestamp - lastStamp;
uint256 mgmtFeeNum = config.managementFee * timeElapsed;
uint256 perfFeeNum = config.performanceFee * SECONDS_IN_YEAR;
if (timeElapsed == 0) {
return 0;
}
uint256 currentTotalUnderlying = strategy.convertToAssets(totalAssets());
if (currentTotalUnderlying == 0) {
return 0;
}
uint256 gain = 0;
if (currentTotalUnderlying > totalUnderlyingStamp) {
gain = currentTotalUnderlying - totalUnderlyingStamp;
}
uint256 fee =
(totalUnderlyingStamp * mgmtFeeNum + gain * perfFeeNum) / (currentTotalUnderlying * SECONDS_IN_YEAR);
return fee;
}
/**
* @dev Internal function to spend allowance
* @param owner Token owner
* @param spender Token spender
* @param amount Amount to spend
*/
function _spendAllowance(address owner, address spender, uint256 amount) internal virtual override {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint256).max) {
require(currentAllowance >= amount, "ERC20: insufficient allowance");
unchecked {
_approve(owner, spender, currentAllowance - amount);
}
}
}
/**
* @dev Adds a contract and function selector to the whitelist
* @param target The target contract address
* @param functionSelector The 4-byte function selector
*/
function addToWhitelist(address target, bytes4 functionSelector) external onlyRole(DEFAULT_ADMIN_ROLE) {
require(target != address(0), "Invalid target address");
require(functionSelector != bytes4(0), "Invalid function selector");
require(!whitelistedTargets[target][functionSelector], "Already whitelisted");
require(target != address(strategy), "Cannot whitelist strategy");
whitelistedTargets[target][functionSelector] = true;
emit Whitelisted(target, functionSelector, true);
}
/**
* @dev Removes a contract and function selector from the whitelist
* @param target The target contract address
* @param functionSelector The 4-byte function selector
*/
function removeFromWhitelist(address target, bytes4 functionSelector) external onlyRole(DEFAULT_ADMIN_ROLE) {
whitelistedTargets[target][functionSelector] = false;
emit Whitelisted(target, functionSelector, false);
}
/// @dev Rewards tokens are sent directly to the fee recipient since they aren't the underlying asset.
/// @param target The target contract to call (Morpho ETH Bundler V2).
/// @param rewardsAsset The rewards asset to claim (MORPHO token).
/// @param payload The payload to pass to the target.
function claimAdditionalRewards(
address target,
address rewardsAsset,
bytes calldata payload
)
external
nonReentrant
onlyRole(DEFAULT_ADMIN_ROLE)
{
require(target != address(0), "Invalid target");
require(rewardsAsset != address(0), "Invalid rewards asset");
require(payload.length >= 4, "Invalid payload");
bytes4 functionSelector = bytes4(payload[:4]);
require(whitelistedTargets[target][functionSelector], "Target not whitelisted");
require(rewardsAsset != address(underlying), "Cannot claim underlying asset");
require(rewardsAsset != address(strategy), "Cannot claim strategy asset");
require(rewardsAsset != address(this), "Cannot claim vault asset");
uint256 rewardsBalanceBefore = IERC20(rewardsAsset).balanceOf(address(this));
uint256 sharesBefore = totalSupply();
uint256 assetsBefore = totalAssets();
(bool success,) = target.call(payload);
require(success, "Call to target failed");
uint256 rewardsBalanceAfter = IERC20(rewardsAsset).balanceOf(address(this));
uint256 rewardsReceived = rewardsBalanceAfter - rewardsBalanceBefore;
require(sharesBefore == totalSupply(), "Shares changed during claim");
require(assetsBefore == totalAssets(), "Assets changed during claim");
if (rewardsReceived > 0) {
IERC20(rewardsAsset).safeTransfer(config.feeRecipient, rewardsReceived);
}
emit RewardsClaimed(target, rewardsAsset, rewardsReceived);
}
/**
* @notice Transfers any non strategy related ERC-20 tokens from the vault to the fee recipient.
* @dev Uses `safeTransfer` since the vault owns the tokens. No approval is needed.
* @param tokenAddress The ERC-20 token contract address to sweep.
*/
function claimMiscellaneousRewards(address tokenAddress) external nonReentrant onlyRole(DEFAULT_ADMIN_ROLE) {
require(tokenAddress != address(0), "Invalid target");
require(tokenAddress != address(underlying), "Cannot claim underlying");
require(tokenAddress != address(strategy), "Cannot claim from strategy");
require(tokenAddress != address(this), "Cannot claim from vault");
IERC20 token = IERC20(tokenAddress);
uint256 balance = token.balanceOf(address(this));
require(balance > 0, "No balance to claim");
token.safeTransfer(config.feeRecipient, balance);
emit MiscellaneousRewardClaimed(tokenAddress, balance);
}
}
"
},
"lib/openzeppelin-contracts-upgradeable/contracts/access/AccessControlUpgradeable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/AccessControl.sol)
pragma solidity ^0.8.20;
import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol";
import {ContextUpgradeable} from "../utils/ContextUpgradeable.sol";
import {ERC165Upgradeable} from "../utils/introspection/ERC165Upgradeable.sol";
import {Initializable} from "../proxy/utils/Initializable.sol";
/**
* @dev Contract module that allows children to implement role-based access
* control mechanisms. This is a lightweight version that doesn't allow enumerating role
* members except through off-chain means by accessing the contract event logs. Some
* applications may benefit from on-chain enumerability, for those cases see
* {AccessControlEnumerable}.
*
* Roles are referred to by their `bytes32` identifier. These should be exposed
* in the external API and be unique. The best way to achieve this is by
* using `public constant` hash digests:
*
* ```solidity
* bytes32 public constant MY_ROLE = keccak256("MY_ROLE");
* ```
*
* Roles can be used to represent a set of permissions. To restrict access to a
* function call, use {hasRole}:
*
* ```solidity
* function foo() public {
* require(hasRole(MY_ROLE, msg.sender));
* ...
* }
* ```
*
* Roles can be granted and revoked dynamically via the {grantRole} and
* {revokeRole} functions. Each role has an associated admin role, and only
* accounts that have a role's admin role can call {grantRole} and {revokeRole}.
*
* By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means
* that only accounts with this role will be able to grant or revoke other
* roles. More complex role relationships can be created by using
* {_setRoleAdmin}.
*
* WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to
* grant and revoke this role. Extra precautions should be taken to secure
* accounts that have been granted it. We recommend using {AccessControlDefaultAdminRules}
* to enforce additional security measures for this role.
*/
abstract contract AccessControlUpgradeable is Initializable, ContextUpgradeable, IAccessControl, ERC165Upgradeable {
struct RoleData {
mapping(address account => bool) hasRole;
bytes32 adminRole;
}
bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;
/// @custom:storage-location erc7201:openzeppelin.storage.AccessControl
struct AccessControlStorage {
mapping(bytes32 role => RoleData) _roles;
}
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.AccessControl")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant AccessControlStorageLocation = 0x02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b626800;
function _getAccessControlStorage() private pure returns (AccessControlStorage storage $) {
assembly {
$.slot := AccessControlStorageLocation
}
}
/**
* @dev Modifier that checks that an account has a specific role. Reverts
* with an {AccessControlUnauthorizedAccount} error including the required role.
*/
modifier onlyRole(bytes32 role) {
_checkRole(role);
_;
}
function __AccessControl_init() internal onlyInitializing {
}
function __AccessControl_init_unchained() internal onlyInitializing {
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IAccessControl).interfaceId || super.supportsInterface(interfaceId);
}
/**
* @dev Returns `true` if `account` has been granted `role`.
*/
function hasRole(bytes32 role, address account) public view virtual returns (bool) {
AccessControlStorage storage $ = _getAccessControlStorage();
return $._roles[role].hasRole[account];
}
/**
* @dev Reverts with an {AccessControlUnauthorizedAccount} error if `_msgSender()`
* is missing `role`. Overriding this function changes the behavior of the {onlyRole} modifier.
*/
function _checkRole(bytes32 role) internal view virtual {
_checkRole(role, _msgSender());
}
/**
* @dev Reverts with an {AccessControlUnauthorizedAccount} error if `account`
* is missing `role`.
*/
function _checkRole(bytes32 role, address account) internal view virtual {
if (!hasRole(role, account)) {
revert AccessControlUnauthorizedAccount(account, role);
}
}
/**
* @dev Returns the admin role that controls `role`. See {grantRole} and
* {revokeRole}.
*
* To change a role's admin, use {_setRoleAdmin}.
*/
function getRoleAdmin(bytes32 role) public view virtual returns (bytes32) {
AccessControlStorage storage $ = _getAccessControlStorage();
return $._roles[role].adminRole;
}
/**
* @dev Grants `role` to `account`.
*
* If `account` had not been already granted `role`, emits a {RoleGranted}
* event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*
* May emit a {RoleGranted} event.
*/
function grantRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
_grantRole(role, account);
}
/**
* @dev Revokes `role` from `account`.
*
* If `account` had been granted `role`, emits a {RoleRevoked} event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*
* May emit a {RoleRevoked} event.
*/
function revokeRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
_revokeRole(role, account);
}
/**
* @dev Revokes `role` from the calling account.
*
* Roles are often managed via {grantRole} and {revokeRole}: this function's
* purpose is to provide a mechanism for accounts to lose their privileges
* if they are compromised (such as when a trusted device is misplaced).
*
* If the calling account had been revoked `role`, emits a {RoleRevoked}
* event.
*
* Requirements:
*
* - the caller must be `callerConfirmation`.
*
* May emit a {RoleRevoked} event.
*/
function renounceRole(bytes32 role, address callerConfirmation) public virtual {
if (callerConfirmation != _msgSender()) {
revert AccessControlBadConfirmation();
}
_revokeRole(role, callerConfirmation);
}
/**
* @dev Sets `adminRole` as ``role``'s admin role.
*
* Emits a {RoleAdminChanged} event.
*/
function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
AccessControlStorage storage $ = _getAccessControlStorage();
bytes32 previousAdminRole = getRoleAdmin(role);
$._roles[role].adminRole = adminRole;
emit RoleAdminChanged(role, previousAdminRole, adminRole);
}
/**
* @dev Attempts to grant `role` to `account` and returns a boolean indicating if `role` was granted.
*
* Internal function without access restriction.
*
* May emit a {RoleGranted} event.
*/
function _grantRole(bytes32 role, address account) internal virtual returns (bool) {
AccessControlStorage storage $ = _getAccessControlStorage();
if (!hasRole(role, account)) {
$._roles[role].hasRole[account] = true;
emit RoleGranted(role, account, _msgSender());
return true;
} else {
return false;
}
}
/**
* @dev Attempts to revoke `role` from `account` and returns a boolean indicating if `role` was revoked.
*
* Internal function without access restriction.
*
* May emit a {RoleRevoked} event.
*/
function _revokeRole(bytes32 role, address account) internal virtual returns (bool) {
AccessControlStorage storage $ = _getAccessControlStorage();
if (hasRole(role, account)) {
$._roles[role].hasRole[account] = false;
emit RoleRevoked(role, account, _msgSender());
return true;
} else {
return false;
}
}
}
"
},
"lib/openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC4626Upgradeable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/ERC4626.sol)
pragma solidity ^0.8.20;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {ERC20Upgradeable} from "../ERC20Upgradeable.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {Initializable} from "../../../proxy/utils/Initializable.sol";
/**
* @dev Implementation of the ERC-4626 "Tokenized Vault Standard" as defined in
* https://eips.ethereum.org/EIPS/eip-4626[ERC-4626].
*
* This extension allows the minting and burning of "shares" (represented using the ERC-20 inheritance) in exchange for
* underlying "assets" through standardized {deposit}, {mint}, {redeem} and {burn} workflows. This contract extends
* the ERC-20 standard. Any additional extensions included along it would affect the "shares" token represented by this
* contract and not the "assets" token which is an independent contract.
*
* [CAUTION]
* ====
* In empty (or nearly empty) ERC-4626 vaults, deposits are at high risk of being stolen through frontrunning
* with a "donation" to the vault that inflates the price of a share. This is variously known as a donation or inflation
* attack and is essentially a problem of slippage. Vault deployers can protect against this attack by making an initial
* deposit of a non-trivial amount of the asset, such that price manipulation becomes infeasible. Withdrawals may
* similarly be affected by slippage. Users can protect against this attack as well as unexpected slippage in general by
* verifying the amount received is as expected, using a wrapper that performs these checks such as
* https://github.com/fei-protocol/ERC4626#erc4626router-and-base[ERC4626Router].
*
* Since v4.9, this implementation introduces configurable virtual assets and shares to help developers mitigate that risk.
* The `_decimalsOffset()` corresponds to an offset in the decimal representation between the underlying asset's decimals
* and the vault decimals. This offset also determines the rate of virtual shares to virtual assets in the vault, which
* itself determines the initial exchange rate. While not fully preventing the attack, analysis shows that the default
* offset (0) makes it non-profitable even if an attacker is able to capture value from multiple user deposits, as a result
* of the value being captured by the virtual shares (out of the attacker's donation) matching the attacker's expected gains.
* With a larger offset, the attack becomes orders of magnitude more expensive than it is profitable. More details about the
* underlying math can be found xref:ROOT:erc4626.adoc#inflation-attack[here].
*
* The drawback of this approach is that the virtual shares do capture (a very small) part of the value being accrued
* to the vault. Also, if the vault experiences losses, the users try to exit the vault, the virtual shares and assets
* will cause the first user to exit to experience reduced losses in detriment to the last users that will experience
* bigger losses. Developers willing to revert back to the pre-v4.9 behavior just need to override the
* `_convertToShares` and `_convertToAssets` functions.
*
* To learn more, check out our xref:ROOT:erc4626.adoc[ERC-4626 guide].
* ====
*/
abstract contract ERC4626Upgradeable is Initializable, ERC20Upgradeable, IERC4626 {
using Math for uint256;
/// @custom:storage-location erc7201:openzeppelin.storage.ERC4626
struct ERC4626Storage {
IERC20 _asset;
uint8 _underlyingDecimals;
}
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ERC4626")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant ERC4626StorageLocation = 0x0773e532dfede91f04b12a73d3d2acd361424f41f76b4fb79f090161e36b4e00;
function _getERC4626Storage() private pure returns (ERC4626Storage storage $) {
assembly {
$.slot := ERC4626StorageLocation
}
}
/**
* @dev Attempted to deposit more assets than the max amount for `receiver`.
*/
error ERC4626ExceededMaxDeposit(address receiver, uint256 assets, uint256 max);
/**
* @dev Attempted to mint more shares than the max amount for `receiver`.
*/
error ERC4626ExceededMaxMint(address receiver, uint256 shares, uint256 max);
/**
* @dev Attempted to withdraw more assets than the max amount for `receiver`.
*/
error ERC4626ExceededMaxWithdraw(address owner, uint256 assets, uint256 max);
/**
* @dev Attempted to redeem more shares than the max amount for `receiver`.
*/
error ERC4626ExceededMaxRedeem(address owner, uint256 shares, uint256 max);
/**
* @dev Set the underlying asset contract. This must be an ERC20-compatible contract (ERC-20 or ERC-777).
*/
function __ERC4626_init(IERC20 asset_) internal onlyInitializing {
__ERC4626_init_unchained(asset_);
}
function __ERC4626_init_unchained(IERC20 asset_) internal onlyInitializing {
ERC4626Storage storage $ = _getERC4626Storage();
(bool success, uint8 assetDecimals) = _tryGetAssetDecimals(asset_);
$._underlyingDecimals = success ? assetDecimals : 18;
$._asset = asset_;
}
/**
* @dev Attempts to fetch the asset decimals. A return value of false indicates that the attempt failed in some way.
*/
function _tryGetAssetDecimals(IERC20 asset_) private view returns (bool ok, uint8 assetDecimals) {
(bool success, bytes memory encodedDecimals) = address(asset_).staticcall(
abi.encodeCall(IERC20Metadata.decimals, ())
);
if (success && encodedDecimals.length >= 32) {
uint256 returnedDecimals = abi.decode(encodedDecimals, (uint256));
if (returnedDecimals <= type(uint8).max) {
return (true, uint8(returnedDecimals));
}
}
return (false, 0);
}
/**
* @dev Decimals are computed by adding the decimal offset on top of the underlying asset's decimals. This
* "original" value is cached during construction of the vault contract. If this read operation fails (e.g., the
* asset has not been created yet), a default of 18 is used to represent the underlying asset's decimals.
*
* See {IERC20Metadata-decimals}.
*/
function decimals() public view virtual override(IERC20Metadata, ERC20Upgradeable) returns (uint8) {
ERC4626Storage storage $ = _getERC4626Storage();
return $._underlyingDecimals + _decimalsOffset();
}
/** @dev See {IERC4626-asset}. */
function asset() public view virtual returns (address) {
ERC4626Storage storage $ = _getERC4626Storage();
return address($._asset);
}
/** @dev See {IERC4626-totalAssets}. */
function totalAssets() public view virtual returns (uint256) {
return IERC20(asset()).balanceOf(address(this));
}
/** @dev See {IERC4626-convertToShares}. */
function convertToShares(uint256 assets) public view virtual returns (uint256) {
return _convertToShares(assets, Math.Rounding.Floor);
}
/** @dev See {IERC4626-convertToAssets}. */
function convertToAssets(uint256 shares) public view virtual returns (uint256) {
return _convertToAssets(shares, Math.Rounding.Floor);
}
/** @dev See {IERC4626-maxDeposit}. */
function maxDeposit(address) public view virtual returns (uint256) {
return type(uint256).max;
}
/** @dev See {IERC4626-maxMint}. */
function maxMint(address) public view virtual returns (uint256) {
return type(uint256).max;
}
/** @dev See {IERC4626-maxWithdraw}. */
function maxWithdraw(address owner) public view virtual returns (uint256) {
return _convertToAssets(balanceOf(owner), Math.Rounding.Floor);
}
/** @dev See {IERC4626-maxRedeem}. */
function maxRedeem(address owner) public view virtual returns (uint256) {
return balanceOf(owner);
}
/** @dev See {IERC4626-previewDeposit}. */
function previewDeposit(uint256 assets) public view virtual returns (uint256) {
return _convertToShares(assets, Math.Rounding.Floor);
}
/** @dev See {IERC4626-previewMint}. */
function previewMint(uint256 shares) public view virtual returns (uint256) {
return _convertToAssets(shares, Math.Rounding.Ceil);
}
/** @dev See {IERC4626-previewWithdraw}. */
function previewWithdraw(uint256 assets) public view virtual returns (uint256) {
return _convertToShares(assets, Math.Rounding.Ceil);
}
/** @dev See {IERC4626-previewRedeem}. */
function previewRedeem(uint256 shares) public view virtual returns (uint256) {
return _convertToAssets(shares, Math.Rounding.Floor);
}
/** @dev See {IERC4626-deposit}. */
function deposit(uint256 assets, address receiver) public virtual returns (uint256) {
uint256 maxAssets = maxDeposit(receiver);
if (assets > maxAssets) {
revert ERC4626ExceededMaxDeposit(receiver, assets, maxAssets);
}
uint256 shares = previewDeposit(assets);
_deposit(_msgSender(), receiver, assets, shares);
return shares;
}
/** @dev See {IERC4626-mint}. */
function mint(uint256 shares, address receiver) public virtual returns (uint256) {
uint256 maxShares = maxMint(receiver);
if (shares > maxShares) {
revert ERC4626ExceededMaxMint(receiver, shares, maxShares);
}
uint256 assets = previewMint(shares);
_deposit(_msgSender(), receiver, assets, shares);
return assets;
}
/** @dev See {IERC4626-withdraw}. */
function withdraw(uint256 assets, address receiver, address owner) public virtual returns (uint256) {
uint256 maxAssets = maxWithdraw(owner);
if (assets > maxAssets) {
revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets);
}
uint256 shares = previewWithdraw(assets);
_withdraw(_msgSender(), receiver, owner, assets, shares);
return shares;
}
/** @dev See {IERC4626-redeem}. */
function redeem(uint256 shares, address receiver, address owner) public virtual returns (uint256) {
uint256 maxShares = maxRedeem(owner);
if (shares > maxShares) {
revert ERC4626ExceededMaxRedeem(owner, shares, maxShares);
}
uint256 assets = previewRedeem(shares);
_withdraw(_msgSender(), receiver, owner, assets, shares);
return assets;
}
/**
* @dev Internal conversion function (from assets to shares) with support for rounding direction.
*/
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256) {
return assets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, rounding);
}
/**
* @dev Internal conversion function (from shares to assets) with support for rounding direction.
*/
function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual returns (uint256) {
return shares.mulDiv(totalAssets() + 1, totalSupply() + 10 ** _decimalsOffset(), rounding);
}
/**
* @dev Deposit/mint common workflow.
*/
function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual {
// If asset() is ERC-777, `transferFrom` can trigger a reentrancy BEFORE the transfer happens through the
// `tokensToSend` hook. On the other hand, the `tokenReceived` hook, that is triggered after the transfer,
// calls the vault, which is assumed not malicious.
//
// Conclusion: we need to do the transfer before we mint so that any reentrancy would happen before the
// assets are transferred and before the shares are minted, which is a valid state.
// slither-disable-next-line reentrancy-no-eth
SafeERC20.safeTransferFrom(IERC20(asset()), caller, address(this), assets);
_mint(receiver, shares);
emit Deposit(caller, receiver, assets, shares);
}
/**
* @dev Withdraw/redeem common workflow.
*/
function _withdraw(
address caller,
address receiver,
address owner,
uint256 assets,
uint256 shares
) internal virtual {
if (caller != owner) {
_spendAllowance(owner, caller, shares);
}
// If asset() is ERC-777, `transfer` can trigger a reentrancy AFTER the transfer happens through the
// `tokensReceived` hook. On the other hand, the `tokensToSend` hook, that is triggered before the transfer,
// calls the vault, which is assumed not malicious.
//
// Conclusion: we need to do the transfer after the burn so that any reentrancy would happen after the
// shares are burned and after the assets are transferred, which is a valid state.
_burn(owner, shares);
SafeERC20.safeTransfer(IERC20(asset()), receiver, assets);
emit Withdraw(caller, receiver, owner, assets, shares);
}
function _decimalsOffset() internal view virtual returns (uint8) {
return 0;
}
}
"
},
"lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/IERC20.sol)
pragma solidity ^0.8.20;
/**
* @dev Interface of the ERC-20 standard as defined in the ERC.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
/**
* @dev Returns the value of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the value of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 value) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 value) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the
* allowance mechanism. `value` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
"
},
"lib/openzeppelin-contracts-upgradeable/contracts/utils/ReentrancyGuardUpgradeable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/ReentrancyGuard.sol)
pragma solidity ^0.8.20;
import {Initializable} from "../proxy/utils/Initializable.sol";
/**
* @dev Contract module that helps prevent reentrant calls to a function.
*
* Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
* available, which can be applied to functions to make sure there are no nested
* (reentrant) calls to them.
*
* Note that because there is a single `nonReentrant` guard, functions marked as
* `nonReentrant` may not call one another. This can be worked around by making
* those functions `private`, and then adding `external` `nonReentrant` entry
* points to them.
*
* TIP: If EIP-1153 (transient storage) is available on the chain you're deploying at,
* consider using {ReentrancyGuardTransient} instead.
*
* TIP: If you would like to learn more about reentrancy and alternative ways
* to protect against it, check out our blog post
* https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
*/
abstract contract ReentrancyGuardUpgradeable is Initializable {
// Booleans are more expensive than uint256 or any type that takes up a full
// word because each write operation emits an extra SLOAD to first read the
// slot's contents, replace the bits taken up by the boolean, and then write
// back. This is the compiler's defense against contract upgrades and
// pointer aliasing, and it cannot be disabled.
// The values being non-zero value makes deployment a bit more expensive,
// but in exchange the refund on every call to nonReentrant will be lower in
// amount. Since refunds are capped to a percentage of the total
// transaction's gas, it is best to keep them low in cases like this one, to
// increase the likelihood of the full refund coming into effect.
uint256 private constant NOT_ENTERED = 1;
uint256 private constant ENTERED = 2;
/// @custom:storage-location erc7201:openzeppelin.storage.ReentrancyGuard
struct ReentrancyGuardStorage {
uint256 _status;
}
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ReentrancyGuard")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant ReentrancyGuardStorageLocation = 0x9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00;
function _getReentrancyGuardStorage() private pure returns (ReentrancyGuardStorage storage $) {
assembly {
$.slot := ReentrancyGuardStorageLocation
}
}
/**
* @dev Unauthorized reentrant call.
*/
error ReentrancyGuardReentrantCall();
function __ReentrancyGuard_init() internal onlyInitializing {
__ReentrancyGuard_init_unchained();
}
function __ReentrancyGuard_init_unchained() internal onlyInitializing {
ReentrancyGuardStorage storage $ = _getReentrancyGuardStorage();
$._status = NOT_ENTERED;
}
/**
* @dev Prevents a contract from calling itself, directly or indirectly.
* Calling a `nonReentrant` function from another `nonReentrant`
* function is not supported. It is possible to prevent this from happening
* by making the `nonReentrant` function external, and making it call a
* `private` function that does the actual work.
*/
modifier nonReentrant() {
_nonReentrantBefore();
_;
_nonReentrantAfter();
}
function _nonReentrantBefore() private {
ReentrancyGuardStorage storage $ = _getReentrancyGuardStorage();
// On the first call to nonReentrant, _status will be NOT_ENTERED
if ($._status == ENTERED) {
revert ReentrancyGuardReentrantCall();
}
// Any calls to nonReentrant after this point will fail
$._status = ENTERED;
}
function _nonReentrantAfter() private {
ReentrancyGuardStorage storage $ = _getReentrancyGuardStorage();
// By storing the original value once again, a refund is triggered (see
// https://eips.ethereum.org/EIPS/eip-2200)
$._status = NOT_ENTERED;
}
/**
* @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
* `nonReentrant` function in the call stack.
*/
function _reentrancyGuardEntered() internal view returns (bool) {
ReentrancyGuardStorage storage $ = _getReentrancyGuardStorage();
return $._status == ENTERED;
}
}
"
},
"lib/openzeppelin-contracts/contracts/interfaces/IERC4626.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC4626.sol)
pragma solidity ^0.8.20;
import {IERC20} from "../token/ERC20/IERC20.sol";
import {IERC20Metadata} from "../token/ERC20/extensions/IERC20Metadata.sol";
/**
* @dev Interface of the ERC-4626 "Tokenized Vault Standard", as defined in
* https://eips.ethereum.org/EIPS/eip-4626[ERC-4626].
*/
interface IERC4626 is IERC20, IERC20Metadata {
event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares);
event Withdraw(
address indexed sender,
address indexed receiver,
address indexed owner,
uint256 assets,
uint256 shares
);
/**
* @dev Returns the address of the underlying token used for the Vault for accounting, depositing, and withdrawing.
*
* - MUST be an ERC-20 token contract.
* - MUST NOT revert.
*/
function asset() external view returns (address assetTokenAddress);
/**
* @dev Returns the total amount of the underlying asset that is “managed” by Vault.
*
* - SHOULD include any compounding that occurs from yield.
* - MUST be inclusive of any fees that are charged against assets in the Vault.
* - MUST NOT revert.
*/
function totalAssets() external view returns (uint256 totalManagedAssets);
/**
* @dev Returns the amount of shares that the Vault would exchange for the amount of assets provided, in an ideal
* scenario where all the conditions are met.
*
* - MUST NOT be inclusive of any fees that are charged against assets in the Vault.
* - MUST NOT show any variations depending on the caller.
* - MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange.
* - MUST NOT revert.
*
* NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the
* “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and
* from.
*/
function convertToShares(uint256 assets) external view returns (uint256 shares);
/**
* @dev Returns the amount of assets that the Vault would exchange for the amount of shares provided, in an ideal
* scenario where all the conditions are met.
*
* - MUST NOT be inclusive of any fees that are charged against assets in the Vault.
* - MUST NOT show any variations depending on the caller.
* - MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange.
* - MUST NOT revert.
*
* NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the
* “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and
* from.
*/
function convertToAssets(uint256 shares) external view returns (uint256 assets);
/**
* @dev Returns the maximum amount of the underlying asset that can be deposited into the Vault for the receiver,
* through a deposit call.
*
* - MUST return a limited value if receiver is subject to some deposit limit.
* - MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of assets that may be deposited.
* - MUST NOT revert.
*/
function maxDeposit(address receiver) external view returns (uint256 maxAssets);
/**
* @dev Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given
* current on-chain conditions.
*
* - MUST return as close to and no more than the exact amount of Vault shares that would be minted in a deposit
* call in the same transaction. I.e. deposit should return the same or more shares as previewDeposit if called
* in the same transaction.
* - MUST NOT account for deposit limits like those returned from maxDeposit and should always act as though the
* deposit would be accepted, regardless if the user has enough tokens approved, etc.
* - MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees.
* - MUST NOT revert.
*
* NOTE: any unfavorable discrepancy between convertToShares and previewDeposit SHOULD be considered slippage in
* share price or some other type of condition, meaning the depositor will lose assets by depositing.
*/
function previewDeposit(uint256 assets) external view returns (uint256 shares);
/**
* @dev Mints shares Vault shares to receiver by depositing exactly amount of underlying tokens.
*
* - MUST emit the Deposit event.
* - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the
* deposit execution, and are accounted for during deposit.
* - MUST revert if all of assets cannot be deposited (due to deposit limit being reached, slippage, the user not
* approving enough underlying tokens to the Vault contract, etc).
*
* NOTE: most implementations will require pre-approval of the Vault with the Vault’s underlying asset token.
*/
function deposit(uint256 assets, address receiver) external returns (uint256 shares);
/**
* @dev Returns the maximum amount of the Vault shares that can be minted for the receiver, through a mint call.
* - MUST return a limited value if receiver is subject to some mint limit.
* - MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of shares that may be minted.
* - MUST NOT revert.
*/
function maxMint(address receiver) external view returns (uint256 maxShares);
/**
* @dev Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given
* current on-chain conditions.
*
* - MUST return as close to and no fewer than the exact amount of assets that would be deposited in a mint call
* in the same transaction. I.e. mint should return the same or fewer assets as previewMint if called in the
* same transaction.
* - MUST NOT account for mint limits like those returned from maxMint and should always act as though the mint
* would be accepted, regardless if the user has enough tokens approved, etc.
* - MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees.
* - MUST NOT revert.
*
* NOTE: any unfavorable discrepancy between convertToAssets and previewMint SHOULD be considered slippage in
* share price or some other type of condition, meaning the depositor will lose assets by minting.
*/
function previewMint(uint256 shares) external view returns (uint256 assets);
/**
* @dev Mints exactly shares Vault shares to receiver by depositing amount of underlying tokens.
*
* - MUST emit the Deposit event.
* - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the mint
* execution, and are accounted for during mint.
* - MUST revert if all of shares cannot be minted (due to deposit limit being reached, slippage, the user not
* approving enough underlying tokens to the Vault contract, etc).
*
* NOTE: most implementations will require pre-approval of the Vault with the Vault’s underlying asset token.
*/
function mint(uint256 shares, address receiver) external returns (uint256 assets);
/**
* @dev Returns the maximum amount of the underlying asset that can be withdrawn from the owner balance in the
* Vault, through a withdraw call.
*
* - MUST return a limited value if owner is subject to some withdrawal limit or timelock.
* - MUST NOT revert.
*/
function maxWithdraw(address owner) external view returns (uint256 maxAssets);
/**
* @dev Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block,
* given current on-chain conditions.
*
* - MUST return as close to and no fewer than the exact amount of Vault shares that would be burned in a withdraw
* call in the same transaction. I.e. withdraw should return the same or fewer shares as previewWithdraw if
* called
* in the same transaction.
* - MUST NOT account for withdrawal limits like those returned from maxWithdraw and should always act as though
* the withdrawal would be accepted, regardle
Submitted on: 2025-09-17 11:36:04
Comments
Log in to comment.
No comments yet.