---
eip: 8286
title: Modular Accounts for Frame Transactions
description: EIP-8141 native account abstraction validation extension for ERC-7579 modular accounts
author: Chiranjeev Mishra (@chiranjeev13), Pedro Gomes (@pedrouid), Alex Forshtat (@forshtat)
discussions-to: https://ethereum-magicians.org/t/erc-8286-modular-accounts-for-frame-transactions/28695
status: Draft
type: Standards Track
category: ERC
created: 2026-06-04
requires: 7579, 8141
---

## Abstract

[ERC-7579](./eip-7579.md) standardizes a core module system for modular smart accounts: module types, the module lifecycle, installation, and account configuration. This proposal extends that system to [EIP-8141](./eip-8141.md) native account abstraction by defining the validation flow for frame transactions. It reuses ERC-7579's module structure unchanged: a validator returns an *approval mode*, and the account applies it through EIP-8141's `APPROVE` instruction during a `VERIFY` frame. Keeping the module system in ERC-7579 and layering account-abstraction-specific validation flows as extensions lets a single module ecosystem serve accounts across multiple account abstraction implementations, without forking modules or vendor lock-in.

## 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.

### Relationship to ERC-7579

This standard is an extension to [ERC-7579](./eip-7579.md) and does not redefine its module system. A compliant account and its modules MUST conform to ERC-7579 for:

- module types and their identifiers,
- the module lifecycle interface (`onInstall`, `onUninstall`, `isModuleType`),
- module installation and configuration (`installModule`, `uninstallModule`, `isModuleInstalled`), and
- account configuration (`accountId`, `supportsModule`, `supportsExecutionMode`).

ERC-7579's execution interface (`execute`, `executeFromExecutor`) is required only of an account that declares the contract-dispatched execution route, see [Execution](#execution).

This standard adds only what EIP-8141 native account abstraction requires: the approval-mode validation flow and the interfaces defined below. It is the EIP-8141 analogue of ERC-7579's own validation flow. Where ERC-7579 defines `validateUserOp` for its [ERC-4337](./eip-4337.md) target, this standard defines `validateFrame`.

### Approval Mode

This standard defines the **approval mode**: a `uint8` bearing the value of the [EIP-8141](./eip-8141.md) `APPROVE` instruction's `scope` operand. It is the result of validation as defined in this standard — a validator returns an approval mode to the account, and the account applies it by executing the `APPROVE` instruction with that value during a `VERIFY` frame.

The bit semantics are equivalent to those of the EIP-8141 scope operand. The two least significant bits form a bitmask:

- bit `0` (payment): the account approves paying the transaction's total gas cost.
- bit `1` (execution): the account approves subsequent frames acting on its behalf as `sender`.

This yields the following modes, which an account MUST interpret consistently with [EIP-8141](./eip-8141.md)'s `APPROVE` scope operand:

| Mode  | Name                            | Payment | Execution |
|-------|---------------------------------|---------|-----------|
| `0x0` | `APPROVE_NONE`                  | no      | no        |
| `0x1` | `APPROVE_PAYMENT`               | yes     | no        |
| `0x2` | `APPROVE_EXECUTION`             | no      | yes       |
| `0x3` | `APPROVE_EXECUTION_AND_PAYMENT` | yes     | yes       |

`APPROVE_SCOPE_MASK` is defined as `0x3`. A frame's *allowed scope* is `frame.flags & APPROVE_SCOPE_MASK`, equivalently `FRAMEPARAM(frameIndex, 0x06)`.

Accounts are NOT REQUIRED to grant every mode. The account MUST NOT approve a mode outside the allowed scope of the executing `VERIFY` frame, and `APPROVE_EXECUTION` is only valid when the frame's resolved target is `tx.sender`. If validation does not succeed, the account MUST NOT call `APPROVE`, which leaves the mode at `APPROVE_NONE` and invalidates the transaction.

An account MUST declare which approval modes it supports through `supportsApprovalMode` (see below), and MUST NOT call `APPROVE` with a mode it does not support.

### EIP-8141 Frame Validator

ERC-7579 defines `validateUserOp` validators and assigns module type id `1` to them.

An **EIP-8141 frame validator** is a module that determines the approval mode a transaction is entitled to.
This standard assigns frame validators a new module type id: `11` <!-- TBD -->.
A module MAY additionally be an ERC-7579 validator (type id `1`) and serve both targets.

When the account's code executes in a `VERIFY` frame, the account selects a frame validator and calls it. The validator returns an approval mode, which the account then applies via `APPROVE`.

In place of ERC-7579's `validateUserOp` validation function, an EIP-8141 validator MUST implement ERC-7579's `IERC7579Module` interface and the `IFrameValidator` interface below:

```solidity
interface IFrameValidator is IERC7579Module {
    /**
     * @dev Validates the active VERIFY frame and returns the approval mode the
     *      validator authorizes for the transaction.
     * @param data validator-specific calldata supplied by the account, e.g. a
     *             signature envelope or policy parameters.
     *
     * Transaction context — the canonical signing hash (`TXPARAM(0x08)`), the executing
     * frame index (`TXPARAM(0x0A)`), the frame's allowed scope
     * (`FRAMEPARAM(frameIndex, 0x06)`), and any frame contents needed for policy — MUST
     * be obtained through EIP-8141 introspection instructions, not through function
     * arguments. This prevents an account (or a caller) from overriding those values.
     *
     * MUST NOT modify state; the validator is invoked within the STATICCALL context of
     * a VERIFY frame.
     * MUST return APPROVE_NONE (0x0) if validation fails.
     * MAY revert to indicate failures not related to the core validation logic directly (e.g. decoding errors).
     * The returned approval mode SHOULD be a subset of the frame's allowed scope; the
     * account is the final authority and MUST mask the result with `allowedScope`
     * before approving.
     */
    function validateFrame(bytes calldata data) external view returns (uint8 approvalMode);
}
```

The account's behavior when running in a `VERIFY` frame targeting itself is:

1. Read the executing frame index `frameIndex = TXPARAM(0x0A)` and its allowed scope `allowedScope = FRAMEPARAM(frameIndex, 0x06)`.
2. Select an installed validator. This standard does not dictate the selection mechanism (see [Validator Selection](#validator-selection)). The account MUST verify the selected module is an installed frame validator (module type id `11` <!-- TBD -->).
3. Call `validateFrame` on the selected validator with the validator-specific calldata only, then mask the result: `approvalMode = approvalMode & allowedScope`.
4. If `approvalMode` has the execution bit (bit `1`) set, the account MUST check the modes presented by the transaction's `SENDER` frames (see [`SENDER` Frame Execution Modes](#sender-frame-execution-modes)) against the execution modes it supports (`supportsExecutionMode`, inherited from `IERC7579AccountConfig`). If `supportsExecutionMode` returns `false` for any presented mode, the account MUST clear the execution bit, leaving `approvalMode = approvalMode & APPROVE_PAYMENT`.
5. If `approvalMode` still has the execution bit set and the account is contract-dispatched (see [Execution](#execution)), the account MUST check that every `SENDER` frame's resolved target (`FRAMEPARAM(i, 0x00)`) is the account itself. If any `SENDER` frame targets another address, the account MUST clear the execution bit.
6. If the resulting `approvalMode` is `APPROVE_NONE`, the account MUST NOT call `APPROVE`; the frame reverts and the transaction is invalid.
7. Otherwise the account MUST call `APPROVE(approvalMode)`.

The raw `signature` bytes of [EIP-8141](./eip-8141.md) `tx.signatures` are intentionally not accessible from the EVM. A validator therefore obtains its authorization material either by reading the metadata of an already protocol-validated signature via the `SIGPARAM` instruction (for the natively supported `SECP256K1` and `P256` schemes), or by receiving a self-contained signature envelope in `data` and verifying it itself (for module-defined schemes such as passkeys, multisig, or BLS).

Because [EIP-8141](./eip-8141.md) permits only a frame's resolved target (or code it `DELEGATECALL`s into) to call `APPROVE`, a validator reached via `STATICCALL` cannot approve on its own. The account remains the sole caller of `APPROVE`.

A validator is called by the account it validates for, so it identifies that account as `msg.sender`. Since it no longer takes the frame context as arguments, a validator SHOULD confirm that `msg.sender` is the transaction's `sender` (`TXPARAM(0x02)`) and the executing frame's resolved target before treating the frame it reads through introspection as its own; otherwise any contract running in the same transaction could `STATICCALL` it and consume validation state that was never meant for that call.

### `SENDER` Frame Execution Modes

This standard defines four execution modes describing the shape of a transaction's `SENDER` frames. They extend ERC-7579's mode encoding with two new `callType` values; the `execType` values keep their ERC-7579 meanings, which apply to frames verbatim:

- `callType` `0x02` <!-- TBD -->: the transaction has exactly one `SENDER` frame.
- `callType` `0x03` <!-- TBD -->: the transaction has multiple `SENDER` frames.
- `execType` `0x00` (revert-all): the frame belongs to an atomic-batch group (all-or-nothing).
- `execType` `0x01` (try): the frame is not part of an atomic batch; a revert discards only that frame.

The mode constants, encoded per ERC-7579's mode layout (`callType` in byte 0, `execType` in byte 1, all remaining bytes zero):

| Constant                   | `callType` | `execType` | Meaning                                      |
|----------------------------|------------|------------|----------------------------------------------|
| `FRAME_MODE_SINGLE`        | `0x02`     | `0x01`     | one standalone `SENDER` frame                |
| `FRAME_MODE_SINGLE_ATOMIC` | `0x02`     | `0x00`     | one `SENDER` frame inside an atomic batch    |
| `FRAME_MODE_BATCH`         | `0x03`     | `0x01`     | multiple `SENDER` frames, reverting independently |
| `FRAME_MODE_BATCH_ATOMIC`  | `0x03`     | `0x00`     | multiple `SENDER` frames in an atomic batch  |

A transaction *presents* a mode for each of its `SENDER` frames: `callType` is `0x02` if the transaction has exactly one `SENDER` frame and `0x03` otherwise; `execType` is `0x00` if the frame belongs to an atomic batch and `0x01` otherwise. A transaction may present several modes at once — for example, an atomic group alongside an ungrouped frame presents both `FRAME_MODE_BATCH_ATOMIC` and `FRAME_MODE_BATCH`.

The account supports a transaction's `SENDER` frames only if `supportsExecutionMode` returns `true` for every presented mode.

These modes are declarations for `supportsExecutionMode` and the validation-time check in step 4 above; they are never passed to `execute`, and the new `callType` values define no `execute` dispatch behavior.

### Account Interface

A compliant account MUST implement the following interface. It extends ERC-7579's `IERC7579AccountConfig` and `IERC7579ModuleConfig`, so module installation and approval-mode capability are exposed through a single account interface alongside the EIP-8141 validation flow. It adds no execution interface; ERC-7579's applies unchanged where the account dispatches execution (see [Execution](#execution)):

```solidity
interface IERC8286FrameAccount is IERC7579AccountConfig, IERC7579ModuleConfig {
    /**
     * @dev Validates the active VERIFY frame and approves the transaction.
     *      Intended to be the call encoded in a VERIFY frame's `data`. The account
     *      selects an installed validator, obtains an approval mode from it, and masks
     *      that mode with the frame's allowed scope. It then checks the result against the
     *      approval modes it supports (`supportsApprovalMode`) and, when the execution
     *      bit is set, checks the transaction's SENDER frames against the execution modes
     *      it supports (`supportsExecutionMode`), before calling APPROVE with the result
     *      (see "EIP-8141 Frame Validator").
     * @param data validator selection and validator-specific calldata.
     * @return approvalMode the approval mode granted (also applied via APPROVE).
     *
     * MUST revert if the executing frame's mode is not VERIFY.
     * MUST NOT modify state other than via the APPROVE instruction; it executes within
     * the STATICCALL context of a VERIFY frame.
     * MUST NOT call APPROVE with a mode outside the frame's allowed scope.
     * MUST clear the execution bit if the transaction's SENDER frames present an
     * execution mode the account does not support (`supportsExecutionMode`, see
     * "SENDER Frame Execution Modes").
     * MUST clear the execution bit if the account is contract-dispatched and any SENDER
     * frame of the transaction resolves to a target other than the account (see
     * "Execution").
     * MUST NOT call APPROVE if validation fails (leaving the mode at APPROVE_NONE).
     */
    function verify(bytes calldata data) external returns (uint8 approvalMode);

    /**
     * @dev Returns whether the account supports a given approval mode.
     * @param approvalMode the approval mode (see "Approval Mode" above), a uint8
     *        bitmask in the range 0x0..0x3.
     *
     * MUST return true if the account is capable of granting this mode during
     * validation and false otherwise.
     */
    function supportsApprovalMode(uint8 approvalMode) external view returns (bool);

    /**
     * @dev Returns the execution route the account uses (see "Execution"):
     *      0x00 (EXECUTION_ROUTE_PROTOCOL) — SENDER frames call their targets directly and
     *           the account takes no part in execution;
     *      0x01 (EXECUTION_ROUTE_CONTRACT) — every SENDER frame targets the account, which
     *           dispatches the operations through ERC-7579's execute.
     *
     * An account returning EXECUTION_ROUTE_CONTRACT MUST implement ERC-7579's execution
     * interface, and its `verify` MUST NOT approve execution for a transaction with a
     * SENDER frame resolving to any other target.
     */
    function executionRoute() external view returns (uint8);
}
```

### Execution

An [EIP-8141](./eip-8141.md) frame transaction executes operations over two routes:

- **Protocol-dispatched**: a `SENDER` frame calls its target directly with `caller = tx.sender`. No account code takes part in the call, so the account has no execution-time enforcement point. Any policy over `SENDER` frames MUST be enforced at validation time, by the validator inspecting them before approving (see [Security Considerations](#security-considerations)).
- **Contract-dispatched**: every `SENDER` frame targets the account itself, invoking ERC-7579's `execute` or `executeFromExecutor` with `caller = tx.sender`. The account's code performs the calls itself, and ERC-7579 execution semantics — execution modes, `supportsExecutionMode`, hooks — apply unchanged.

The protocol enforces no such distinction: any `SENDER` frame may target the account or any other address, and a single transaction may mix both. An account MUST therefore choose one route and declare it through `executionRoute` (see [Account Interface](#account-interface)):

| Route  | Name                            | Meaning                                                       |
|--------|---------------------------------|---------------------------------------------------------------|
| `0x00` | `EXECUTION_ROUTE_PROTOCOL`      | `SENDER` frames call their targets directly                   |
| `0x01` | `EXECUTION_ROUTE_CONTRACT`      | every `SENDER` frame targets the account, which dispatches    |

A protocol-dispatched account MAY omit the `execute` and `executeFromExecutor` functions entirely. A contract-dispatched account MUST implement ERC-7579's execution interface, and its `VERIFY` frame MUST NOT approve execution unless every `SENDER` frame in the transaction resolves to the account (step 5 above). That check is what makes the route a guarantee rather than a convention: approving execution without it would let one `SENDER` frame call an arbitrary target with the account's own authority (`caller = tx.sender`), past the account's execution logic, while the rest of the transaction went through `execute`.

Because a contract-dispatched account is reached by a `SENDER` frame, the call arrives as a self-call and the account's ordinary `msg.sender == address(this)` access control authorizes it. An account that instead accepts `execute` calls from `DEFAULT` frames MUST NOT authorize them by `msg.sender == ENTRY_POINT` alone. In EIP-8141 the `ENTRY_POINT` address as caller carries no authorization — unlike an [ERC-4337](./eip-4337.md) EntryPoint, which calls the account only after validating its user operation. The account MUST authenticate such a call itself, by confirming that its own `VERIFY` frame in this transaction has succeeded (`FRAMEPARAM(i, 0x05)`), or by applying its ordinary ERC-7579 access control.

This standard therefore defines no new execution interface: ERC-7579 already covers contract-dispatched execution, and no interface can cover protocol-dispatched `SENDER` frames.

## Rationale

### Extension to ERC-7579 rather than a standalone standard

The value of a modular account standard is a portable module ecosystem: a validator, executor, or hook written once should work across accounts and wallets. Defining an independent module system for EIP-8141 would fork that ecosystem, forcing modules and tooling to choose between account abstraction implementations. Instead, this standard reuses ERC-7579's module system unchanged and adds only the EIP-8141 validation flow.

This follows the upgrade path ERC-7579 describes for itself: as modular accounts are built on account abstraction implementations other than its original [ERC-4337](./eip-4337.md) target, the implementation-specific validation flow is moved into a separate, optional extension. This proposal is that extension for EIP-8141. The intended end state is ERC-7579 as the implementation-agnostic core module system, with per-implementation validation extensions layered on top.

### Approval mode as the validation result

ERC-7579 standardizes a compact encoding for the execution side (its execution mode), letting an account express execution behavior in a single value. EIP-8141 provides the protocol-native counterpart for the validation side: the `APPROVE` scope. This standard surfaces validation results as that scope so the account never has to translate between a foreign validation-data encoding and the protocol's own approval semantics. `supportsApprovalMode` complements ERC-7579's `supportsExecutionMode`: the former declares which approval scopes the account can grant, the latter which execution shapes it will honor.

### One execution route per account

Because the protocol draws no line between the two routes, an account that leaves the choice open gets the weaker of them. A transaction could send some operations through `execute` and carry others past it as plain `SENDER` frames, and the account's execution logic — modes, hooks, whatever ERC-7579 policy it installs — would cover only part of what the transaction does. Requiring the account to commit to a route, and checking the commitment during validation, turns the distinction into something a validator can rely on.

For a contract-dispatched account the check is cheap and complete: every `SENDER` frame resolves to the account, so every operation passes through `execute`, and the execution-time enforcement point ERC-7579 assumes is restored under EIP-8141. A protocol-dispatched account gives that up deliberately, in exchange for the protocol calling its targets directly, and pays for it by enforcing its whole policy during validation.

### No new execution interface

ERC-7579's `execute` is the point where an account enforces execution policy: it decodes the mode, checks the call type against `supportsExecutionMode`, and reverts if unsupported. Under EIP-8141 that flow can be reused on the contract-dispatched route: a `SENDER` frame targets the account and calls its `execute`, and the account performs and checks the calls exactly as it would under [ERC-4337](./eip-4337.md) — with the call arriving as a self-call from `tx.sender`, which the account authorizes without having to read anything into the `ENTRY_POINT` caller.

On the protocol-dispatched route there is nothing to attach an interface to: the protocol calls a `SENDER` frame's target directly — no account code is involved in making the call — and the only gate is the transaction-scoped `sender_approved` flag set once by `APPROVE_EXECUTION`.

For protocol-dispatched `SENDER` frames the enforcement point therefore moves entirely to the validation frames: a validator inspects the transaction's `SENDER` frames during the `VERIFY` frame and only approves if they satisfy policy. `supportsExecutionMode` accordingly answers two kinds of query: for ordinary ERC-7579 modes it reports what the account's `execute` can process, and for the frame modes defined in "`SENDER` Frame Execution Modes" it declares which frame shapes the account's validators are willing to authorize — a policy enforced at validation time, not a capability checked at execution time.

#### Frame modes are policy declarations

The frame modes cover only the single/batch and atomic/non-atomic dimensions because the other ERC-7579 mode dimensions have no `SENDER`-frame counterpart: a `SENDER` frame is a plain protocol-orchestrated `CALL`, so there is no delegatecall analogue (account-level delegation exists only via [EIP-7702](./eip-7702.md) at the frame level), no staticcall analogue (the `STATICCALL` context in EIP-8141 is the `VERIFY` frame, not execution), and no room for custom `modeSelector`/`modePayload` semantics (the protocol owns dispatch).

The frame modes also describe a **policy** choice rather than a **capability**. In [ERC-4337](./eip-4337.md) an account must implement code to handle a batch, so `supportsExecutionMode` reports whether that code exists. Under EIP-8141 the protocol performs batching, atomicity, and dispatch with no account code, so every account is structurally capable of all of them; what an account or validator declares and enforces is which shapes it is *willing to authorize* (for example, refusing to approve a multi-frame transaction), not which it is able to perform.

### Validator Selection

One recommended approach for accounts to select the validator module, where [EIP-8250](./eip-8250.md) keyed nonces are available, is to derive the validator from the keyed nonce, for example by interpreting the validator's address as encoded in `TXPARAM(0x0B)` (`nonce_keys[0]`); because keyed nonces are committed by the canonical signing hash (`TXPARAM(0x08)`), this binds validator selection to the signed transaction.

## Security Considerations

### Execution is unconditional once approved

Granting `APPROVE_EXECUTION` (or `APPROVE_EXECUTION_AND_PAYMENT`) is a one-shot, transaction-wide authorization. After a `VERIFY` frame sets `sender_approved`, **every** subsequent `SENDER` frame in the transaction executes with `caller = tx.sender` and is not checked again. EIP-8141 consults no allow-list, target restriction, or execution-mode support on the account, and no account code mediates those calls. A validator that returns `APPROVE_EXECUTION` therefore authorizes arbitrary calls (arbitrary targets, values, and calldata) on the account's behalf.

Because EIP-8141 provides no execution-time fallback for `SENDER` frames, all execution policy MUST be enforced at validation time. This includes both the account's declared `supportsExecutionMode` set and any validator-level constraint (allow-list, spend limit, target restriction). Before granting `APPROVE_EXECUTION`, the account and its validator MUST inspect the transaction's `SENDER` frames (their resolved target, value, and calldata) and MUST NOT approve if any frame presents an unsupported execution mode or falls outside policy. Frame contents are available during validation through EIP-8141's introspection instructions (`FRAMEPARAM`, `FRAMEDATALOAD`, `FRAMEDATACOPY`) and MUST be read that way rather than accepted as account-supplied inputs (see "EIP-8141 Frame Validator"). Validators SHOULD grant the narrowest sufficient approval mode rather than `APPROVE_EXECUTION_AND_PAYMENT` by default.

The contract-dispatched route narrows this: when every `SENDER` frame resolves to the account, `execute` remains the gate on what actually happens. It is worth only as much as the check that establishes it — one unchecked frame pointed elsewhere still executes as `tx.sender`, so an account that declares contract dispatch without enforcing the check has granted the unconditional authorization above while believing it has not.

### `DEFAULT` frames and the `ENTRY_POINT` caller

An [EIP-8141](./eip-8141.md) `DEFAULT` frame executes its target with `caller = ENTRY_POINT` and requires no prior approval, so any frame transaction can invoke the account's code with that caller. EIP-8141 gates `APPROVE` on the executing frame's resolved target and flags, not on its mode; only a `VERIFY` frame, however, runs under `STATICCALL` restrictions and invalidates the whole transaction when it reverts. An account MUST NOT take `msg.sender == ENTRY_POINT` as evidence that it is executing in a `VERIFY` frame: it MUST check the frame's mode via `FRAMEPARAM` before approving, as required of `verify` (see "Account Interface"). Without that check, a `DEFAULT` frame targeting the account runs the validation flow without `STATICCALL` protection, and a failed validation reverts only that frame while the rest of the transaction proceeds.

The same caution applies to execution entrypoints: a `DEFAULT` frame can call the account's `execute` with `caller = ENTRY_POINT` in a transaction the account never validated, so `execute` MUST NOT be authorized by caller identity alone (see [Execution](#execution)).

### Validator trust

An installed validator is fully trusted with respect to the account: because approval is unconditional, a malicious or buggy validator that approves execution can drain or take over the account. Accounts MUST apply the same authorization control to installing a validator as to any other account-critical operation.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
