Alert Source Discuss
⚠️ Draft Standards Track: Core

EIP-8130: Account Abstraction by Account Configuration

Enable account abstraction feature set through onchain account configurations and a new transaction type

Authors Chris Hunter (@chunter-cb) <chris.hunter@coinbase.com>
Created 2025-10-14
Discussion Link https://ethereum-magicians.org/t/eip-8130-account-abstraction-by-account-configurations/25952
Requires EIP-2718, EIP-7702

Abstract

This proposal introduces a new EIP-2718 transaction type and an onchain Account Configuration system that together provide account abstraction — custom authentication, call batching, and gas sponsorship. Accounts register owners with onchain verifier contracts. Transactions declare which verifier to use, enabling nodes to filter transactions without executing wallet code. No EVM changes are required. The contract infrastructure is designed to be shared across chains as a common base layer for account management.

Motivation

Account abstraction proposals that delegate validation to wallet code force nodes to simulate arbitrary EVM before accepting a transaction. This requires full state access, tracing infrastructure, and reputation systems to bound the cost of invalid submissions.

This proposal separates verification from account logic. Each transaction explicitly declares its verifier — a contract that takes a hash and signature data and returns the authenticated owner. This makes validation predictable: wallets know the rules, and nodes can see exactly what computation a transaction requires before executing it. Nodes may optionally filter on verifier identity, accepting only known verifiers (ECDSA, P256, WebAuthn, multisig, post-quantum) and rejecting the rest without execution.

New signature algorithms deploy as verifier contracts and are adopted by nodes independently with no protocol upgrade required.

Specification

Constants

Name Value Comment
AA_TX_TYPE TBD EIP-2718 transaction type
AA_PAYER_TYPE TBD Magic byte for payer signature domain separation
AA_BASE_COST 15000 Base intrinsic gas cost
ACCOUNT_CONFIG_ADDRESS TBD Account Configuration system contract address
NONCE_MANAGER_ADDRESS TBD Nonce Manager precompile address
TX_CONTEXT_ADDRESS TBD Transaction Context precompile address
DEFAULT_ACCOUNT_ADDRESS TBD Default wallet implementation for auto-delegation
DEPLOYMENT_HEADER_SIZE 14 Size of the deployment header in bytes

Account Configuration

Each account can authorize a set of owners through the Account Configuration Contract at ACCOUNT_CONFIG_ADDRESS. This contract handles owner authorization, account creation, change sequencing, and delegates signature verification to onchain Verifier Contracts.

Owners are identified by their ownerId, a 32-byte identifier derived by the verifier from public key material. The protocol does not enforce a derivation algorithm — each verifier defines its own convention (see ownerId Conventions). Owners can be modified via calls within EVM execution by calling the authenticated config change functions.

Default behavior: The EOA owner is implicitly authorized by default but can be revoked on the contract.

Storage Layout

Each owner occupies a single owner_config slot containing the verifier address (20 bytes) and a scope byte (1 byte) with 11 bytes reserved. The scope byte controls which authentication contexts the owner is valid for (see Owner Scope). Owners are revoked by setting the verifier to address(0).

Field Bytes Description
verifier 0–19 Verifier contract address
scope 20 Permission bitmask (0x00 = unrestricted)
reserved 21–31 Reserved for future use (must be zero)

Implicit EOA authorization: An unregistered owner (owner_config slot is empty) is implicitly authorized with the K1 verifier if ownerId == bytes32(bytes20(account)). The empty slot’s scope byte is 0x00 (unrestricted), granting full permissions by default. This allows every existing EOA to send AA transactions immediately without prior registration. The implicit authorization is revoked if the owner is explicitly revoked (verifier set to address(0)).

Owner Scope

The scope byte in owner_config is a permission bitmask that restricts which authentication contexts an owner can be used in. A value of 0x00 means unrestricted — the owner is valid in all contexts. Any non-zero value restricts the owner to contexts where the corresponding bit is set.

Bit Value Name Context
0 0x01 SIGNATURE ERC-1271 via verifySignature()
1 0x02 SENDER sender_auth validation
2 0x04 PAYER payer_auth validation
3 0x08 CONFIG Config change authorizer_auth

The protocol checks scope after verifier execution: scope == 0x00 || (scope & context_bit) != 0.

The protocol validates signatures by reading owner_config directly and delegating authentication to Verifier Contracts — see Validation for the full flow. Owner enumeration is performed off-chain via OwnerAuthorized / OwnerRevoked event logs. No owner count is enforced on-chain — gas costs naturally bound owner creation.

2D Nonce Storage

Nonce state is managed by a precompile at NONCE_MANAGER_ADDRESS, isolating high-frequency nonce writes from the Account Configuration Contract’s owner storage (see Why a Nonce Precompile?). The protocol reads and increments nonce slots directly during AA transaction processing; the precompile exposes a read-only getNonce() interface to the EVM.

Verifier Contracts

Each owner’s verifier address in owner_config determines which contract performs verification. See Verifiers for the full interface, dispatch rules, and execution model.

Account Lock

Account lock state is stored in a single packed 32-byte slot:

Field Description
locked Owner configuration is frozen — config changes rejected
unlock_delay Seconds required between unlock request and unlock
unlock_requested_at Timestamp of pending unlock request (0 = none)

When locked is set, all config changes are rejected — both config change entries in account_changes and applyConfigChange() via EVM. The lock cannot be removed without a timelock delay.

Lock operations are managed via EVM methods on the Account Configuration Contract, authorized via the account’s isValidSignature (ERC-1271).

Lifecycle:

  1. Lock: Call lock(account, unlockDelay, signature). Sets locked = true with the specified unlockDelay (seconds).
  2. Request unlock: Call requestUnlock(account, signature). Records unlock_requested_at = block.timestamp.
  3. Unlock: Call unlock(account, signature). Requires block.timestamp >= unlock_requested_at + unlock_delay. Clears locked and resets unlock_requested_at.

Verifiers

Each owner is associated with a verifier, a contract that performs signature verification. The verifier address is stored in owner_config. Signatures use a compact 1-byte type prefix to identify native verifiers (0x010x04), which the protocol resolves to addresses via the Account Configuration Contract’s immutable constants. Custom verifiers use 0x00 followed by the contract address in the signature. All verifiers implement IVerifier.verify(hash, data). Verifiers that need transaction context (sender address, calls, payer) read it from the Transaction Context precompile at TX_CONTEXT_ADDRESS (see Transaction Context). The protocol validates the returned ownerId against owner_config and checks the owner’s scope against the authentication context.

Verifiers are executed via STATICCALL. Verifier addresses MUST NOT be EIP-7702 delegations — reject if the code at the verifier address starts with the delegation designator (0xef0100). Execution is metered; nodes SHOULD enforce a configurable per-verifier gas cap and a configurable verifier allowlist for mempool acceptance (see Mempool Acceptance).

Any contract implementing IVerifier can be permissionlessly deployed and registered as an owner’s verifier.

Native Verifiers

Type Name Algorithm ownerId Derivation
0x01 K1 secp256k1 (ECDSA) bytes32(bytes20(ecrecover(hash, v, r, s)))
0x02 P256_RAW secp256r1 / P-256 (raw ECDSA) keccak256(pub_key_x \|\| pub_key_y)
0x03 P256_WEBAUTHN secp256r1 / P-256 (WebAuthn) keccak256(pub_key_x \|\| pub_key_y)
0x04 DELEGATE Delegated validation bytes32(bytes20(delegate_address))
0x00 Custom Contract-defined Contract-defined

Native verifier addresses are stored as immutable constants in the Account Configuration Contract and exposed via getNativeVerifiers().

K1 and DELEGATE share the bytes32(bytes20(address)) convention, enabling on-chain authorization checks by Ethereum address.

DELEGATE: Delegates validation to another account’s owners. The verifier parses delegate_address (20) || nested_verifier_type (1) || nested_data from data, derives ownerId = bytes32(bytes20(delegate_address)), resolves the nested verifier type to an address, and calls nested_verifier.verify(hash, nested_data) to get nested_ownerId. It then checks owner_config(delegate_address, nested_ownerId) matches the nested verifier. Only 1 hop is permitted; nested DELEGATE results in rejection.

Account Types

This proposal supports three paths for accounts to use AA transactions:

Account Type How It Works Key Recovery
Existing Smart Contracts Already-deployed accounts (e.g., ERC-4337 wallets) register owners via the system contract (see Smart Wallet Migration Path) Wallet-defined
EOAs EOAs send AA transactions using their existing secp256k1 key (K1 verifier). If the account has no code, the protocol auto-delegates to DEFAULT_ACCOUNT_ADDRESS (see Block Execution). Accounts MAY override with an explicit authorization_list (EIP-7702) delegation Wallet-defined; EOA recoverable via 1559/7702 transaction flows
New Accounts (No EOA) Created via a create entry in account_changes with CREATE2 address derivation; runtime bytecode placed at address, owners + verifiers configured, calls handles initialization Wallet-defined

AA Transaction Type

A new EIP-2718 transaction with type AA_TX_TYPE:

AA_TX_TYPE || rlp([
  chain_id,
  from,               // Sender address (20 bytes) | empty for EOA signature
  nonce_key,          // 2D nonce channel (uint192)
  nonce_sequence,     // Sequence within channel (uint64)
  expiry,             // Unix timestamp (seconds)
  max_priority_fee_per_gas,
  max_fee_per_gas,
  gas_limit,
  authorization_list, 
  account_changes,    // Account creation and/or config change operations | empty
  calls,              // [[{to, data}, ...], ...] | empty
  payer,              // empty = sender-paid, payer_address = specific payer
  sender_auth,
  payer_auth          // empty = sender-pay, verifier_type || data = sponsored (same format as sender_auth)
])

call = rlp([to, data])   // to: address, data: bytes

Field Definitions

Field Description
chain_id Chain ID per EIP-155
from Sending account address. Required (non-empty) for configured owner signatures. Empty for EOA signatures—address recovered via ecrecover. The presence or absence of from is the sole distinguisher between EOA and configured owner signatures.
nonce_key 2D nonce channel key (uint192) for parallel transaction processing
nonce_sequence Must equal current sequence for (from, nonce_key). Incremented after inclusion regardless of execution outcome
expiry Unix timestamp (seconds since epoch). Transaction invalid when block.timestamp > expiry. A value of 0 means no expiry
max_priority_fee_per_gas Priority fee per gas unit (EIP-1559)
max_fee_per_gas Maximum fee per gas unit (EIP-1559)
gas_limit Maximum gas
authorization_list EIP-7702 authorization list for delegating to a wallet implementation
account_changes Empty: No account changes. Non-empty: Array of typed entries — create (type 0x00) for account deployment and config change (type 0x01) for owner management. See Account Changes
calls Empty: No calls. Non-empty: Array of call phases — see Call Execution
payer Gas payer identity. Empty: Sender pays. 20-byte address: This specific payer required. See Payer Modes
sender_auth See Signature Format
payer_auth Payer authorization. Empty: self-pay. Non-empty: verifier_type || data — same format as sender_auth. See Payer Modes

Intrinsic Gas

intrinsic_gas = AA_BASE_COST + tx_payload_cost + sender_auth_cost + payer_auth_cost + nonce_key_cost + bytecode_cost + account_changes_cost

sender_auth_cost: For EOA signatures (from empty): 6,000 gas (ecrecover + 1 SLOAD + overhead). For configured owner signatures (from set): 1 SLOAD (owner_config) + verifier type resolution + cold code access + actual gas consumed by verifier execution (metered from gas_limit). Nodes SHOULD enforce a configurable per-verifier gas cap as a mempool acceptance rule.

payer_auth_cost: 0 for self-pay (payer empty). Otherwise, the same sender_auth_cost model applies to the payer’s verifier.

Component Value
tx_payload_cost Standard per-byte cost over the entire RLP-serialized transaction: 16 gas per non-zero byte, 4 gas per zero byte, consistent with EIP-2028. Ensures all transaction fields (account_changes, authorization_list, sender_auth, calls, etc.) are charged for data availability
nonce_key_cost 22,100 gas for first use of a nonce_key (cold SLOAD + SSTORE set), 5,000 gas for existing keys (cold SLOAD + warm SSTORE reset)
bytecode_cost 0 if no create entry in account_changes. Otherwise: 32,000 (deployment base) + code deposit cost (200 gas per deployed byte). Byte costs for bytecode are covered by tx_payload_cost
account_changes_cost Per applied config change entry: authorizer auth verification cost (same model as sender_auth_cost) + num_operations × 20,000 per SSTORE. Per skipped entry (already applied): 2,100 (SLOAD to check sequence). 0 if no config change entries in account_changes

Signature Format

Signature format is determined by the from field:

EOA signature (from empty): Raw 65-byte ECDSA signature (r || s || v). The sender address is recovered via ecrecover.

Configured owner signature (from set):

verifier_type (1 byte) || data

For native verifiers (0x010x04), the type byte maps to the protocol-defined verifier address. For custom verifiers (0x00), the next 20 bytes are the contract address: 0x00 || address (20) || data. The data portion is verifier-specific — each verifier defines its own wire format (e.g., K1 expects r,s,v; P256 expects r,s,pub_key_x,pub_key_y,pre_hash; custom verifiers define their own).

Validation
  1. Resolve sender: If from empty, ecrecover derives the sender address (EOA path).
  2. Parse verifier: Read the first byte of sender_auth as the verifier type. For 0x00 (custom), read the next 20 bytes as the verifier address. For 0x010x04, resolve the type to the native verifier address.
  3. Set transaction context: Populate the Transaction Context precompile with sender, payer, and calls (see Transaction Context).
  4. Verify: Call verifier.verify(hash, data) via STATICCALL. Returns ownerId (or bytes32(0) for invalid).
  5. Authorize: SLOAD owner_config(from, ownerId). Require that the verifier address matches the resolved verifier. Implicit EOA rule: if the slot is empty and ownerId == bytes32(bytes20(from)) and verifier is K1, treat as authorized.
  6. Check scope: Read the scope byte from owner_config. Determine the context bit: 0x02 (SENDER) for sender_auth, 0x04 (PAYER) for payer_auth, 0x01 (SIGNATURE) for verifySignature(), 0x08 (CONFIG) for config change authorizer_auth. Require scope == 0x00 || (scope & context_bit) != 0.

Signature Payload

Sender and payer use different type bytes for domain separation, preventing signature reuse attacks:

Sender signature hash — all tx fields through payer, excluding sender_auth and payer_auth:

keccak256(AA_TX_TYPE || rlp([
  chain_id, from, nonce_key, nonce_sequence, expiry,
  max_priority_fee_per_gas, max_fee_per_gas, gas_limit,
  authorization_list, account_changes, calls,
  payer
]))

Payer signature hash — all tx fields through calls, excluding payer, sender_auth, and payer_auth:

keccak256(AA_PAYER_TYPE || rlp([
  chain_id, from, nonce_key, nonce_sequence, expiry,
  max_priority_fee_per_gas, max_fee_per_gas, gas_limit,
  authorization_list, account_changes, calls
]))

Payer Modes

Gas payment and sponsorship are controlled by two independent fields:

payer — the sender’s commitment regarding the gas payer, included in the sender’s signed hash:

Value Mode Description
empty Self-pay Sender pays their own gas
payer_address (20 bytes) Sponsored Sender binds tx to a specific sponsor

payer_auth — uses the same verifier_type || data format as sender_auth:

payer payer_auth Payer Address Validation
empty empty from Self-pay — no payer validation
address verifier \|\| data payer field Sponsored — any verifier. Reads payer’s owner_config, validates against payer address

Any authorized owner with SENDER scope can sign self-pay transactions.

Account Changes

The account_changes field is an array of typed entries for account creation and owner management:

Type Name Description
0x00 Create Deploy a new account with initial owners (must be first, at most one)
0x01 Config change Owner management: authorizeOwner, revokeOwner

Create entries are authorized by sender_auth — the initial ownerIds are salt-committed to the derived address. Config change entries carry their own authorizer_auth and use a sequence counter for deterministic cross-chain ordering. Nodes SHOULD enforce a configurable per-transaction limit on the number of config change entries (mempool rule).

Create Entry

New smart contract accounts can be created with pre-configured owners in a single transaction. The bytecode is the runtime code placed directly at the account address — it is not executed during deployment. The account’s initialization logic runs via calls in the execution phase that follows:

rlp([
  0x00,               // type: create
  user_salt,          // bytes32: User-chosen uniqueness factor
  bytecode,           // bytes: Runtime bytecode placed directly at the account address
  initial_owners        // Array of [verifier, ownerId, scope] tuples
])

Initial owners are registered with their specified scope. Wallet initialization code can lock the account via calls in the execution phase (e.g., calling lock() on the Account Configuration Contract).

Address Derivation

Addresses are derived using the CREATE2 address formula with the Account Configuration Contract (ACCOUNT_CONFIG_ADDRESS) as the deployer. The initial_owners are sorted by ownerId before hashing to ensure address derivation is order-independent (the same set of owners always produces the same address regardless of the order specified):

sorted_owners = sort(initial_owners, by: ownerId)

owners_commitment = keccak256(ownerId_0 || verifier_0 || scope_0 || ownerId_1 || verifier_1 || scope_1 || ... || ownerId_n || verifier_n || scope_n)

effective_salt = keccak256(user_salt || owners_commitment)
deployment_code = DEPLOYMENT_HEADER(len(bytecode)) || bytecode
address = keccak256(0xff || ACCOUNT_CONFIG_ADDRESS || effective_salt || keccak256(deployment_code))[12:]

The owners_commitment uses ownerId || verifier || scope (53 bytes) per owner — consistent with how the Account Configuration Contract identifies and configures owners.

DEPLOYMENT_HEADER(n) is a fixed 14-byte EVM loader that returns the trailing bytecode (see Appendix: Deployment Header for the full opcode sequence). On non-8130 chains, createAccount() constructs deployment_code and passes it as init_code to CREATE2. On 8130 chains, the protocol constructs the same deployment_code for address derivation but places bytecode directly (no execution). Both paths produce the same address — callers only provide bytecode; the header is never user-facing.

Users can receive funds at counterfactual addresses before account creation.

Validation (Create Entry)

When a create entry is present in account_changes:

  1. Parse [0x00, user_salt, bytecode, initial_owners] where each entry is [verifier, ownerId, scope]
  2. Reject if any duplicate ownerId values exist
  3. Sort by ownerId: sorted_owners = sort(initial_owners, by: ownerId)
  4. Compute owners_commitment = keccak256(ownerId_0 || verifier_0 || scope_0 || ... || ownerId_n || verifier_n || scope_n)
  5. Compute effective_salt = keccak256(user_salt || owners_commitment)
  6. Compute deployment_code = DEPLOYMENT_HEADER(len(bytecode)) || bytecode
  7. Compute expected = keccak256(0xff || ACCOUNT_CONFIG_ADDRESS || effective_salt || keccak256(deployment_code))[12:]
  8. Require from == expected
  9. Require code_size(from) == 0 (account not yet deployed)
  10. Validate sender_auth against one of initial_owners (ownerId resolved from auth must match an entry’s ownerId)

Config Change Entry

Config change entries manage the account’s owners. Each entry includes a chain_id field where 0 means valid on any chain, allowing replay across chains to synchronize owner state.

Config Change Format
rlp([
  0x01,               // type: config change
  chain_id,           // uint64: 0 = valid on any chain
  sequence,           // uint64: monotonic ordering
  operations,         // Array of operations
  authorizer_auth     // Signature from an owner valid at this sequence
])

operation = rlp([
  op_type,              // uint8: operation type (see below)
  verifier,             // address: verifier contract (authorizeOwner only)
  ownerId,              // bytes32: owner identifier
  scope                 // uint8: permission bitmask (authorizeOwner only, 0x00 = unrestricted)
])

Operation types:

op_type Name Description Fields Used
0x01 authorizeOwner Authorize a new owner with scope verifier, ownerId, scope
0x02 revokeOwner Revoke an existing owner (sets verifier to address(0)) ownerId

Config Change Authorization

Each config change entry represents a set of operations authorized at a specific sequence number. The authorizer_auth must be valid against the account’s owner configuration at the point after all previous entries in the list have been applied. The authorizing owner must have CONFIG scope (see Owner Scope).

The sequence number is scoped to a 2D channel defined by the chain_id: 0 uses the multichain sequence channel (valid on any chain), while a specific chain_id uses that chain’s local channel.

Config Change Signature Payload

Entry signatures use ABI-encoded type hashing. Operations within an entry are individually ABI-encoded and hashed into an array digest:

TYPEHASH = keccak256("ConfigChange(address account,uint64 chainId,uint64 sequence,ConfigOperation[] operations)ConfigOperation(uint8 opType,address verifier,bytes32 ownerId,uint8 scope)")

operationHashes = [keccak256(abi.encode(opType, verifier, ownerId, scope)) for each operation]
operationsHash = keccak256(abi.encodePacked(operationHashes))

digest = keccak256(abi.encode(TYPEHASH, account, chainId, sequence, operationsHash))

Domain separation from transaction signatures (AA_TX_TYPE, AA_PAYER_TYPE) is structural — transaction hashes use keccak256(type_byte || rlp([...])), which cannot produce the same prefix as abi.encode(TYPEHASH, ...).

The authorizer_auth follows the same Signature Format as sender_auth (verifier_type || data), validated against the account’s owner state at that point in the sequence.

Account Config Change Paths

Owners can be modified through two portable paths:

  account_changes (tx field) applyConfigChange() (EVM)
Authorization Signed operation (any verifier) Direct verification via verifier + owner_config, isValidSignature fallback (ERC-1271)
Availability Always (8130 chains) Always (any chain)
Portability Cross-chain (chain_id 0) or chain-specific Cross-chain (chain_id 0) or chain-specific
Sequence Increments channel’s change_sequence Increments channel’s change_sequence
When processed Before code deployment (8130 only) During EVM execution (any chain)

Both paths share the same signed operations and change_sequence counters. applyConfigChange() uses a two-tier authorization model: the Account Configuration Contract parses the verifier type byte from authorizerAuth, resolves to the verifier address, calls the verifier to get the ownerId, and checks owner_config (including the implicit EOA rule). If direct verification fails (owner not found in owner_config), the contract falls back to the account’s isValidSignature (ERC-1271) for authorization. This fallback enables migration for existing wallets that have not yet registered owners. Anyone (including relayers) can call these functions; authorization comes from the signed operation, not the caller. All owner modification paths are blocked when the account is locked (see Account Lock).

Execution (Account Changes)

account_changes entries are processed in order before call execution:

  1. Create entry (if present): Register initial_owners in Account Config storage for from — for each [verifier, ownerId, scope] tuple, write owner_config (verifier address and scope byte). Initialize lock state to safe defaults: locked = false, unlockDelay = 0, unlockRequestedAt = 0.
  2. Config change entries (if any): Apply operations in entry order. Reject transaction if account is locked (see Account Lock).
  3. Code placement (if create entry present): Place bytecode at from (code is placed directly, not executed).

Execution

Call Execution

The protocol dispatches calls directly from from to each call’s to address:

Parameter Value
from (caller) from (the sender)
to call.to
tx.origin from
msg.sender at target from
msg.value 0
data call.data

Calls carry no ETH value. ETH transfers are initiated by the account’s wallet bytecode via the CALL opcode (see Why No Value in Calls?).

Phases execute in order from a single gas pool (gas_limit). Within each phase, calls execute in order and are atomic — if any call in a phase reverts, all state changes for that phase are discarded and remaining phases are skipped. Completed phases persist — their state changes are committed and survive later phase reverts.

Common patterns:

  • Simple call: [[{to, data}]] — one phase, one call
  • Atomic batch: [[call_a, call_b, call_c]] — one phase, all-or-nothing
  • Sponsor + user: [[sponsor_payment], [user_action_a, user_action_b]] — sponsor in phase 0 (committed), user actions in phase 1 (atomic, skipped if sponsor fails)

Transaction Context

The Transaction Context precompile at TX_CONTEXT_ADDRESS provides read-only access to the current AA transaction’s metadata. The precompile reads directly from the client’s in-memory transaction state — protocol “writes” are effectively zero-cost. Gas is charged as a base cost plus 3 gas per 32 bytes of returned data, matching CALLDATACOPY pricing.

Function Returns Available
getSender() address — the account being validated (from) Validation + Execution
getPayer() address — gas payer (from for self-pay, payer for sponsored) Validation + Execution
getOwnerId() bytes32 — authenticated owner’s ownerId Execution only
getCalls() Call[][] — full calls array Validation + Execution

If the wallet needs the verifier address or scope, it calls getOwner(account, ownerId) on the Account Configuration Contract.

Non-8130 chains: No code at TX_CONTEXT_ADDRESS; STATICCALL returns zero/default values.

Portability

The system is split into storage and verification layers with different portability characteristics:

Component 8130 chains Non-8130 chains
Account Configuration Contract Protocol reads storage directly for validation; EVM interface available Standard contract (ERC-4337 compatible factory)
Verifier Contracts Protocol calls verifiers via STATICCALL Same onchain contracts callable by account config contract and wallets
Transaction Context Precompile at TX_CONTEXT_ADDRESS — protocol populates, verifiers read No code at address; STATICCALL returns zero/default values
Nonce Manager Precompile at NONCE_MANAGER_ADDRESS Not applicable; nonce management by existing systems (e.g., ERC-4337 EntryPoint)

All contracts are deployed at deterministic CREATE2 addresses across chains.

Validation Flow

Mempool Acceptance

  1. Verify sender_auth. Verify account_changes contains at most one create entry (type 0x00, must be first). Nodes SHOULD enforce a configurable limit on the number of config change entries (type 0x01).
  2. Resolve sender: if from set, use it; if empty, ecrecover from sender_auth
  3. Determine effective owner state: a. If create entry present in account_changes: verify address derivation, code_size(from) == 0, use initial_owners b. Else: read from Account Config storage
  4. If config change entries present in account_changes: reject if account is locked (see Account Lock). Simulate applying operations in sequence, skip already-applied entries.
  5. Validate sender_auth against resulting owner state (see Validation). Require SENDER scope on the resolved owner.
  6. Resolve payer from payer and payer_auth:
    • payer empty and payer_auth empty: self-pay. Payer is from. Reject if balance insufficient.
    • payer = 20-byte address (sponsored): payer_auth uses any verifier. Validate payer_auth against the payer address’s owner_config. Require PAYER scope on the resolved owner.
  7. Verify nonce, payer ETH balance, expiry
  8. Mempool threshold: gas payer’s pending count below node-configured limits.

Nodes SHOULD restrict mempool acceptance to a configured verifier allowlist. Nodes MAY adopt stateful verifier support independently, with state-read tracing for invalidation tracking.

Block Execution

  1. If account_changes contains config change entries, read lock state for from. Reject transaction if account is locked.
  2. ETH gas deduction from payer (sponsor for sponsored, from for self-pay). Transaction is invalid if payer has insufficient balance.
  3. Increment nonce in Nonce Manager storage.
  4. Process authorization_list (EIP-7702). Then, if code_size(from) == 0 and no create entry is present in account_changes, auto-delegate from to DEFAULT_ACCOUNT_ADDRESS (set code to 0xef0100 || DEFAULT_ACCOUNT_ADDRESS). This delegation persists.
  5. Process account_changes entries in order (see Execution (Account Changes)).
  6. Set transaction context on the Transaction Context precompile (sender, payer, ownerId, calls).
  7. Execute calls per Call Execution semantics.

Unused gas is refunded to the payer. For step 5, the protocol SHOULD inject log entries into the transaction receipt (e.g., OwnerAuthorized, AccountCreated) matching the events defined in the IAccountConfig interface, following the protocol-injected log pattern established by EIP-7708.

RPC Extensions

eth_getTransactionCount: Extended with optional nonceKey parameter (uint192) to query 2D nonce channels. Reads from the Nonce Manager precompile at NONCE_MANAGER_ADDRESS.

eth_getTransactionReceipt: AA transaction receipts include:

  • payer (address): Gas payer address (from for self-pay, specified payer for sponsored).
  • status (uint8): 0x01 = all phases succeeded (or calls was empty), 0x00 = one or more phases reverted. Existing tools checking status == 1 remain correct for the success path.
  • phaseStatuses (uint8[]): Per-phase status array. Each entry is 0x01 (success) or 0x00 (reverted). Phases after a revert are not executed and reported as 0x00. Empty if calls was empty.

Appendix: Storage Layout

The protocol reads storage directly from the Account Configuration Contract (ACCOUNT_CONFIG_ADDRESS) and Nonce Manager (NONCE_MANAGER_ADDRESS). The storage layout is defined by the deployed contract bytecode — slot derivation follows from the contract’s Solidity storage declarations. The final deployed contract source serves as the canonical reference for slot locations.

Appendix: Deployment Header

The DEPLOYMENT_HEADER(n) is a 14-byte EVM loader that copies trailing bytecode into memory and returns it. The header encodes bytecode length n into its PUSH2 instructions:

DEPLOYMENT_HEADER(n) = [
  0x61, (n >> 8) & 0xFF, n & 0xFF,    // PUSH2 n        (bytecode length)
  0x60, 0x0E,                          // PUSH1 14       (offset: bytecode starts after 14-byte header)
  0x60, 0x00,                          // PUSH1 0        (memory destination)
  0x39,                                // CODECOPY       (copy bytecode from code[14..] to memory[0..])
  0x61, (n >> 8) & 0xFF, n & 0xFF,    // PUSH2 n        (bytecode length)
  0x60, 0x00,                          // PUSH1 0        (memory offset)
  0xF3                                 // RETURN         (return bytecode from memory)
]

Rationale

Why Verifier Contracts?

Enables permissionless extension — deploy a new verifier contract, nodes update their allowlist, no protocol upgrade required. The verifier returns the ownerId rather than accepting it as input, so the protocol never needs algorithm-specific logic — routing, derivation, and validation are all handled by the verifier. All verifiers share a single verify(hash, data) interface with no type-based dispatch. Owner scope provides protocol-enforced role separation without verifier cooperation.

Why a Nonce Precompile?

Nonce state is isolated in a dedicated precompile (NONCE_MANAGER_ADDRESS). This separation is motivated by their fundamentally different access patterns and portability requirements:

Property Owner Config Nonces
Write frequency Rare (owner rotation) Every AA transaction
Read frequency Every validation Every validation
EVM writes Yes (applyConfigChange) No (protocol-only increments)
Portability Required (for non 8130 chains) Not required (8130-only)

The Nonce Manager has no EVM-writable state and no portability requirement — a precompile rather than a system contract.

Why a Transaction Context Precompile?

Transaction context (sender, payer, ownerId, calls) is immutable transaction metadata — it never changes during execution. A precompile is the natural fit:

  • Zero protocol write cost: The precompile reads directly from the client’s in-memory transaction struct — no HashMap insert, no journaling, no rollback tracking.
  • Pull model: Verifiers read only what they need. Pure verifiers pay nothing for context they don’t use.
  • Forward compatible: New context fields are added as new precompile functions — no interface changes to IVerifier or existing verifier contracts.

Why CREATE2 for Account Creation?

The create entry uses the CREATE2 address formula with ACCOUNT_CONFIG_ADDRESS as the deployer address for cross-chain portability:

  1. Deterministic addresses: Same user_salt + bytecode + initial_owners produces the same address on any chain
  2. Pre-deployment funding: Users can receive funds at counterfactual addresses before account creation
  3. Portability: Same deployment_code produces the same address on both 8130 and non-8130 chains (see Address Derivation)
  4. Front-running prevention: initial_owners in the salt prevents attackers from deploying with different owners (see Create Entry)

Smart Wallet Migration Path

Existing ERC-4337 smart accounts migrate to native AA without redeployment:

  1. Register owners: Call applyConfigChange() — since no owners exist in owner_config yet, the Account Configuration Contract falls back to the account’s isValidSignature (ERC-1271) for authorization. Existing ERC-4337 wallets already implement this, so initial owner registration works without code changes.
  2. Upgrade wallet logic: Update contract to delegate isValidSignature to the Account Configuration Contract’s verifySignature() function for owner and verifier infrastructure, and read getOwnerId() from the Transaction Context precompile during execution to identify which owner authorized the transaction
  3. Backwards compatible: Wallet can still accept ERC-4337 UserOps via EntryPoint alongside native AA transactions

Why Call Phases?

Phases provide two atomic batching levels without per-call mode flags:

  • Atomic batching: One phase, all-or-nothing.
  • Sponsor protection: Payment in phase 0 persists even if user actions in phase 1 revert.
  • Paymaster inspection: Verifiers can inspect calls via the Transaction Context precompile to validate payment terms.

Why Direct Dispatch?

The protocol dispatches each call directly to the specified to address with msg.sender = from. Owners with SENDER scope are authorized to send transactions at the protocol level. Every account has wallet bytecode (via auto-delegation or explicit deployment), so calls route through the wallet for ETH-carrying operations.

Why No Value in Calls?

Calls carry no ETH value — all ETH movement is initiated by wallet bytecode via the CALL opcode. Since every account has bytecode (auto-delegated if needed), no capability is lost.

This enables mempools to provide higher rate limits to locked accounts: signatures are stable and ETH balance only decreases through gas fees.

Why Account Lock?

The locked flag freezes the owner set, preventing config changes while locked. This serves three use cases:

  • Privacy: Shared accounts with permissive verifiers (e.g., privacy relayers with an “always true” verifier) need immutable owner configuration — without lock, any user could modify the owner set.
  • High throughput: Locked accounts have a stable authentication surface. Nodes can cache owner state aggressively, knowing it cannot change without an observable timelock period.

Why One Slot Per Owner?

The protocol reads everything it needs for authorization and scope checking in one SLOAD. Reserved bytes provide an extension path for future protocol-level owner policy.

Why Owner Scope?

Without scope, all owners have equal authority — any owner can sign as sender, approve gas payment, appear through ERC-1271, and authorize config changes. This is insufficient when accounts have owners serving different roles, like for example running a payer for ERC-20 tokens.

The 0x00 = unrestricted default ensures backward compatibility.

Why No Public Key Storage?

Public keys are not stored in the Account Configuration Contract. Instead, owners are identified by ownerId (bytes32) and public key material is provided at signing time in the verifier-specific data portion of the signature. This design is motivated by three factors:

  • State growth: Public key storage is permanent state growth. For PQ keys (1,000+ bytes), this means 40+ storage slots per owner. Calldata goes to data availability (temporary); storage is permanent. The trend is toward higher SLOAD costs and cheaper DA.
  • Gas efficiency: Calldata is cheaper than cold SLOADs for all key sizes. P256: ~2,048 gas calldata vs ~6,300 gas cold SLOADs. PQ: ~21,000 gas calldata vs ~88,000 gas cold SLOADs.
  • Simplicity: One storage slot per owner (owner_config). No variable-length public key encoding, no multi-slot reads, no length fields. Registration is a single SSTORE.

The protocol never needs to know how any algorithm works.

Why bytes32 ownerId?

The full 32-byte keccak256 output provides ~2^85 quantum collision resistance (vs ~2^53 for bytes20 via BHT), which is adequate for post-quantum keys. It also fits a single storage slot and aligns with keccak256 output without truncation.

ownerId Conventions

Each verifier defines how it derives ownerId from signature data (see Native Verifiers).

Backwards Compatibility

No breaking changes. Existing EOAs and smart contracts function unchanged. Adoption is opt-in:

  • EOAs continue sending standard transactions
  • ERC-4337 infrastructure continues operating
  • Accounts gain AA capabilities by configuring owners. EOAs sending their first AA transaction are auto-delegated to DEFAULT_ACCOUNT_ADDRESS if they have no code. Accounts MAY override with an explicit authorization_list (EIP-7702) delegation or use a create entry in account_changes for custom wallet implementations

Reference Implementation

IAccountConfig

interface IAccountConfig {
    struct Owner {
        address verifier;
        bytes32 ownerId;
        uint8 scope;        // 0x00 = unrestricted
    }

    struct ConfigOperation {
        uint8 opType;       // 0x01 = authorizeOwner, 0x02 = revokeOwner
        address verifier;   // authorizeOwner only
        bytes32 ownerId;
        uint8 scope;        // authorizeOwner only (0x00 = unrestricted)
    }

    event OwnerAuthorized(address indexed account, bytes32 indexed ownerId, address verifier, uint8 scope);
    event OwnerRevoked(address indexed account, bytes32 indexed ownerId);
    event AccountCreated(address indexed account, bytes32 userSalt, bytes32 codeHash);
    event ChangeApplied(address indexed account, uint64 sequence);
    event AccountLocked(address indexed account, uint32 unlockDelay);
    event UnlockRequested(address indexed account, uint32 effectiveAt);
    event AccountUnlocked(address indexed account);
    
    // Account creation (factory)
    function createAccount(bytes32 userSalt, bytes calldata bytecode, InitialOwner[] calldata initialOwners) external returns (address);
    function getAddress(bytes32 userSalt, bytes calldata bytecode, InitialOwner[] calldata initialOwners) external view returns (address);
    
    // Portable owner changes (direct verification via owner_config, isValidSignature fallback for migration)
    function applyConfigChange(address account, uint64 chainId, uint64 sequence, ConfigOperation[] calldata operations, bytes calldata authorizerAuth) external;
    function getChangeSequence(address account, uint64 chainId) external view returns (uint64);
    
    // Account lock (authorized via isValidSignature on the account)
    function lock(address account, uint32 unlockDelay, bytes calldata signature) external;
    function requestUnlock(address account, bytes calldata signature) external;
    function unlock(address account, bytes calldata signature) external;
    
    // Read functions
    function isAuthorized(address account, bytes32 ownerId) external view returns (bool);
    function getOwner(address account, bytes32 ownerId) external view returns (address verifier, uint8 scope);
    function getLockState(address account) external view returns (bool locked, uint32 unlockDelay, uint32 unlockRequestedAt);
    
    // Native verifiers (immutable, updated only via protocol upgrade)
    function getNativeVerifiers() external view returns (address k1, address p256Raw, address p256WebAuthn, address delegate);
    function getVerifierAddress(uint8 verifierType) external view returns (address);
    
    // Signature verification (checks SIGNATURE scope bit, includes implicit EOA rule)
    function verifySignature(address account, bytes32 hash, bytes calldata auth) external view returns (bool valid, bytes32 ownerId, address verifier);
}

IVerifier

interface IVerifier {
    function verify(
        bytes32 hash,
        bytes calldata data
    ) external view returns (bytes32 ownerId);
}

Stateful verifiers read from the Transaction Context precompile (see Transaction Context). When called via EVM (e.g., verifySignature()), the precompile returns zero/default values, so stateful verifiers that require context naturally reject non-protocol calls.

ITxContext (Precompile)

struct Call {
    address to;
    bytes data;
}

interface ITxContext {
    function getSender() external view returns (address);
    function getPayer() external view returns (address);
    function getOwnerId() external view returns (bytes32);
    function getCalls() external view returns (Call[][] memory);
}

Read-only. Gas is charged as a base cost plus 3 gas per 32 bytes of returned data.

INonceManager (Precompile)

interface INonceManager {
    function getNonce(address account, uint192 nonceKey) external view returns (uint64);
}

Read-only. The protocol manages nonce storage directly; there are no state-modifying functions.

Security Considerations

Validation Surface: For pure verifiers, the only state that can invalidate a validated transaction is owner_config revocation or nonce consumption — revalidation is a single SLOAD. Stateful verifiers additionally depend on traced state; invalidation tracking is a mempool concern.

Replay Protection: Transactions include chain_id, 2D nonce, and expiry.

Owner Scope: Protocol-enforced after verifier execution — a verifier cannot bypass scope checking.

Owner Management: Config change authorization requires CONFIG scope. The EOA owner is implicitly authorized with unrestricted scope; revocable via portable config change. All owner modification paths are blocked when the account is locked.

ownerId Binding: The protocol checks that the verifier’s returned ownerId maps back to that verifier in owner_config — preventing a malicious verifier from claiming ownership of another verifier’s owners.

Payer Security: AA_TX_TYPE vs AA_PAYER_TYPE domain separation prevents signature reuse between sender and payer roles. The payer field in the sender’s signed hash binds to a specific payer address. Scope enforcement adds a second layer — PAYER-only owners cannot be used as sender_auth, and vice versa.

Account Creation Security: initial_owners (verifier + ownerId + scope tuples) are salt-committed, preventing front-running of owner assignment. Wallet bytecode should be inert when uninitialized as it can be permissionlessly deployed.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Chris Hunter (@chunter-cb) <chris.hunter@coinbase.com>, "EIP-8130: Account Abstraction by Account Configuration [DRAFT]," Ethereum Improvement Proposals, no. 8130, October 2025. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-8130.