This proposal extends EIP-5792 to allow dapps to downgrade their required atomicity guarantees and control the behaviour after a failed/reverted call. It introduces the batch-scope concept of strict
vs. loose
atomicity, where a strict
batch remains atomic in the face of chain reorgs and a loose
batch does not; and the per-call ability to continue after a failed/reverted call (continue
) or stop processing (halt
).
While the base EIP-5792 specification works extremely well for smart contract wallets, it does not allow the expression of the full range of flow control options that wallets can implement. For example, a dapp may only be submitting a batch for gas savings and not care about whether all calls are reverted on failure. A wallet may only be able to offer a limited form of atomicity through block builder backchannels, but that may be sufficient for a trading platform.
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.
The following subsections are modifications to the API endpoints from EIP-5792.
If a request does not match the schema defined below, the wallet MUST reject the request with an error code of INVALID_SCHEMA
.
wallet_sendCalls
The following JSON Schema SHALL be inserted, in the request object, as values of either the batch-scope or call-scope capabilities
objects (as appropriate) with a key of flowControl
.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"properties": {
"optional": {
"type": "boolean"
},
"atomicity": {
"enum": ["strict", "loose", "none"]
}
}
}
[
{
"version": "1.0",
"from": "0xd46e8dd67c5d32be8058bb8eb970870f07244567",
"chainId": "0x01",
"calls": [],
"capabilities": {
"flowControl": {
"atomicity": "loose"
}
}
}
]
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"properties": {
"optional": {
"type": "boolean"
},
"onFailure": {
"enum": ["rollback", "halt", "continue"]
}
}
}
[
{
"version": "1.0",
"from": "0xd46e8dd67c5d32be8058bb8eb970870f07244567",
"chainId": "0x01",
"calls": [
{
"to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567",
"value": "0x182183",
"data": "0xfbadbaf01",
"capabilities": {
"flowControl": {
"onFailure": "continue"
}
}
}
]
}
]
wallet_getCapabilities
The following JSON Schema is inserted into the per-chain object returned from wallet_getCapabilities
with a key of flowControl
.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"none": { "$ref": "#/$defs/onFailure" },
"loose": { "$ref": "#/$defs/onFailure" },
"strict": { "$ref": "#/$defs/onFailure" }
},
"$defs": {
"onFailure": {
"type": "array",
"uniqueItems": true,
"minItems": 1,
"items": {
"enum": ["rollback", "halt", "continue"]
}
}
}
}
{
"0x1": {
"flowControl": {
"loose": ["halt", "continue"],
"strict": ["continue"]
}
}
}
A rollback is informally defined as "causing no meaningful changes on chain." A rolled back batch only makes gas accounting and bookkeeping (eg. nonce) changes. In other words, a rollback is the default behaviour of EIP-5792 when a call fails.
A critical call is a call that causes the entire batch to rollback on failure,
and correspondingly a non-critical call does not. Specifically, a critical call
has a call-scope onFailure
of rollback
(or no onFailure
present),
while non-critical calls have either halt
or continue
.
This proposal introduces three atomicity levels: strict, loose, and none; enabled
by setting batch-scope atomicity
to strict
, loose
, or none
respectively.
Strict may also be enabled by omitting atomicity
entirely.
Strict atomicity is simply naming the default behaviour of EIP-5792: calls within a single batch MUST be contiguous and applied atomically (or the batch rolled back.)
Loose atomicity, on the other hand, is a weaker guarantee. In the event of a block reorg, any number of calls from the batch MAY appear on chain (possibly interspersed with other transactions). If there are no block reorgs, loose atomicity MUST provide the same guarantees as strict.
The none level of atomicity only provides the guarantee that the calls appear on chain in the order they are in the batch. Any number of calls from the batch MAY appear on chain (possibly interspersed with other transactions).
wallet_sendCalls
The wallet MUST reject wallet_sendCalls
requests with error code
MISSING_CAP
where both:
flowControl
capability is not present; andflowControl
capability is present.Note that the above requirement still applies if the call-scope flowControl
capability is marked as optional.
When flowControl
is present in the batch-scope capabilities
, the following
changes override the behaviour specified in EIP-5792.
These requirements defined in EIP-5792 are removed:
The wallet:
- MUST NOT await for any calls to be finalized to complete the batch
- MUST submit multiple calls as an atomic unit in a single transaction
- MAY revert all calls if any call fails
- MUST not execute any further calls after a failed call
- MAY reject the request if one or more calls in the batch is expected to fail, when simulated sequentially
The wallet:
atomicity
level as equivalent to strict
.atomicity
is strict
.atomicity
is loose
.atomicity
is none
.atomicity
is none
.atomicity
is loose
or none
.flowControl
capability as equivalent to
setting onFailure
to rollback
.onFailure
mode as equivalent to rollback
.onFailure
set to
halt
.onFailure
set to continue
.REJECTED_LEVEL
) batches containing at least one
critical call if the batch requests an atomicity level that the wallet can
provide but the user rejected (such as might happen with an
EIP-7702 set code transaction.)UNSUPPORTED_LEVEL
) batches containing at least
one critical call if the batch requests an atomicity level the wallet cannot
provide for any reason other than user rejection.strict
but not loose
SHOULD NOT reject loose
batches and SHOULD instead upgrade the request to strict atomicity.UNSUPPORTED_ON_FAIL
) batches containing
unsupported onFailure
modes.UNSUPPORTED_FLOW
) batches containing
unsupported combinations/orderings of call-scope onFailure
modes.rollback
when used in a none
batch, even if the
batch is upgraded to loose
or strict
atomicity. This also applies to
calls that do not specify an explicit onFailure
mode.ROLLBACK_EXPECTED
) the request if the batch is
expected to be rolled back.wallet_getCallsStatus
When wallet_getCallsStatus
is called with a batch identifier corresponding to
a batch submitted with the batch-scope flowControl
capability enabled, the
following changes override the behaviour defined in EIP-5792. Note that:
atomicity
to strict
and omitting the flowControl
capability for
all calls), the following changes still apply.These requirements defined in EIP-5792 are removed:
- If a wallet executes multiple calls atomically in a single transaction,
wallet_getCallsStatus
MUST return an object with areceipts
field that contains a single transaction receipt, corresponding to the transaction in which the calls were included.- If a wallet executes multiple calls non-atomically through some
capability
defined elsewhere,wallet_getCallsStatus
MUST return an object with areceipts
field that contains an array of receipts for all transactions containing batch calls that were included onchain. This includes the batch calls that were included on-chain but eventually reverted.
The returned capabilities object:
flowControl
key set to exactly the boolean true
.The returned receipts
array:
(...)
is a receipt from
a single transaction.[(successful A, successful B)]
[(successful A), (successful B)]
[(successful A, unsuccessful B), (successful B)]
[(unsuccessful A), (successful A), (successful B)]
[(successful A, unsuccessful B), (successful A, successful B)]
[(successful A, successful A), (successful B)]
wallet_getCallsStatus
requests, with only new
receipts being appended.[(unsuccessful A)]
followed by [(unsuccessful A), (successful A)]
is valid; but[(unsuccessful A)]
followed by [(successful A)]
should be avoided.This proposal modifies some of the status codes for use with EIP-5792's
GetCallsResult.status
field, and introduces the following new codes:
Code | Description |
---|---|
102 |
Partially Executed |
207 |
Partial Success |
An "included" call, in this section, is defined as having either been successfully or unsuccessfully executed. A call that has been recorded on chain, but has not yet been executed, does not qualify as included. Executed calls contained in batches that may still be rolled back also do not qualify as included.
A batch is "complete" when all of the calls in the batch (up to and including a
failed call with an onFailure
mode of halt
should one be present) have been
included and the wallet will not resubmit failed calls.
100
PendingStatus 100
MUST NOT be returned if any calls in the batch have been included
on chain.
102
Partially ExecutedStatus 102
SHALL be returned only when all of the following are true:
Responses with status 102
MUST contain at least one receipt, and SHOULD
contain receipts for all transactions with calls that have been included.
Note that a receipt capturing a failed call does not mean the call will ultimately fail. Wallets can resubmit calls (eg. with a higher gas limit), and the call may be executed successfully eventually.
200
ConfirmedStatus 200
MUST NOT be returned if any calls in the batch failed (including
batch rollback, and the onFailure
modes halt
/continue
).
207
Partial SuccessStatus 207
SHALL be returned only when all of the following are true:
onFailure
mode of continue
has been included and failed;onFailure
mode of rollback
have been included and failed;onFailure
mode of halt
have been included and failed;
and500
Chain Rules FailureTo clarify, status 500
is the correct code when the batch has rolled back or
when all calls are non-critical and have all failed.
If any calls are included and succeeded, one of 200
, 207
, or 600
should be
returned instead.
600
Partial Chain Rules FailureStatus 600
SHALL be returned only when all of the following are true:
onFailure
mode of halt
has been
included and failed;onFailure
mode of rollback
have been included and failed;
andwallet_getCapabilities
The response to wallet_getCapabilities
indicates what call-scope onFailure
modes are supported for each supported batch-scope atomicity
level for
batches with two or more calls. Support, here, means "natively supports." A
wallet that offers strict
atomicity but not loose
MUST NOT advertise
support for loose
(even if the wallet will upgrade loose
to strict
without an error.)
The wallet:
atomicity
levels.onFailure
modes in each atomicity
level. The levels do not need to support the same modes.rollback
before halt
is fine, but halt
before rollback
is not—then
both rollback
and halt
have to be included in the array.A plain EOA might offer halt
functionality by submitting one transaction per
block, and continue
by submitting all calls at once.
{
"0x1": {
"flowControl": {
"none": [ "halt", "continue" ]
}
}
}
Unlike a plain EOA, a shielded mempool can provide additional guarantees about
transaction atomicity. In this example, the wallet only offers the
onFailure
mode of continue
when using none
atomicity, but offers all three levels when using
loose
.
{
"0x1": {
"flowControl": {
"none": [ "continue" ]
"loose": [ "rollback", "halt", "continue" ]
}
}
}
In this example, the wallet will service batches specifying none
and loose
as if they requested strict
. Even though the batches will work, the
wallet_getCapabilities
response does not list none
or loose
.
{
"0x1": {
"flowControl": {
"strict": [ "rollback" ]
}
}
}
Name | Value |
---|---|
INVALID_SCHEMA |
|
MISSING_CAP |
|
REJECTED_LEVEL |
|
UNSUPPORTED_LEVEL |
|
UNSUPPORTED_ON_FAIL |
|
UNSUPPORTED_FLOW |
|
ROLLBACK_EXPECTED |
TBD
No backward compatibility issues found.
App developers cannot treat each call in a batch as an independent transaction unless the atomicity level is strict. In other words, there may be additional untrusted transactions between any of the calls in a batch. Calls that failed may eventually flip to succeeding, and vice versa. Even strictly atomic batches can flip between succeeding/failing in the face of a block reorg. The calls in loosely atomic batches can be included in separate, non-contiguous blocks. There is no constraint over how long it will take all the calls in a batch to be included. Apps should encode deadlines and timeout behaviors in the smart contract calls, just as they do today for transactions, including ones otherwise bundled.
Copyright and related rights waived via CC0.