DCIPs/EIPS/eip-4626.md

613 lines
24 KiB
Markdown

---
eip: 4626
title: Tokenized Vaults
description: Tokenized Vaults with a single underlying EIP-20 token.
author: Joey Santoro (@joeysantoro), t11s (@transmissions11), Jet Jadeja (@JetJadeja), Alberto Cuesta Cañada (@alcueca), Señor Doggo (@fubuloubu)
discussions-to: https://ethereum-magicians.org/t/eip-4626-yield-bearing-vault-standard/7900
status: Final
type: Standards Track
category: ERC
created: 2021-12-22
requires: 20, 2612
---
## Abstract
The following standard allows for the implementation of a standard API for tokenized Vaults
representing shares of a single underlying [EIP-20](./eip-20.md) 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](./eip-4626.md) 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](./eip-2612.md) 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.
```yaml
- 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.
```yaml
- 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.
```yaml
- 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.
```yaml
- 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.
```yaml
- 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.
```yaml
- 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.
```yaml
- 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.
```yaml
- 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.
```yaml
- 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.
```yaml
- 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.
```yaml
- 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.
```yaml
- 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.
```yaml
- 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.
```yaml
- 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.
```yaml
- 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.
```yaml
- 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.
```yaml
- 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.
```yaml
- 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](https://github.com/Rari-Capital/solmate/blob/main/src/mixins/ERC4626.sol):
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](https://github.com/fubuloubu/ERC4626):
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](../LICENSE.md).