Vault

Git Source

Inherits: ERC4626, Multicall, AccessControlDefaultAdminRules, ReentrancyGuard

Author: Molecular Labs

Vault contract that can allocate a single lender asset over various isolated lending pairs on Ion Protocol. This contract is a fork of the Metamorpho contract licnesed under GPL-2.0 with changes to administrative logic, underlying data structures, and lending interactions to be made compatible with Ion Protocol.

State Variables

OWNER_ROLE

bytes32 public constant OWNER_ROLE = keccak256("OWNER_ROLE");

ALLOCATOR_ROLE

bytes32 public constant ALLOCATOR_ROLE = keccak256("ALLOCATOR_ROLE");

IDLE

IIonPool public constant IDLE = IIonPool(address(uint160(uint256(keccak256("IDLE_ASSET_HOLDINGS")))));

DECIMALS_OFFSET

uint8 public immutable DECIMALS_OFFSET;

ION_POOL_SUPPLY_CAP_SLOT

bytes32 public immutable ION_POOL_SUPPLY_CAP_SLOT = 0xceba3d526b4d5afd91d1b752bf1fd37917c20a6daf576bcb41dd1c57c1f67e09;

ION_POOL_LIQUIDITY_SLOT

bytes32 public immutable ION_POOL_LIQUIDITY_SLOT = 0xceba3d526b4d5afd91d1b752bf1fd37917c20a6daf576bcb41dd1c57c1f67e08;

BASE_ASSET

IERC20 public immutable BASE_ASSET;

MAX_SUPPORTED_MARKETS

uint8 public constant MAX_SUPPORTED_MARKETS = 32;

supportedMarkets

EnumerableSet.AddressSet supportedMarkets;

supplyQueue

IIonPool[] public supplyQueue;

withdrawQueue

IIonPool[] public withdrawQueue;

feeRecipient

address public feeRecipient;

feePercentage

uint256 public feePercentage;

lastTotalAssets

uint256 public lastTotalAssets;

caps

mapping(IIonPool => uint256) public caps;

Functions

constructor

constructor(
    IERC20 _baseAsset,
    address _feeRecipient,
    uint256 _feePercentage,
    string memory _name,
    string memory _symbol,
    uint48 initialDelay,
    address initialDefaultAdmin,
    MarketsArgs memory marketsArgs
)
    ERC4626(_baseAsset)
    ERC20(_name, _symbol)
    AccessControlDefaultAdminRules(initialDelay, initialDefaultAdmin);

updateFeePercentage

Updates the fee percentage.

Input must be in [RAY]. Ex) 2% would be 0.02e27.

function updateFeePercentage(uint256 _feePercentage) external onlyRole(OWNER_ROLE);

Parameters

NameTypeDescription

_feePercentage

uint256

The percentage of the interest accrued to take as a management fee.

updateFeeRecipient

Updates the fee recipient.

function updateFeeRecipient(address _feeRecipient) external onlyRole(OWNER_ROLE);

Parameters

NameTypeDescription

_feeRecipient

address

The recipient address of the shares minted as fees.

addSupportedMarkets

Add markets that can be supplied and withdrawn from.

Elements in supportedMarkets must be a valid IonPool or an IDLE address. Valid IonPools require the base asset to be the same. Duplicate addition to the EnumerableSet will revert. The allocationCaps of the new markets being introduced must be set.

function addSupportedMarkets(
    IIonPool[] memory marketsToAdd,
    uint256[] memory allocationCaps,
    IIonPool[] memory newSupplyQueue,
    IIonPool[] memory newWithdrawQueue
)
    external
    onlyRole(OWNER_ROLE);

Parameters

NameTypeDescription

marketsToAdd

IIonPool[]

Array of new markets to be added.

allocationCaps

uint256[]

Array of allocation caps for only the markets to be added.

newSupplyQueue

IIonPool[]

Desired supply queue of IonPools for all resulting supported markets.

newWithdrawQueue

IIonPool[]

Desired withdraw queue of IonPools for all resulting supported markets.

_addSupportedMarkets

function _addSupportedMarkets(
    IIonPool[] memory marketsToAdd,
    uint256[] memory allocationCaps,
    IIonPool[] memory newSupplyQueue,
    IIonPool[] memory newWithdrawQueue
)
    internal;

removeSupportedMarkets

Removes a supported market and updates the supply and withdraw queues without the removed market.

The allocationCap values of the markets being removed are automatically deleted. Whenever a market is removed, the queues must be updated without the removed market.

function removeSupportedMarkets(
    IIonPool[] calldata marketsToRemove,
    IIonPool[] calldata newSupplyQueue,
    IIonPool[] calldata newWithdrawQueue
)
    external
    onlyRole(OWNER_ROLE);

Parameters

NameTypeDescription

marketsToRemove

IIonPool[]

Markets being removed.

newSupplyQueue

IIonPool[]

Desired supply queue of all supported markets after the removal.

newWithdrawQueue

IIonPool[]

Desired withdraw queue of all supported markets after the removal.

updateSupplyQueue

Update the order of the markets in which user deposits are supplied.

Each IonPool in the queue must be part of the supportedMarkets set.

function updateSupplyQueue(IIonPool[] memory newSupplyQueue) external onlyRole(ALLOCATOR_ROLE);

Parameters

NameTypeDescription

newSupplyQueue

IIonPool[]

The new supply queue ordering.

_updateSupplyQueue

function _updateSupplyQueue(IIonPool[] memory newSupplyQueue) internal;

updateWithdrawQueue

Update the order of the markets in which the deposits are withdrawn.

The IonPool in the queue must be part of the supportedMarkets set.

function updateWithdrawQueue(IIonPool[] memory newWithdrawQueue) external onlyRole(ALLOCATOR_ROLE);

Parameters

NameTypeDescription

newWithdrawQueue

IIonPool[]

The new withdraw queue ordering.

_updateWithdrawQueue

function _updateWithdrawQueue(IIonPool[] memory newWithdrawQueue) internal;

_validateQueueInput

*The input array contains ordered IonPools.

  • Must not contain duplicates.

  • Must be the same length as the supportedMarkets array.

  • Must not contain indices that are out of bounds of the supportedMarkets EnumerableSet's underlying array. The above rule enforces that the queue must have all and only the elements in the supportedMarkets set.*

function _validateQueueInput(IIonPool[] memory queue) internal view;

Parameters

NameTypeDescription

queue

IIonPool[]

The queue being validated.

updateAllocationCaps

Update allocation caps for specified IonPools or the IDLE pool.

The allocation caps are applied to pools in the order of the array within supportedMarkets. The elements inside ionPools must exist in supportedMarkets. To update the IDLE pool, use the IDLE constant address.

function updateAllocationCaps(IIonPool[] calldata ionPools, uint256[] calldata newCaps) external onlyRole(OWNER_ROLE);

Parameters

NameTypeDescription

ionPools

IIonPool[]

The array of IonPools whose caps will be updated.

newCaps

uint256[]

The array of new allocation caps to be applied.

reallocate

Reallocates the base asset supply position across the specified IonPools. This call will revert if the resulting allocation in an IonPool violates the pool's supply cap.

*Depending on the order of deposits and withdrawals to and from markets, the function could revert if there is not enough assets withdrawn to deposit later in the loop. A key invariant is that the total assets withdrawn should be equal to the total assets supplied. Otherwise, revert.

  • Negative value indicates a withdrawal.

  • Positive value indicates a supply.*

function reallocate(MarketAllocation[] calldata allocations) external onlyRole(ALLOCATOR_ROLE) nonReentrant;

Parameters

NameTypeDescription

allocations

MarketAllocation[]

Array that indicates how much to deposit or withdraw from each market.

accrueFee

Manually accrues fees and mints shares to the fee recipient.

function accrueFee() external onlyRole(OWNER_ROLE) returns (uint256 newTotalAssets);

_supplyToIonPool

Iterates through the supply queue to deposit the desired amount of assets. Reverts if the deposit amount cannot be filled due to the allocation cap or the supply cap.

External functions calling this must be non-reentrant in case the underlying IonPool implements callback logic.

function _supplyToIonPool(uint256 assets) internal;

Parameters

NameTypeDescription

assets

uint256

The amount of assets that will attempt to be supplied.

_withdrawFromIonPool

Iterates through the withdraw queue to withdraw the desired amount of assets. Will revert if there is not enough liquidity or if trying to withdraw more than the caller owns.

External functions calling this must be non-reentrant in case the underlying IonPool implements callback logic.

function _withdrawFromIonPool(uint256 assets) internal;

Parameters

NameTypeDescription

assets

uint256

The desired amount of assets to be withdrawn.

deposit

Transfers the specified amount of assets from the sender, supplies into the underlying IonPool markets, and mints a corresponding amount of shares.

All incoming deposits are deposited in the order specified in the deposit queue.

function deposit(uint256 assets, address receiver) public override nonReentrant returns (uint256 shares);

Parameters

NameTypeDescription

assets

uint256

Amount of tokens to be deposited.

receiver

address

The address to receive the minted shares.

mint

Mints the specified amount of shares and deposits a corresponding amount of assets.

Converts the shares to assets and iterates through the deposit queue to allocate the deposit across the supported markets.

function mint(uint256 shares, address receiver) public override nonReentrant returns (uint256 assets);

Parameters

NameTypeDescription

shares

uint256

The exact amount of shares to be minted.

receiver

address

The address to receive the minted shares.

withdraw

Withdraws specified amount of assets from IonPools and sends them to the receiver in exchange for burning the owner's vault shares.

All withdraws are withdrawn in the order specified in the withdraw queue. The owner needs to approve the caller to spend their shares.

function withdraw(
    uint256 assets,
    address receiver,
    address owner
)
    public
    override
    nonReentrant
    returns (uint256 shares);

Parameters

NameTypeDescription

assets

uint256

The exact amount of assets to be transferred out.

receiver

address

The receiver of the assets transferred.

owner

address

The owner of the vault shares.

redeem

Redeems the exact amount of shares and receives a corresponding amount of assets.

After withdrawing assets, the user gets exact assets out. But in the IonPool, the resulting total underlying claim may have decreased by a bit above the assets amount due to rounding in the pool's favor. In that case, the resulting totalAssets() will be smaller than just the newTotalAssets - assets. Predicting the exact resulting totalAssets() requires knowing how much liquidity is being withdrawn from each pool, which is not possible to know until the actual iteration on the withdraw queue. So we acknowledge the dust difference here. If the lastTotalAssets is slightly greater than the actual totalAssets, the impact will be that the calculated interest accrued during fee distribution will be slightly less than the true value.

function redeem(
    uint256 shares,
    address receiver,
    address owner
)
    public
    override
    nonReentrant
    returns (uint256 assets);

Parameters

NameTypeDescription

shares

uint256

The exact amount of shares to be burned and redeemed.

receiver

address

The address that receives the transferred assets.

owner

address

The address that holds the shares to be redeemed.

decimals

Returns the decimals places of the token.

function decimals() public view override(ERC4626) returns (uint8);

maxDeposit

Returns the maximum amount of assets that the vault can supply on Ion.

The max deposit amount is limited by the vault's allocation cap and the underlying IonPools' supply caps.

function maxDeposit(address) public view override returns (uint256);

Returns

NameTypeDescription

<none>

uint256

The max amount of assets that can be supplied.

maxMint

Returns the maximum amount of vault shares that can be minted.

Max mint is limited by the max deposit based on the Vault's allocation caps and the IonPools' supply caps. The conversion from max suppliable assets to shares preempts the shares minted from fee accrual.

function maxMint(address) public view override returns (uint256);

Returns

NameTypeDescription

<none>

uint256

The max amount of shares that can be minted.

maxWithdraw

Returns the maximum amount of assets that can be withdrawn.

Max withdraw is limited by the owner's shares and the liquidity available to be withdrawn from the underlying IonPools. The max withdrawable claim is inclusive of accrued interest and the extra shares minted to the fee recipient.

function maxWithdraw(address owner) public view override returns (uint256 assets);

Parameters

NameTypeDescription

owner

address

The address that holds the assets.

Returns

NameTypeDescription

assets

uint256

The max amount of assets that can be withdrawn.

maxRedeem

Calculates the total withdrawable amount based on the available liquidity in the underlying pools and converts it to redeemable shares.

Max redeem is derived from รงonverting the _maxWithdraw to shares. The conversion takes into account the total supply and total assets inclusive of accrued interest and the extra shares minted to the fee recipient.

function maxRedeem(address owner) public view override returns (uint256);

Parameters

NameTypeDescription

owner

address

The address that holds the shares.

Returns

NameTypeDescription

<none>

uint256

The max amount of shares that can be withdrawn.

totalAssets

Returns the total claim that the vault has across all supported IonPools.

IonPool.balanceOf returns the rebasing balance of the lender receipt token that is pegged 1:1 to the underlying supplied asset.

function totalAssets() public view override returns (uint256 assets);

Returns

NameTypeDescription

assets

uint256

The total assets held on the contract and inside the underlying pools by this vault.

previewDeposit

Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given current on-chain conditions.

Inclusive of manager fee.

function previewDeposit(uint256 assets) public view override returns (uint256);

previewMint

Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given current on-chain conditions.

Inclusive of manager fee.

function previewMint(uint256 shares) public view override returns (uint256);

previewWithdraw

Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, given current on-chain conditions.

Inclusive of manager fee.

function previewWithdraw(uint256 assets) public view override returns (uint256);

previewRedeem

Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block, given current on-chain conditions.

Inclusive of manager fee.

function previewRedeem(uint256 shares) public view override returns (uint256);

_decimalsOffset

function _decimalsOffset() internal view override returns (uint8);

_deposit

function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal override;

_withdraw

function _withdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares) internal override;

_maxDeposit

function _maxDeposit() internal view returns (uint256 maxDepositable);

_maxWithdraw

function _maxWithdraw(address owner)
    internal
    view
    returns (uint256 assets, uint256 newTotalSupply, uint256 newTotalAssets);

_accrueFee

function _accrueFee() internal returns (uint256 newTotalAssets);

_accruedFeeShares

The total accrued vault revenue is the difference in the total iToken holdings from the last accrued timestamp and now.

function _accruedFeeShares() internal view returns (uint256 feeShares, uint256 newTotalAssets);

_convertToSharesWithFees

NOTE The IERC4626 natspec recommends that the _convertToAssets and _convertToShares "MUST NOT be inclusive of any fees that are charged against assets in the Vault." However, all deposit/mint/withdraw/redeem flow will accrue fees before processing user requests, so manager fee must be accounted for to accurately reflect the resulting state. All preview functions will rely on this WithFees version of the _convertTo function.

function _convertToSharesWithFees(uint256 assets, Math.Rounding rounding) internal view returns (uint256);

_convertToAssetsWithFees

NOTE The IERC4626 natspec recommends that the _convertToAssets and _convertToShares "MUST NOT be inclusive of any fees that are charged against assets in the Vault." However, all deposit/mint/withdraw/redeem flow will accrue fees before processing user requests, so manager fee must be accounted for to accurately reflect the resulting state. All preview functions will rely on this WithFees version of the _convertTo function.

function _convertToAssetsWithFees(uint256 shares, Math.Rounding rounding) internal view returns (uint256);

_convertToSharesWithTotals

Returns the amount of shares that the vault would exchange for the amount of assets provided. This function is used to calculate the conversion between shares and assets with parameterizable total supply and total assets variables.

function _convertToSharesWithTotals(
    uint256 assets,
    uint256 newTotalSupply,
    uint256 newTotalAssets,
    Math.Rounding rounding
)
    internal
    view
    returns (uint256);

_convertToAssetsWithTotals

Returns the amount of assets that the vault would exchange for the amount of shares provided. This function is used to calculate the conversion between shares and assets with parameterizable total supply and total assets variables.

function _convertToAssetsWithTotals(
    uint256 shares,
    uint256 newTotalSupply,
    uint256 newTotalAssets,
    Math.Rounding rounding
)
    internal
    view
    returns (uint256);

_updateLastTotalAssets

function _updateLastTotalAssets(uint256 newLastTotalAssets) internal;

_zeroFloorSub

function _zeroFloorSub(uint256 x, uint256 y) internal pure returns (uint256 z);

_simulateWithdrawIon

Emulates the actual _withdrawFromIonPool accounting to predict accurately how much of the input assets will be left after withdrawing as much as it can. The difference between this return value and the input assets is the exact amount that will be withdrawn.

function _simulateWithdrawIon(uint256 assets) internal view returns (uint256);

Returns

NameTypeDescription

<none>

uint256

The remaining assets to be withdrawn. NOT the amount of assets that were withdrawn.

_withdrawable

The max amount of assets withdrawable from a given IonPool considering the vault's claim and the available liquidity. A minimum of this contract's total claim on the underlying and the available liquidity in the pool.

function _withdrawable(IIonPool pool) internal view returns (uint256);

Returns

NameTypeDescription

<none>

uint256

The max amount of assets withdrawable from this IonPool.

_depositable

The max amount of assets depositable to a given IonPool. Depositing the minimum between the two diffs ensures that the deposit will not violate the allocation cap or the supply cap.

function _depositable(IIonPool pool) internal view returns (uint256);

Returns

NameTypeDescription

<none>

uint256

The max amount of assets depositable to this IonPool.

getSupportedMarkets

Returns the array representation of the supportedMarkets set.

function getSupportedMarkets() external view returns (address[] memory);

Returns

NameTypeDescription

<none>

address[]

Array of supported IonPools.

containsSupportedMarket

Returns whether the market is part of the supportedMarkets set.

function containsSupportedMarket(address pool) external view returns (bool);

Parameters

NameTypeDescription

pool

address

The address of the IonPool to be checked.

Returns

NameTypeDescription

<none>

bool

The pool is supported if true. If not, false.

supportedMarketsAt

Returns the element in the array representation of supportedMarkets. index must be strictly less than the length of the array.

function supportedMarketsAt(uint256 index) external view returns (address);

Parameters

NameTypeDescription

index

uint256

The index to be queried on the supportedMarkets array.

Returns

NameTypeDescription

<none>

address

Address at the index of supportedMarkets.

supportedMarketsIndexOf

Returns the index of the specified market in the array representation of supportedMarkets.

The _positions mapping inside the EnumerableSet.Set returns the index of the element in the _values array plus 1. The _positions value of 0 means that the value is not in the set. If the value is not in the set, this call will revert. Otherwise, it will return the position - 1 value to return the index of the element in the array.

function supportedMarketsIndexOf(address pool) external view returns (uint256);

Parameters

NameTypeDescription

pool

address

The address of the IonPool to be queried.

Returns

NameTypeDescription

<none>

uint256

The index of the pool's location in the array. The return value will always be greater than zero as this function would revert if the market is not part of the set.

supportedMarketsLength

Length of the array representation of supportedMarkets.

function supportedMarketsLength() external view returns (uint256);

Returns

NameTypeDescription

<none>

uint256

The length of the supportedMarkets array.

_supportedMarketsIndexOf

function _supportedMarketsIndexOf(address pool) internal view returns (uint256);

Events

UpdateSupplyQueue

event UpdateSupplyQueue(address indexed caller, IIonPool[] newSupplyQueue);

UpdateWithdrawQueue

event UpdateWithdrawQueue(address indexed caller, IIonPool[] newWithdrawQueue);

ReallocateWithdraw

event ReallocateWithdraw(IIonPool indexed pool, uint256 assets);

ReallocateSupply

event ReallocateSupply(IIonPool indexed pool, uint256 assets);

FeeAccrued

event FeeAccrued(uint256 feeShares, uint256 newTotalAssets);

UpdateLastTotalAssets

event UpdateLastTotalAssets(uint256 lastTotalAssets, uint256 newLastTotalAssets);

Errors

InvalidQueueLength

error InvalidQueueLength(uint256 queueLength, uint256 supportedMarketsLength);

AllocationCapExceeded

error AllocationCapExceeded(uint256 resultingSupplied, uint256 allocationCap);

InvalidReallocation

error InvalidReallocation(uint256 totalSupplied, uint256 totalWithdrawn);

InvalidMarketRemovalNonZeroSupply

error InvalidMarketRemovalNonZeroSupply(IIonPool pool);

InvalidUnderlyingAsset

error InvalidUnderlyingAsset(IIonPool pool);

MarketAlreadySupported

error MarketAlreadySupported(IIonPool pool);

MarketNotSupported

error MarketNotSupported(IIonPool pool);

AllSupplyCapsReached

error AllSupplyCapsReached();

NotEnoughLiquidityToWithdraw

error NotEnoughLiquidityToWithdraw();

InvalidIdleMarketRemovalNonZeroBalance

error InvalidIdleMarketRemovalNonZeroBalance();

InvalidQueueContainsDuplicates

error InvalidQueueContainsDuplicates();

MarketsAndAllocationCapLengthMustBeEqual

error MarketsAndAllocationCapLengthMustBeEqual();

IonPoolsArrayAndNewCapsArrayMustBeOfEqualLength

error IonPoolsArrayAndNewCapsArrayMustBeOfEqualLength();

InvalidFeePercentage

error InvalidFeePercentage();

MaxSupportedMarketsReached

error MaxSupportedMarketsReached();

Structs

MarketAllocation

struct MarketAllocation {
    IIonPool pool;
    int256 assets;
}

MarketsArgs

struct MarketsArgs {
    IIonPool[] marketsToAdd;
    uint256[] allocationCaps;
    IIonPool[] newSupplyQueue;
    IIonPool[] newWithdrawQueue;
}