EIP-4626: Yield Bearing Vault Standard

if the vault is denominated in A on input and output, totalAssets should return the total value of A that would be received by liquidating the position in B.

Is B a derivative of A like cDAI:DAI?

Thanks for taking the time!

A is WETH and B is icETH, and the swapping happens through a DEX. icETH has a debt component of WETH and an equity component of stETH.

So, we would need an oracle to get DEX prices.

1 Like

ideally the vault would report underlying in WETH as I mentioned above. Make sure the share price is manipulation resistant (an oracle would be a good way to do this).

What should be the flow for the native currency (ETH) as an underlying?
Is it better to change the deposit/withdraw to payable or have an additional function for handling ETH deposits (depositETH/withdrawETH)?

Its a bit late now that the ERC is finalized, but I’m wondering why “minOut” / “maxIn” parameters were not included in the deposit, mint, withdraw and redeem functions.

While the preview functions give an accurate prediction, this may change between the moment a user signs a transaction and the moment its mined. Frontrunning in particular make this ERC possibly dangerous without these additional checks.


I would recommend this ERC gets extended to include

function deposit(uint256 assets, address receiver, uint256 minShares) external returns (uint256 shares) {
    shares = deposit(assets, receiver);
    require(shares >= minShares);
}

function mint(uint256 shares, address receiver, uint256 maxAssets) external returns (uint256 assets)  {
    assets = mint(shares, receiver);
    require(assets <= maxAssets);
}

function withdraw(uint256 assets, address receiver, address owner, uint256 maxShares) external returns (uint256 shares) {
    shares = withdraw(assets, receiver, owner);
    require(shares <= maxShares);
}

function redeem(uint256 shares, address receiver, address owner, uint256 minAssets) external returns (uint256 assets) {
    assets = redeem(shares, receiver, owner);
    require(assets >= minAssets);
}
2 Likes

As per the spec,

totalAssets

“Total amount of the underlying asset that is “managed” by Vault.”

Should the totalAssets return the underlying asset deposited + extra assets including airdrops, donation etc OR only assets deposited to the vault ?
If its latter, then only returning vault balance of asset is not enough.

At mStable we are building a number of different EIP-4626 vaults. One of them deposits assets in an AMM. We have implemented a mechanism to protect against sandwich attacks by limiting the amount of slippage allowed on operational functions like deposit, mint, withdraw and redeem.

My question is in regards to the preview functions. Should the preview functions revert if there is too much slippage like the operational functions?

For previewDeposit, the EIP states “MUST NOT revert due to vault specific user/global limits. MAY revert due to other conditions that would also cause deposit to revert.”

So is too much slippage a “vault specific user/global limit”?
Or is it “other conditions that would also cause deposit to revert”?

Personally, don’t think the preview functions should revert if there is more slippage than what the vault allows.

Should totalAssets include slippage or not?

The EIP for convertToShares and convertToAssets states

MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange.

Does this same statement apply to totalAssets?

BTW, I don’t like the “or other on-chain conditions” statement. Transactions can only see on-chain conditions so it’s impossible for convertToShares and convertToAssets not to reflect on-chain conditions. eg the amount of shares and assets in the vault is an on-chain condition.

Personally, I think totalAssets needs to be consistent with convertToShares and convertToAssets and not include slippage or fees.

I have another clarification to the preview functions.

The specification for previewDeposit states

Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given current on-chain conditions.

The “current block” implies that the on-chain conditions is at a block level and not transaction level. That is, previewDeposit needs to allow for other transactions being included before it in the block. For vaults that have slippage, that would mean previewDeposit would have to return the worst case of a sandwich attacked transaction before it reverted with a slippage error.

But the previewDeposit specification also states

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.

This statement implies that the on-chain conditions is at a transaction level and not block level.

Personally, I think the preview functions should be at a transaction level rather than block level. I would recommend changing the first line of the previewDeposit specification to

Allows an on-chain or off-chain user to simulate the effects of their deposit at the current transaction, given current on-chain conditions.

The same also applies to mint, redeem and withdraw.

100% agree with that.

1 Like

I think its for the caller to decide what outcomes are good for him.

  • It the caller is a contract, it should do a preview, and only continue if that is ok
  • It the caller is an EOA, then it should either use an extensions like this, or use a “router” like this

I would like to suggest an extension of EIP-4626 that allows for more cost-efficient redemptions. Currently, redemptions return the underlying asset of a vault. However, in cases where yield is generated by taking positions in assets other than the vault’s underlying asset, it would be more efficient for an EOA to source their own liquidity upon redemption, for example via an off-chain RfQ.

A solution would be a multi-asset redeem function that returns a basket of tokens. The return values of such a function and the corresponding preview would be dynamic arrays address[] and uint256[] for the tokens and amounts (to be) returned.

To revert, or not to revert, that is the question.

The specifications does not explicitly state if a deposit, mint, withdraw, redeem should revert or not revert if zero amounts are returned.

Looking at the various implementations, most are going with they should not revert unless there is an integer overflow. For example, rounding down the result by subtracting it by one when the value is an unsigned zero will revert.

See Should ERC4626 txs revert with zero amounts? · Issue #314 · transmissions11/solmate · GitHub

I’m too late for this comment, given the standard has been finalized and is now seeming to get some traction (yay!). So I guess I’m asking for advice. In accounting, Assets - Liabilities == Equity. Given many vaults don’t have liabilities, Assets == Equity. However, for vaults that do have liabilities (accrued fees, or senior loans to the vault), they are not the same. I’d love to see totalLiabilities() and totalEquity() as part of the standard, but I’m a little late for that. Here’s the advice I’d like: seems like a lot of clients might make the assumption that totalAssets() == convertToAssets(totalSupply()). So I have a choice: implement so totalAssets() is actually equity value (assets - liabilities), so that assumption is correct, and provide a nonstandard totalLiabilities() function so clients could add them to get actual total assets. Or, totalAssets() can be the total amount managed by the vault, and there’s a separate function totalEquity() which equals convertToAssets(totalSupply()). Thoughts?

Could someone point out a compliant implementation of ERC4626 with fees? We don’t know how to interpret the specification’s wording about functions being “inclusive of fees”.

There are many different types of fees that you could use, but some common ones are:

  1. Deposit fee, typically a % of assets deposited is taken as a fee, with shares issued being against the remaining portion
  2. Withdrawal fee, a % of shares (or assets) is levied as a fee for exiting the vault, with a portion of the assets represented by those shares being returned
  3. Performance fee, a % of total yield is deducted
  4. Management fee, a % of the assets your shares represent reduces over time at a specific rate (like how a Trust works e.g. GBTC)

In all cases, we designed 4626 to be inclusive of fees so that apples to apples comparisons of protocols are more possible. For example, on deposit and withdrawal, the language enforces that it must be the total amount coming in, and the final amount issued as well as the total amount redeemed and the final amount sent out. This means that withdrawal and deposit fees should be levied internally to the code, and the amount post fee is given as an output. The end result is the effective exchange rate you paid is worse, which means the vault would accurately describe the exchange of assets to shares and back again that is occurring.

Management fees and performance fees are a little easier, both are basically the rate at which the conversion functions change their output. The guidance there is that both preview and convert functions should be inclusive of these types of fees so that their answer remains accurate to what real depositors would see.

Originally we tried to be more specific about fees, but that’s a hard battle because there are so many different ways of imposing them. The guidance provided by ERC4626 is more general, essentially just try to make your functions be as honest as possible to what real depositors should expect.

2 Likes

It is unclear to me what the events should be when dealing with fees.

Lets take an example, where there is a 5% deposit fee,

  • a user calls deposits with 100 DAI
  • 5 DAI go to the feeRecipient (whoever that is)
  • 95 DAI go to the vault
  • 95 shares are minted in exchange.

In that context, previewDeposit(100) is 95. But what should the use for assets ? 100? 95?

Same question for a mint:

  • a user calls mint with 100 shares
    • these 100 shares need 100 DAI, plus 6 DAI of fees
  • 6 DAI go to the feeRecipient (whoever that is)
  • 100 DAI go to the vault
  • 100 shares are minted in exchange.

In that context, previewMint(100) is 106. But what should the use for assets in the event ? 100? 106?

FIY, the event is described as

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.

- 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

said otherwize, should the “assets” params in these event reflect:

  • How much the user paid/got
  • How much the vault gained/lost

How much the user paid/got

After some discussion we think this is the right option according to the guideline shared above:

1 Like

Please who can help me with a list of platforms that talks with ERC 4626 standard? I mean platforms that expects to integrate dapps built on ERC 4626 → ERC-4626: Tokenized Vaults

1 Like