EIP-4626: Yield Bearing Vault Standard

I noticed how the EIP doesn’t really includes a function that computes the APY and returns something nice.

In my experience, this is another aspect that all of the lending protocols and yield optimizers are doing slightly different, making life hard on DeFi dashboard apps. Here are a few examples of the differences between the protocols:

Can we have a simple abstract function that returns the APY as a percentage with 4 decimals?

I am in favor of an APY function as well.

This smells more like an extension

There is pricePerShare. Anything more complex should be submitted as a separate EIP that builds on this one (not even as an extension), because describing it would make it really difficult to get this EIP across the finish line.

Last call before Last Call folx:

I like the proposal. Started to play around with it few days back. I’ve refitted old Yearn V2 arch (less work than with V3) to be compatible with 4626, using 4626 minimal implementation in place of Yearn vault and with controller/strategies untouched (using basic dai strat).

I was mostly interested in shares/underlying calculations playing nice and that seems to work out of the box (screenshot in readme). I didn’t do extensive testing, actually only scripted simulation. Ran it against forked network and actual Compound strategy with earn/harvest calls, setting min/max parameters in the Vault.

https://github.com/shortdoom/vault-fun

Not included in repo, but 4626 in current form allows for xSushi-like (yield bearing tokens) implementation fully out-of-the-box.

Hope this passes and gets cemented. It should be as easy to implement AAVE-like lending or Balancer-like swap-vaults as it was with Yearn’s V2.

Hi all - great proposal, really appreciate the work to standardize this! I’m chiming in as I worked with Buttonwood and Ampleforth to build a vault token which rebases to $1/token but maintains the value of the underlying: ButtonToken. Interestingly, despite being in a very different design space, we ended up with a pretty similar interface to what you have in EIP4626. I wanted to share a couple of suggestions from our team:

  • It would be useful to include redeemAll(address to) and/or withdrawAll(address to) functions (as an extension). This is especially useful for vault designs which have asynchronous balance updates (like the rebasing mechanics of ours). For example, a frontend trying to close a position will fetch balance, then call redeem, after which the balance may have changed causing either a revert or dust. Of course, this could be solved with an external contract to combine redeem(vault.balanceOf(me), to, me), but we think that this is an important enough feature to include as an extension.

  • Are the Deposit and Withdraw events necessary given that Transfer(0x0, ..) and Transfer(.., 0x0) are required by ERC20 spec in this case? In the past, some tokens have used Mint and Burn events to extend ERC20 spec which led to confusion thinking that they could then omit the Transfer event itself.

  • For what it’s worth, we preferred the underlying terminology to asset as it was less ambiguous (when reading asset in isolation it’s unclear whether it refers to underlying or vault token)

Thanks and really appreciate the hard work on this proposal!

2 Likes

Great efforts to standardize this!

Without being aware of this EIP, we have been talking to a few people about creating a standard like this - but luckily there is already an effort going on, so we will join in on this effort.

If there are people interested in doing something with this EIP in practice at ETHDenver, please talk to me on discord: FrankB#5720

Concluding for now with that this effort will especially lead to enormous defi innovation if it is used to create many small lego bricks, which can then be aggregated together into new EIP-4626-tokens.

Starting to implement and found that description for assetsPerShare isn’t enough in part of scaling. Now it described as

The current exchange rate of shares to assets, quoted per unit share e.g. 10 ** Vault.decimals()

So “e.g.” is not look like hard requirement and may be ignored, but at standart should defined scale of assetsPerShare result or we need another method like assetsPerShareDigits that will define scale.

Question appeared because of in my usage there are tokens with 6 digits in external balances but 27 (ray) rate for rebasing balance calculation and for me be better to stay at e27 or at least e18 for preventing potential precision issues

1 Like

We wanted to skip being too prescriptive here, but that’s why we made the ordering of the arguments to these methods the way they are. In Vyper, you can do the following:

def withdraw(
    assets: uint256 = MAX_UINT256,  # Meaning "withdraw all"
    receiver: address = msg.sender,  # Sends to caller by default
    sender: address = msg.sender,  # Redeem from caller by default
) -> uint256:  # Number of shares minted
    ...

and that will create withdraw() (withdraw all and send assets to self), withdraw(uint256) (withdraw some and send assets to self), withdraw(uint256,address) (withdraw some and send assets to receiver), and withdraw(uint256,address,address) (withdraw some from sender and send assets to receiver)

The thing to note is that you would handle this yourself in your implementation as per your needs.

This is a fair point. If both the Vault and the asset Token implement the events correctly, you could get the same information as these events by following the transfer events from both ERC20 implementations in the transaction. I would support removing these events in clarification that Transfer events must follow ERC20 convention of minting/burning in their place.

assets translates better into other languages vs. underlying, it was also shorter to spell, and didn’t require further clarification on it’s use e.g. underlyingTokensOf vs. assetsOf

I agree with this, and would recommend submitting a PR to remove the “e.g.” language as it is an exact definition.

Being “per unit share” means it has an exact definition, but could also add a recommendation that the decimals of Vault matches asset to reduce units confusion, and that “care should be taken in precision loss on advanced calculations”.

PR with clarification “per unit share”: https://github.com/ethereum/EIPs/pull/4764

For Withdraw event there is a little bit misleading - sender field should be msg.sender or owner from redeem params?

In the events, I think we should specify from which perspective the assets value is, let’s say that we use a token which has a fee or tax, when sending 100 tokens, the vault might only receive 98, so we should specifiy whether it is the amount of assets sent by sender, or the amount of assets received by the vault, I would be in favor of the using the assets from the vault perspective.

Few thoughts based on recent updates in EIP and after following Rari’s solmate/vault repositories to keep my minimal implementation up to date.

Vaults most basic primitive is share accounting and then everything else. I think in case of Vault as such, accounting operations are in reality on two sets of funds:

a) Total funds under management, equal to sum of all token transfers into the Vault, wherever currently residing (in Vault or outside of it as deployed liquidity)
b) Idle funds (Funds in Vault, not deployed liquidity, may be collateral or exit amount for withdraws etc.)

And between those sits necessity to allow for an easy plug-in of per protocol specific accounting. I see that Rari vaults (and solmate implementation) introduces a concept of a float variable (totalFloat) to keep track of Vault-only funds. However, its kept internal and returned by totalAsset() getter. I think it’s worth iterating.

EIP already proposes totalAsset() function. I think that totalAsset() and totalFloat should also be kept separate. Additionally, totalFloat may become public variable to avoid creating separate getter. At the same time, totalAsset() could stay free to be overridden by developers. For example, in case of yearn v2 it’s useful to return total assets across vault + strategy, but it’s also useful to know value of idle funds in the vault only. I imagine this can be true for many more modern implementations of vault architecture.

Hello,

I’m just juming in from the EIP, and I have not yet read the full discussion here.

  1. Any reason to not include a solidity interface in the document? I understand solidity is not the only way to write smart contract, but its definitely the most common one.

  2. In the description of the deposit function, we can read

Mints shares Vault shares to receiver by depositing exactly amount of underlying tokens.

Yet the function interface describe bellow as no mention of amount, am I to understand that the first (assets) argument is that amount?

1 Like

Hello,
I also have an issue with the mint function, if the underlying asset is a token with fees or taxes on transfer on it, it won’t work according to the plan.

Let’s say previewMint says 100 shares requires 100 underlying assets based on current assetsPerShare condition (previewMint has no way to know in advance without executing the actual transfer how much fees or taxes is going to be taken out of the transfer).

if we mint 100 shares, when the transfer of 100 underlying assets is going to be done, the vault might for instance only receive 98 underlying assets, and hence we should only mint 98 shares.
This could be acceptable, but the function only returns the amount of assets (would this assets value be 100 or 98, this has to be agreed, I would say 98 since we need to account from the contract perspective).
I would request that we also return the number of actual shares created.

The owner of the redeemed shares.

It should be the raw input and net output (e.g. Net of any fees). This way everyone is on an equal playing field from a data analysis perspective. Otherwise, deposit/withdrawal fees won’t be represented in the output.

Basically, log whatever the input argument is and whatever the output argument is.

You can add this function separately, nothing stops you. We have added the max* functions to account for any limits on deposits or withdrawals. The preview* functions can account for slippage loss (or fees).

We are trying out a meta EIP where interfaces can be specified as Yaml for easier use in tooling. It is easy to convert from this format to solidity or any other language (in fact Vyper supports loading JSON ABI files directly). Eventually, the EIP website can display example interfaces in all languages that add a parser.

This is a typo! Thanks for catching. If you don’t mind submitting a PR to update, I will approve it.

See above discussion about max* functions. Note that the transaction will revert if it cannot exchange exactly 98 assets for shares. If you get less shares than you expect (e.g. Slippage loss), you should assert (as the caller) a particular slippage bound on the amount of shares you get in return.

The spec is looking good. The following comments are on the use of the term caller rather than owner:

  • maxWithdraw should use input argument owner and not caller as the account calling the withdraw function can be different to the owner of the shares.
  • previewWithdraw should have an owner parameter like the withdraw function does. The function caller can be different to the owner of the shares for withdrawals.
  • Change withdraw text from “Any discrepancy could cause a revert due to tight slippage bounds by caller” to “Any discrepancy could cause a revert due to tight slippage bounds by owner”.
  • maxRedeem should use input argument owner and not caller as the account calling the redeem function can be different to the owner of the shares.
  • previewRedeem should have an owner parameter like the redeem function does. The function caller can be different to the owner of the shares for redemptions.
  • Change redeem text from “Any discrepancy could cause a revert due to tight slippage bounds by caller" to “Any discrepancy could cause a revert due to tight slippage bounds by owner”.
  • Change sender in the Withdraw event to owner as both redeem and withdraw allow the owner of the shares to be different to the sender of the transaction call. Alternatively, leave sender and add a new owner input argument to the Withdraw event. Or, rename sender to caller to be consistent with the rest of the spec and add owner.
  • Change sender in the Deposit event to caller to be consistent with the term used in the rest of the specification.
1 Like

Don’t know where else to post this, but I’ve made a thread about promoting usage of this standard on the Maker forums: Scaling On-chain collateral (in a bear market), using EIP-4626 - Proposal Ideas - The Maker Forum

Making EIP-4626 as simple as possible will improve the amount of expected integrations greatly.

Some more comments:

  • The assetsOf function uses depositor while the redeem and withdraw functions use owner for the same thing. I’d recommend using owner in assetsOf for consistency.
  • Change maxDeposit note from “Total number of underlying assets that callercan be deposit.” to “The maximum number of underlying assets the caller can deposit.”
  • Change deposit note from “Mints shares Vault shares to receiver by depositing exactly amount of underlying tokens.” to “Mints enough vault shares to the receiver from transferring the exact assets amount of underlying asset tokens from the caller.”
  • Change maxMint note from “Total number of underlying shares that caller
    can be mint.” to “The maximum number of vault shares the caller
    can mint.”
  • Change mint note from “Mints exactly sharesVault shares to receiver
    by depositing amount of underlying tokens.” to “Mint exact shares amount of vault
    shares to the receiver from transferring enough underlying asset tokens from the caller.”
  • Change maxWithdraw note from “Total number of underlying assets that caller
    can withdraw.” to “The maximum number of underlying asset tokens the owner can withdraw.”
  • Change withdraw note from “Redeems shares from owner and sends assets of underlying tokens to receiver.” to “Burns enough vault shares from the owner to transfer the exact assets amount of underlying asset tokens to the receiver.”
  • Change redeem note from “Redeems shares from owner and sends assets of underlying tokens to receiver.” to “Burns exact shares amount of vault shares from the owner and transfers enough underlying asset tokens to the receiver.”