Tokenized Vaults
Abstract
The following standard allows for the implementation of a standard API for tokenized Vaults representing shares of a single underlying EIP-20 token. This standard is an extension on the EIP-20 token that provides basic functionality for depositing and withdrawing tokens and reading balances.
Motivation
Tokenized Vaults have a lack of standardization leading to diverse implementation details. Some various examples include lending markets, aggregators, and intrinsically interest bearing tokens. This makes integration difficult at the aggregator or plugin layer for protocols which need to conform to many standards, and forces each protocol to implement their own adapters which are error prone and waste development resources.
A standard for tokenized Vaults will lower the integration effort for yield-bearing vaults, while creating more consistent and robust implementation patterns.
Specification
All EIP-4626 tokenized Vaults MUST implement EIP-20 to represent shares.
If a Vault is to be non-transferrable, it MAY revert on calls to transfer
or transferFrom
.
The EIP-20 operations balanceOf
, transfer
, totalSupply
, etc. operate on the Vault “shares”
which represent a claim to ownership on a fraction of the Vault’s underlying holdings.
All EIP-4626 tokenized Vaults MUST implement EIP-20’s optional metadata extensions.
The name
and symbol
functions SHOULD reflect the underlying token’s name
and symbol
in some way.
EIP-4626 tokenized Vaults MAY implement EIP-2612 to improve the UX of approving shares on various integrations.
Definitions:
- asset: The underlying token managed by the Vault. Has units defined by the corresponding EIP-20 contract.
- share: The token of the Vault. Has a ratio of underlying assets exchanged on mint/deposit/withdraw/redeem (as defined by the Vault).
- fee: An amount of assets or shares charged to the user by the Vault. Fees can exists for deposits, yield, AUM, withdrawals, or anything else prescribed by the Vault.
- slippage: Any difference between advertised share price and economic realities of deposit to or withdrawal from the Vault, which is not accounted by fees.
Methods
asset
The address of the underlying token used for the Vault for accounting, depositing, and withdrawing.
MUST be an EIP-20 token contract.
MUST NOT revert.
- name: asset
type: function
stateMutability: view
inputs: []
outputs:
- name: assetTokenAddress
type: address
totalAssets
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.
- name: totalAssets
type: function
stateMutability: view
inputs: []
outputs:
- name: totalManagedAssets
type: uint256
convertToShares
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 unless due to integer overflow caused by an unreasonably large input.
MUST round down towards 0.
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.
- name: convertToShares
type: function
stateMutability: view
inputs:
- name: assets
type: uint256
outputs:
- name: shares
type: uint256
convertToAssets
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 unless due to integer overflow caused by an unreasonably large input.
MUST round down towards 0.
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.
- name: convertToAssets
type: function
stateMutability: view
inputs:
- name: shares
type: uint256
outputs:
- name: assets
type: uint256
maxDeposit
Maximum amount of the underlying asset that can be deposited into the Vault for the receiver
, through a deposit
call.
MUST return the maximum amount of assets deposit
would allow to be deposited for receiver
and not cause a revert, which MUST NOT be higher than the actual maximum that would be accepted (it should underestimate if necessary). This assumes that the user has infinite assets, i.e. MUST NOT rely on balanceOf
of asset
.
MUST factor in both global and user-specific limits, like if deposits are entirely disabled (even temporarily) it MUST return 0.
MUST return 2 ** 256 - 1
if there is no limit on the maximum amount of assets that may be deposited.
MUST NOT revert.
- name: maxDeposit
type: function
stateMutability: view
inputs:
- name: receiver
type: address
outputs:
- name: maxAssets
type: uint256
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.
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 due to vault specific user/global limits. MAY revert due to other conditions that would also cause deposit
to revert.
Note that 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.
- name: previewDeposit
type: function
stateMutability: view
inputs:
- name: assets
type: uint256
outputs:
- name: shares
type: uint256
deposit
Mints shares
Vault shares to receiver
by depositing exactly assets
of underlying tokens.
MUST emit the Deposit
event.
MUST support EIP-20 approve
/ transferFrom
on asset
as a deposit flow.
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 that most implementations will require pre-approval of the Vault with the Vault’s underlying asset
token.
- name: deposit
type: function
stateMutability: nonpayable
inputs:
- name: assets
type: uint256
- name: receiver
type: address
outputs:
- name: shares
type: uint256
maxMint
Maximum amount of shares that can be minted from the Vault for the receiver
, through a mint
call.
MUST return the maximum amount of shares mint
would allow to be deposited to receiver
and not cause a revert, which MUST NOT be higher than the actual maximum that would be accepted (it should underestimate if necessary). This assumes that the user has infinite assets, i.e. MUST NOT rely on balanceOf
of asset
.
MUST factor in both global and user-specific limits, like if mints are entirely disabled (even temporarily) it MUST return 0.
MUST return 2 ** 256 - 1
if there is no limit on the maximum amount of shares that may be minted.
MUST NOT revert.
- name: maxMint
type: function
stateMutability: view
inputs:
- name: receiver
type: address
outputs:
- name: maxShares
type: 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.
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 due to vault specific user/global limits. MAY revert due to other conditions that would also cause mint
to revert.
Note that 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.
- name: previewMint
type: function
stateMutability: view
inputs:
- name: shares
type: uint256
outputs:
- name: assets
type: uint256
mint
Mints exactly shares
Vault shares to receiver
by depositing assets
of underlying tokens.
MUST emit the Deposit
event.
MUST support EIP-20 approve
/ transferFrom
on asset
as a mint flow.
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 that most implementations will require pre-approval of the Vault with the Vault’s underlying asset
token.
- name: mint
type: function
stateMutability: nonpayable
inputs:
- name: shares
type: uint256
- name: receiver
type: address
outputs:
- name: assets
type: uint256
maxWithdraw
Maximum amount of the underlying asset that can be withdrawn from the owner
balance in the Vault, through a withdraw
call.
MUST return the maximum amount of assets that could be transferred from owner
through withdraw
and not cause a revert, which MUST NOT be higher than the actual maximum that would be accepted (it should underestimate if necessary).
MUST factor in both global and user-specific limits, like if withdrawals are entirely disabled (even temporarily) it MUST return 0.
MUST NOT revert.
- name: maxWithdraw
type: function
stateMutability: view
inputs:
- name: owner
type: address
outputs:
- name: maxAssets
type: 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.
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, regardless if the user has enough shares, etc.
MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees.
MUST NOT revert due to vault specific user/global limits. MAY revert due to other conditions that would also cause withdraw
to revert.
Note that any unfavorable discrepancy between convertToShares
and previewWithdraw
SHOULD be considered slippage in share price or some other type of condition, meaning the depositor will lose assets by depositing.
- name: previewWithdraw
type: function
stateMutability: view
inputs:
- name: assets
type: uint256
outputs:
- name: shares
type: uint256
withdraw
Burns shares
from owner
and sends exactly assets
of underlying tokens to receiver
.
MUST emit the Withdraw
event.
MUST support a withdraw flow where the shares are burned from owner
directly where owner
is msg.sender
.
MUST support a withdraw flow where the shares are burned from owner
directly where msg.sender
has EIP-20 approval over the shares of owner
.
MAY support an additional flow in which the shares are transferred to the Vault contract before the withdraw
execution, and are accounted for during withdraw
.
SHOULD check msg.sender
can spend owner funds, assets needs to be converted to shares and shares should be checked for allowance.
MUST revert if all of assets
cannot be withdrawn (due to withdrawal limit being reached, slippage, the owner not having enough shares, etc).
Note that some implementations will require pre-requesting to the Vault before a withdrawal may be performed. Those methods should be performed separately.
- name: withdraw
type: function
stateMutability: nonpayable
inputs:
- name: assets
type: uint256
- name: receiver
type: address
- name: owner
type: address
outputs:
- name: shares
type: uint256
maxRedeem
Maximum amount of Vault shares that can be redeemed from the owner
balance in the Vault, through a redeem
call.
MUST return the maximum amount of shares that could be transferred from owner
through redeem
and not cause a revert, which MUST NOT be higher than the actual maximum that would be accepted (it should underestimate if necessary).
MUST factor in both global and user-specific limits, like if redemption is entirely disabled (even temporarily) it MUST return 0.
MUST NOT revert.
- name: maxRedeem
type: function
stateMutability: view
inputs:
- name: owner
type: address
outputs:
- name: maxShares
type: 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.
MUST return as close to and no more than the exact amount of assets that would be withdrawn in a redeem
call in the same transaction. I.e. redeem
should return the same or more assets
as previewRedeem
if called in the same transaction.
MUST NOT account for redemption limits like those returned from maxRedeem and should always act as though the redemption would be accepted, regardless if the user has enough shares, etc.
MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees.
MUST NOT revert due to vault specific user/global limits. MAY revert due to other conditions that would also cause redeem
to revert.
Note that any unfavorable discrepancy between convertToAssets
and previewRedeem
SHOULD be considered slippage in share price or some other type of condition, meaning the depositor will lose assets by redeeming.
- name: previewRedeem
type: function
stateMutability: view
inputs:
- name: shares
type: uint256
outputs:
- name: assets
type: uint256
redeem
Burns exactly shares
from owner
and sends assets
of underlying tokens to receiver
.
MUST emit the Withdraw
event.
MUST support a redeem flow where the shares are burned from owner
directly where owner
is msg.sender
.
MUST support a redeem flow where the shares are burned from owner
directly where msg.sender
has EIP-20 approval over the shares of owner
.
MAY support an additional flow in which the shares are transferred to the Vault contract before the redeem
execution, and are accounted for during redeem
.
SHOULD check msg.sender
can spend owner funds using allowance.
MUST revert if all of shares
cannot be redeemed (due to withdrawal limit being reached, slippage, the owner not having enough shares, etc).
Note that some implementations will require pre-requesting to the Vault before a withdrawal may be performed. Those methods should be performed separately.
- name: redeem
type: function
stateMutability: nonpayable
inputs:
- name: shares
type: uint256
- name: receiver
type: address
- name: owner
type: address
outputs:
- name: assets
type: uint256
Events
Deposit
sender
has exchanged assets
for shares
, and transferred those shares
to owner
.
MUST be emitted when tokens are deposited into the Vault via the mint
and deposit
methods.
- name: Deposit
type: event
inputs:
- name: sender
indexed: true
type: address
- name: owner
indexed: true
type: address
- name: assets
indexed: false
type: uint256
- name: shares
indexed: false
type: uint256
Withdraw
sender
has exchanged shares
, owned by owner
, for assets
, and transferred those assets
to receiver
.
MUST be emitted when shares are withdrawn from the Vault in EIP-4626.redeem
or EIP-4626.withdraw
methods.
- name: Withdraw
type: event
inputs:
- name: sender
indexed: true
type: address
- name: receiver
indexed: true
type: address
- name: owner
indexed: true
type: address
- name: assets
indexed: false
type: uint256
- name: shares
indexed: false
type: uint256
Rationale
The Vault interface is designed to be optimized for integrators with a feature complete yet minimal interface. Details such as accounting and allocation of deposited tokens are intentionally not specified, as Vaults are expected to be treated as black boxes on-chain and inspected off-chain before use.
EIP-20 is enforced because implementation details like token approval and balance calculation directly carry over to the shares accounting. This standardization makes the Vaults immediately compatible with all EIP-20 use cases in addition to EIP-4626.
The mint method was included for symmetry and feature completeness.
Most current use cases of share-based Vaults do not ascribe special meaning to the shares such that
a user would optimize for a specific number of shares (mint
) rather than specific amount of underlying (deposit
).
However, it is easy to imagine future Vault strategies which would have unique and independently useful share representations.
The convertTo
functions serve as rough estimates that do not account for operation specific details like withdrawal fees, etc.
They were included for frontends and applications that need an average value of shares or assets, not an exact value possibly including slippage or other fees.
For applications that need an exact value that attempts to account for fees and slippage we have included a corresponding preview
function to match each mutable function. These functions must not account for deposit or withdrawal limits, to ensure they are easily composable, the max
functions are provided for that purpose.
Backwards Compatibility
EIP-4626 is fully backward compatible with the EIP-20 standard and has no known compatibility issues with other standards. For production implementations of Vaults which do not use EIP-4626, wrapper adapters can be developed and used.
Reference Implementation
See Solmate EIP-4626: a minimal and opinionated implementation of the standard with hooks for developers to easily insert custom logic into deposits and withdrawals.
See Vyper EIP-4626: a demo implementation of the standard in Vyper, with hooks for share price manipulation and other testing needs.
Security Considerations
Fully permissionless use cases could fall prey to malicious implementations which only conform to the interface but not the specification. It is recommended that all integrators review the implementation for potential ways of losing user deposits before integrating.
If implementors intend to support EOA account access directly, they should consider adding an additional function call for deposit
/mint
/withdraw
/redeem
with the means to accommodate slippage loss or unexpected deposit/withdrawal limits, since they have no other means to revert the transaction if the exact output amount is not achieved.
The methods totalAssets
, convertToShares
and convertToAssets
are estimates useful for display purposes,
and do not have to confer the exact amount of underlying assets their context suggests.
The preview
methods return values that are as close as possible to exact as possible. For that reason, they are manipulable by altering the on-chain conditions and are not always safe to be used as price oracles. This specification includes convert
methods that are allowed to be inexact and therefore can be implemented as robust price oracles. For example, it would be correct to implement the convert
methods as using a time-weighted average price in converting between assets and shares.
Integrators of EIP-4626 Vaults should be aware of the difference between these view methods when integrating with this standard. Additionally, note that the amount of underlying assets a user may receive from redeeming their Vault shares (previewRedeem
) can be significantly different than the amount that would be taken from them when minting the same quantity of shares (previewMint
). The differences may be small (like if due to rounding error), or very significant (like if a Vault implements withdrawal or deposit fees, etc). Therefore integrators should always take care to use the preview function most relevant to their use case, and never assume they are interchangeable.
Finally, EIP-4626 Vault implementers should be aware of the need for specific, opposing rounding directions across the different mutable and view methods, as it is considered most secure to favor the Vault itself during calculations over its users:
-
If (1) it’s calculating how many shares to issue to a user for a certain amount of the underlying tokens they provide or (2) it’s determining the amount of the underlying tokens to transfer to them for returning a certain amount of shares, it should round down.
-
If (1) it’s calculating the amount of shares a user has to supply to receive a given amount of the underlying tokens or (2) it’s calculating the amount of underlying tokens a user has to provide to receive a certain amount of shares, it should round up.
The only functions where the preferred rounding direction would be ambiguous are the convertTo
functions. To ensure consistency across all EIP-4626 Vault implementations it is specified that these functions MUST both always round down. Integrators may wish to mimic rounding up versions of these functions themselves, like by adding 1 wei to the result.
Although the convertTo
functions should eliminate the need for any use of an EIP-4626 Vault’s decimals
variable, it is still strongly recommended to mirror
the underlying token’s decimals
if at all possible, to eliminate possible sources of confusion and simplify integration across front-ends and for other off-chain users.
Copyright
Copyright and related rights waived via CC0.