Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ERC7540: requester => controller #34

Merged
merged 7 commits into from
May 29, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 43 additions & 43 deletions ERCS/erc-7540.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ The existing definitions from [ERC-4626](./eip-4626.md) apply. In addition, this
- asynchronous deposit Vault: a Vault that implements asynchronous Requests for deposit flows
- 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`
- controller: 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
Expand All @@ -58,7 +58,7 @@ Asynchronous deposit Vaults MUST override the ERC-4626 specification as follows:
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` SHOULD be renamed to `requester`, and the requester MUST be `msg.sender` unless the `requester` has approved the `msg.sender` as an operator.
2. The `owner` field of `redeem` and `withdraw` SHOULD be renamed to `controller`, and the controller MUST be `msg.sender` unless the `controller` has approved the `msg.sender` as an operator.
3. `previewRedeem` and `previewWithdraw` MUST revert for all callers and inputs.

### Request Lifecycle
Expand All @@ -67,9 +67,9 @@ After submission, Requests go through Pending, Claimable, and Claimed stages. An

| **State** | **User** | **Vault** |
|-------------|---------------------------------|-----------|
| 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` |
| Pending | `requestDeposit(assets, controller, owner)` | `asset.transferFrom(owner, vault, assets)`; `pendingDepositRequest[controller] += assets` |
| Claimable | | *Internal Request fulfillment*: `pendingDepositRequest[controller] -= assets`; `claimableDepositRequest[controller] += assets` |
| Claimed | `deposit(assets, receiver)` | `claimableDepositRequest[controller] -= assets`; `vault.balanceOf[receiver] += shares` |

Note that `maxDeposit` increases and decreases in sync with `claimableDepositRequest`.

Expand All @@ -90,17 +90,17 @@ 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 `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.
When `requestId==0`, the Vault MUST use purely the `controller` to discriminate the request state. The Pending and Claimable state of multiple requests from the same `controller` would be aggregated. If a Vault returns `0` for the `requestId` of any request, it MUST return `0` for all requests.

### Methods

#### requestDeposit

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 `requester`. See [Request Ids](#request-ids) section for more info.
The output `requestId` is used to partially discriminate the request along with the `controller`. See [Request Ids](#request-ids) section for more info.

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.
When the Request is Claimable, `claimableDepositRequest` will be increased for the `controller`. `deposit` or `mint` can subsequently be called by `controller` 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.

Expand All @@ -122,7 +122,7 @@ MUST emit the `RequestDeposit` event.
inputs:
- name: assets
type: uint256
- name: requester
- name: controller
type: address
- name: owner
type: address
Expand All @@ -133,7 +133,7 @@ MUST emit the `RequestDeposit` event.

#### pendingDepositRequest

The amount of requested `assets` in Pending state for the `requester` with the given `requestId` to `deposit` or `mint`.
The amount of requested `assets` in Pending state for the `controller` with the given `requestId` to `deposit` or `mint`.

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

Expand All @@ -149,7 +149,7 @@ MUST NOT revert unless due to integer overflow caused by an unreasonably large i
inputs:
- name: requestId
type: uint256
- name: requester
- name: controller
type: address

outputs:
Expand All @@ -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 `requester` with the given `requestId` to `deposit` or `mint`.
The amount of requested `assets` in Claimable state for the `controller` with the given `requestId` to `deposit` or `mint`.

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

Expand All @@ -175,7 +175,7 @@ MUST NOT revert unless due to integer overflow caused by an unreasonably large i
inputs:
- name: requestId
type: uint256
- name: requester
- name: controller
type: address

outputs:
Expand All @@ -187,15 +187,15 @@ 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 `requester`. See [Request Ids](#request-ids) section for more info.
The output `requestId` is used to partially discriminate the request along with the `controller`. 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.

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`, or the `owner` has approved the `msg.sender` as an operator.

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.
When the Request is Claimable, `claimableRedeemRequest` will be increased for the `controller`. `redeem` or `withdraw` can subsequently be called by `controller` 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.

Expand All @@ -211,7 +211,7 @@ MUST emit the `RequestRedeem` event.
inputs:
- name: shares
type: uint256
- name: requester
- name: controller
type: address
- name: owner
type: address
Expand All @@ -222,7 +222,7 @@ MUST emit the `RequestRedeem` event.

#### pendingRedeemRequest

The amount of requested `shares` in Pending state for the `requester` with the given `requestId` to `redeem` or `withdraw`.
The amount of requested `shares` in Pending state for the `controller` with the given `requestId` to `redeem` or `withdraw`.

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

Expand All @@ -238,7 +238,7 @@ MUST NOT revert unless due to integer overflow caused by an unreasonably large i
inputs:
- name: requestId
type: uint256
- name: requester
- name: controller
type: address

outputs:
Expand All @@ -248,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 `requester` with the given `requestId` to `redeem` or `withdraw`.
The amount of requested `shares` in Claimable state for the `controller` with the given `requestId` to `redeem` or `withdraw`.

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

Expand All @@ -264,7 +264,7 @@ MUST NOT revert unless due to integer overflow caused by an unreasonably large i
inputs:
- name: requestId
type: uint256
- name: requester
- name: controller
type: address

outputs:
Expand All @@ -274,15 +274,15 @@ 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 a `requester`.
Returns `true` if the `operator` is approved as an operator for a `controller`.

```yaml
- name: isOperator
type: function
stateMutability: view

inputs:
- name: requester
- name: controller
type: address
- name: operator
type: address
Expand Down Expand Up @@ -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 `requester` 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 `controller` input of type `address`:

- `deposit(uint256 assets, address receiver, address requester)`
- `mint(uint256 shares, address receiver, address requester)`
- `deposit(uint256 assets, address receiver, address controller)`
- `mint(uint256 shares, address receiver, address controller)`

The `requester` field is used to look up the Request for which the `assets` should be claimed.
The `controller` 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 `requester` or an operator approved by `requester`.
Calls MUST revert unless `msg.sender` is either equal to `controller` or an operator approved by `controller`.

When the `Deposit` event is emitted, the first parameter MUST be the `requester`, and the second parameter MUST be the `receiver`.
When the `Deposit` event is emitted, the first parameter MUST be the `controller`, 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`. `requester` 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`. `controller` 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.

Expand All @@ -344,7 +344,7 @@ MUST be emitted when a deposit Request is submitted using the `requestDeposit` m
type: event

inputs:
- name: requester
- name: controller
indexed: true
type: address
- name: owner
Expand All @@ -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. `requester` controls this Request, but is not necessarily the `owner`.
`sender` has locked `shares`, owned by `owner`, in the Vault to Request a redemption. `controller` controls this Request, but is not necessarily the `owner`.

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

Expand All @@ -372,7 +372,7 @@ MUST be emitted when a redemption Request is submitted using the `requestRedeem`
type: event

inputs:
- name: requester
- name: controller
indexed: true
type: address
- name: owner
Expand All @@ -391,7 +391,7 @@ MUST be emitted when a redemption Request is submitted using the `requestRedeem`

#### `OperatorSet`

The `requester` has set the `approved` status to an `operator`.
The `controller` has set the `approved` status to an `operator`.

MUST be logged when the operator status is set.

Expand All @@ -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: requester
- name: controller
indexed: true
type: address
- name: operator
Expand Down Expand Up @@ -482,7 +482,7 @@ The state transition of a Request from Pending to Claimable happens at the Vault

### 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 a `requester`.
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 `controller`.

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.

Expand All @@ -508,37 +508,37 @@ The interface is fully backward compatible with [ERC-4626](./eip-4626.md). The s

mapping(address => uint256) public claimableDepositRequest;

mapping(address requester => mapping(address operator => bool)) public isOperator;
mapping(address controller => mapping(address operator => bool)) public isOperator;

function requestDeposit(uint256 assets, address requester, address owner) external returns (uint256 requestId) {
function requestDeposit(uint256 assets, address controller, address owner) external returns (uint256 requestId) {
require(assets != 0);
require(owner == msg.sender || isOperator[owner][msg.sender]);

requestId = 0; // no requestId associated with this request

asset.safeTransferFrom(owner, address(this), assets); // asset here is the Vault underlying asset

pendingDepositRequest[requester] += assets;
pendingDepositRequest[controller] += assets;

emit DepositRequest(requester, owner, requestId, msg.sender, assets);
emit DepositRequest(controller, owner, requestId, msg.sender, assets);
return requestId;
}

/**
* Include some arbitrary transition logic here from Pending to Claimable
*/

function deposit(uint256 assets, address receiver, address requester) external returns (uint256 shares) {
function deposit(uint256 assets, address receiver, address controller) external returns (uint256 shares) {
require(assets != 0);
require(requester == msg.sender || isOperator[requester][msg.sender]);
require(controller == msg.sender || isOperator[controller][msg.sender]);

claimableDepositRequest[requester] -= assets; // underflow would revert if not enough claimable assets
claimableDepositRequest[controller] -= 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(requester, receiver, assets, shares);
emit Deposit(controller, receiver, assets, shares);
}

function setOperator(address operator, bool approved) public returns (bool) {
Expand Down
Loading