Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-7902: Wallet Capabilities for Account Abstraction

EIP-5792 capabilities that allow applications to specify EIP-8141 frame transaction parameters for calls

Authors Yoav Weiss (@yoavw), Alex Forshtat (@forshtat), Dror Tirosh (@drortirosh), Shahaf Nacson (@shahafn)
Created 2025-03-01
Discussion Link https://ethereum-magicians.org/t/erc-wallet-capabilities-for-account-abstraction/23122
Requires EIP-5792, EIP-7702, EIP-7906, EIP-8141, EIP-8250, EIP-8272

Abstract

EIP-8141 and its extensions define a new EIP-2718 transaction type for account abstraction with a rich and innovative set of features. The wallets adopting EIP-8141 must have a mechanism to expose these features to their users and applications. Previously, a limited subset of these features has been available through ERC-4337 UserOperations.

EIP-5792 defines a JSON-RPC API for communication between wallets and dApps, and allows the API to be extended with “capabilities”.

This proposal defines capabilities that a wallet can implement to expose the features of an EIP-8141 frame transaction to dApps: an expiring time limit on validity, keyed nonces (EIP-8250), a sponsored gas payment, atomic frames batches, verified recent roots (EIP-8272), and post-transaction assertions (EIP-7906). It also defines the capability that allows a dApp to take part in suggesting the EIP-7702 account implementation.

Motivation

EIP-8141 turns a transaction into a list of frames. A wallet that receives calls through EIP-5792 decides how many frames to build, which validation to run, who pays, and what is checked after the calls ran. A dApp has no way to influence any of this, although some of these choices depend on what the dApp knows:

  • A dApp that sends independent transactions from one account picks a nonce key for each, so that the transactions do not depend on each other’s order. A dApp that needs a transaction to take effect at most once asks for the single-use form.
  • A dApp that builds a proof against an onchain root knows which recent root it used.
  • A dApp that sponsors gas knows the paymaster and the data it expects.
  • A dApp knows which of its calls must succeed together and which are independent.
  • A dApp or a wallet can bound the outcome of a transaction with an on-chain assertion, but only if the assertion reaches the frame list.

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.

Terms defined by EIP-8141, including frame, VERIFY, DEFAULT, SENDER, APPROVE, validation prefix and ENTRY_POINT, have the meanings given there. The frame names used by its public mempool rules (deploy, self_verify, only_verify, pay, expiry_verify, user_op) are used here as well. ASSERT is the frame mode defined by EIP-7906. recent_root_verify is the frame defined by EIP-8272.

An atomic frames batch is a maximal contiguous run of frames that all succeed or are all reverted, marked with ATOMIC_BATCH_FLAG. EIP-8141 calls it an atomic batch. This document always says atomic frames batch, so that it is clear when it speaks of frames. For all the calls of a request, it says the calls of the request.

Scope

These capabilities apply to a wallet_sendCalls request on a chain where the wallet builds an EIP-8141 frame transaction for the request. The transaction sender is the from of the request. Every call of the request MUST specify to. The wallet MUST reject a request with a call that does not, because a frame without a target addresses the sender and cannot express contract creation. The wallet MAY choose everything that capabilities did not explicitly specify.

A wallet reports the capabilities of this proposal that it supports in wallet_getCapabilities, under their identifiers. This proposal does not define the parameters that the wallet reports for them.

Call Dispatch

By default, this specification does not say which frames carry the calls. The wallet decides how the account executes each call. It may build a frame in SENDER mode that carries the call, with the fields listed for SENDER under the Call Frame Capability. It may also build a DEFAULT frame that calls the execution function of the account, which is the function of the account contract that makes a call on behalf of the account, with the call encoded in the calldata. A wallet uses the second form when the policy of the account, the rules by which the account accepts frames, does not allow SENDER frames. An account that has no code and no EIP-7702 delegation runs the default code of EIP-8141, which returns successfully for a DEFAULT frame without making any call, so the wallet MUST use SENDER frames for such an account.

Whichever form the wallet chooses, it MUST preserve the outcomes that the request describes:

  • the calls execute in the order of the request;
  • if atomicRequired is true, the wallet executes all calls of the request atomically, as if all of them were in one atomic frames batch or in a single frame that calls the account, which executes them atomically;
  • if atomicRequired is false, the calls of an atomic frames batch are executed atomically, and for a call outside an atomic frames batch, this specification does not specify any changes for the outcomes of any other call.

Calls execute atomically if they succeed or fail together. The wallet can execute calls atomically in one of two ways, which are virtually equivalent for the purposes of this proposal: an atomic frames batch with one frame for each call, or a single frame that calls a function of the account that executes all the calls and reverts if any of them fails. The wallet chooses the way. A call with the callFrame capability needs a frame of its own, so the wallet uses an atomic frames batch for a set of calls that includes one.

A wallet MAY combine adjacent calls in one frame. The wallet MUST NOT combine a call that carries the callFrame capability with any other call.

Capabilities

EIP-7702 Authorization Capability

Identifier: eip7702Auth

Asks the wallet to make sure the account is delegated to a given smart account implementation.

type Eip7702AuthCapabilityParams = {
  delegation: string,      // an address or a name of an implementation the wallet knows
  version?: string,        // version of the named implementation
  account?: `0x${string}`, // EOA authorizing the delegation, defaulting to the `from` of the request
}

delegation is either the address of the delegation target or a wallet-supplied well-known name of an implementation. Names are defined by the wallet. If delegation is an address, version MUST be omitted. The wallet MUST reject the request if it does not recognize the named delegation or if the delegation address is not one it accepts. The zero address is accepted by a wallet that supports the removal of a delegation.

The wallet MUST ensure that, before the calls execute, the code of account is the EIP-7702 delegation designator for the target, or is empty if the target is the zero address.

The wallet chooses the target as follows:

  • If delegation is an address, the target is that address.
  • If delegation is a name and version is given, the target is that version of that implementation.
  • If delegation is a name and version is omitted, and account is already delegated to any version of that implementation, the target is the current delegation. Otherwise, the target is the version of that implementation that the wallet chooses.

The wallet then acts as follows:

  • If account is already delegated to the target, or the target is the zero address and account is not delegated, the wallet takes no action.
  • Otherwise, the wallet signs an authorization for the target and submits it in an EIP-7702 set-code transaction. This replaces a delegation to any other implementation, and removes the delegation when the target is the zero address. The chain_id of the authorization MUST be the chainId of the request, and MUST NOT be zero.

The wallet MUST reject a request whose account is not an account that the user has connected to the dApp. The wallet MUST also reject a request whose account has code that is not an EIP-7702 delegation designator, because such an account cannot be delegated.

The fee of the set-code transaction is not paid through paymasterFrame. The wallet MUST reject the request if it cannot arrange the payment of that fee.

An EIP-8141 frame transaction has no authorization list. When the calls are sent as a frame transaction, the set-code transaction is therefore a separate transaction that the wallet submits first. It is not part of what atomicRequired makes atomic, and logs of the set-code transaction MUST NOT be reported as logs of the calls.

Validity Time Limit Capability

Identifier: validityTimeRange

Asks the wallet to make the transaction invalid after a given time. The identifier names a range, but the capability limits only the end of the validity.

type ValidityTimeRangeCapabilityParams = {
  validUntil: `0x${string}`, // the transaction is valid only until this timestamp, in seconds
}

The wallet MUST add an expiry_verify frame to the transaction as defined by EIP-8141, with these fields:

Field Value
mode VERIFY
flags 0
target EXPIRY_VERIFIER
value 0
limits.state 0
data validUntil as an 8-byte unsigned big-endian integer

This frame MUST be the first frame of the transaction. The wallet MUST reject a validUntil that does not fit in 8 bytes.

The wallet MUST verify that validUntil is in the future, and MUST present it to the user in a human-readable form.

Keyed Nonce Capability

Identifier: keyedNonce

Asks the wallet to use EIP-8250 nonce keys for the transaction.

type KeyedNonceCapabilityParams = {
  nonceKeys: `0x${string}`[], // the nonce keys of the transaction, each a uint256
  nonceSeq?: `0x${string}`,   // the shared sequence number, a uint64; 0xffffffffffffffff is the single-use form
}

The wallet MUST set nonce_keys of the transaction to nonceKeys.

The wallet MUST reject nonceKeys that EIP-8250 does not allow: more than 16 keys, a key that does not fit in a uint256, or keys that are not strictly increasing. The wallet MUST also reject an empty nonceKeys: EIP-8250 uses the legacy account nonce for an empty list, and a dApp gets that by not sending this capability. A key 0 is an ordinary key stored in NONCE_MANAGER. It does not select the legacy account nonce.

All selected keys share one sequence number, and EIP-8250 requires it to equal the current sequence of every selected key, except in the single-use form.

If nonceSeq is omitted, the wallet reads the current sequence of every key and uses it as nonce_seq. The wallet MUST reject the request if the keys do not currently share one sequence number, or if that number is 0xffffffffffffffff, because such a key is exhausted.

If nonceSeq is given and is not 0xffffffffffffffff, the wallet uses it as nonce_seq. The wallet MUST reject the request if it differs from the current sequence of any key, because the transaction would be invalid.

If nonceSeq is 0xffffffffffffffff, the transaction uses the single-use form. The wallet uses it as nonce_seq and MUST reject the request if any key is not unused. The transaction exhausts every key, and none of them can be used again.

The wallet MUST include correct estimates for the gas cost of incrementing all the specified nonce keys in the fee estimate it presents to the user.

The public mempool limits the sum of the state gas limits of the frames in the validation prefix. A wallet SHOULD warn the user when the sum of these limits, which includes the state gas charged for the keys that have not been used before, exceeds that limit.

Paymaster Frame Capability

Identifier: paymasterFrame

Asks the wallet to have a given “Paymaster” contract accept the payment for the transaction. The application either provides the parameters of the paymaster directly (the static form), or points the wallet to a paymaster service that provides them (the service form).

This capability does not support a paymaster that is an account without code, which EIP-8141 handles with its default code. The wallet MUST reject a request whose paymaster has no code and no EIP-7702 delegation.

The wallet MUST also reject a request, or a result of the paymaster service, whose paymaster is the sender of the transaction. The sender would pay for a transaction that is presented as sponsored.

This capability does not cover paying the fees in a token. That requires a frame from the account of the sender that transfers the token to the sponsor, and the frame layout below has no place for it.

A signature of the paymaster is exchanged as an entry of the signatures list of the transaction, in this form. The fields are serialized as in the canonical JSON serialization of such an entry.

type PaymasterSignatureEntry = {
  scheme: `0x${string}`,                // 0x0 ARBITRARY, 0x1 SECP256K1, 0x2 P256
  signer?: `0x${string}` | null,        // the signer of the paymaster; required unless the scheme is ARBITRARY
  msg?: `0x${string}` | null,           // a 32-byte digest; omitted, null or 0x for the transaction signature hash
  signature?: `0x${string}` | null,     // the bytes; omitted, null or 0x for a signature placeholder
}
type PaymasterFrameCapabilityParams =
  | {
      // static form
      paymaster: `0x${string}`,          // the target of the pay frame
      data: `0x${string}`,               // the calldata of the pay frame
      executionGasLimit: `0x${string}`,  // execution gas limit of the pay frame
      signature?: PaymasterSignatureEntry, // a signature of the paymaster, with final bytes and an explicit msg
    }
  | {
      // service form
      url: string,                       // URL of a paymaster service
      context: Record<string, any>,      // data for the service, opaque to the wallet
    }

In both forms the wallet MUST add a pay frame to the transaction with these fields. The values of paymaster, data and executionGasLimit are given by the request or by the paymaster service.

Field Value
mode VERIFY
flags 0x1 (APPROVE_PAYMENT)
target paymaster
value 0
limits.execution executionGasLimit
limits.state set by the wallet
data data

The pay frame MUST follow the frame that approves execution for the sender.

The approval of the payment charges state gas to the pay frame. With keyedNonce, the charge is for each nonce key that has not been used before. Without it, the charge is for the creation of the sender account, if it does not exist yet. The wallet MUST set limits.state of the pay frame to cover this charge, otherwise the transaction is invalid. Neither the request nor the paymaster service sets this limit.

A dApp MUST NOT mark this capability as optional, and a wallet that supports it MUST reject a request that does. A wallet that supports this capability MUST NOT send the transaction without the pay frame.

In the static form, the data of the pay frame is final when the request is made. The static form is therefore usable only with paymasters whose approval does not depend on a signature over the transaction that the wallet assembles.

If the request includes a signature, it is a signature entry of the paymaster with final bytes. Its msg MUST be an explicit 32-byte digest, because the application cannot sign the hash of a transaction that the wallet has not assembled. The wallet MUST reject a signature with an empty msg. The wallet appends the entry to the signatures list of the transaction as its last entry, and MUST NOT add entries after it.

Paymaster Service Interface

In the service form, the wallet obtains the pay frame from a paymaster service by calling two JSON-RPC methods at url. Both take the same parameters:

// [tx, context]
type FramePaymasterParams = [
  object,                  // the unsigned frame transaction, in JSON
  Record<string, any>,     // the `context` of the capability
]

The tx parameter is the unsigned transaction serialized in JSON. It includes the chain ID. This proposal does not define the serialization of a frame transaction in JSON. A service and a wallet use the canonical one.

A signature placeholder is a signature entry without signature bytes. A gas estimation method that accepts signature placeholders treats it as a valid signature of its scheme. The estimation witness is the signature of an ARBITRARY entry as it is during gas estimation.

The paymaster signs the signature hash of EIP-8141, which does not cover the raw bytes of signatures with an empty msg. A signature of the paymaster is therefore never part of the data of the pay frame. It is an entry of the signatures list of the transaction, and when its msg is empty it can be produced after every other field of the transaction is fixed. The bytes of an entry with an explicit msg are covered by the hash, and the ordering rules for that case are given under frame_getPaymasterSignature.

frame_getPaymasterData

The wallet calls this method after it builds the transaction without the pay frame and before it estimates gas. In tx, the frame gas limits and the fees may be estimates, there is no pay frame, and the signature entries are placeholders without bytes.

type FrameGetPaymasterDataResult = {
  sponsor?: { name: string, icon?: string },  // who pays; not verified; icon is a data URI
  paymaster: `0x${string}`,                   // the target of the pay frame
  data: `0x${string}`,                        // the calldata of the pay frame
  executionGasLimit: `0x${string}`,           // execution gas limit of the pay frame
  signature?: PaymasterSignatureEntry,        // the signature entry the service will add
  isFinal?: boolean,                          // the wallet does not call frame_getPaymasterSignature
}

The icon of sponsor MUST be a data URI as defined in RFC 2397. A wallet MUST render an SVG image with the <img> tag, so that no untrusted script is executed.

The paymaster and data fields, and the scheme, signer and msg of signature, are final: the signature hash covers them, and the wallet MUST NOT expect them to change.

The wallet builds the pay frame from paymaster, data and executionGasLimit. If signature is present, the wallet appends that entry to the signatures list of the transaction as its last entry, and MUST NOT add entries after it. The service does not specify the position of the entry. The wallet then estimates gas and sets the final frame gas limits and fees.

For the SECP256K1 and P256 schemes, the protocol validates the signature, and the signature of the entry MUST NOT carry bytes unless isFinal is true. The wallet estimates gas with the entry as a signature placeholder, using an estimation method that accepts signature placeholders. The service signs once, in frame_getPaymasterSignature.

For the ARBITRARY scheme, the paymaster contract checks the witness in the EVM, and an empty witness fails that check. The signature of the entry MUST then carry witness bytes that the contract accepts in the simulation of the transaction that the wallet submits for estimation, which is tx with the pay frame and the signature entry added as returned. The wallet MUST estimate gas on that transaction, without changing its frames, gas limits, fees or the entry of the paymaster, and the final witness MUST have the length of the estimation witness. The entries of the sender are not covered by this rule. The wallet estimates them as it would without a paymaster, for example with a stub that follows the same code path as a real witness. The service signs a transaction that is never sent, so it MUST do so only if it is willing to sponsor that transaction as it is.

The final gas limits and fees that the wallet sets after the estimation change the signature hash, which is why the service signs again in frame_getPaymasterSignature.

If isFinal is true, the wallet does not call frame_getPaymasterSignature. A signature returned with isFinal is final. It cannot depend on the final transaction, so its msg MUST be an explicit 32-byte digest.

The service MAY refuse to sponsor the transaction by returning a JSON-RPC error. The wallet then rejects the request. It does not continue without the paymaster.

frame_getPaymasterSignature

The wallet calls this method when isFinal is not true, after the transaction is complete. In tx, the pay frame and the signature entry are present, as a placeholder or, for the ARBITRARY scheme, with the estimation witness, and all fields that the signature hash covers are final, except the bytes of the paymaster’s own entry when its msg is explicit, which this call returns.

type FrameGetPaymasterSignatureResult = {
  signature: `0x${string}`,  // the bytes of the signature entry
}

The service signs the signature hash of the transaction, or the explicit msg of the entry, and returns the bytes of the signature only. Everything else about the entry is fixed by the result of frame_getPaymasterData.

The wallet puts signature into the entry. The wallet MUST NOT change any other field of the transaction. The wallet MUST reject a result whose length is wrong: 65 bytes for SECP256K1, 128 bytes for P256, and the length of the estimation witness for ARBITRARY.

The wallet MUST obtain signatures in an order that keeps the signature hash unchanged after it is signed. EIP-8141 leaves the raw bytes of a signature entry out of the signature hash when the msg of the entry is empty, and includes them when the msg is an explicit 32-byte digest.

  • Signatures of entries with an empty msg can be made in any order. Adding one of them to the transaction does not change the hash that another one signs, so the signature of the sender and the signature of the paymaster do not depend on each other.
  • The bytes of an entry with an explicit msg are part of the hash. The wallet MUST have the final bytes of every such entry in the transaction before any signature with an empty msg is made, including the signature of the sender. Otherwise the signature covers a hash that no longer matches the transaction, and the transaction is invalid.

A transaction is eligible for the public mempool only under the mempool validation rules of EIP-8141. A wallet that decides to accept paymasters outside these rules MUST tell the user that the transaction may not propagate.

The public mempool limits the gas of the validation prefix to MAX_VERIFY_GAS. The limits.execution of the validation frames, including the pay frame, and the intrinsic cost of the signatures share this limit. The wallet SHOULD warn the user when the sum exceeds it. The mempool also admits only MAX_PENDING_TXS_USING_NON_CANONICAL_PAYMASTER pending transactions that use a paymaster other than the canonical one, and the canonical paymaster accepts a single SECP256K1 signer.

Atomic Frames Batch Capability

Identifier: atomicFramesBatch

This is a call-level capability. It places calls in atomic frames batches, so that their frames succeed or are reverted together.

type AtomicFramesBatchCapabilityParams = {
  id: number, // non-negative integer naming the atomic frames batch
}

Calls with the same id belong to the same atomic frames batch. The calls of an atomic frames batch MUST be adjacent in the request. The wallet executes the calls of an atomic frames batch atomically, in either way described in Call Dispatch. In the way with frames, the wallet builds one frame for each call of the atomic frames batch, and MUST set ATOMIC_BATCH_FLAG on every one of these frames except the last, as defined by EIP-8141. The frames MAY have different modes. An atomic frames batch of one call has no effect.

When atomicRequired is false, calls without this capability are independent, as described in Call Dispatch.

The wallet MUST reject a request whose atomic frames batches violate the rules of EIP-8141, for example because calls of the same atomic frames batch are not adjacent.

The atomicRequired field of the request MUST be false when any call carries this capability. Otherwise the wallet MUST reject the request. Setting atomicRequired to true is equivalent to placing all calls of the request in one atomic frames batch, so a request that has both is ambiguous.

A dApp that needs all of its calls to be atomic sets atomicRequired to true and does not use this capability.

Call Frame Capability

Identifier: callFrame

This is a call-level capability. It overrides the choice of the frame for one call, which the wallet otherwise makes as described in Call Dispatch.

type CallFrameCapabilityParams = {
  mode: "SENDER" | "DEFAULT",
}

For SENDER, the wallet MUST build one frame with these fields:

Field Value
mode SENDER
target to of the call
value value of the call
data data of the call

For DEFAULT, the wallet MUST build one frame with these fields, without wrapping the call:

Field Value
mode DEFAULT
target to of the call
value 0
data data of the call

The value of a call with mode set to DEFAULT MUST be absent or zero, because only SENDER frames carry value. The wallet MUST reject the request otherwise.

The to of a call with mode set to DEFAULT MUST be the sender account of the transaction. The wallet MUST reject the request otherwise. The caller of a DEFAULT frame is ENTRY_POINT, and EIP-5792 requires the wallet to send the calls from from, so a DEFAULT frame can only enter the sender account, which then makes any further calls itself. If the sender account has no code and no EIP-7702 delegation, the frame does nothing, so the wallet MUST reject the request in that case.

The frame of a call that carries this capability is the only frame of that call. It may be part of an atomic frames batch.

If the wallet cannot build the requested frame, for example because the policy of the account does not allow SENDER frames, it MUST reject the request, also when the capability is marked optional. A wallet may ignore an optional capability only if it does not support the capability at all.

Recent Roots Capability

Identifier: recentRoots

Asks the wallet to include verified recent roots as defined by EIP-8272.

type RecentRootsCapabilityParams = {
  tuples: {
    sourceId: `0x${string}`, // bytes32
    slot: `0x${string}`,     // uint64
    root: `0x${string}`,     // bytes32
  }[],
}

The wallet MUST add one recent_root_verify frame with these fields:

Field Value
mode VERIFY
flags 0
target RECENT_ROOT_ADDRESS
value 0
limits.state 0
data for each tuple, in the given order: sourceId, slot as an 8-byte big-endian integer, and root, concatenated

The wallet MUST reject a request with no tuples or with more than MAX_RECENT_ROOT_REFERENCES tuples.

For each tuple, the wallet MUST check that slot is lower than the current slot and that the difference is not larger than RECENT_ROOT_USABLE_WINDOW, with the current slot defined as in EIP-8272 for public mempool validation.

Post-Transaction Outcome Assertions Capability

Identifier: outcomeAssertions

Asks the wallet to add assertions that run after the calls, as ASSERT frames of EIP-7906.

type OutcomeAssertionsCapabilityParams = {
  entries: {
    assertionsFormat: string,
    assertions: unknown,
  }[],
}

This proposal does not define what assertions contains. Every entry names the format that interprets it. A format is identified by a unique versioned string, given as assertionsFormat.

A specification of an assertion format is outside the scope of this document. A format MUST define the mapping of its assertions to frames, so that the signed transaction does not depend on the wallet.

The wallet maps each entry to frames as the specification of its format defines, and appends the resulting frames to the end of the transaction. A frame produced by a format MUST be an ASSERT frame. The wallet MUST reject the request if an entry names a format or version it does not support.

The wallet MAY add ASSERT frames of its own after the frames requested by the application. The wallet MUST NOT remove or reorder the frames requested by the application.

Frame Layout

The wallet builds the frames of a request in this order. Frames in brackets are optional and are present only when requested.

[expiry_verify] [recent_root_verify] [deploy] <validation of the sender> [pay]
<frames of the calls, with ATOMIC_BATCH_FLAG set as required by atomicFramesBatch>
[ASSERT frames]

The validation of the sender is the VERIFY frames that approve the execution for the sender, running the code of the account or the default code of EIP-8141.

The wallet MUST ensure that:

  • there is at most one expiry_verify frame, and if present it is the first frame;
  • there is at most one recent_root_verify frame, and it comes immediately after the expiry_verify frame if there is one, and before every other frame;
  • no VERIFY frame follows the validation prefix;
  • the ASSERT frames form a contiguous suffix of the frame list;
  • the number of frames does not exceed MAX_FRAMES.

A transaction that follows this layout does not violate the structural rules of the public mempool. Its eligibility also depends on the mempool rules for the validation prefix and for the paymaster.

Errors

A wallet that rejects a request in accordance with this proposal returns an error with one of the following codes, defined by EIP-5792. The message field describes the cause.

Code Failure
-32602 A capability value is malformed or breaks the rules of its capability.
-32602 The request is valid but contradicts the state of the chain, for example a nonceSeq that differs from the current sequence of a key, or a recent root outside the usable window.
-32602 The request combines atomicRequired set to true with atomicFramesBatch.
-32602 An entry of outcomeAssertions names a format or version that the wallet does not support.
4100 The account of eip7702Auth is not an account that the user has connected to the dApp.
5700 The wallet does not support a capability that is not marked optional.
5700 The wallet supports a capability but cannot build the frames it requires, for example a callFrame mode that the policy of the account does not allow, or the paymaster service refuses to sponsor the transaction.
5740 The transaction exceeds MAX_FRAMES after the wallet adds its own frames.

Calls Status

A frame transaction has no transaction-level status, only the status of each frame: 1 for success, 0 for failure and 2 for a skipped frame. The wallet derives the result of wallet_getCallsStatus as follows. In this section, the frames of the calls are the frames that carry the calls of the request, not the frames of the validation prefix, the pay frame or the ASSERT frames.

The result contains one receipt, the receipt of the frame transaction. A set-code transaction of eip7702Auth is not included. The logs of the receipt are the logs of the frames of the calls, in frame order.

A frame of a call took effect if its status is 1 and it is not in an atomic frames batch that contains a frame with the status 0 or 2. EIP-8141 rolls back the whole atomic frames batch when one of its frames fails, and the frames that ran before the failure keep the status 1.

The status of the batch is:

  • 200 if every frame of the calls took effect and no ASSERT frame has the status 0;
  • 500 if the transaction was included and no frame of the calls took effect, or an ASSERT frame has the status 0;
  • 600 if some frames of the calls took effect and others did not.

An ASSERT frame with the status 0 reverts the execution body of the transaction, but the frames of the calls keep their own status. The wallet MUST therefore check the ASSERT frames before it reports 200.

The status of the receipt is 0x1 if the status of the batch is 200, and 0x0 otherwise.

If the wallet put several calls into one frame and cannot determine which of them failed, it MUST NOT report 200 unless it knows that every one of them succeeded.

In the result of wallet_getCallsStatus, atomic is true if the wallet executed all calls of the request atomically, in either way described in Call Dispatch, and false otherwise.

The codes for a batch that is not yet included, and for a failure outside the chain, are the ones defined by EIP-5792.

Rationale

eip7702Auth and EIP-8141

EIP-8141 has no authorization list. A dApp that asks for a delegation therefore can no longer count on it being part of the same transaction.

The capability keeps its meaning, “the account is delegated to this implementation when the calls run”, and leaves the wallet free to meet it with a separate transaction. The parameters follow the form that deployed implementations of the capability already use.

Format-agnostic assertions

Defining the exact content of an assertion request, such as which balance bounds or which events to check, is not needed for the definition of the outcomeAssertions capability. Fixing a format here would tie the lifecycle of the format to the lifecycle of the capability. With a named and versioned format, wallets can support several formats, new formats do not require a change to this proposal, and a wallet can reject a format it does not understand instead of misreading it.

The format has to define the mapping to frames so that the signed transaction does not depend on the wallet. EIP-7906 recommends that assertions take their reference values from the data of the ASSERT frame, which the transaction signature commits to.

Paymaster service

The service form follows the structure of ERC-7677: the capability gives the wallet a URL and a context, the wallet first asks for the data of the paymaster, which it needs to estimate gas, then asks for the signature once the transaction is final. frame_getPaymasterData corresponds to pm_getPaymasterStubData of ERC-7677, and frame_getPaymasterSignature corresponds to pm_getPaymasterData, which returns the final paymaster data. Here the method returns only the signature. The static form exists for paymasters that need nothing from the final transaction.

The methods have the prefix frame_ and not pm_, so that a service and a wallet cannot confuse them with the ERC-7677 methods, which exchange UserOperations. They take no EntryPoint address, because the entry point of frame transactions is fixed, and no separate chain ID, because the transaction carries it.

A paymaster that needs a signature over the transaction cannot be given its signature in a static request. The signature hash covers the frames, their gas limits and the fees, and the wallet decides most of them. For an entry with an empty msg, the hash does not cover the bytes of the signature, so a paymaster that signs after the wallet fixed everything else does not change what it signed. That is why the signature is an entry of the signatures list and not part of the data of the pay frame, and why the service is called twice. The bytes of an entry with an explicit msg are part of the hash, so they have to be final before anyone signs it, and the signature of the sender comes after them.

The first call is still needed. The wallet needs the pay frame to estimate gas, and the service knows its own validation cost better than the wallet does. The signature entry has to exist during estimation, because its metadata is covered by the hash and its length affects the cost of the transaction. For the schemes that the protocol validates, estimation methods can take a signature placeholder, an entry without bytes, so the service signs only the final transaction and nothing is signed that is not sent. The ARBITRARY scheme is the exception: the paymaster contract checks the witness in the EVM, so estimation needs a witness that it accepts, which is a signature of a transaction that is never sent. The call also lets the service refuse early and tell the wallet who pays.

This proposal does not define a JSON serialization of a frame transaction. Wallets and services need one canonical form, and defining it is not the job of a wallet capability.

Backwards Compatibility

Wallets and applications that implemented support for ERC-4337 UserOperations are expected to upgrade to EIP-8141 over time. This includes switching to smart contracts and tooling, including the wallet_sendCalls capabilities, designed for EIP-8141 from day one.

Security Considerations

eip7702Auth

This is the most sensitive capability in the document. There is no limit to the damage that can be done by signing the wrong authorization.

Wallets MUST take extreme care when working with EIP-7702. Wallets MUST maintain a strict shortlist of well-known and publicly audited smart account implementations that are acceptable as delegation, and every name and version a wallet accepts MUST resolve to an address on that shortlist, except that the zero address removes a delegation. A vulnerability or malicious code in the delegation target can drain the whole account.

Because the delegation is a separate transaction, it takes effect even if the calls are later rejected by the user or fail. The wallet MUST ask the user to confirm the delegation separately from the calls, and MUST show the user which account is being delegated, whether or not it is from.

A version that differs from the current delegation makes the wallet replace the code of an account that works today. A dApp can use this to move an account to a version the user did not choose. The same happens without a version when the account is delegated to something other than the requested implementation. The wallet MUST show the user the current and the new implementation and version whenever it changes a delegation, and MUST do the same when a zero address delegation removes one.

paymasterFrame

The pay frame is validated in a static context, so its data cannot move assets by itself.

A signature of the paymaster with an explicit msg is valid for every transaction that the paymaster accepts for that digest. The digest SHOULD bind the sender, the nonce keys and sequence number, and an expiry. Otherwise anyone who sees the signature can reuse it.

In the service form, the wallet sends the unsigned transaction, including the calls, to the URL given by the application. The application already knows the calls, but not the other frames, the nonce or the fees. The wallet builds the pay frame itself from the fields of the result, with the mode, flags and value fixed by this proposal, so a service cannot choose them. The sponsor information comes from the service and is not verified, and the wallet MUST NOT present it as verified. The payer of the transaction is the paymaster, and the wallet MUST NOT present anyone else as the payer on the basis of sponsor. A service that changes a field after frame_getPaymasterData invalidates the signature, so the wallet MUST NOT accept such a change.

For the ARBITRARY scheme only, the estimation witness is a real signature. It authorizes the transaction as simulated, with the estimated gas limits and fees. A service signs it only if it is willing to sponsor that transaction as it is. For the other schemes the service signs once, the final transaction.

keyedNonce

The nonce keys are public and permanent. A dApp that derives a key from private information makes it visible. Each new key costs state gas that the payer is charged, up to 16 keys per transaction. A dApp could choose keys to increase the cost for the user. The wallet MUST include this cost in the displayed fee.

The nonce keys are consumed when the payment is approved. They stay consumed if a later frame reverts or an ASSERT frame fails, so in the single-use form a key can never be used again after such a failure. The wallet SHOULD warn the user when a request combines the single-use form with outcomeAssertions, or with calls that may revert.

Nonce keys belong to the account, and every dApp that the user connects to shares them. A dApp SHOULD derive its keys from a value specific to itself, such as a hash that includes its origin, and not use small integers, which another dApp is likely to use too. The wallet SHOULD warn the user when a request uses the single-use form, because it permanently spends the keys.

Transactions with overlapping key sets from the same sender depend on each other’s sequence numbers. A dApp that reuses a key across unrelated transactions loses the independence that keys are meant to provide.

callFrame

A DEFAULT frame is executed with ENTRY_POINT as the caller and needs no approval from the sender. With mode set to DEFAULT, the dApp chooses a target and calldata that run with that caller. This is harmless unless a contract treats ENTRY_POINT as an authority on its own. An account that does so lets any transaction trigger its execution function. An account that executes calls on behalf of ENTRY_POINT SHOULD check that the sender of the transaction is the account itself. The wallet MUST show the user the target and the calldata of the frame, and SHOULD decode the inner call when it can.

In a SENDER frame, the target sees the sender account as msg.sender, and ORIGIN is the sender. In a DEFAULT frame, the sender account is called by ENTRY_POINT. The calls that the account then makes have the account as msg.sender, but ORIGIN is ENTRY_POINT in the whole call tree. A contract that reads tx.origin therefore sees ENTRY_POINT and not the user, and can behave differently depending on the frame the wallet chose. A dApp that depends on it SHOULD set mode explicitly.

atomicFramesBatch

A dApp that assumes that later calls only run after earlier calls succeeded can be wrong, when atomicRequired is false, for calls outside an atomic frames batch: this specification does not say whether the following calls still execute after a failed call. A dApp that depends on the order of success MUST use atomicRequired or an atomic frames batch.

recentRoots

The tuples are not trusted by the wallet. A tuple shows only that a source wrote a value in a slot. The wallet MUST NOT describe a root as verified information about the content it commits to.

A tuple that was valid when the wallet built the transaction may stop being valid before inclusion, because the usable window ends or because a reorg removes the entry. The transaction is then invalid, not reverted, and the user pays nothing, but it will not execute.

outcomeAssertions

The security considerations of EIP-7906 apply to every assertion format. Three of them matter most here.

  • An assertion that checks too little is worse than no assertion, because it gives a false sense of safety. The wallet SHOULD only advertise a format whose checks it can describe to the user.
  • An ASSERT failure reverts only the execution body. The validation prefix, including the payment of gas and the consumption of the nonce keys, stays. An assertion that fails costs the user the gas of the transaction, and in the single-use form of keyedNonce it also costs the key. A hostile dApp can use assertions to waste a user’s gas, but it cannot use them to move assets. A wallet MAY therefore accept assertions from a dApp it does not trust, but they only add to the protection of the user. The wallet MUST NOT replace its own assertions with those of the dApp.
  • Silently dropping an assertion is the dangerous failure. The wallet MUST reject a request whose assertions it cannot include. A wallet may ignore an optional capability only if it does not support the capability at all.

A dApp that requires its assertion in every transaction of an account, rather than in one request, cannot do that through this capability. EIP-7906 describes how an account enforces an ASSERT frame in its own validation.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Yoav Weiss (@yoavw), Alex Forshtat (@forshtat), Dror Tirosh (@drortirosh), Shahaf Nacson (@shahafn), "ERC-7902: Wallet Capabilities for Account Abstraction [DRAFT]," Ethereum Improvement Proposals, no. 7902, March 2025. Available: https://eips.ethereum.org/EIPS/eip-7902.