This proposal defines a new EIP-2718 transaction type and an onchain system contract that together provide account abstraction — batching, gas sponsorship, and authentication using any cryptographic system. Accounts configure authorized keys and verifiers in the system contract; the protocol validates transactions using native implementations for recognized algorithms, and via sandboxed pure-function contracts for any other scheme.
Provide account abstraction primitives that enable post-quantum authentication and migration, privacy systems, and programmable accounts — in a highly performant, highly optimizable way. Pure-function verifiers with known state inputs let nodes determine validity from state alone, enabling aggressive caching and parallel execution without execution tracing, staking or reputation systems.
Permissionless deployment of new authentication mechanisms as EVM contracts, proving utility before protocol enshrining, following the same path as precompiles today.
Permissionless pay-during-execution flows enable arbitrary gas payment mechanisms, including ERC-20 token payments, without protocol-level token awareness.
Shared account infrastructure for wallets to build on and clients to optimize.
| 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 |
NATIVE_VERIFIER_ADDRESSES | TBD | Native verifier addresses (K1, P256_RAW, P256_WEBAUTHN, DELEGATE) |
NONCE_MANAGER_ADDRESS | TBD | Nonce Manager precompile address |
MAX_ACCOUNT_CHANGES | 10 | Maximum account change + key change entries (types 0x01, 0x02) in account_changes per transaction |
MAX_SIGNATURE_SIZE | 2048 | Maximum signature size in bytes (DoS prevention) |
DEFAULT_ACCOUNT_ADDRESS | TBD | Default account predeploy for EOAs without code |
DEPLOYMENT_HEADER_SIZE | 14 | Size of the deployment header in bytes |
KEY_CHANGE_TYPEHASH | keccak256("KeyChange(address account,uint64 chainId,uint64 sequence,KeyOperation[] operations)KeyOperation(uint8 opType,address verifier,bytes32 keyId,uint8 flags)") | ABI type hash for key change entry signing |
ACCOUNT_CHANGE_TYPEHASH | keccak256("AccountChange(address account,uint64 chainId,uint64 sequence,AccountOperation[] operations)AccountOperation(uint8 opType,uint8 flags,uint32 unlockDelay)") | ABI type hash for account change entry signing |
Each account can authorize a set of keys through the Account Configuration Contract at ACCOUNT_CONFIG_ADDRESS. This contract handles key authorization, account creation, change sequencing, and delegates signature verification to onchain Verifier Contracts.
Keys are identified by their keyId, a 32-byte identifier chosen by the wallet. The protocol does not enforce a derivation algorithm — wallets derive keyId from public key material using conventions appropriate for their verifier (see Recommended keyId Conventions). Keys can be modified via calls within EVM execution by calling the authenticated key change functions.
Default behavior: The EOA key is implicitly authorized by default but can be revoked on the contract.
Each key occupies a single key_config slot (packed: verifier, flags). No public key data is stored — public keys are provided in calldata at signing time or recovered natively for ECDSA curves. Keys are revoked by setting verifier to address(0). A per-chain change sequence counter supports Entry Authorization. See Appendix: Storage Layout for the full slot derivation.
The protocol validates signatures by reading key_config directly and delegating authentication to Verifier Contracts — see Validation for the full flow. Key enumeration is performed off-chain via KeyAuthorized / KeyRevoked event logs. No key count is enforced on-chain — gas costs naturally bound key creation.
Nonce state is managed by a precompile at NONCE_MANAGER_ADDRESS, isolating high-frequency nonce writes from the Account Configuration Contract's key 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. See Appendix: Storage Layout for slot derivation.
Each key's verifier address in key_config determines which contract performs verification. All verifiers implement IAuthVerifier.verify() (see Reference Implementation). The Account Configuration Contract calls verifiers directly via staticcall for EVM-initiated signature checks.
On 8130 chains, the protocol does not call verifier contracts for AA transaction validation — it reads key_config directly and uses native implementations for native verifiers or sandbox execution for unknown ones. For native auth types (0x01–0x03), the protocol performs key recovery or verification internally without calling a verifier at all.
The DELEGATE_VERIFIER is an exception to the pure function model — it reads the delegated account's key_config from the Account Configuration Contract and chains to the nested verifier, enforcing a 1-hop limit.
Account-level policy is stored in a single packed 32-byte slot at keccak256(base_slot || "account_policy") (see Appendix: Storage Layout). Policy flags are modified via portable account change operations (setAccountPolicy, op_type 0x03 — see Account Change Entry).
An account can lock itself to freeze its key configuration. When locked:
account_changes, applyAccountChange() / applyKeyChange() via EVM) are rejectedLifecycle:
setAccountPolicy operation (op_type 0x03) with locked = true and unlockDelay (seconds).requestUnlock operation (op_type 0x04, chain-specific chain_id required) records unlock_requested_at = block.timestamp.unlock operation (op_type 0x05, chain-specific chain_id required) requires block.timestamp >= unlock_requested_at + unlock_delay. Clears the lock and resets unlock_requested_at.requestUnlock and unlock operations require a specific chain_id (not 0) because they depend on block.timestamp, which varies by chain.
Wallet bytecode SHOULD enforce ETH movement restrictions when locked — refusing outbound value transfers while allowing zero-value external calls. This enables locked accounts to interact with contracts (token transfers, DeFi, etc.) while ensuring ETH balance only decreases via gas payment. Accounts using an ERC-1167 minimal proxy to DEFAULT_ACCOUNT_ADDRESS inherit this behavior and are trivially recognizable by nodes (45 bytes, deterministic pattern). See High-Rate Mempool Acceptance for how nodes use this guarantee.
Each key is associated with a verifier — a contract at a known address that performs signature verification. The verifier address is stored in key_config and identifies the signature algorithm. On 8130 chains, the protocol recognizes native verifier addresses and uses built-in implementations; unknown verifiers are executed in a sandboxed environment (see Sandbox Verifiers).
Authentication in sender_auth and payer_auth is dispatched by a leading auth_type byte. Native verifiers that support key recovery earn dedicated auth types, eliminating keyId from the signature. P256 auth types include the public key for verification via RIP-7212 — the protocol derives keyId from the provided key. Sandbox and non-native verifiers use the explicit type:
| auth_type | Name | Algorithm | Resolves keyId | Auth Data |
|---|---|---|---|---|
0x00 | Explicit | Any verifier | Parsed from auth_data | keyId (32) || data |
0x01 | K1 recovery | secp256k1 (ECDSA) | ecrecover → (x,y) → keyId | r,s,v (65) |
0x02 | P256 raw verify | secp256r1 / P-256 | keccak256(x || y) → keyId | r (32) || s (32) || pub_key_x (32) || pub_key_y (32) || pre_hash (1) |
0x03 | P256 WebAuthn verify | secp256r1 / P-256 (WebAuthn) | keccak256(x || y) → keyId | authenticatorData || cDJ_len (2) || clientDataJSON || r (32) || s (32) || pub_key_x (32) || pub_key_y (32) |
When a sandbox verifier is promoted to native, it gains a dedicated auth type — users get standardized signatures with no key re-registration.
| Verifier | Address | Algorithm | Auth Types |
|---|---|---|---|
| K1 | K1_VERIFIER | secp256k1 (ECDSA) | 0x01 (recovery), 0x00 (explicit) |
| P256_RAW | P256_RAW_VERIFIER | secp256r1 / P-256 (raw ECDSA) | 0x02 (verify), 0x00 (explicit) |
| P256_WEBAUTHN | P256_WEBAUTHN_VERIFIER | secp256r1 / P-256 (WebAuthn) | 0x03 (verify), 0x00 (explicit) |
| DELEGATE | DELEGATE_VERIFIER | Delegated validation | 0x00 (explicit only) |
All verifiers implement the same IAuthVerifier.verify() interface (see Verifier Contracts).
DELEGATE: Delegates validation to another account's configuration. The keyId is bytes32(bytes20(address)) — the delegated account's address zero-padded to 32 bytes. Only 1 hop is permitted (see DELEGATE).
Any contract can serve as a verifier — enabling permissionless addition of new signature algorithms without protocol changes. The protocol executes unknown verifiers in a sandbox that enforces purity at runtime: state-accessing opcodes (SLOAD, BALANCE, TIMESTAMP, etc.) and non-allowlisted external calls are trapped, ensuring deterministic results from calldata inputs alone. Sandbox execution is metered; nodes SHOULD enforce a configurable per-verifier gas cap. See Appendix: Sandbox Execution Environment for the runtime rules.
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 keys via the system contract (see Smart Wallet Migration Path) | Wallet-defined |
| EOAs | If no code is set, the protocol delegates to DEFAULT_ACCOUNT_ADDRESS on the first AA transaction (EIP-7702 delegation write). EOAs can set a custom delegation via authorization_list instead | 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, keys + verifiers configured, call execution handles initialization | Wallet-defined |
A new EIP-2718 transaction with type AA_TX_TYPE:
| Field | Description |
|---|---|
chain_id | Chain ID per EIP-155 |
from | Sending account address. Required (non-empty) for configured key signatures. Empty for EOA signatures—address recovered via ecrecover. The presence or absence of from is the sole distinguisher between EOA and configured key 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. Overrides default account delegation when present |
account_changes | Empty: No account changes. Non-empty: Array of typed entries — create (type 0x00) for account deployment, account change (type 0x01) for policy management, and key change (type 0x02) for key management. See Account Changes |
committed_calldata | Empty: No committed call. Non-empty: Calldata (bytes) delivered to from. See Call Execution |
calldata | Empty: No call. Non-empty: Calldata (bytes) delivered to from. See Call Execution |
payer | Gas payer identity. Empty: Sender pays. 0x00: Open — any K1 payer can sponsor (payer resolved via ecrecover). 20-byte address: Committed — this specific payer required. See Payer Modes |
sender_auth | See Signature Format |
payer_auth | Payer authorization. Empty: self-pay. Non-empty: `auth_type |
sender_key_cost: Determined by the auth_type byte in sender_auth:
| auth_type | Gas | Rationale |
|---|---|---|
| EOA (no key_config) | 6,000 | ecrecover (3,000) + 1 SLOAD (key_config) + overhead |
0x01 K1 recovery | 6,000 | ecrecover (3,000) + 1 SLOAD (key_config) + overhead |
0x02 P256 raw verify | 7,000 | P256 verify via RIP-7212 (3,450) + 1 SLOAD (key_config) + overhead |
0x03 P256 WebAuthn verify | 12,000 + calldata_gas | P256 verify via RIP-7212 (3,450) + WebAuthn parsing + 1 SLOAD + overhead |
0x00 Explicit (native verifier) | Same as native auth type above | Same crypto operation as the corresponding native auth type (e.g., K1 = 6,000, P256 = 7,000) |
0x00 Explicit (sandbox) | 6,000 + metered execution gas | 1 SLOAD (key_config) + overhead + cold code access + sandbox execution (metered; nodes SHOULD enforce a per-verifier gas cap) |
0x00 Explicit (DELEGATE) | 3,000 + nested | 1 SLOAD + nested auth_type cost |
All types read key_config (1 SLOAD) for authorization, policy checks, and verifier address. Additional calldata cost is charged via tx_payload_cost. Sandbox verifiers incur additional upfront cost: cold code access to load the verifier bytecode before sandbox execution begins.
payer_key_cost: 0 for self-pay (payer empty). Otherwise, the same sender_key_cost table above applies to the payer's auth_type.
| 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, committed_calldata, calldata, 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 account change or key change entry: authorizer auth verification cost (based on authorizer's auth_type, using the sender_key_cost table above) + num_operations × 20,000 per SSTORE. Per skipped entry (already applied): 2,100 (SLOAD to check sequence). 0 if no account change or key change entries in account_changes |
Signature format is determined by the from field and auth_type byte:
EOA signature (from empty): Raw 65-byte ECDSA signature (r || s || v). The sender address is recovered via ecrecover.
Configured key signature (from set):
| auth_type | Name | Sender Auth Format | Total Size | Protocol Reads |
|---|---|---|---|---|
0x00 | Explicit | 0x00 || keyId (32) || data | 33+ bytes | 1 SLOAD + potential code read |
0x01 | K1 recovery | 0x01 || r,s,v (65) | 66 bytes | 1 SLOAD |
0x02 | P256 raw verify | 0x02 || r,s,pub_key_x,pub_key_y,pre_hash (129) | 130 bytes | 1 SLOAD |
0x03 | P256 WebAuthn verify | 0x03 || authenticatorData || cDJ_len (2) || clientDataJSON || r,s,pub_key_x,pub_key_y (128) | Variable | 1 SLOAD |
from empty, ecrecover derives the sender address (EOA path).auth_type — K1 recovers the address, P256 types hash the public key, explicit parses keyId directly.key_config(from, keyId). Reject if verifier == address(0). EOA default: keyId == bytes32(bytes20(from)) with empty slot is valid (K1_VERIFIER).verify(account, keyId, hash, data) on the verifier.For DELEGATE_VERIFIER, the keyId is bytes32(bytes20(delegate_address)) — the delegated account's address zero-padded. The protocol reads the delegated account's key configuration and parses the nested_auth from the remaining data. The nested auth uses the same auth_type || auth_data format and is validated against the delegated account's keys. Only 1 hop is permitted; nested DELEGATE_VERIFIER results in an immediate mempool drop.
Example (Account B delegates to Account A, which has a P256 key; explicit type with nested P256 verify):
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:
Payer signature hash — all tx fields through calldata, excluding payer, sender_auth, and payer_auth:
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 |
0x00 | Open | Any K1 signer can sponsor. Payer address recovered via ecrecover from payer_auth |
payer_address (20 bytes) | Committed | Sender binds tx to a specific sponsor |
payer_auth — uses the same auth_type || auth_data format as sender_auth. The payer address is always resolved from the payer field, never from payer_auth:
payer | payer_auth | Payer Address | Validation |
|---|---|---|---|
| empty | empty | from | Self-pay — no payer validation |
0x00 | 0x01 || r,s,v (65) | ecrecover result | Open — K1 recovery only. Recovered address is the payer |
| address | auth_type || auth_data | payer field | Committed — any auth type. Reads payer's key_config, validates against payer address |
Open mode is restricted to auth_type 0x01 (K1 recovery) because it is the only auth type that recovers an address from the signature alone, requiring no additional payer identification. Committed mode supports all auth types since the payer address is explicit.
Deferred gas deduction: If the sender has insufficient balance and committed_calldata is present, gas deduction is deferred until after committed_calldata executes — allowing the sender to acquire ETH during that frame (e.g., via a token swap). The sender must hold sufficient ETH after committed_calldata completes or the transaction is invalid. Builders bear the risk of unpaid computation; mempools are expected to reject deferred-gas transactions via a simple balance check unless they opt in. See Security Considerations for validation risks.
Self-pay is rejected if the signing key's disableGasPayment flag is set.
The account_changes field is an array of typed entries for account creation, account policy management, and key management:
| Type | Name | Description | Max per tx |
|---|---|---|---|
0x00 | Create | Deploy a new account with initial keys | 1 (must be first) |
0x01 | Account change | Account-level policy: lock, unlock, setAccountPolicy | MAX_ACCOUNT_CHANGES (shared) |
0x02 | Key change | Key management: authorizeKey, revokeKey | MAX_ACCOUNT_CHANGES (shared) |
Create entries are authorized by sender_auth — the initial keyIds are salt-committed to the derived address. Account change and key change entries carry their own authorizer_auth and share a common sequence counter for deterministic cross-chain ordering. The combined total of type 0x01 and 0x02 entries must not exceed MAX_ACCOUNT_CHANGES.
New smart contract accounts can be created with pre-configured keys 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 when calldata is delivered to the account via the execution phase that follows:
Initial keys and account policy are registered with hardcoded safe defaults (see Execution (Account Changes) for exact values). No user-specified key policy or account policy is accepted during initialization — this prevents front-running attacks from setting unsafe policy. Wallet initialization code can modify policy during the calldata execution phase by calling applyAccountChange() / applyKeyChange() with pre-signed operations.
Addresses are derived using the CREATE2 address formula with the Account Configuration Contract (ACCOUNT_CONFIG_ADDRESS) as the deployer. The initial_keys are sorted by keyId before hashing to ensure address derivation is order-independent (the same set of keys always produces the same address regardless of the order specified):
The keys_commitment uses keyId || verifier (52 bytes) per key — consistent with how the Account Configuration Contract identifies keys.
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.
When a create entry is present in account_changes:
[0x00, user_salt, bytecode, initial_keys] where each entry is [verifier, keyId]keyId values existsorted_keys = sort(initial_keys, by: keyId)keys_commitment = keccak256(keyId_0 || verifier_0 || ... || keyId_n || verifier_n)effective_salt = keccak256(user_salt || keys_commitment)deployment_code = DEPLOYMENT_HEADER(len(bytecode)) || bytecodeexpected = keccak256(0xff || ACCOUNT_CONFIG_ADDRESS || effective_salt || keccak256(deployment_code))[12:]from == expectedcode_size(from) == 0 (account not yet deployed)sender_auth against one of initial_keys (keyId resolved from auth must match an entry's keyId)Account change entries modify account-level policy — lock state and unlock timelocks. They share the same wrapper format and sequence counter as key change entries (see Entry Authorization).
Operation types:
| op_type | Name | Description | Fields Used |
|---|---|---|---|
0x03 | setAccountPolicy | Set account-level policy flags | flags (bit 0 = locked; bits 1-7: reserved), unlock_delay |
0x04 | requestUnlock | Request lock removal (chain-specific only) | None (records block.timestamp) |
0x05 | unlock | Complete lock removal (chain-specific only) | None (checks timelock) |
Operations 0x04 and 0x05 MUST use a specific chain_id (not 0) because they depend on block.timestamp.
Key change entries manage the account's signing keys — adding new keys and revoking existing ones. Each entry includes a chain_id field where 0 means valid on any chain, allowing replay across chains to synchronize key state.
Operation types:
| op_type | Name | Description | Fields Used |
|---|---|---|---|
0x01 | authorizeKey | Authorize a new key | verifier, keyId, flags |
0x02 | revokeKey | Revoke an existing key (sets verifier to address(0)) | keyId |
Account change and key change entries share a common authorization model. Each entry represents a set of operations authorized at a specific sequence number. The authorizer_auth must be valid against the account's key configuration at the point after all previous entries in the list have been applied. The authorizer key must have disableKeyAdmin == 0; keys with disableKeyAdmin == 1 cannot sign change operations.
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. Account change and key change entries share the same sequence counter — a key change at sequence 3 and an account change at sequence 4 replay in the same order on every chain.
Entry signatures use ABI-encoded type hashing. Each entry type has a distinct type hash constant (KEY_CHANGE_TYPEHASH, ACCOUNT_CHANGE_TYPEHASH) derived from its canonical type string. Operations within an entry are individually ABI-encoded and hashed into an array digest.
Key change digest:
Account change digest:
Distinct type hashes prevent cross-type confusion between key change and account change entries. 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 (auth_type || auth_data), validated against the account's key state at that point in the sequence.
Keys and account policy can be modified through two portable paths:
account_changes (tx field) | applyAccountChange() / applyKeyChange() (EVM) | |
|---|---|---|
| Authorization | Signed operation (any verifier) | Account's isValidSignature (ERC-1271) |
| Availability | Always (8130 chains) | Always (any chain, requires code_size(account) > 0) |
| 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. applyAccountChange() and applyKeyChange() authorize via the account's isValidSignature (ERC-1271) — anyone (including relayers) can call them; authorization comes from the account's validation logic, not the caller. This requires code_size(account) > 0. All modification paths are blocked when the account is locked (see Account Lock).
account_changes entries are processed in order before call execution:
initial_keys in Account Config storage for from — for each [verifier, keyId] tuple, write key_config (verifier, flags). Set flags = 0x00 (hardcoded safe defaults). Initialize account_policy to safe defaults: locked = false, unlockDelay = 0, unlockRequestedAt = 0.disableKeyAdmin == 1.bytecode at from (code is placed directly, not executed).Both committed_calldata and calldata are delivered to from as individual calls:
| Parameter | Value |
|---|---|
to | from |
tx.origin | from |
msg.sender | from |
msg.value | 0 |
data | committed_calldata or calldata |
Both phases share a single gas pool from gas_limit. committed_calldata executes first; calldata receives the remainder. For accounts without code, calls succeed with no effect. See Block Execution steps 7–8 for ordering and commit/revert semantics.
The wallet fully interprets both payloads — batching, multicall, or any other execution pattern is handled by the wallet's code, not the protocol.
During AA transaction execution, accounts can query the Account Configuration Contract for the current transaction's authorization context:
getCurrentPayer() returns the gas payer address (from for self-pay, recovered payer for sponsored)getCurrentSigner() returns the full key configuration of the key that authorized the transaction (see IAccountConfig)The protocol injects this context using EIP-1153 transient storage (TSTORE) on the Account Configuration Contract before call execution. Only two values are written:
| Slot | Value | Size |
|---|---|---|
keccak256("context.payer") | Payer address | 20 bytes |
keccak256("context.signer") | Signer keyId | 32 bytes |
getCurrentPayer() reads payer via TLOAD. getCurrentSigner() reads keyId via TLOAD, then looks up key_config from persistent storage. Transient storage is automatically cleared at transaction end.
Non-8130 chains: These functions return zero/default values since no protocol writes to transient storage.
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 uses native implementations for native verifiers; sandbox for unknown | Same onchain contracts callable by account config contract and wallets |
| Nonce Manager | Precompile at NONCE_MANAGER_ADDRESS | Not applicable; nonce management by existing systems (e.g., ERC-4337 EntryPoint) |
The Account Configuration Contract is identical Solidity bytecode on every chain (deployed via CREATE2). Verifier contracts are also deployed at deterministic addresses. See Why Verifier Contracts? for the design rationale.
sender_auth size ≤ MAX_SIGNATURE_SIZE. Verify account_changes contains at most one create entry (type 0x00, must be first) and at most MAX_ACCOUNT_CHANGES combined account change (type 0x01) and key change (type 0x02) entries.from set, use it; if empty, ecrecover from sender_authaccount_changes: verify address derivation, code_size(from) == 0, use initial_keys
b. Else: read from Account Config storageaccount_changes: reject if account is locked (see Account Lock). For each entry, verify the authorizer key has disableKeyAdmin == 0; reject if non-admin. Otherwise, simulate applying operations in sequence, skip already-applied entries.sender_auth against resulting key state (see Validation)payer and payer_auth:
payer empty and payer_auth empty: self-pay. Reject if disableGasPayment flag is set. Payer is from. If balance insufficient and committed_calldata non-empty: deferred — nodes MAY reject, or SHOULD simulate committed_calldata to verify resulting balance covers gas. If balance insufficient and committed_calldata empty: reject.payer = 0x00 (open): payer_auth must be 0x01 || r,s,v. Recover payer address via ecrecover. Read payer's key_config to verify the key is authorized.payer = 20-byte address (committed): payer_auth uses any auth type. Validate payer_auth against the payer address's key_config.When a gas payer (sender for self-pay, or sponsor) exceeds the standard per-account pending threshold, nodes MAY grant elevated limits if all of the following hold:
account_policy — locked == true and unlock_requested_at == 0 (no pending unlock)gas_limit × max_fee_per_gas across all pending transactions charged to this payer does not exceed the payer's ETH balanceWhen these conditions hold, ETH balance can only decrease via gas payments — nodes can track committed gas precisely and accept transactions up to the balance limit rather than applying a fixed pending count.
If a node observes unlock_requested_at become non-zero for a locked account, it SHOULD stop accepting new high-rate transactions for that account and allow existing pending transactions to drain during the unlock_delay period.
from for self-pay). If self-pay balance is insufficient: defer to step 7 if committed_calldata present, otherwise transaction is invalid.code_size(from) == 0 and no create entry in account_changes, set the code of from to 0xef0100 || DEFAULT_ACCOUNT_ADDRESS.account_changes entries in order (see Execution (Account Changes)):
a. Create entry (if present): Register initial_keys in Account Config storage with hardcoded safe defaults. Initialize account_policy to safe defaults.
b. Account change and key change entries (if any): Apply operations to Account Config storage in entry order. Reject transaction if account is locked (see Account Lock). Reject if any authorizer key has disableKeyAdmin == 1.
c. Code placement (if create entry present): Place bytecode at from (code is placed directly, not executed).committed_calldata as a self-call to from if non-empty (committed independently). If committed_calldata reverts, its state changes are discarded and step 8 is skipped. Deferred gas deduction: if gas was not deducted in step 1, deduct from from after this step; clients SHOULD emit a trace entry for this protocol-level balance change. Transaction is invalid and cannot be included if from has insufficient ETH at this point.calldata as a self-call to from if non-empty (skipped if step 7 reverted)Unused gas is refunded to the payer. For step 5, the protocol SHOULD inject log entries into the transaction receipt (e.g., KeyAuthorized, AccountCreated) matching the events defined in the IAccountConfig interface, following the protocol-injected log pattern established by EIP-7708.
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, recovered/specified payer for sponsored).status (uint8): 0x00 = reverted (committed_calldata reverted, calldata skipped), 0x01 = success (both frames succeeded or were empty), 0x02 = partial (committed_calldata succeeded, calldata reverted). Existing tools checking status == 1 remain correct for the success path.Account Configuration Contract (ACCOUNT_CONFIG_ADDRESS):
Nonce Manager Precompile (NONCE_MANAGER_ADDRESS):
Unknown verifiers are executed in a sandbox that enforces purity at runtime. The sandbox traps opcodes that would break determinism, guaranteeing that verification results depend solely on the calldata inputs:
Allowed external calls: STATICCALL to allowlisted precompile addresses only. This enables verifiers to use existing precompiles (modexp, SHA-256, ecrecover, etc.) as building blocks.
Interface: The protocol passes (account, keyId, hash, data) as ABI-encoded calldata, where data is the verifier-specific portion of the explicit auth (0x00) auth_data after the keyId. The verifier handles public key extraction from data, keyId verification, and cryptographic validation — returning true only if the evidence proves control of the given keyId.
Because sandbox results are fully determined by (codehash, calldata), nodes can cache verification outcomes permanently. Nodes MAY also perform static bytecode analysis as an optimization to reject contracts containing trapped opcodes before execution.
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:
Enables permissionless extension of signature algorithms — deploy a new verifier contract, no protocol changes required. Pure-function verifiers allow heavy node-level optimization (caching, parallel execution, deterministic gas). High-use verifiers can be promoted to native auth types with dedicated recovery optimizations, following the same path as precompiles today. Non-standard verifiers (e.g., DELEGATE) can implement custom logic beyond pure signature checking if/when needed.
Storage layout is consensus-critical: Because the protocol reads storage slots directly on 8130 chains, the keccak-derived slot layout becomes a consensus rule. The layout is intentionally simple — one slot per key (keccak-derived from bytes32 keyId) — to minimize the likelihood of future changes.
Nonce state is isolated in a dedicated precompile (NONCE_MANAGER_ADDRESS) rather than stored alongside key configurations in the Account Configuration Contract. This separation is motivated by their fundamentally different access patterns and portability requirements:
| Property | Key Config | Nonces |
|---|---|---|
| Write frequency | Rare (key rotation) | Every AA transaction |
| Read frequency | Every validation | Every validation |
| EVM writes | Yes (applyKeyChange, applyAccountChange) | No (protocol-only increments) |
| Portability | Required (for non 8130 chains) | Not required (8130-only) |
Why a precompile instead of a system contract? Unlike the Account Configuration Contract — which must be a full Solidity contract for cross-chain portability and EVM-writable key management — the Nonce Manager has no EVM-writable state and no portability requirement. Nonce increments are exclusively protocol-level operations.
The create entry uses the CREATE2 address formula with ACCOUNT_CONFIG_ADDRESS as the deployer address for cross-chain portability:
user_salt + bytecode + initial_keys produces the same address on any chaindeployment_code produces the same address on both 8130 and non-8130 chains (see Address Derivation)initial_keys in the salt prevents attackers from deploying with different keys (see Create Entry)Existing ERC-4337 smart accounts migrate to native AA without redeployment:
applyKeyChange(), the Account Configuration Contract calls isValidSignature on the account to authorize — existing ERC-4337 wallets already implement ERC-1271, so this works without code changes. The account's existing validation logic (e.g., validateUserOp / owner check) verifies the key change signature.isValidSignature to the Account Configuration Contract's key and verifier infrastructure, and call getCurrentSigner() during execution to identify which key authorized the transactionThe system uses verifier addresses for permissionless algorithm registration and auth type bytes for native protocol optimization. They serve complementary roles:
key_config): Enable permissionless deployment of new algorithms as contracts. Any verifier can be registered — no protocol change needed. This is how sandbox verifiers (PQ, BLS, etc.) work.0x01–0x03) are a protocol-level optimization earned when a verifier is promoted to native.0x00: The universal fallback. All verifiers work with explicit type — the verifier address in storage determines how data is parsed. IAuthVerifier.verify() provides the uniform interface.When a sandbox verifier is promoted to native, it can gain a dedicated auth type. Existing keys continue working — the verifier address and keyId are unchanged; only the signature format changes.
committed_calldata + calldata)?The committed_calldata frame persists regardless of what happens in calldata, enabling three patterns:
calldata reverts — robust permissioned sponsorship without protocol-level token awareness.committed_calldata (e.g., via a token swap) — gas deduction defers to after this frame.committed_calldata persists even if calldata reverts, ensuring account state is never left partially initialized.During call execution, the protocol delivers committed_calldata and calldata to the account as self-calls (msg.sender == from). An alternative design uses a dedicated entry point address as msg.sender to distinguish protocol-initiated calls from recursive self-calls. Self-call was chosen for three reasons:
Same as 7702: The 7702 path currently has EOAs calling into their wallet code as sender.
ERC-4337 compatibility: Existing smart wallets gate execution on msg.sender == entryPoint || msg.sender == address(this). The self-call path is already trusted, enabling day-1 adoption without code changes.
EOAs need code to interpret calldata. Without a default, every EOA's first AA transaction requires an authorization_list entry to delegate to a wallet which is unnecessary friction. The protocol instead auto-delegates to DEFAULT_ACCOUNT_ADDRESS via a standard EIP-7702 designator, giving every EOA a working smart wallet immediately. Custom implementations can override via authorization_list at any time. Signing an 8130 transaction with no code at your address implicitly approves designating to the default account.
Single read for account-level policies. The locked flag gives nodes guarantees to grant high throughput to certain accounts, which is needed for permissioned payer infrastructure or high throughput accounts.
All key data fits in a single 32-byte storage slot (see Appendix: Storage Layout), using only 21 of 32 bytes and leaving 11 bytes reserved. The protocol reads everything it needs for authorization, sponsorship policy, and key management authority in one SLOAD — the entire per-key cost is a single storage read. The disableKeyAdmin bit controls whether a key can sign portable key change operations; non-admin keys (disableKeyAdmin == 1) are restricted to transaction signing only. The disableGasPayment bit controls whether a key can authorize self-pay transactions. Reserved bits in the flags byte and reserved bytes in the slot provide an extension path for future protocol-level key policy.
The native verifier set is intentionally minimal: K1, P256_RAW, P256_WEBAUTHN, and DELEGATE. Other algorithms (BLS, post-quantum, etc.) will start as sandbox verifiers.
0x01 — 66-byte signatures with no public key in calldata or storage.0x02 / 0x03) with a standardized wire format — 130-byte raw signatures including the public key and pre_hash byte.Algorithms like BLS12-381 can be deployed as sandbox verifiers wrapping EIP-2537 precompiles. Because verifiers are deployed as deterministic CREATE2 contracts (not precompiles), promotion to native status is seamless for accounts: the protocol recognizes the existing contract address, switches to native execution, and assigns a dedicated auth type. No key re-registration or migration is required. Post-quantum schemes, ZK-proof-based auth, and exotic curves follow the same path.
Public keys are not stored in the Account Configuration Contract. Instead, keys are identified by keyId (bytes32) and public key material is provided at signing time — either recovered natively for ECDSA curves or included in calldata for other algorithms. This design is motivated by three factors:
key_config). No variable-length public key encoding, no multi-slot reads, no length fields. Registration is a single SSTORE.Wallets derive keyId client-side using conventions appropriate for their algorithm. The protocol does not enforce derivation — the verifier handles authentication including keyId-to-public-key binding.
Using the full 32-byte keccak256 output (rather than truncating to 20 bytes) provides stronger quantum collision resistance. A bytes20 keyId offers only ~2^53 quantum collision resistance (via the BHT algorithm), which is insufficient. The full bytes32 provides ~2^85, which is adequate for post-quantum keys.
ECDSA keys (K1, P256) are quantum-broken at the key level regardless, but PQ keys need the full 256-bit keyId space to remain secure against quantum collision attacks. The bytes32 keyId also naturally fits a single storage slot and aligns with keccak256 output without truncation.
The protocol does not enforce keyId derivation — wallets choose conventions appropriate for their verifier:
| Algorithm | Recommended keyId | Rationale |
|---|---|---|
| K1 (secp256k1) | bytes32(bytes20(keccak256(x || y)[12:])) | Address-padded, preserving isAuthorized(account, bytes32(bytes20(addr))) queries |
| P256 | keccak256(x || y) | Full 256-bit hash of uncompressed point |
| DELEGATE | bytes32(bytes20(address)) | Delegated account address, zero-padded |
| Sandbox / PQ | keccak256(publicKey) | Full 256-bit hash of raw public key material |
K1 and DELEGATE share the bytes32(bytes20(address)) convention, enabling on-chain authorization checks by Ethereum address.
No breaking changes. Existing EOAs and smart contracts function unchanged. Adoption is opt-in:
DEFAULT_ACCOUNT_ADDRESS on their first AA transaction if no code is set; explicit EIP-7702 delegation or a create entry in account_changes can be used for custom wallet implementationsRead-only. The protocol manages nonce storage directly; there are no state-modifying functions.
Deterministic Validation: Validation requires only state reads (key_config, nonce, balance) and pure-function execution — native implementations for recognized verifiers, sandboxed contracts with node-enforced gas caps for others (see Sandbox Verifiers). Results are fully determined by current state, making validation easily cacheable and parallelizable. This eliminates invalidation-based DoS without requiring a reputation system or tracing.
Deferred Gas Deduction: When self-pay defers gas deduction to after committed_calldata, the deterministic validation property is broken — validity depends on execution outcome. Mempools preserve the deterministic property by default via a simple balance check, rejecting deferred-gas transactions unless they opt in. Block builders who accept deferred transactions bear the risk of unpaid computation.
Replay Protection: Transactions include chain_id, 2D nonce, and expiry.
Key Management: All key modifications require cryptographic authorization — either a signed operation in account_changes or a signature validated via isValidSignature. No msg.sender-gated key management functions exist, eliminating an entire class of wallet code vulnerabilities (delegatecall exploits, reentrancy, hidden multicall payloads) from escalating to key compromise. Non-admin keys (disableKeyAdmin == 1) cannot sign account change or key change operations — preventing a compromised session key from escalating privileges. EOA key is implicitly authorized by default; revocable via portable key change (cross-chain). Accounts SHOULD have at least one configured key before revoking the EOA key. MAX_ACCOUNT_CHANGES bounds per-transaction processing cost but can be bypassed via relayer.
keyId Binding: For K1 recovery (0x01), the protocol recovers the public key and derives keyId natively — binding is implicit in the recovery math. For P256 types (0x02, 0x03), the public key is provided in auth data and keyId is derived via keccak256(pub_key_x || pub_key_y) — binding is enforced by the protocol's derivation. For explicit auth type (0x00), the verifier is responsible for verifying that the provided evidence corresponds to the keyId. Registering a keyId that doesn't correspond to any public key the user controls renders the key unusable — this is self-inflicted and has no security impact on the account.
Delegation: See DELEGATE for hop limits and nested signature rules. The DELEGATE verifier enforces a 1-hop limit at both protocol and contract level.
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 (committed address) or permits any K1 sponsor (open = 0x00). Open mode restricts to K1 recovery so the payer address is always verifiable. The payer's auth mechanism is independent of the sender's commitment.
Signature Size Limits: Signatures exceeding MAX_SIGNATURE_SIZE MUST be rejected to prevent DoS via oversized signatures.
Account Lock: The lock mechanism (see Account Lock) provides a protocol-enforced guarantee that keys cannot change. ETH movement restrictions are enforced by wallet bytecode and key restrictions. The unlockDelay timelock gives nodes a window to observe unlock_requested_at becoming non-zero and stop accepting high-rate transactions before the lock is removed. Nodes SHOULD treat any non-zero unlock_requested_at as equivalent to unlocked for mempool acceptance purposes.
Account Creation Security: initial_keys (verifier + keyId tuples) are salt-committed, preventing front-running of key assignment. Initial keys and account policy use hardcoded safe defaults to prevent front-running and bad-state attacks. Permissionless deployment via createAccount() is safe — even if front-run, the account is created with the owner's keyIds and safe defaults. Wallet bytecode should be inert when uninitialized.
Copyright and related rights waived via CC0.