EIP-7540: Asynchronous ERC-4626 Tokenized Vaults


eip: 7540
title: Asynchronous ERC-4626 Tokenized Vaults
description: Extension of ERC-4626 with asynchronous deposit and redemption support
author: Jeroen Offerijns (@hieronx), Alina Sinelnikova (@ilinzweilin), Vikram Arun (@vikramarun), Joey Santoro (@joeysantoro)
discussions-to: async vaults erc by Joeysantoro · Pull Request #10 · ethereum/ERCs · GitHub
status: Draft
type: Standards Track
category: ERC
created: 2023-10-18
requires: 20, 165, 4626

Abstract

The following standard extends ERC-4626 by adding support for asynchronous deposit and redemption flows. The async flows are called Requests.

New methods are added to asynchronously Request a deposit or redemption, and view the pending status of the Request. The existing deposit, mint, withdraw, and redeem ERC-4626 methods are used for executing Claimable Requests.

Implementations can choose to whether to add asynchronous flows for deposits, redemptions, or both.

Motivation

The ERC-4626 Tokenized Vaults standard has helped to make yield-bearing tokens more composable across decentralized finance. The standard is optimized for atomic deposits and redemptions up to a limit. If the limit is reached, no new deposits or redemptions can be submitted.

This limitation does not work well for any smart contract system with asynchronous actions or delays as a prerequisite for interfacing with the Vault (e.g. real-world asset protocols, undercollateralized lending protocols, cross-chain lending protocols, liquid staking tokens, or insurance safety modules).

This standard expands the utility of ERC-4626 Vaults for asynchronous use cases. The existing Vault interface (deposit/withdraw/mint/redeem) is fully utilized to claim asynchronous Requests.

Specification

Definitions:

The existing definitions from ERC-4626 apply. In addition, this spec defines:

  • Request: a function call that initiates an asynchronous deposit/redemption flow
  • Pending: the state where a Request has been made but is not yet Claimable
  • Claimable: the state where a Request is processed by the Vault enabling the user to claim corresponding shares (for async deposit) or assets (for async redeem)
  • Claimed: the state where a Request is finalized by the user and the user receives the output token (e.g. shares for a deposit Request)
  • Claim function: the corresponding Vault method to bring a Request to Claimed state (e.g. deposit or mint claims shares from requestDeposit). Lower case claim always describes the verb action of calling a Claim function.
  • operator: the account specified by the sender of the Request which has the right to claim a given Request once it is Claimable
  • asynchronous deposit Vault: a Vault that implements asynchronous Requests for deposit flows
  • asynchronous redemption Vault: a Vault that implements asynchronous redemption flows
  • fully asynchronous Vault: a Vault that implements asynchronous Requests for both deposit and redemption

Request Flows

EIP-X Vaults MUST implement one or both of asynchronous deposit and redemption Request flows. If either flow is not implemented in a Request pattern, it MUST use the ERC-4626 standard synchronous interaction pattern.

All EIP-X asynchronous tokenized Vaults MUST implement ERC-4626 with overrides for certain behavior described below.

Asynchronous deposit Vaults MUST override the ERC-4626 specification as follows:

  1. The deposit and mint methods do not transfer asset to the Vault, because this already happened on requestDeposit.
  2. previewDeposit and previewMint MUST revert for all callers and inputs.

Asynchronous redeem Vaults MUST override the ERC-4626 specification as follows:

  1. The redeem and withdraw methods do not transfer shares to the Vault, because this already happened on requestRedeem.
  2. The owner/operator field of redeem and withdraw MUST be msg.sender to prevent the theft of requested redemptions by a non-owner/operator.
  3. previewRedeem and previewWithdraw MUST revert for all callers and inputs.

Request Lifecycle

After submission, Requests go through Pending, Claimable, and Claimed stages. An example lifecycle for a deposit Request is visualized in the table below.

State User Vault
Pending requestDeposit(assets, operator) asset.transferFrom(msg.sender, vault, assets); pendingDepositRequest[operator] += assets
Claimable Internal Request fulfillment: pendingDepositRequest[msg.sender] -= assets; maxDeposit[operator] += assets
Claimed deposit(assets, receiver) maxDeposit[msg.sender] -= assets; vault.balanceOf[receiver] += shares

An important Vault inequality is that following a Request(s), the cumulative requested quantity MUST be more than pendingDepositRequest + maxDeposit - claimed. The inequality may come from fees or other state transitions outside implemented by Vault logic such as cancellation of a Request, otherwise this would be a strict equality.

Requests MUST NOT skip or otherwise short-circuit the Claim state. In other words, to initiate and claim a Request, a user MUST call both request* and the corresponding Claim function separately, even in the same block.

For asynchronous Vaults, the exchange rate between shares and assets including fees and yield is up to the Vault implementation. In other words, pending redemption Requests MAY NOT be yield bearing and MAY NOT have a fixed exchange rate.

Methods

requestDeposit

Transfers assets from msg.sender into the Vault and submits a Request for asynchronous deposit/mint. This places the Request in Pending state, with a corresponding increase in pendingDepositRequest for the amount assets.

When the Request is Claimable, maxDeposit and maxMint will be increased for the case where the receiver input is the operator. deposit or mint can subsequently be called by operator to receive shares. A Request MAY transition straight to Claimable state but MUST NOT skip the Claimable state.

The shares that will be received on deposit or mint MAY NOT be equivalent to the value of convertToShares(assets) at the time of Request, as the price can change between Request and Claim.

MUST support ERC-20 approve / transferFrom on asset as a deposit Request flow.

MUST revert if all of assets cannot be requested for deposit/mint (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.

MUST emit the RequestDeposit event.

- name: requestDeposit
  type: function
  stateMutability: nonpayable

  inputs:
    - name: assets
      type: uint256
    - name: operator
      type: address

pendingDepositRequest

The amount of requested assets in Pending state for the operator to deposit or mint.

MUST NOT include any assets in Claimable state for deposit or mint.

MUST NOT show any variations depending on the caller.

MUST NOT revert unless due to integer overflow caused by an unreasonably large input.

- name: pendingDepositRequest
  type: function
  stateMutability: view

  inputs:
    - name: operator
      type: address

  outputs:
    - name: assets
      type: uint256

requestRedeem

Assumes control of shares from owner and submits a Request for asynchronous redeem/withdraw. This places the Request in Pending state, with a corresponding increase in pendingRedeemRequest for the amount shares.

MAY support either a locking or a burning mechanism for shares depending on the Vault implemention.

If a Vault uses a locking mechanism for shares, those shares MUST be burned from the Vault balance before or upon claiming the Request.

MUST support a redeem Request flow where the control of shares is taken from owner directly where msg.sender has ERC-20 approval over the shares of owner.

When the Request is Claimable, maxRedeem and maxWithdraw will be increased for the case where the owner input is the operator. redeem or withdraw can subsequently be called by operator to receive assets. A Request MAY transition straight to Claimable state but MUST NOT skip the Claimable state.

The assets that will be received on redeem or withdraw MAY NOT be equivalent to the value of convertToAssets(shares) at time of Request, as the price can change between Pending and Claimed.

SHOULD check msg.sender can spend owner funds using allowance.

MUST revert if all of shares cannot be requested for redeem / withdraw (due to withdrawal limit being reached, slippage, the owner not having enough shares, etc).

MUST emit the RequestRedeem event.

- name: requestRedeem
  type: function
  stateMutability: nonpayable

  inputs:
    - name: shares
      type: uint256
    - name: operator
      type: address
    - name: owner
      type: address

pendingRedeemRequest

The amount of requested shares in Pending state for the operator to redeem or withdraw.

MUST NOT include any shares in Claimable state for redeem or withdraw.

MUST NOT show any variations depending on the caller.

MUST NOT revert unless due to integer overflow caused by an unreasonably large input.

- name: pendingRedeemRequest
  type: function
  stateMutability: view

  inputs:
    - name: operator
      type: address

  outputs:
    - name: shares
      type: uint256

Events

DepositRequest

sender has locked assets in the Vault to Request a deposit. operator controls this Request.

MUST be emitted when a deposit Request is submitted using the requestDeposit method.

- name: DepositRequest
  type: event

  inputs:
    - name: sender
      indexed: true
      type: address
    - name: operator
      indexed: true
      type: address
    - name: assets
      indexed: false
      type: uint256

RedeemRequest

sender has locked shares, owned by owner, in the Vault to Request a redemption. operator controls this Request.

MUST be emitted when a redemption Request is submitted using the requestRedeem method.

- name: RedeemRequest
  type: event

  inputs:
    - name: sender
      indexed: true
      type: address
    - name: operator
      indexed: true
      type: address
    - name: owner
      indexed: true
      type: address
    - name: assets
      indexed: false
      type: uint256

ERC-165 support

Smart contracts implementing this standard MUST implement the ERC-165 supportsInterface function.

Asynchronous deposit Vaults MUST return the constant value true if 0xea446681 is passed through the interfaceID argument.

Asynchronous redemption Vaults MUST return the constant value true if 0x2e9dd5bd is passed through the interfaceID argument.

Rationale

Symmetry and Non-inclusion of requestWithdraw and requestMint

In ERC-4626, the spec was written to be fully symmetrical with respect to converting assets and shares by including deposit/withdraw and mint/redeem.

Due to the asynchronous nature of Requests, the Vault can only operate with certainty on the quantity that is fully known at the time of the Request (assets for deposit and shares for redeem). The deposit Request flow cannot work with a mint call, because the amount of assets for the requested shares amount may fluctuate before the fulfillment of the Request. Likewise, the redemption Request flow cannot work with a withdraw call.

Optionality of flows

Certain use cases are only asynchronous on one flow but not the other between Request and redeem. A good example of an asynchronous redemption Vault is a liquid staking token. The unstaking period necessitates support for asynchronous withdrawals, however, deposits can be fully synchronous.

Non Inclusion of a Request Cancelation Flow

In many cases, canceling a Request may not be straightforward or even technically feasible. The state transition of cancelations could be synchronous or asynchronous, and the way to claim a cancelation interfaces with the remaining Vault functionality in complex ways.

A separate EIP should be developed to standardize behavior of cancelling a pending Request. Defining the cancel flow is still important for certain classes of use cases for which the fulfillment of a Request can take a considerable amount of time.

Request Implementation flexibility

The standard is flexible enough to support a wide range of interaction patterns for Request flows. Pending Requests can be handled via internal accounting, globally or on per-user levels, use ERC-20 or ERC-721, etc.

Likewise yield on redemption Requests can accrue or not, and the exchange rate of any Request may be fixed or variable depending on the implementation.

Not allowing short-circuiting for claims

If claims can short circuit, this creates ambiguity for integrators and complicates the interface with overloaded behavior on Request functions.

Instead there can be router contracts which atomically check for Claimable amounts immediately upon Request. Frontends can dynamically route Requests in this way depending on the state and implementation of the Vault.

Operator function parameter on requestDeposit and requestRedeem

To support flows where a smart contract manages the Request lifecycle on behalf of a user, the operator parameter is included in the requestDeposit and requestRedeem functions. This is not called owner because the assets or shares are not transferred from this account on Request submission, unlike the behaviour of an owner on redeem. It is also not called receiver because the shares or assets are not necessarily transferred on claiming the Request, this can be chosen by the operator when they call deposit, mint, redeem, or withdraw.

No Outputs for Request functions

requestDeposit and requestRedeem may not have a known exchange rate that will happen when the Request becomes Claimed. Returning the corresponding assets or shares could not work in this case.

The Requests could also output a timestamp representing the minimum amount of time expected for the Request to become Claimable, however not all Vaults will be able to return a reliable timestamp.

No Event for Claimable state

The state transition of a Request from Pending to Claimable happens at the Vault implementation level and is not specified in the standard. Requests may be batched into the Claimable state, or the state may transition automatically after a timestamp has passed. It is impractical to require an event to emit after a Request becomes Claimable at the user or batch level.

Reversion of Preview Functions in Async Request Flows

The preview functions do not take an address parameter, therefore the only way to discriminate discrepancies in exchange rate are via the msg.sender. However, this could lead to integration/implementation complexities where support contracts cannot determine the output of a claim on behalf of an operator.

In addition, there is no on-chain benefit to previewing the Claim step as the only valid state transition is to Claim anyway. If the output of a Claim is undesirable for any reason, the calling contract can revert on the output of that function call.

It reduces code and implementation complexity at little to no cost to simply mandate reversion for the preview functions of an async flow.

Mandated Support for ERC-165

Implementing support for ERC-165 is mandated because of the optionality of flows. Integrations can use the supportsInterface method to check whether a vault is fully asynchronous, partially asynchronous, or fully synchronous, and use a single contract to support all cases.

Backwards Compatibility

The interface is fully backwards compatible with ERC-4626. The specification of the deposit, mint, redeem, and withdraw methods is different as described in Specification.

Reference Implementation

WIP

Security Considerations

The methods pendingDepositRequest and pendingRedeemRequest are estimates useful for display purposes, and can be outdated due to the asynchronicity.

In general, asynchronicity concerns make state transitions in the Vault much more complex and vulnerable to security risks. Access control on Vault operations, clear documentation of state transitioning, and invariant checks should all be performed to mitigate these risks.

It is worth highlighting again here that the Claim functions for any asynchronous flows MUST enforce that msg.sender == operator/owner to prevent theft of Claimable assets or shares

Copyright

Copyright and related rights waived via CC0.

6 Likes

The Request lifecycle, encompassing Pending, Claimable, and Claimed stages, offers a clear process for asynchronous interactions. This is very useful.

Awesome stuff folks great EIP to help standardise this async flow. We’ve had to deal with the exact same problem over at Maple. We’ve had an async flow for redemptions in production since December last year on our 4626 vault that has worked well for us so far. Will share some learnings and feedback in the coming days but the proposal looks good!

1 Like

Looks familiar :eyes:

Hi Joey, nice to see you since the days of the Rari hack and the drama that followed.

I’ve been working on a similar design with Astrolab.fi for cross-chain vaults, which is a likely use case. After some iterations and testing, we decided to ditch this design. Why? Mainly, bad UX.

  • There is no way to know how much you’ll get when you request to redeem your share tokens. This creates a lot of moral hazard, as there’s a risk that the users get a lot less than what they thought they would get. In a classic ERC4626, you can estimate it precisely by doing a static call simulation beforehand.

  • In the above specs, locking shares when redeeming is optional. Since it’s a very important feature, I think it shouldn’t be optional as it creates ambiguity, or at least have a designated view call, or a specific function name, so the user/integrator to know what expect. Because vaults are often user/retail facing, and the point of this EIP is to standardize them, I believe making it straightforward would have a great value.

  • Most vault developers will make it mandatory, to avoid spam from users. A problem arises here in that a user asking for a redeem and locking his shares is “naked”. If, during a bank run, everyone asks to redeem shares, only to see a portion of the requests fulfilled, you can’t sell your remaining claims OTC or to a liquidity pool.

As for Astrolab.fi, we went with a model including an internal stable swap, to process atomically withdrawals. A share token is worth x “virtual asset tokens” that are swapped for the “real asset” tokens in the reserve (that can themselves be invested in a liquid form). This solves the above points:

  • You can do a static call to estimate precisely how much you’ll get when withdrawing.

  • It is ERC4626 compliant. You can use add a wrapper to set a minAmount to prevent any front-running.

  • In case of a bank run, if the internal stableswap gets depleted, convexity kicks in and slippage increases. This also allows buying back discounted share tokens, either by other users, or by the vault itself. Slippage on the way out thus becomes a proxy of time/money arbitrage.

  • If large depositors want to redeem without slippage, they can set up a limit order to redeem at their desired price. Or a redemption mechanism similar to what is proposed here can be set up. It has however drawbacks, as explained above.

Vaults with illiquid assets are hard to get right, and I’m happy that we can have this discussion !

Hey everyone sharing some thoughts and learnings from our experience running async 4626 vaults at Maple Finance over the past year.

What we like:

  • Acknowledgement of the changing exchange rate between requests for deposit/redemption and the actual deposit/redeem. We’ve also as a result not implemented a withdraw flow as a result. Support the non-inclusion of requestWithdraw and requestMint

  • Support that the standard proposes optionality of flows as there may be cases where both redemptions and deposits need to be requested, but this is a case by case basis depending on the RWA in question. (e.g our of business hours deposits can cause APY drag on other LPs).

  • Support that the standard doesn’t enforce that yield should stop or continue to accrue during requests and as a result if there is a fixed or variable exchange rate. This is super important as different RWA’s have different requirements and should be left to the implementor.

Open Questions / Considerations:

  • Why not just use approvals instead of introducing an operator param ? The owner can approve an operator ahead of a requestRedemption call and once a redemption is claimable assuming an operator has the appropriate approval amount they can call redeem on behalf of the user. This gives flexibility to the owner to decide who can redeem on their behalf once a redemption is requested, as some RWA assets could take weeks to liquidate and become claimable and a LP might want to change who can claim.
  • Just to clarify the above quote, a user may request to redeem but in another block once redemptions are processed can the redemptions be pushed directly to the LP as opposed to requiring the claim step? Whilst I agree for smart contract integrations a redeem flow where the owner/operator pull funds via a redeem call is required some LPs may prefer just to have to request the redemption and let the vault operator push funds directly to the LP as part of processing redemptions.

Closing thoughts:

  • Overall supportive of the proposal would like some discussion about the need for an operator.

  • Agree figuring out cancellations of requests is complex, we’ve got a specific implementation that works for our redemption mechanism which you can see below.

  • Feel free to take a look at our Pool.sol and PoolManager.sol contracts where we implement the requestRedeem and redeem async flows that we’ve had in production for the past year here.

1 Like

This is a valid point and at least worth adding to the security considerations. Any mechanism which requires asynchronicity by definition cannot quote the user a min amount out and any kind of slippage protection would add too many edge cases and implementation complexity. The core design assumption is that depositors trust the vault as many async vaults have more centralized backend operations. Therefore having the ambiguity is the lesser evil and ok as long as users are aware.

It isn’t optional, the shares must either be locked in the vault or burned depending on implementation details.

This should also be added to the security section. Vaults may even wrap claims in an NFT or ERC-20 for some secondary liquidity. Still a core aspect of the assumptions and design of the vault.

An operator param is better than an approval because the operator param is stored internally in the vault implementation as part of the same call. Requiring users and the vault to maintain a separate approval status for async pending deposits, async pending redeems, and internal shares (which are all not fungible with each other). Some vaults could require operator=msg.sender to remove this implementation complexity.

sure the LPs prefer this but is it economical or wise to allow the standard to acommodate the use case?

Too much optionality makes it impossible for integrating smart contracts to intelligently handle all use cases. Is it a two step deposit? what about withdrawals? can the vault push tokens to me at some random time?

Push per LP is generally expensive and intractable, and pull use cases scale much better. Higher abstraction layers such as EIP-712 signatures and smart wallets can handle the second step, but the standard should explicitly not allow this in my opinion.

That being said I am open to having it be allowed if there are better arguments that outweigh the negatives.

Thanks for putting this interesting proposal! Makes me think of the work I had started a year ago when we started to think about the idea with Angle (reference here)
Lots of similar thoughts, notably on the fact that in the process to redeem a request you must account for shares and not assets, which makes it otherwise manipulable. Sometimes wishing I had make this work more broadly available.

I do believe that this EIP provides a better naming than what I had, and the ability to do async deposit as well which I hadn’t thought of in my original work.
So fully supportive of the EIP how it is now on my end, and glad to see all these iterations on the initial idea.

I fear however that the vanilla implementation for a contract under this EIP is going to be trickier than expected. While this EIP provides clarity, there may be a looot of degrees of liberty for people implementing it when it comes to the underlying design choices they can make. This is in fact similar to what you sometimes have for ERC4626 vaults, where when developing contracts you often have to deal with exceptions (e.g dealing with a loss in lending protocol) which force you to find workarounds to respect the standard.

On this note, in my past work, I had shared some ideas of how a redeemRequest call could look like under the hood to handle a queue system of multiple redeeming one by one. Happy to share a corresponding implementation if some are interested.

3 Likes

I agree completely. I think the developer framework around this EIP looks a lot more like a guidebook with different implementations than a set in stone OpenZeppelin base contract.

Would love to see this!

Hey, joey, thx for sharing! For the majority of LST protocol, their vault asset is Native ETH not ERC20, would like to know how could this standard could be used for LST protocol?

1 Like

There is another EIP for native ETH based 4626 vaults: EIP-7535: ETH (Native Asset) Tokenized Vault

2 Likes

My main concern is with approvals an owner can revoke and give another operator the approval in order to process the redemption request. In the context of RWA assets redemption lead times could be hours to weeks if not longer and having the owner locked into a specific operator is problematic.

I see two potential ways forward, one is with approvals as already discussed or alternatively a nested mapping where the owner stores the operator e.g

function pendingRequestRedeem(address owner, address operator) returns (uint256 shares)

this way an owner can go back and change the operator that would service their redemption once it becomes claimable.

That is fine in our case we see using both but as long as the EIP requirement is fulfilled I don’t see any harm in protocols extending the standard further (lol) to support differing use cases.

If you folks need any help with writing the reference implementation let me know, Maple are heavy users of the 4626 vaults already (with async flows) and would be happy to contribute to getting this across the line.

1 Like

I just learned about this ERC and I’m very interrested.

My understanding is that assets that are Pending can be queried using the pendingDepositRequest and pendingRedeemRequest function, where as the assets that are Claimable can be queried using the ERC4626 maxDeposit and maxRedeem functions. However, users have no way to know when the assets/shares will become Claimable.
IMO there should be a standard discovery mechanism to helps users (and front end) understand the duration/delay of async operations.