Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EIP-4626: Tokenized Vault Standard #4626

Merged
merged 19 commits into from
Jan 8, 2022
Merged
148 changes: 148 additions & 0 deletions eip-4626.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
---
eip: 4626
title: Tokenized Vault Standard
description: A standard for tokenized vaults with a single underlying ERC20 token.
lightclient marked this conversation as resolved.
Show resolved Hide resolved
author: Joey Santoro (@joeysantoro), t11s (@transmissions11), Jet Jadeja (@JetJadeja)
discussions-to: https://ethereum-magicians.org/t/eip-4626-yield-bearing-vault-standard/7900
status: Draft
type: Standards Track
category: ERC
created: 2021-12-22
---

## Abstract

The following standard allows for the implementation of a standard API for tokenized vaults with a single underlying ERC20 within smart contracts. This standard is an extension on the ERC20 token that provides basic functionality for depositing and withdrawing tokens and reading balances.
lightclient marked this conversation as resolved.
Show resolved Hide resolved

## 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 ERC20. If a vault is to be non-transferrable, it MAY revert on calls to transfer or transferFrom. The ERC20 operations balanceOf, transfer, totalSupply, etc. operate on the vault "shares" which represent ownership in the underlying.

### Methods
lightclient marked this conversation as resolved.
Show resolved Hide resolved

#### deposit

`function deposit(address _to, uint256 _value) public returns (uint256 _shares)`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any way that address to send shares to can be the second parameter? In yearn vaults, we have them in this order because in decreasing order of usage:

  • deposit everything for yourself
  • deposit a specific amount for yourself
  • deposit a specific amount and give to someone else

Depositing everything to another person is rare

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for withdraw

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool with me, makes sense in languages (vyper) that have optional args

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a decent preference for the other order given it follows ERC20 spec. Wouldn't yearn need to still make adapters for the remaining functions anyway? What is the gain by switching if its only limited backward compatibility

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a side note, the likelihood any protocol makes an adapter is quite low, I didn't make the suggestion for backwards compatibility reasons.

Anyways, I don't think ERC20 plays a huge role here in this spec in terms of driving the interface of these operations. Merely consider that the addition of override functions might be a good way to reduce gas costs for common operations, if the ordering is correct.

So adding:

function deposit() public returns (uint256 _shares) // Deposit all to self
function deposit(uint256 _value) public returns (uint256 _shares) // Deposit some to self
function deposit(uint256 _value, address _to) public returns (uint256 _shares) // Deposit some to other

As another side note... I really wish people would start writing ERC specifications in YAML per EIP-2069 but that never really took off 😞


Deposits `_value` tokens into the vault and grants ownership of them to `_to`.

MUST emit the `Deposit` event.

#### withdraw

`function withdraw(address _to, uint256 _value) public returns (uint256 _shares)`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love the idea of standardizing Vaults.
I respect your work with Rari and Fei.

But every single Vault Product in DeFi uses shares to account for withdraw, this recommended standard is invalidating every other vault code.

Additionally some / most shares end up being a token of their own and people think of them that way, (bDigg, bveCVX, yveCRV, etc...)

I recommend having withdraw use the number of shares.

For context:
Yearn V2:
https://github.com/yearn/yearn-vaults/blob/67e7ddda69c24f6e85a56338a3519a461b20d107/contracts/Vault.vy#L1025

Yearn V1:
https://github.com/yearn/yearn-protocol/blob/7b7d4042f87d6e854b00de9228c01f6f587cf0c0/contracts/vaults/yVault.sol#L101

Badger Vaults:
https://github.com/Badger-Finance/badger-sett-1.5

Mushroom Finance:
https://etherscan.io/address/0x0c0291f4c12f04da8b4139996c720a89d28ca069#code#L887

Inverse Finance:
https://github.com/InverseFinance/inverse-protocol/blob/55ff82d2e2af075832555fa3e933f73410845e43/contracts/vault/Vault.sol#L53

The only notable example that doesn't follow this pattern is Idle Finance, which has a unique interface:
https://github.com/Idle-Labs/idle-contracts/blob/develop/contracts/IdleTokenV3_1.sol

As an addendum, to avoid further invalidating already existing projects, there could be multiple overloaded withdraw signatures similarly to how Yearn V2 did which would allow most projects to comply to the standard without changing it

Copy link
Contributor Author

@Joeysantoro Joeysantoro Jan 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am open to reordering or making redeem required which is the shares based model. I do like the symmetry of deposit/withdraw both using the same underlying token accounting and having redeem or withdrawShares use the shares model. I'm also comfortable having withdraw be the shares and withdrawUnderlying be for underlying. Combined with @alcueca's feedback, I believe the standard can be somewhat more opinionated on some of these details.

Wdyt @transmissions11?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree on this, I think the norm of most protocols is to use value for depositing & shares for withdrawing, so making the default withdraw function to use shares will minimise unexpected surprises to devs.
Since the interface is for a vault, having an additional withdrawValue is maybe necessary? But I think the devs can easily do some math and use the core withdraw function if necessary. This is probably better than forcing the function in the interface.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, see above. I think that both the withdraw and deposit features should accept both value or shares as inputs.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to agree to disagree, I'm not making any statement about what I think the code should look like.

The code that is being used in production by the majority of protocols looks like the following
function deposit(uint256 amount)
function withdraw(uint256 shares)

Happy to go into detail as to why that is the case, but this interface has survived over hundreds of forks on all EVM compatible chains, and as such shows wisdom in its simplicity

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of the cToken-based protocols (Compound, Rari, Cream, etc) use mint(uint amount), redeem(uint shares), redeemUnderlying(uint amount) so I think whichever way is chosen there's going to be a group of existing protocols that aren't natively compatible.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

withdraw standing for shares is flat out illogical. you don't "withdraw" a check at the bank, or "withdraw" a winning lottery ticket. you redeem them. you withdraw $100

You can't withdraw to someone else either, the bank analogy falls flat

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

withdraw standing for shares is flat out illogical. you don't "withdraw" a check at the bank, or "withdraw" a winning lottery ticket. you redeem them. you withdraw $100

As for withdrawing $100 dollars, the reason why fork over fork never killed the withdraw(shares) is the level of complexity or close to impossibility to withdraw all shares.

Illustrated by literally the first withdraw I found on Rari's smart contract

https://etherscan.io/tx/0xcada09b35d35aaee76fdaf2a00b8f275b07ced7eb8c80cf48cc5dac42c4832ea

Screenshot 2022-01-07 at 15 40 02

Screenshot 2022-01-07 at 15 40 14

Did the depositor really want to keep those 0.02 tokens? Or is the implementation that forces them into something they don't want?

Copy link

@davidcallanan davidcallanan Jan 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't withdraw to someone else either, the bank analogy falls flat

@GalloDaSballo

Definitions:

(Google) withdraw: remove or take away (something) from a particular place or position
(Merriam Webster) withdraw: to remove (money) from a place of deposit

It doesn't seem to say anything about where the money ends up (the destination), so I think you can withdraw into someone else's account. On apps like Binance (a crypto exchange), you just withdraw to some arbitrary wallet address, Binance doesn't care who owns the destination address. If anything, withdraw is commonly used to refer to a separate destination than your own.

But, most importantly, withdrawals have always historically referred to the actual asset (the underlying asset). I agree that redeem would make more sense here (when it's in terms of the wrapper token/shares), but I can understand your point of view since many protocols have used the term withdraw here (IMO) incorrectly.

Copy link

@davidcallanan davidcallanan Jan 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the reason why fork over fork never killed the withdraw(shares) is the level of complexity or close to impossibility to withdraw all shares

Regardless, the term "redeem" is perhaps better-fitted than "withdraw".

I agree, there definitely may be an added complexity when supporting withdraw(underlying).


Withdraws `_value` tokens from the vault and transfers them to `_to`.

MUST emit the `Withdraw` event.

#### withdrawFrom

`function withdrawFrom(address _from, address _to, uint256 _value) public returns (uint256 _shares)`

Withdraws `_value` tokens from owner `_from` and transfers them to `_to`.

MUST share the same authorization mechanism as transferFrom.

MUST emit the `Withdraw` event.

#### redeem

`function redeem(address _to, uint256 _shares) public returns (uint256 _value)`

Redeems a specific number of `_shares` for underlying tokens and transfers them to `_to`.

MUST emit the `Withdraw` event.

#### redeemFrom

`function redeemFrom(address _from, address _to, uint256 _shares) public returns (uint256 _value)`

Redeems a specific number of `_shares` from owner `_from` for underlying tokens and transfers them to `_to`.

MUST share the same authorization mechanism as transferFrom.

MUST emit the `Withdraw` event.

#### totalHoldings
`function totalHoldings() public view returns (uint256)`
Joeysantoro marked this conversation as resolved.
Show resolved Hide resolved
Joeysantoro marked this conversation as resolved.
Show resolved Hide resolved

Returns the total amount of underlying tokens held/managed by the vault.

#### balanceOfUnderlying
`function balanceOfUnderlying(address _owner) public view returns (uint256)`

Returns the total amount underlying tokens held in the vault for `_owner`.

#### 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.

#### calculateShares
`function calculateShares(uint256 underlyingAmount) public view returns (uint256 sharesAmount)`

Returns the amount of vault shares corresponding to a given underlying amount.

`calculateShares(calculateUnderlying(sharesAmount))` MUST equal `sharesAmount`

#### calculateUnderlying

`function calculateUnderlying(uint256 sharesAmount) public view returns (uint256 underlyingAmount)`;

Returns the amount of underlying tokens corresponding to a given amount of vault shares.

`calculateUnderlying(calculateShares(underlyingAmount))` MUST equal `underlyingAmount`

### 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 minimal implementation and integration logic while maintaining flexibility for both parties. 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.

lightclient marked this conversation as resolved.
Show resolved Hide resolved
ERC20 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 ERC20 use cases in addition to ERC4626.

## Backwards Compatibility

ERC4626 is fully backward compatible with the ERC20 standard and has no known compatibility issues with other standards. For production implementations of vaults which do not use ERC4626, wrapper adapters can be developed and used.
lightclient marked this conversation as resolved.
Show resolved Hide resolved

## Reference Implementation

[Solmate Minimal Implementation](https://github.com/Rari-Capital/solmate/pull/88) - a tokenized vault using the ERC-20 extension with hooks for developers to add logic in deposit and withdraw.

[Rari Vaults](https://github.com/Rari-Capital/vaults/blob/main/src/Vault.sol) are an implementation that is nearly ready for production release. Any discrepancies between the vaults abi and this ERC will be adapted to conform to the ERC before mainnet deployment.

## 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](https://creativecommons.org/publicdomain/zero/1.0/).