EIP-4626: Tokenized Vault Standard
A standard for tokenized vaults with a single underlying ERC-20 token.
Author | Joey Santoro, t11s, Jet Jadeja, Alberto Cuesta Cañada |
---|---|
Discussions-To | https://ethereum-magicians.org/t/eip-4626-yield-bearing-vault-standard/7900 |
Status | Review |
Type | Standards Track |
Category | ERC |
Created | 2021-12-22 |
Table of Contents
Abstract
The following standard allows for the implementation of a standard API for tokenized vaults with a single underlying ERC-20 token within smart contracts. This standard is an extension on the ERC-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 (Compound, Aave, Fuse), aggregators (Yearn, Rari Vaults, Idle), and intrinsically interest bearing tokens (xSushi). This makes integration difficult at the aggregator or plugin layer for protocols which need to conform to many standards. This forces each protocol to implement their own adapters which are error prone and waste development resources.
A standard for tokenized vaults will allow for a similar cambrian explosion to ERC-20, unlocking access to yield and other strategies in a variety of applications with little specialized effort from developers.
Specification
All tokenized vaults MUST implement ERC-20. If a vault is to be non-transferrable, it MAY revert on calls to transfer or transferFrom. The ERC-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.
Methods
deposit
function deposit(address to, uint256 value) public returns (uint256 shares)
Mints shares
amount of vault tokens to to
by depositing exactly value
underlying tokens.
shares
MUST equal the return value of a previewMint
call, with value
as a parameter, executed immediately before deposit
in the same transaction.
MUST support the ERC-20 transferFrom flow where the vault has at least value
approval over msg.sender’s balance of underlying.
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 emit the Deposit
event.
mint
function mint(address to, uint256 shares) public returns (uint256 value)
Mints exactly shares
amount of vault tokens to to
by depositing value
underlying tokens.
value
MUST equal the return value of a previewMint
call, with shares
as a parameter, executed immediately before withdraw
in the same transaction.
MUST support the ERC-20 transferFrom flow where the vault has at least value
approval over msg.sender’s balance of underlying.
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 emit the Deposit
event.
withdraw
function withdraw(address from, address to, uint256 value) public returns (uint256 shares)
Burns shares
vault tokens from from
, withdrawing exactly value
underlying tokens to to
.
shares
MUST equal the return value of a previewWithdraw
call, with value
as a parameter, executed immediately before mint
in the same transaction.
MUST support the ERC-20 flow in which the from
address holds the shares
vault tokens being burned. If from != msg.sender
, then msg.sender
MUST have ERC-20 approval over at least shares
vault tokens from from
.
MAY support an additional flow in which the vault tokens being burned are owned by the vault contract before the withdraw
execution, instead of being owned by from
.
MUST emit the Withdraw
event.
redeem
function redeem(address from, address to, uint256 shares) public returns (uint256 value)
Burns exactly shares
vault tokens from from
, withdrawing value
underlying tokens to to
.
value
MUST equal the return value of a previewRedeem
call, with shares
as a parameter, executed immediately before redeem
in the same transaction.
MUST support the ERC-20 flow in which the from
address holds the shares
vault tokens being burned. If from != msg.sender
, then msg.sender
MUST have ERC-20 approval over at least shares
vault tokens from from
.
MAY support an additional flow in which the vault tokens being burned are owned by the vault contract before the redeem
execution, instead of being owned by from
.
MUST emit the Withdraw
event.
underlying
function underlying() public view returns (address)
Returns the address of the token the vault uses for accounting, depositing, and withdrawing.
MUST return the address of a token implementing the ERC-20 standard.
totalUnderlying
function totalUnderlying() public view returns (uint256)
Returns the total amount of underlying tokens managed by the vault.
balanceOfUnderlying
function balanceOfUnderlying(address owner) public view returns (uint256)
Returns the value in underlying terms of the vault tokens held by owner
. Equivalent to previewRedeem(balanceOf(owner))
.
exchangeRate
function exchangeRate() public view returns (uint256)
Returns the value in underlying terms of one vault token. Equivalent to previewRedeem(10 ** decimals())
.
previewDeposit
function previewDeposit(uint256 underlyingAmount) public view returns (uint256 shareAmount)
Returns the amount of vault tokens that would be obtained if depositing a given amount of underlying tokens in a deposit
call.
previewMint
function previewMint(uint256 shareAmount) public view returns (uint256 underlyingAmount)
Returns the amount of underlying tokens that need to be given to the vault to obtain a given amount of vault tokens in a mint
call.
previewWithdraw
function previewWithdraw(uint256 underlyingAmount) public view returns (uint256 shareAmount)
Returns the amount of vault tokens that need to be burned to obtain a given amount of underlying tokens in a withdraw
call.
previewRedeem
function previewRedeem(uint256 shareAmount) public view returns (uint256 underlyingAmount)
Returns the amount of underlying tokens that would be obtained by redeeming a given amount of vault tokens in a redeem
call.
Events
Deposit
MUST be emitted when tokens are deposited into the vault.
event Deposit(address indexed from, address indexed to, uint256 value)
Where from
is the user who triggered the deposit for value
underlying tokens to the vault, and to
is the user who is able to withdraw the deposited tokens.
Withdraw
MUST be emitted when tokens are withdrawn from the vault by a depositor.
event Withdraw(address indexed from, address indexed to, uint256 value)
Where from
is the owner who and held value
underlying tokens in the vault, and to
is the user who received the withdrawn tokens.
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.
ERC-20 is forced 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 ERC-20 use cases in addition to ERC-4626.
The vaults are opinionated on a default deposit/withdraw flow because it gives integrators a default pattern to implement. Special use cases can be overloaded on these functions by including additional logic.
The mint function was included for symmetry and feature completeness. Most current use cases of shares 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.
A single exchangeRate()
function can only be guaranteed to be exact with one of the four mutable methods, unless significant conditions are placed on the use cases that can comply with this standard. Use cases that require to know the value of a vault position need to know the result of a redeem
call, without executing it. On the other hand, integrators that intend to call withdraw
on vaults with the user approving only the exact amount of underlying need the result of a withdraw
call. Similar use cases can be found for deposit
and mint
.
As such, the exchangeRate()
function has been kept for ease of integration on part of the simpler use cases, but preview functions have been included for each one of the four mutable methods. In each case, the value of a preview function is only guaranteed to equal the return value of the relted mutable function if called immediately before in the same transaction.
Backwards Compatibility
ERC-4626 is fully backward compatible with the ERC-20 standard and has no known compatibility issues with other standards. For production implementations of vaults which do not use ERC-4626, wrapper adapters can be developed and used.
Reference Implementation
See Solmate ERC6426: a minimal and opinionated implementation of the standard with hooks for developers to easily insert custom logic into deposits and withdrawals.
Security Considerations
This specification has similar security considerations to the ERC-20 interface. Fully permissionless yield aggregators, for example, could fall prey to malicious implementations which only conform to the interface but not the specification.
Copyright
Copyright and related rights waived via CC0.
Citation
Please cite this document as:
Joey Santoro, t11s, Jet Jadeja, Alberto Cuesta Cañada, "EIP-4626: Tokenized Vault Standard," Ethereum Improvement Proposals, no. 4626, December 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4626.