ERC-4626 Tokenized Vault Standard (Tokenized Vaults)
Final · Vaults & DeFi · created 2021-12-22
Defines a common deposit and withdrawal interface for a vault that accepts a single ERC-20 token and gives back shares in the form of a token. Wallets and apps do not have to write different integration code for each vault.
Why it is needed
Ethereum has many vault-style services that return a share token when you deposit tokens. Lending markets, aggregators that split funds across multiple places, and tokens that earn interest just by being held are typical examples. However, these vaults have no standard, so their implementation details differ. As a result, integration is difficult for aggregators or plugin layers that must support many standards at once, and each protocol had to build its own adapter. Adapters are prone to mistakes and waste development resources. This standard aims to unify the share token as ERC-20 and define deposit, withdrawal and query functions, which reduces the effort of integrating yield-bearing vaults and makes implementations more consistent and robust.
What to implement
- name: asset type: function stateMutability: view inputs: [] outputs: - name: assetTokenAddress type: address: Returns the address of the underlying token the vault uses for accounting and for deposits and withdrawals. It must not revert.- name: totalAssets type: function stateMutability: view inputs: [] outputs: - name: totalManagedAssets type: uint256: Returns the total amount of underlying assets the vault manages. It includes the portion that has grown from yield and fees.- name: convertToShares type: function stateMutability: view inputs: - name: assets type: uint256 outputs: - name: shares type: uint256: Calculates how many shares a given amount of assets would convert to under ideal conditions. Fees and slippage are excluded, and the result is rounded down.- name: convertToAssets type: function stateMutability: view inputs: - name: shares type: uint256 outputs: - name: assets type: uint256: Calculates how many assets a given amount of shares would convert to under ideal conditions. Fees and slippage are excluded, and the result is rounded down.- name: maxDeposit type: function stateMutability: view inputs: - name: receiver type: address outputs: - name: maxAssets type: uint256: Returns the maximum amount of assets that can be deposited with deposit for receiver. If there is no limit, it is 2 ** 256 - 1.- name: previewDeposit type: function stateMutability: view inputs: - name: assets type: uint256 outputs: - name: shares type: uint256: Calculates in advance the shares you would receive for depositing assets in the current block. The actual deposit gives this amount or more.- name: deposit type: function stateMutability: nonpayable inputs: - name: assets type: uint256 - name: receiver type: address outputs: - name: shares type: uint256: Deposits exactly assets and mints shares to receiver. Emits a Deposit event.- name: maxMint type: function stateMutability: view inputs: - name: receiver type: address outputs: - name: maxShares type: uint256: Returns the maximum amount of shares that can be minted with mint for receiver. If there is no limit, it is 2 ** 256 - 1.- name: previewMint type: function stateMutability: view inputs: - name: shares type: uint256 outputs: - name: assets type: uint256: Calculates in advance the assets you would pay to have shares minted in the current block. The actual mint takes this amount or less.- name: mint type: function stateMutability: nonpayable inputs: - name: shares type: uint256 - name: receiver type: address outputs: - name: assets type: uint256: Mints exactly shares to receiver and takes the required assets. Emits a Deposit event.- name: maxWithdraw type: function stateMutability: view inputs: - name: owner type: address outputs: - name: maxAssets type: uint256: Returns the maximum amount of assets that can be withdrawn with withdraw from the owner's balance.- name: previewWithdraw type: function stateMutability: view inputs: - name: assets type: uint256 outputs: - name: shares type: uint256: Calculates in advance the shares that would be burned to withdraw assets in the current block. The actual withdraw burns this amount or less.- name: withdraw type: function stateMutability: nonpayable inputs: - name: assets type: uint256 - name: receiver type: address - name: owner type: address outputs: - name: shares type: uint256: Burns the owner's shares and sends exactly assets to receiver. Emits a Withdraw event.- name: maxRedeem type: function stateMutability: view inputs: - name: owner type: address outputs: - name: maxShares type: uint256: Returns the maximum amount of shares that can be redeemed with redeem from the owner's balance.- name: previewRedeem type: function stateMutability: view inputs: - name: shares type: uint256 outputs: - name: assets type: uint256: Calculates in advance the assets you would receive for redeeming shares in the current block. The actual redeem gives this amount or more.- name: redeem type: function stateMutability: nonpayable inputs: - name: shares type: uint256 - name: receiver type: address - name: owner type: address outputs: - name: assets type: uint256: Burns exactly shares of the owner's shares and sends the assets to receiver. Emits a Withdraw event.- 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: Emitted when sender converts assets to shares and delivers them to owner. It is emitted from deposit and mint.- 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: Emitted when sender converts owner's shares to assets and delivers them to receiver. It is emitted from withdraw and redeem.
In depth
Shares must be ERC-20 tokens
A vault that follows the standard must implement EIP-20 to represent shares. All EIP-20 behavior such as balanceOf, transfer and totalSupply applies to the vault's shares. A share represents ownership of a portion of the underlying assets the vault holds. If you want to make shares non-transferable, you may revert calls to transfer and transferFrom (MAY). The optional metadata extension of EIP-20 must be implemented. The name and symbol must reflect the name and symbol of the underlying token in some way (SHOULD). To improve the share approval experience across services, EIP-2612 may be implemented (MAY).
Four terms: asset, share, fee, slippage
An asset is the underlying token the vault manages, and its unit is set by the corresponding EIP-20 contract. A share is the vault's token, and the vault decides the rate at which shares are exchanged for the underlying asset on mint, deposit, withdraw and redeem. A fee is an amount of assets or shares the vault charges users, and it can be applied to any item the vault chooses, such as deposits, yield, assets under management (AUM) or withdrawals. Slippage is the part of the difference between the quoted share price and the actual deposit or withdrawal result that is not explained by fees.
View functions: asset, totalAssets, convertToShares, convertToAssets
asset returns the address of the underlying token used for accounting, deposits and withdrawals. It MUST be an EIP-20 token contract and MUST NOT revert. totalAssets is the total amount of underlying assets the vault manages. It SHOULD include any compounding that occurs from yield, MUST include any fees charged against vault assets, and MUST NOT revert. convertToShares and convertToAssets give the amount of shares or assets the vault would exchange under ideal conditions. They do not include fees and do not vary by caller. They also do not reflect on-chain conditions such as slippage. They MUST NOT revert except for integer overflow caused by unreasonably large inputs, and MUST round down toward 0. The value shows the price per share an average user would expect, not the price for a specific user.
max functions: the maximum amount currently possible
maxDeposit and maxMint are the maximum amount of assets and the maximum amount of shares that can be put in for the receiver through deposit or mint. maxWithdraw and maxRedeem are the maximum amount of assets and the maximum amount of shares that can be taken out of the owner's balance through withdraw or redeem. None of the four functions may return a value higher than the actual maximum that would be accepted, and they should underestimate if necessary. They MUST reflect both global limits and per-user limits, and return 0 if the action is disabled, even temporarily. maxDeposit and maxMint return 2 ** 256 - 1 if there is no limit. They assume the user has unlimited assets, so they must not rely on the balanceOf of asset. None of the four functions may revert.
preview functions: exact simulation within the same block
previewDeposit, previewMint, previewWithdraw and previewRedeem compute the result of an actual call in advance, under the on-chain conditions of the current block. previewDeposit returns a value no greater than the shares that deposit would mint, and previewRedeem returns a value no greater than the assets that redeem would return. previewMint returns a value no less than the assets that mint would take, and previewWithdraw returns a value no less than the shares that withdraw would burn. In other words, within the same transaction the actual function gives the user a result equal to or better than the preview. They MUST include deposit or withdrawal fees, and they ignore the limits from the max functions and any approval status, assuming the call is always accepted. They MUST NOT revert because of vault limits, but may revert for other conditions that would cause the actual function to revert. If the convertTo value and the preview value differ unfavorably, the difference should be treated as slippage in the share price.
Deposit and withdrawal functions: deposit, mint, withdraw, redeem
deposit takes in exactly assets of the underlying token and mints the corresponding shares to the receiver. mint mints exactly shares and takes the assets required. Both MUST emit the Deposit event and MUST support the EIP-20 approve and transferFrom flow for asset. They MAY additionally support a flow in which the underlying token is transferred to the vault before execution. They MUST revert if they cannot process the full amount. withdraw burns the owner's shares and sends exactly assets to the receiver. redeem burns exactly shares and sends the assets. Both MUST emit the Withdraw event and MUST support both the case where the owner is msg.sender and the case where msg.sender has EIP-20 approval over the owner's shares. They SHOULD check with allowance whether msg.sender can spend the owner's funds. withdraw converts the assets to shares and then checks against those shares. Some implementations require a prior request to the vault before withdrawal, and that step is performed separately.
Deposit and Withdraw events
The Deposit event means the sender converted assets into shares and delivered those shares to the owner. It MUST be emitted when tokens enter the vault through mint and deposit. sender and owner are indexed, and assets and shares are not indexed. The Withdraw event means the sender converted the owner's shares into assets and delivered those assets to the receiver. It MUST be emitted when shares leave through redeem and withdraw. sender, receiver and owner are indexed, and assets and shares are not indexed.
Design rationale and compatibility
The interface is designed to be feature-complete yet minimal, tailored to the parties that integrate with it. How deposited tokens are accounted for and allocated is deliberately left unspecified. On-chain, a vault is treated as a black box, and the standard assumes it is inspected off-chain before use. EIP-20 is required because implementation details such as approvals and balance calculations carry directly into share accounting, and because it is immediately compatible with all existing EIP-20 use cases. mint is included for symmetry and completeness. Targeting a specific number of shares is rare today, but one can imagine strategies in which the shares themselves carry unique meaning. The convertTo functions are rough estimates that ignore fees and are meant for frontends. Exact values come from the preview function paired with each state-changing function, and limits come from the max functions. That preview does not reflect limits is a choice made to make composition easier. This standard is fully compatible with EIP-20, and existing vaults that do not use the standard can be wrapped with an adapter. The status is Final.
Things to watch out for
- Malicious implementations that only match the interface: In permissionless settings, integrators can be targeted by malicious implementations that match the interface but do not follow the specification. Before integrating, we recommend reviewing the implementation for any path through which user deposits could be lost.
- Slippage protection for direct EOA calls: To let a plain externally owned account (EOA) use a vault directly, consider adding extra functions to deposit, mint, withdraw and redeem that can handle slippage loss or unexpected limits, because there is no other way to revert the transaction when the exact resulting amount is not obtained.
- Differences between view functions, and price oracles: totalAssets, convertToShares and convertToAssets are display estimates and do not guarantee the exact amount of underlying assets. The preview functions give values as accurate as possible, so they can be manipulated by changing on-chain conditions and are not always safe to use as price oracles. The convert functions are allowed to be inaccurate, so they can be implemented with a robust oracle such as a time-weighted average price.
- Do not use preview functions interchangeably: The assets received for redeeming shares (previewRedeem) and the assets that must be paid to mint the same number of shares (previewMint) can differ greatly. The difference may be small, like a rounding error, or very large if there are deposit or withdrawal fees. Use the preview function that matches the use case, and do not assume they can be used interchangeably.
- Rounding direction and decimals: It is safest when calculations favor the vault over the user. When calculating the shares to mint for the assets a user paid, or the assets to send for the shares returned, round down. When calculating the shares that must be paid to receive desired assets, or the assets that must be paid to receive desired shares, round up. The convertTo functions, whose direction is ambiguous, are fixed to always round down for consistency. Integrators that need rounding up can emulate it themselves, for example by adding 1 wei to the result. We strongly recommend that decimals match the underlying token where possible.