EIP.tools

EIP.tools

⚠️ DraftStandards Track: Core

EIP-8253: Bump nonce of zero-nonce storage accounts

Set nonce to 1 for accounts with empty code, zero nonce, and non-empty storage via an irregular state transition

Authors
Created2026-05-05
Discussion Linkhttps://ethereum-magicians.org/t/eip-8253-upgrade-pre-spurious-dragon-accounts/28505
Requires

Markdown

https://raw.githubusercontent.com/ethereum/EIPs/ma...

EIP-GPT summary

Contents
AbstractMotivationSpecificationAccount listMainnet account listState transitionApplication to non-Mainnet chainsInteraction with EIP-7928RationaleBackwards CompatibilityTest CasesReference ImplementationSecurity ConsiderationsCopyright

Abstract

At the start of the designated fork block, set the nonce to 1 for a per-chain, configured list of accounts that have empty code, zero nonce, and non-empty storage. Bumping the nonce restores the invariant that zero nonce and empty code imply empty storage: the accounts then fail the EIP-684 precondition for contract creation (nonce == 0), so no runtime storage check at contract creation is needed.

Motivation

EIP-684 requires zero nonce and zero code length at a CREATE destination. Pre-EIP-161, CREATE did not increment the nonce before init code ran, so a constructor that wrote storage and returned no code left an account with zero nonce and non-empty storage. This is the only shape where zero nonce and empty code do not imply empty storage; a creation landing on it would inherit that storage. The alternative fix is to additionally require empty storage at the destination of every contract creation. This however needs an extra check in runtime code, and for some clients/database layouts also another disk read, which is paid forever, guarding against a non-growing address set after Spurious Dragon.

This EIP restores the invariant with a one-time, per-account nonce bump. After the fork, the targeted accounts have nonce == 1, so EIP-684 rejects any future CREATE/CREATE2 to them and no runtime storage check is required. The targeted addresses are unreachable on Mainnet (see Security Considerations); the purpose is to retire the shape. The motivation parallels EIP-7523, which similarly cleaned up empty accounts to retire EIP-161's "touch" edge cases from re-execution and test logic. Removing this account shape from future state likewise retires the analysis and test-case burden it imposes on subsequent EIPs.

Specification

The key words "MUST", "MUST NOT", in this document are to be interpreted as described in RFC 2119.

Account list

The list of targeted accounts is a chain configuration parameter, in the same way as system contract addresses. For a given chain, the list MUST contain exactly the accounts that satisfy all of the following in the pre-state of the fork block:

  • nonce == 0,
  • codeHash == keccak256("") (EmptyCodeHash),
  • storageRoot != keccak256(RLP("")) (EmptyRootHash).

Such accounts can only be produced by a contract creation before EIP-161, or by a genesis allocation. A chain on which EIP-161 has been active since genesis, and whose genesis does not allocate accounts of this shape, has an empty list; on such a chain this EIP is a no-op. The list of one chain MUST NOT be reused on another chain without re-deriving it against that chain's state (see Security Considerations).

Mainnet account list

The targeted Mainnet accounts are published as targeted-accounts.json (28 entries). Each entry contains the 20-byte address and its keccak256 hash. The published file additionally records each account's non-zero storage slots (slot key, its hash, and its value), as supporting information for the predicate storageRoot != EmptyRootHash: the storage root of each account can be reconstructed from the file alone and compared to the reported root. The slot information is not used by the state transition.

The construction procedure and verification (against eth_getProof on latest) are documented in methodology.md. The 224-entry pre-EIP-161-deletion superset at the Spurious Dragon block is published alongside, as zero-nonce-matches.jsonl.

State transition

At the start of the fork block, before any pre-execution system contract calls (e.g. those introduced by EIP-2935, EIP-4788, EIP-7002, EIP-7251) and before any transactions, for each account A in the list:

  1. The nonce of A MUST be set to 1.
  2. balance, codeHash, and storageRoot of A are not modified.

The transition is unconditional: it does not inspect A before writing the nonce. The list requirement above guarantees that the nonce of A is 0 before the transition, so the write is an increment from 0 to 1. If the list is empty, the transition does nothing.

The transition produces no transaction, receipt, or log, and consumes no gas.

Application to non-Mainnet chains

Chains that carry Mainnet state from after Spurious Dragon (including Mainnet shadow forks) apply the Mainnet list. Chains on which EIP-161 has been active since genesis, which includes all current public test networks, have an empty list. Other chains have to generate the list for their own state using the procedure in methodology.md, listing both the address preimages and their hashes. Test chains and test fixtures that exercise the transition allocate accounts of the targeted shape in genesis and name them in the chain configuration, in the same way system contracts are exercised. To exercise the Mainnet list itself, mainnet-prestate-alloc.json provides the 28 Mainnet accounts (balance, zero nonce, storage) as a genesis alloc fragment; a devnet that allocates these in genesis and configures the Mainnet list activates the EIP against the exact Mainnet shape of the targeted accounts.

Interaction with EIP-7928

If EIP-7928 is active at the fork block, the BAL MUST encode the changes at block_access_index = 0, the index shared with the pre-execution system contract calls. For each targeted account A, the transition contributes to the AccountChanges entry of A:

  • nonce_changes: a single NonceChange [0, 1] (block_access_index = 0, new_nonce = 1);
  • nothing to storage_changes, storage_reads, balance_changes, or code_changes.

If nothing else in the block accesses A, its entry is [A, [], [], [], [[0, 1]], []]. Transactions and system contract calls in the same block add their own changes to the same entry. An empty list contributes nothing to the BAL.

Re-applying the BAL to the pre-fork-block state root MUST reproduce the post-fork-block state root.

Rationale

Irregular state transition vs. runtime check. A runtime storage check at every contract creation leaves the offending state in place and pays a per-creation cost forever to guard a closed address set. This EIP pays a one-time consensus mutation instead, which is the same pattern Ethereum has used for prior cleanups (e.g. EIP-7523).

Nonce bump vs. storage clear. Clearing each targeted account's storage was considered. It is more invasive: it deletes at least one, but up to multiple, slots per account. The storageRoot has to be re-calculated (and should evaluate to the empty storage root) which is a more complex algorithm to change the value of the account RLP field than simply changing the value (the nonce from 0 to 1). For Mainnet, this would need 129 storage entries on 28 accounts, where the nonce bump only needs 28 nonce bumps for the 28 accounts. Bumping the nonce is the smallest possible change that still defuses the EIP-684 collision risk: a single per-account scalar update. The accounts continue to look like contracts (post-bump they have nonce == 1, which is the standard "deployed contract" shape), only without code: a state that is already representable on Ethereum: if one creates a contract and writes to storage, but deposits code of length 1, this account will thus have non-empty storage, nonce 1, and no code.

Smaller test surface. Pre-EIP, the protocol had to reason about an account shape (zero nonce, no code, non-empty storage) that no other path can produce post-Spurious-Dragon. Removing this shape from future state retires that edge case from re-execution logic and from EIP test surfaces.

BAL encoding. Bumping a nonce is exactly one NonceChange per account; no other field of A changes. The BAL therefore lets a block builder emit these changes at index 0 for correct state root calculation without re-executing the irregular transition.

List as chain configuration, unconditional transition. A conditional transition ("bump only if A still satisfies the predicate") was considered and rejected. It would allow the Mainnet list to be applied on chains it does not describe, where the listed addresses are absent or, as discussed in Security Considerations, can be pre-created by the original deployers with a different shape. Every such chain would then carry read-only BAL entries for addresses it never needed to touch, and the BAL contents would depend on a runtime check. Placing the predicate on the list instead keeps the transition and the BAL unconditional, and moves the check to where it can be verified offline against the pre-fork state. Chains without such accounts carry an empty list, which is the same pattern as system contract addresses that are absent from a chain's configuration.

Backwards Compatibility

This is a consensus change and requires a hard fork. Targeted accounts have empty code, so they are not callable as contracts and have no associated key. After the fork they have nonce == 1, no code, the same balance, and the same storage. A future CREATE/CREATE2 to such an address now reverts under EIP-684 (because nonce != 0) instead of succeeding and inheriting the existing storage.

Test Cases

No execution path after Spurious Dragon can produce an account of the targeted shape, so tests allocate such accounts in the pre-state and supply the list through the chain configuration. For the CREATE case, the test picks a targeted address that is derivable from a sender it controls.

  1. Nonce bump. A has empty code, zero nonce, balance b, and non-zero storage S. After the fork block, A has nonce 1, balance b, empty code, and storage S unchanged.
  2. CREATE reverts. A CREATE/CREATE2 targeting A reverts under EIP-684 (nonce != 0). Without this EIP, the same operation succeeds under EIP-684 and the new contract starts with the pre-existing storage. Tested as the first transaction of the fork block.
  3. CALL unchanged. A CALL to A still does not charge new-account gas, since A remains in the trie. The call returns success with no observable effect (empty code).
  4. Non-targeted accounts unaffected. Accounts with code, non-zero nonce, or not in the list are untouched.
  5. BAL correctness (if EIP-7928 is active). For each targeted account, the BAL has a single NonceChange [0, 1] and all other change lists empty. Re-applying the BAL reproduces the post-state root.
  6. BAL merging (if EIP-7928 is active). A transaction in the fork block calls A with value. The entry of A carries the NonceChange [0, 1] and the transaction's BalanceChange; A appears once in the BAL.
  7. Empty list. With an empty list, the fork block changes no state and contributes nothing to the BAL.

Reference Implementation

def apply_irregular_state_transition(state, account_list):
    for entry in account_list:
        # Guaranteed by the list requirement; a failure here is a
        # misconfigured list, not a condition the transition handles.
        assert state.get_nonce(entry.address) == 0
        state.set_nonce(entry.address, 1)
        # balance, codeHash, and storageRoot are intentionally not modified.

The list construction procedure (predicate, re-execution of pre-Spurious-Dragon blocks, the snapshot scan, eth_getProof verification) is described in methodology.md. The published list MUST be byte-equal to the output of that procedure run against Mainnet at the fork block height.

Security Considerations

  • List correctness. Both an omission and a spurious entry are consensus-relevant. An omission leaves the collision risk in place for that account. Because the transition is unconditional, a spurious entry naming an absent account creates a nonce-1 account out of nothing (which is not empty under EIP-161 and persists), and an entry naming an account with a non-zero nonce overwrites that nonce. A list that violates the requirement in the Account list section is a chain misconfiguration, in the same category as a wrong system contract address; this EIP does not define behaviour for it. The list is reproducible from canonical state via the procedure in methodology.md; multi-client and external verification before scheduling the fork is required. The eth_getProof liveness filter from the methodology is cheap and SHOULD be re-run against the pre-fork state before the fork is scheduled.
  • The Mainnet set is closed. The nonce of a targeted account can only change through a transaction signed by that account, an EIP-7702 authorization signed by that account, or a contract creation landing on it. The targeted addresses are contract addresses with no known key, which rules out the first two. A CREATE can only land on one of these addresses from the original deployer at the original nonce, and those nonces have long passed; CREATE2 cannot reach an address derived from CREATE. For the two accounts created by factory contracts, the original deployer is a contract at nonce 0: the factories no longer exist, they were themselves CREATE children of an EOA whose nonce has passed, and any contract created after EIP-161 starts at nonce 1, so nonce 0 is unreachable even in principle. The accounts have no code, so they cannot self-destruct, and their balances can only grow. The Mainnet list is therefore stable between publication and the fork block.
  • The list MUST NOT be reused on another chain. 26 of the 28 Mainnet accounts were created by contract-creation transactions from 12 externally owned accounts. On a chain where such a deployer's nonce is below the nonce it used on Mainnet, the key holder can re-create the same address with arbitrary init code, giving it a nonce of 1 or higher, possibly code, and possibly storage. At the time of writing this is the case for all 12 deployers on Sepolia. Applying the Mainnet list on such a chain would roll a nonce back or create an account, as described under list correctness. The remaining 2 accounts were created by CREATE from factory contracts at factory nonce 0, which no post-EIP-161 contract can have, so they can only pre-exist through a genesis allocation. Every chain derives its own list from its own pre-fork state; for chains with EIP-161 active since genesis that list is empty.
  • No supply change. Balances and storage are preserved; only the nonce is bumped.
  • Genesis allocations. A genesis allocation can contain accounts of the targeted shape. Such accounts belong in that chain's list. They are not found by re-executing blocks, but the snapshot scan in the methodology does find them.

Copyright and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth