From ad73e23a114a6bfb17214059f0ded01489d6004c Mon Sep 17 00:00:00 2001 From: Joey <31974730+Joeysantoro@users.noreply.github.com> Date: Thu, 26 Oct 2023 16:40:09 -0400 Subject: [PATCH] ERC-7540: Asynchronous ERC-4626 Tokenized Vaults (#10) * async vaults erc * Update ERCS/erc-7540.md Co-authored-by: lightclient <14004106+lightclient@users.noreply.github.com> * Update ERCS/erc-7540.md Co-authored-by: lightclient <14004106+lightclient@users.noreply.github.com> --------- Co-authored-by: lightclient <14004106+lightclient@users.noreply.github.com> --- ERCS/erc-7540.md | 323 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 ERCS/erc-7540.md diff --git a/ERCS/erc-7540.md b/ERCS/erc-7540.md new file mode 100644 index 0000000000000..352cbf3c128f0 --- /dev/null +++ b/ERCS/erc-7540.md @@ -0,0 +1,323 @@ +--- +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: https://ethereum-magicians.org/t/eip-7540-asynchronous-erc-4626-tokenized-vaults/16153 +status: Draft +type: Standards Track +category: ERC +created: 2023-10-18 +requires: 20, 165, 4626 +--- + +## Abstract + +The following standard extends [ERC-4626](./eip-4626.md) 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](./eip-4626.md) 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 + +ERC-7540 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 ERC-7540 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](./eip-20.md) `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. + +```yaml +- 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. + +```yaml +- 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. + +```yaml +- 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. + +```yaml +- 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. + +```yaml +- 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. + +```yaml +- 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](./eip-165.md) support + +Smart contracts implementing this standard MUST implement the [ERC-165](./eip-165.md) `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](./eip-721.md), 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](./eip-165.md) + +Implementing support for [ERC-165](./eip-165.md) is mandated because of the [optionality of flows](#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](./eip-4626.md). The specification of the `deposit`, `mint`, `redeem`, and `withdraw` methods is different as described in [Specification](#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](../LICENSE.md).