From c2cddc455fd9c2dc3d4b6dd567ad6e8518fc56f1 Mon Sep 17 00:00:00 2001 From: Jeroen <1748621+hieronx@users.noreply.github.com> Date: Tue, 21 May 2024 18:36:08 +0200 Subject: [PATCH] Update ERC-7540: Update ERC7540 and ERC7575 Merged by EIP-Bot. --- ERCS/erc-7540.md | 211 ++++++++++++++++------------------------------- ERCS/erc-7575.md | 94 +++++++++++++++++---- 2 files changed, 149 insertions(+), 156 deletions(-) diff --git a/ERCS/erc-7540.md b/ERCS/erc-7540.md index dd10e839cc..c75ec25865 100644 --- a/ERCS/erc-7540.md +++ b/ERCS/erc-7540.md @@ -25,7 +25,7 @@ The ERC-4626 Tokenized Vaults standard has helped to make yield-bearing tokens m 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. +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 @@ -39,8 +39,10 @@ The existing definitions from [ERC-4626](./eip-4626.md) apply. In addition, this - 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`). Lowercase claim always describes the verb action of calling a Claim function. - 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 +- asynchronous redemption Vault: a Vault that implements asynchronous Requests for redemption flows +- fully asynchronous Vault: a Vault that implements asynchronous Requests for both deposit and redemption flows +- requester: owner of the Request, who can manage any actions related to the Request including claiming the `assets` or `shares` +- operator: an account that can manage Requests on behalf of another account. ### Request Flows @@ -50,13 +52,13 @@ All ERC-7540 asynchronous tokenized Vaults MUST implement ERC-4626 with override 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`. +1. The `deposit` and `mint` methods do not transfer `assets` 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` field of `redeem` and `withdraw` MUST be `msg.sender` unless the `owner` has approved the `msg.sender` as an operator. +2. The `owner` field of `redeem` and `withdraw` SHOULD be renamed to `requester`, and the requester MUST be `msg.sender` unless the `request` has approved the `msg.sender` as an operator. 3. `previewRedeem` and `previewWithdraw` MUST revert for all callers and inputs. ### Request Lifecycle @@ -65,15 +67,15 @@ After submission, Requests go through Pending, Claimable, and Claimed stages. An | **State** | **User** | **Vault** | |-------------|---------------------------------|-----------| -| Pending | `requestDeposit(assets, receiver, owner, data)` | `asset.transferFrom(owner, vault, assets)`; `pendingDepositRequest[receiver] += assets` | -| Claimable | | *Internal Request fulfillment*: `pendingDepositRequest[owner] -= assets`; `claimableDepositRequest[owner] += assets` | -| Claimed | `deposit(assets, receiver)` | `claimableDepositRequest[owner] -= assets`; `vault.balanceOf[receiver] += shares` | +| Pending | `requestDeposit(assets, requester, owner)` | `asset.transferFrom(owner, vault, assets)`; `pendingDepositRequest[requester] += assets` | +| Claimable | | *Internal Request fulfillment*: `pendingDepositRequest[requester] -= assets`; `claimableDepositRequest[requester] += assets` | +| Claimed | `deposit(assets, receiver)` | `claimableDepositRequest[requester] -= assets`; `vault.balanceOf[receiver] += shares` | Note that `maxDeposit` increases and decreases in sync with `claimableDepositRequest`. 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. Vaults MUST NOT "push" tokens onto the user after a Request, users MUST "pull" the tokens via the Claim function. +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. Vaults MUST NOT "push" tokens onto the user after a Request, users MUST "pull" the tokens via the Claim function. 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. @@ -88,7 +90,7 @@ If a Request becomes partially claimable, all requests of the same `requestId` M There are no assumptions or requirements of requests with different `requestId`. I.e. they MAY transition to Claimable at different times and exchange rates with no ordering or correlation enforced in any way. -When `requestId==0`, the Vault MUST use purely the `owner` to discriminate the request state. The Pending and Claimable state of multiple requests from the same `owner` would be aggregated. If a Vault returns `0` for the `requestId` of any request, it MUST return `0` for all requests. +When `requestId==0`, the Vault MUST use purely the `requester` to discriminate the request state. The Pending and Claimable state of multiple requests from the same `requester` would be aggregated. If a Vault returns `0` for the `requestId` of any request, it MUST return `0` for all requests. ### Methods @@ -96,11 +98,9 @@ When `requestId==0`, the Vault MUST use purely the `owner` to discriminate the r Transfers `assets` from `owner` into the Vault and submits a Request for asynchronous `deposit`. This places the Request in Pending state, with a corresponding increase in `pendingDepositRequest` for the amount `assets`. -The output `requestId` is used to partially discriminate the request along with the `receiver`. See [Request Ids](#request-ids) section for more info. +The output `requestId` is used to partially discriminate the request along with the `requester`. See [Request Ids](#request-ids) section for more info. -If the length of `data` is not 0, the Request MUST send an `onERC7540DepositReceived` callback to `receiver` following the interface of `ERC7540DepositReceiver` described in [Request Callbacks](#request-callbacks) section. If the length of `data` is 0, the Request MUST NOT send a callback. - -When the Request is Claimable, `claimableDepositRequest` will be increased for the `receiver`. `deposit` or `mint` can subsequently be called by `receiver` to receive `shares`. A Request MAY transition straight to Claimable state but MUST NOT skip the Claimable state. +When the Request is Claimable, `claimableDepositRequest` will be increased for the `requester`. `deposit` or `mint` can subsequently be called by `requester` 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. @@ -122,12 +122,10 @@ MUST emit the `RequestDeposit` event. inputs: - name: assets type: uint256 - - name: receiver + - name: requester type: address - name: owner type: address - - name: data - type: bytes outputs: - name: requestId type: uint256 @@ -135,7 +133,7 @@ MUST emit the `RequestDeposit` event. #### pendingDepositRequest -The amount of requested `assets` in Pending state for the `owner` to `deposit` or `mint`. +The amount of requested `assets` in Pending state for the `requester` with the given `requestId` to `deposit` or `mint`. MUST NOT include any `assets` in Claimable state for `deposit` or `mint`. @@ -149,7 +147,9 @@ MUST NOT revert unless due to integer overflow caused by an unreasonably large i stateMutability: view inputs: - - name: owner + - name: requestId + type: uint256 + - name: requester type: address outputs: @@ -159,7 +159,7 @@ MUST NOT revert unless due to integer overflow caused by an unreasonably large i #### claimableDepositRequest -The amount of requested `assets` in Claimable state for the `owner` to `deposit` or `mint`. +The amount of requested `assets` in Claimable state for the `requester` with the given `requestId` to `deposit` or `mint`. MUST NOT include any `assets` in Pending state for `deposit` or `mint`. @@ -173,7 +173,9 @@ MUST NOT revert unless due to integer overflow caused by an unreasonably large i stateMutability: view inputs: - - name: owner + - name: requestId + type: uint256 + - name: requester type: address outputs: @@ -185,7 +187,7 @@ MUST NOT revert unless due to integer overflow caused by an unreasonably large i Assumes control of `shares` from `owner` and submits a Request for asynchronous `redeem`. This places the Request in Pending state, with a corresponding increase in `pendingRedeemRequest` for the amount `shares`. -The output `requestId` is used to partially discriminate the request along with the `receiver`. See [Request Ids](#request-ids) section for more info. +The output `requestId` is used to partially discriminate the request along with the `requester`. See [Request Ids](#request-ids) section for more info. MAY support either a locking or a burning mechanism for `shares` depending on the Vault implementation. @@ -193,14 +195,10 @@ If a Vault uses a locking mechanism for `shares`, those `shares` MUST be burned 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`, or the `owner` has approved the `msg.sender` as an operator. -If the length of `data` is not 0, the Request MUST send an `onERC7540RedeemReceived` callback to `receiver` following the interface of `ERC7540RedeemReceiver` described in [Request Callbacks](#request-callbacks) section. If the length of `data` is 0, the Request MUST NOT send a callback. - -When the Request is Claimable, `claimableRedeemRequest` will be increased for the `receiver`. `redeem` or `withdraw` can subsequently be called by `receiver` to receive `assets`. A Request MAY transition straight to Claimable state but MUST NOT skip the Claimable state. +When the Request is Claimable, `claimableRedeemRequest` will be increased for the `requester`. `redeem` or `withdraw` can subsequently be called by `requester` 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 the 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. @@ -213,12 +211,10 @@ MUST emit the `RequestRedeem` event. inputs: - name: shares type: uint256 - - name: receiver + - name: requester type: address - name: owner type: address - - name: data - type: bytes outputs: - name: requestId - type: uint256 @@ -226,7 +222,7 @@ MUST emit the `RequestRedeem` event. #### pendingRedeemRequest -The amount of requested `shares` in Pending state for the `owner` to `redeem` or `withdraw`. +The amount of requested `shares` in Pending state for the `requester` with the given `requestId` to `redeem` or `withdraw`. MUST NOT include any `shares` in Claimable state for `redeem` or `withdraw`. @@ -240,7 +236,9 @@ MUST NOT revert unless due to integer overflow caused by an unreasonably large i stateMutability: view inputs: - - name: owner + - name: requestId + type: uint256 + - name: requester type: address outputs: @@ -250,7 +248,7 @@ MUST NOT revert unless due to integer overflow caused by an unreasonably large i #### claimableRedeemRequest -The amount of requested `shares` in Claimable state for the `owner` to `redeem` or `withdraw`. +The amount of requested `shares` in Claimable state for the `requester` with the given `requestId` to `redeem` or `withdraw`. MUST NOT include any `shares` in Pending state for `redeem` or `withdraw`. @@ -264,7 +262,9 @@ MUST NOT revert unless due to integer overflow caused by an unreasonably large i stateMutability: view inputs: - - name: owner + - name: requestId + type: uint256 + - name: requester type: address outputs: @@ -274,7 +274,7 @@ MUST NOT revert unless due to integer overflow caused by an unreasonably large i #### `isOperator` -Returns `true` if the `operator` is approved as an operator for an `owner`. +Returns `true` if the `operator` is approved as an operator for a `requester`. ```yaml - name: isOperator @@ -282,7 +282,7 @@ Returns `true` if the `operator` is approved as an operator for an `owner`. stateMutability: view inputs: - - name: owner + - name: requester type: address - name: operator type: address @@ -320,22 +320,22 @@ MUST return True. #### `deposit` and `mint` overloaded methods -Implementations MUST support an additional overloaded `deposit` and `mint` method on the specification from [ERC-4626](./eip-4626.md), with an additional `owner` input of type `address`: +Implementations MUST support an additional overloaded `deposit` and `mint` method on the specification from [ERC-4626](./eip-4626.md), with an additional `requester` input of type `address`: -- `deposit(uint256 assets, address receiver, address owner)` -- `mint(uint256 shares, address receiver, address owner)` +- `deposit(uint256 assets, address receiver, address requester)` +- `mint(uint256 shares, address receiver, address requester)` -The `owner` field is used to look up the Request for which the `assets` should be claimed. +The `requester` field is used to look up the Request for which the `assets` should be claimed. -Calls MUST revert unless `msg.sender` is either equal to `owner` or an operator approved by `owner`. +Calls MUST revert unless `msg.sender` is either equal to `requester` or an operator approved by `requester`. -When the `Deposit` event is emitted, the first parameter MUST be the `sender`, and the second parameter MUST be the `owner`. +When the `Deposit` event is emitted, the first parameter MUST be the `requester`, and the second parameter MUST be the `receiver`. ### Events #### DepositRequest -`owner` has locked `assets` in the Vault to Request a deposit with request ID `requestId`. `receiver` controls this Request. `sender` is the caller of the `requestDeposit` which may not be equal to the `owner`. +`owner` has locked `assets` in the Vault to Request a deposit with request ID `requestId`. `requester` controls this Request. `sender` is the caller of the `requestDeposit` which may not be equal to the `owner`. MUST be emitted when a deposit Request is submitted using the `requestDeposit` method. @@ -344,7 +344,7 @@ MUST be emitted when a deposit Request is submitted using the `requestDeposit` m type: event inputs: - - name: receiver + - name: requester indexed: true type: address - name: owner @@ -363,7 +363,7 @@ MUST be emitted when a deposit Request is submitted using the `requestDeposit` m #### RedeemRequest -`sender` has locked `shares`, owned by `owner`, in the Vault to Request a redemption. `receiver` controls this Request, but is not necessarily the `owner`. +`sender` has locked `shares`, owned by `owner`, in the Vault to Request a redemption. `requester` controls this Request, but is not necessarily the `owner`. MUST be emitted when a redemption Request is submitted using the `requestRedeem` method. @@ -372,7 +372,7 @@ MUST be emitted when a redemption Request is submitted using the `requestRedeem` type: event inputs: - - name: receiver + - name: requester indexed: true type: address - name: owner @@ -391,7 +391,7 @@ MUST be emitted when a redemption Request is submitted using the `requestRedeem` #### `OperatorSet` -The `owner` has set the `approved` status to an `operator`. +The `requester` has set the `approved` status to an `operator`. MUST be logged when the operator status is set. @@ -402,7 +402,7 @@ MAY be logged when the operator status is set to the same status it was before t type: event inputs: - - name: owner + - name: requester indexed: true type: address - name: operator @@ -413,73 +413,13 @@ MAY be logged when the operator status is set to the same status it was before t type: bool ``` -### Request Callbacks - -All methods which initiate a request (including `requestId==0`) include a `data` parameter, which if nonzero length MUST send a callback to the receiver. - -There are two interfaces, `ERC7540DepositReceiver` and `ERC7540RedeemReceiver` which each define the single callback method to be called. - -#### `ERC7540DepositReceiver` -The interface to be called on `requestDeposit`. - -`sender` is the `msg.sender` of the original `requestDeposit` call. `owner` is the `owner` of the `requestDeposit`. `requestId` is the output `requestId` of the `requestDeposit`, `assets` is the amount transferred on `requestDeposit`, and `data` is the `data` of the `requestDeposit`. - -This function MUST return `0x6d7e2da0` upon successful execution of the callback. - -```yaml -- name: onERC7540DepositReceived - type: function - - inputs: - - name: sender - type: address - - name: owner - type: address - - name: requestId - type: uint256 - - name: assets - type: uint256 - - name: data - type: bytes - outputs: - - name: interfaceId - type: bytes4 -``` - -#### `ERC7540RedeemReceiver` -The interface to be called on `requestRedeem`. - -`sender` is the `msg.sender` of the original `requestRedeem` call. `owner` is the `owner` of the `requestRedeem`. `requestId` is the output `requestId` of the `requestRedeem`, `shares` is the amount transferred on `requestRedeem`, and `data` is the `data` of the `requestRedeem`. - -This function MUST return `0x01a2e97e` upon successful execution of the callback. - -```yaml -- name: onERC7540RedeemReceived - type: function - - inputs: - - name: sender - type: address - - name: owner - type: address - - name: requestId - type: uint256 - - name: shares - type: uint256 - - name: data - type: bytes - outputs: - - name: interfaceId - type: bytes4 -``` - ### [ERC-165](./eip-165.md) support Smart contracts implementing this Vault standard MUST implement the [ERC-165](./eip-165.md) `supportsInterface` function. -Asynchronous deposit Vaults MUST return the constant value `true` if `0x1683f250` is passed through the `interfaceID` argument. +Asynchronous deposit Vaults MUST return the constant value `true` if `0x3a2f2433` is passed through the `interfaceID` argument. -Asynchronous redemption Vaults MUST return the constant value `true` if `0x0899cb0b` is passed through the `interfaceID` argument. +Asynchronous redemption Vaults MUST return the constant value `true` if `0x620ee8e4` is passed through the `interfaceID` argument. ### [ERC-7575](./eip-7575.md) support @@ -487,42 +427,36 @@ Smart contracts implementing this Vault standard MUST implement the [ERC-7575](. ## Rationale -### Including Request IDs but Not Including a Claim by ID method +### Including Request IDs but not including a Claim by ID method Requests in an Asynchronous Vault have properties of NFTs or Semi-Fungible tokens due to their asynchronicity. However, trying to pigeonhole all ERC-7540 Vaults into supporting [ERC-721](./eip-721) or [ERC-1155](./eip-1155) for Requests would create too much interface bloat. Using both an id and address to discriminate Requests allows for any of these use cases to be developed at an external layer without adding too much complexity to the core interface. -Certain Vaults especially `requestId==0` cases benefit from using the underlying [ERC-4626](./eip-4626) methods for claiming because there is no discrimination at the `requestId` level. This standard is written primarily with those use cases in mind. A future standard can optimize for nonzero request ID with support for claiming and transferring requests discriminated also with a `requestId`. - -### Callbacks - -Callbacks on Request calls can be used among other things to allow Requests to become fully [ERC-721](./eip-721) or [ERC-1155](./eip-1155) compatible in an external layer. - -This can support flows where a smart contract manages the Request lifecycle on behalf of a user. +Certain Vaults, especially `requestId==0` cases, benefit from using the underlying [ERC-4626](./eip-4626) methods for claiming because there is no discrimination at the `requestId` level. This standard is written primarily with those use cases in mind. A future standard can optimize for nonzero request ID with support for claiming and transferring requests discriminated also with a `requestId`. ### 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. +Due to the nature of Requests, asynchronous Vaults 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`). Therefore 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 +### 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. +Certain use cases are only asynchronous on one side of the deposit or redeem Request flow. 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 +### 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 the 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 +### 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 +### 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. @@ -536,19 +470,19 @@ The 2-step approach used in the standard may be abstracted into a 1-step approac In the case where a Request may become Claimable immediately in the same block, there can be router contracts that 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 to handle this edge case. -### No Outputs for Request functions +### 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 +### 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 the exchange rate is 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 `owner`. +The preview functions do not take an address parameter, therefore the only way to discriminate discrepancies in the exchange rate is 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 a `requester`. 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. @@ -574,9 +508,9 @@ The interface is fully backward compatible with [ERC-4626](./eip-4626.md). The s mapping(address => uint256) public claimableDepositRequest; - mapping(address owner => mapping(address operator => bool)) public isOperator; + mapping(address requester => mapping(address operator => bool)) public isOperator; - function requestDeposit(uint256 assets, address receiver, address owner, bytes calldata data) external returns (uint256 requestId) { + function requestDeposit(uint256 assets, address requester, address owner) external returns (uint256 requestId) { require(assets != 0); require(owner == msg.sender || isOperator[owner][msg.sender]); @@ -584,14 +518,9 @@ The interface is fully backward compatible with [ERC-4626](./eip-4626.md). The s asset.safeTransferFrom(owner, address(this), assets); // asset here is the Vault underlying asset - pendingDepositRequest[owner] += assets; + pendingDepositRequest[requester] += assets; - // Perform the callback - if (data.length != 0) { - require(ERC7540Receiver(receiver).onERC7540DepositReceived(msg.sender, owner, requestId, assets, data) == ERC7540Receiver.onERC7540DepositReceived.selector, "receiver failed"); - } - - emit DepositRequest(receiver, owner, requestId, msg.sender, assets); + emit DepositRequest(requester, owner, requestId, msg.sender, assets); return requestId; } @@ -599,17 +528,17 @@ The interface is fully backward compatible with [ERC-4626](./eip-4626.md). The s * Include some arbitrary transition logic here from Pending to Claimable */ - function deposit(uint256 assets, address receiver, address owner) external returns (uint256 shares) { + function deposit(uint256 assets, address receiver, address requester) external returns (uint256 shares) { require(assets != 0); - require(owner == msg.sender || isOperator[owner][msg.sender]); + require(requester == msg.sender || isOperator[requester][msg.sender]); - claimableDepositRequest[owner] -= assets; // underflow would revert if not enough claimable assets + claimableDepositRequest[requester] -= assets; // underflow would revert if not enough claimable assets shares = convertToShares(assets); // this naive example uses the instantaneous exchange rate. It may be more common to use the rate locked in upon Claimable stage. balanceOf[receiver] += shares; - emit Deposit(owner, receiver, assets, shares); + emit Deposit(requester, receiver, assets, shares); } function setOperator(address operator, bool approved) public returns (bool) { @@ -627,8 +556,6 @@ In general, asynchronicity concerns make state transitions in the Vault much mor * The view methods for viewing Pending and Claimable request states (e.g. pendingDepositRequest) are estimates useful for display purposes but can be outdated. The inability to know the final exchange rate on any Request requires users to trust the implementation of the asynchronous Vault in the computation of the exchange rate and fulfillment of their Request. * Shares or assets locked for Requests can be stuck in the Pending state. Vaults may elect to allow for the fungibility of pending claims or implement some cancellation functionality to protect users. -Lastly, it is worth highlighting again here that the Claim functions for any asynchronous flows MUST enforce that msg.sender == owner to prevent theft of Claimable assets or shares. - ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md). \ No newline at end of file diff --git a/ERCS/erc-7575.md b/ERCS/erc-7575.md index 0f67bbe554..2de52a0fe8 100644 --- a/ERCS/erc-7575.md +++ b/ERCS/erc-7575.md @@ -13,13 +13,13 @@ requires: 20, 165, 2771, 4626 ## Abstract -The following standard adapts [ERC-4626](./eip-4626.md) to support multiple assets or entry points for the same share token. +The following standard adapts [ERC-4626](./eip-4626.md) to support multiple assets or entry points for the same share token. This also enables Vaults which don't have a true share token but rather convert between two arbitrary external tokens. -It adds a new `share` method to allow the [ERC-20](./eip-20.md) dependency to be externalized. +It adds a new `share` method to the Vault, to allow the [ERC-20](./eip-20.md) dependency to be externalized. -This also enables Vaults which don't have a true share token but rather convert between two arbitrary external tokens. +It also adds Vault-to-Share lookup to the share token. -Lastly, it enforces [ERC-165](./eip-165.md) support for Vaults. +Lastly, it enforces [ERC-165](./eip-165.md) support for Vaults and the share token. ## Motivation @@ -29,13 +29,15 @@ One missing use case that is not supported by [ERC-4626](./eip-4626.md) is Vault ### Definitions: -The existing definitions from [ERC-4626](./eip-4626.md) apply. +The existing definitions from [ERC-4626](./eip-4626.md) apply. In addition, this spec defines: -- Multi-Asset Vaults: A Vault which has multiple assets/entry points +- Multi-Asset Vaults: A Vault which has multiple assets/entry points. The Multi-Asset Vault refers to the group of [ERC-7575](./eip-7575.md) contracts with the entry points for a specific asset, linked to one common `share` token. - Pipe: A converter from one token to another (unidirectional or bidirectional) ### Methods +All [ERC-7575](./eip-7575.md) Vaults MUST implement [ERC-4626](./eip-4626.md) excluding the [ERC-20](./eip-20.md) methods and events. + #### share The address of the underlying `share` received on deposit into the Vault. MUST return an address of an [ERC-20](./eip-20.md) share representation of the Vault. @@ -73,16 +75,12 @@ A Pipe MAY be either unidirectional or bidirectional. A unidirectional Pipe SHOULD implement only the entry function(s) `deposit` and/or `mint`. -### [ERC-165](./eip-165.md) support - -Smart contracts implementing [ERC-7575](./eip-7575.md) MUST implement the [ERC-165](./eip-165.md) `supportsInterface` function. - -It MUST return the constant value `true` if `0x2f0a18c5` is passed through the `interfaceID` argument. - ### Share-to-Vault lookup The [ERC-20](./eip-20.md) implementation of `share` SHOULD implement a `vault` method, that returns the address of the Vault for a specific `asset`. +SHOULD emit the `VaultUpdate` event when a Vault linked to the share changes. + ```yaml - name: vault type: function @@ -97,26 +95,94 @@ The [ERC-20](./eip-20.md) implementation of `share` SHOULD implement a `vault` m type: address ``` +### [ERC-165](./eip-165.md) support + +Vaults implementing [ERC-7575](./eip-7575.md) MUST implement the [ERC-165](./eip-165.md) `supportsInterface` function. The Vault contract MUST return the constant value `true` if `0x2f0a18c5` is passed through the `interfaceID` argument. + +The share contract SHOULD implement the [ERC-165](./eip-165.md) `supportsInterface` function. The share token MUST return the constant value `true` if `0xf815c03d` is passed through the `interfaceID` argument. + +### Events + +#### VaultUpdate + +The Vault linked to the share has been updated. + +```yaml +- name: VaultUpdate + type: event + + inputs: + - name: asset + indexed: true + type: address + - name: vault + indexed: false + type: address +``` + ## Rationale This standard is intentionally flexible to support both existing [ERC-4626](./eip-4626.md) Vaults easily by the introduction of a single new method, but also flexible to support new use cases by allowing separate share tokens. ### Ability to externalize [ERC-20](./eip-20.md) Dependency + By allowing `share != address(this)`, the Vault can have an external contract managing the [ERC-20](./eip-20.md) functionality of the Share. In the case of Multi-Asset, this avoids the confusion that might arise if each Vault itself were required to be an [ERC-20](./eip-20.md), which could confuse integrators and front-ends. This approach also enables the creation of new types of Vaults, such as Pipes, which facilitate the conversion between two external [ERC-20](./eip-20.md) tokens. These Pipes could be unidirectional (i.e. only for assets to shares via deposit/mint, or shares to assets via redeem/withdraw) or bidirectional for both entry and exit flows. ### Including Share-to-Vault lookup optionally -The `vault` method is included to look up a Vault for a `share` by its `asset`. This enables integrations to easily query Multi-Asset Vaults. +The `vault` method is included to look up a Vault for a `share` by its `asset`, combined with the `VaultUpdate` event and [ERC-165](./eip-165.md) support. This enables integrations to easily query Multi-Asset Vaults. This is optional, to maintain backward compatibility with use cases where the `share` is an existing deployed contract. ## Backwards Compatibility -Existing [ERC-4626](./eip-4626.md) Vaults can be made compatible with [ERC-7575](./eip-7575.md) by adding a single `share` method that returns the address of the Vault. +[ERC-7575](./eip-7575.md) Vaults are compatible with [ERC-4626](./eip-4626.md) except for the [ERC-20](./eip-20.md) functionality which has been removed. + +## Reference Implementation + +```solidity + // This code snippet is incomplete pseudocode used for example only and is no way intended to be used in production or guaranteed to be secure + + contract Share is ERC20 { + mapping (address asset => address) vault; + + function updateVault(address asset, address vault_) public { + vault[asset] = vault_; + emit UpdateVault(asset, vault_); + + function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { + return interfaceId == 0xf815c03d || interfaceId == 0x01ffc9a7; + } + } + } + + contract TokenAVault is ERC7575 { + address public share = address(Share); + address public asset = address(TokenA); + + // ERC4626 implementation + + function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { + return interfaceId == 0x2f0a18c5 || interfaceId == 0x01ffc9a7; + } + } + + contract TokenBVault is ERC7575 { + address public share = address(Share); + address public asset = address(TokenB); + + // ERC4626 implementation + + function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { + return interfaceId == 0x2f0a18c5 || interfaceId == 0x01ffc9a7; + } + } + +``` ## Security Considerations