Cancelation for ERC-7540 Tokenized Vaults
Abstract
The following standard extends ERC-7540 by adding support for asynchronous cancelation flows.
New methods are added to asynchronously cancel a deposit or redeem Request, view the status of the cancelation Request, and claim the assets or shares as a result of the cancelation Request.
Motivation
Shares or assets locked for Requests can be stuck in the Pending state. For some use cases, such as redeeming from a pool of long-dated real-world assets, this can take a considerable amount of time.
This standard expands the scope of Asynchronous ERC-7540 Vaults by adding cancelation support.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Definitions
The existing definitions from ERC-7540 apply.
Cancelation Lifecycle
After submission, cancelation Requests go through Pending, Claimable, and Claimed stages. An example lifecycle for a deposit cancelation Request is visualized in the table below.
State | User | Vault |
---|---|---|
Pending | cancelDepositRequest(requestId, controller) |
pendingCancelDepositRequest[controller] = true |
Claimable | Internal cancelation fulfillment: pendingCancelDepositRequest[controller] = false ; claimableCancelDepositRequest[controller] = assets |
|
Claimed | claimCancelDepositRequest(requestId, receiver, controller) |
claimableDepositRequest[controller] -= assets ; asset.balanceOf[receiver] += assets |
pendingCancelDepositRequest
and claimableCancelDepositRequest
are defined in the Methods section.
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 cancel* 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 MAY skip straight from the Pending to the Claimable stage, in the case of synchronous cancelation flows.
While a deposit cancelation Request is Pending, new deposit Requests are blocked. Likewise, while a redeem cancelation Request is Pending, new redeem Requests are blocked.
Methods
cancelDepositRequest
Submits a Request for asynchronous deposit cancelation. This places the Request in Pending state, with a corresponding increase in pendingCancelDepositRequest
for the full amount of the pending deposit Request.
When the cancelation is Pending, new deposit Requests are blocked and requestDeposit
MUST revert.
When the cancelation is Claimable, claimableCancelDepositRequest
will be increased for the controller
. claimCancelDepositRequest
can subsequently be called by controller
to receive assets
. A Request MAY transition straight to Claimable state but MUST NOT skip the Claimable state.
controller
MUST equal msg.sender
unless the controller
has approved the msg.sender
as an operator.
MUST emit the CancelDepositRequest
event.
- name: cancelDepositRequest
type: function
stateMutability: nonpayable
inputs:
- name: requestId
type: uint256
- name: controller
type: address
outputs:
pendingCancelDepositRequest
Whether the given requestId
and controller
have a pending deposit cancelation Request.
MUST NOT show any variations depending on the caller.
MUST NOT revert unless due to integer overflow caused by an unreasonably large input.
- name: pendingCancelDepositRequest
type: function
stateMutability: view
inputs:
- name: requestId
type: uint256
- name: controller
type: address
outputs:
- name: isPending
type: bool
claimableCancelDepositRequest
The amount of assets
in Claimable cancelation state for the controller
to claim.
MUST NOT show any variations depending on the caller.
MUST NOT revert unless due to integer overflow caused by an unreasonably large input.
- name: claimableCancelDepositRequest
type: function
stateMutability: view
inputs:
- name: requestId
type: uint256
- name: controller
type: address
outputs:
- name: assets
type: uint256
claimCancelDepositRequest
Claims the deposit cancelation Request with requestId
and controller
.
Transfers assets
to receiver
.
controller
MUST equal msg.sender
unless the controller
has approved the msg.sender
as an operator.
MUST emit the ClaimCancelDepositRequest
event.
- name: claimCancelDepositRequest
type: function
stateMutability: nonpayable
inputs:
- name: requestId
type: uint256
- name: receiver
type: address
- name: controller
type: address
outputs:
cancelRedeemRequest
Submits a Request for asynchronous redeem cancelation. This places the Request in Pending state, with a corresponding increase in pendingCancelRedeemRequest
for the full amount of the pending redeem Request.
When the cancelation is Pending, new redeem Requests are blocked and requestRedeem
MUST revert.
When the cancelation is Claimable, claimableCancelRedeemRequest
will be increased for the controller
. claimCancelRedeemRequest
can subsequently be called by controller
to receive shares
. A Request MAY transition straight to Claimable state but MUST NOT skip the Claimable state.
controller
MUST equal msg.sender
unless the controller
has approved the msg.sender
as an operator.
MUST emit the CancelRedeemRequest
event.
- name: cancelRedeemRequest
type: function
stateMutability: nonpayable
inputs:
- name: requestId
type: uint256
- name: controller
type: address
outputs:
pendingCancelRedeemRequest
Whether the given requestId
and controller
have a pending redeem cancelation Request.
MUST NOT show any variations depending on the caller.
MUST NOT revert unless due to integer overflow caused by an unreasonably large input.
- name: pendingCancelRedeemRequest
type: function
stateMutability: view
inputs:
- name: requestId
type: uint256
- name: controller
type: address
outputs:
- name: isPending
type: bool
claimableCancelRedeemRequest
The amount of shares
in Claimable cancelation state for the controller
to claim.
MUST NOT show any variations depending on the caller.
MUST NOT revert unless due to integer overflow caused by an unreasonably large input.
- name: claimableCancelRedeemRequest
type: function
stateMutability: view
inputs:
- name: requestId
type: uint256
- name: controller
type: address
outputs:
- name: shares
type: uint256
claimCancelRedeemRequest
Claims the redeem cancelation Request with requestId
and controller
.
Transfers assets
to receiver
.
controller
MUST equal msg.sender
unless the controller
has approved the msg.sender
as an operator.
MUST emit the ClaimCancelRedeemRequest
event.
- name: claimCancelRedeemRequest
type: function
stateMutability: nonpayable
inputs:
- name: requestId
type: uint256
- name: receiver
type: address
- name: owner
type: address
outputs:
Events
CancelDepositRequest
controller
has requested cancelation of their deposit Request with request ID requestId
. sender
is the caller of the cancelDepositRequest
which may not be equal to the controller
.
MUST be emitted when a deposit cancelation Request is submitted using the cancelDepositRequest
method.
- name: CancelDepositRequest
type: event
inputs:
- name: controller
indexed: true
type: address
- name: requestId
indexed: true
type: uint256
- name: sender
indexed: false
type: address
CancelDepositClaim
controller
has claimed their deposit cancelation Request with request ID requestId
. receiver
is the destination of the assets
. sender
is the caller of the claimCancelDepositRequest
which may not be equal to the controller
.
MUST be emitted when a deposit cancelation Request is submitted using the claimCancelDepositRequest
method.
- name: CancelDepositClaim
type: event
inputs:
- name: controller
indexed: true
type: address
- name: receiver
indexed: true
type: address
- name: requestId
indexed: true
type: uint256
- name: sender
indexed: false
type: address
- name: assets
indexed: false
type: uint256
CancelRedeemRequest
controller
has requested cancelation of their deposit Request with request ID requestId
. sender
is the caller of the cancelRedeemRequest
which may not be equal to the controller
.
MUST be emitted when a redeem cancelation Request is submitted using the cancelRedeemRequest
method.
- name: CancelRedeemRequest
type: event
inputs:
- name: controller
indexed: true
type: address
- name: requestId
indexed: true
type: uint256
- name: sender
indexed: false
type: address
CancelRedeemClaim
controller
has claimed their redeem cancelation Request with request ID requestId
. receiver
is the destination of the shares
. sender
is the caller of the claimCancelRedeemRequest
which may not be equal to the controller
.
MUST be emitted when a redeem cancelation Request is submitted using the claimCancelRedeemRequest
method.
- name: CancelRedeemClaim
type: event
inputs:
- name: controller
indexed: true
type: address
- name: receiver
indexed: true
type: address
- name: requestId
indexed: true
type: uint256
- name: sender
indexed: false
type: address
- name: shares
indexed: false
type: uint256
ERC-165 support
Smart contracts implementing this Vault standard MUST implement the ERC-165 supportsInterface
function.
Asynchronous deposit Vaults with cancelation support MUST return the constant value true
if 0x8bf840e3
is passed through the interfaceID
argument.
Asynchronous redemption Vaults with cancelation support MUST return the constant value true
if 0xe76cffc7
is passed through the interfaceID
argument.
Rationale
Blocking Requests during Cancelation
When cancelDepositRequest
is called by a controller
, new deposit Requests are blocked for this controller
, and the equivalent applies to the redeem flow.
This requirement simplifies the possible states of vaults implementing asynchronous cancelation flows.
The alternative would create possible states where a cancelation is pending and a new deposit Request is triggered, leading to the current state being complex to read for integrators.
Mandated Support for ERC-165
Implementing support for ERC-165 is mandated because of the optionality of flows as defined in ERC-7540. Integrations can use the supportsInterface
method to check whether a vault is fully asynchronous, partially asynchronous, or fully synchronous (for which it is just following the ERC-4626), and use a single contract to support all cases.
Backwards Compatibility
The interface is fully backwards compatible with ERC-7540.
Security Considerations
Existing security considerations from ERC-7540 apply.
Copyright
Copyright and related rights waived via CC0.