Skip to content

EIP-7702: Authority accounts warmed after validation instead of before #1447

@bshastry

Description

@bshastry

Summary

In process_authorization_list() (test/state/state.cpp), authority accounts are warmed (added to the EIP-2929 access list) after code/nonce validation instead of before. Per EIP-7702 step 4, the authority address must be added to accessed_addresses unconditionally for any authorization that passes initial signature/chain-id checks, regardless of whether subsequent code/nonce validation succeeds.

The Problem

The buggy code uses get_or_insert() which conflates two distinct operations:

  1. EIP-2929 access list warming — a metadata operation that should NOT mutate the state trie
  2. Account creation/touching — a state mutation that creates the account in the trie
// BUGGY CODE
auto& authority = state.get_or_insert(*auth.signer, {.erase_if_empty = true});
authority.access_status = EVMC_ACCESS_WARM;

// Validation happens AFTER the account is already created in state
if (authority.code_hash != Account::EMPTY_CODE_HASH &&
    !is_code_delegated(state.get_code(*auth.signer)))
    continue;  // validation fails, but account was already touched/warmed
if (auth.nonce != authority.nonce)
    continue;  // validation fails, but account was already touched/warmed

When a failed authorization's signer is later accessed (e.g., via CALL), the account is already warm in evmone but cold in geth, causing a 2,500 gas difference per affected account (warm access = 100 gas vs cold access = 2,600 gas).

Additionally, because get_or_insert() creates the account in the state, empty accounts touched by a failed authorization may be deleted per EIP-161, causing further state root mismatches.

Geth Reference

In geth (core/state_transition.go), warming and validation are separate concerns:

// geth: applyAuthorization()
st.state.AddAddressToAccessList(authority)  // Pure access-list add, no state mutation

// Validation checks follow independently
if code := st.state.GetCode(authority); len(code) != 0 {
    if !types.IsDelegation(code) {
        return authority, ErrAuthorizationWrongCodePresent
    }
}
if have := st.state.GetNonce(authority); have != auth.Nonce {
    return authority, ErrAuthorizationNonceMismatch
}

Reproducer

test_account_warming (single_invalid_nonce_authorization_single_signer)
{
  "tests/prague/eip7702_set_code_tx/test_gas.py::test_account_warming[fork_Osaka-state_test-single_invalid_nonce_authorization_single_signer-check_delegated_account_first_False]": {
    "_info": {
      "hash": "0xb8343f0a111ad175dad7f0b36df16bb40efda9a1690144dd1f49718316f3d3b8",
      "comment": "`execution-specs` generated test",
      "filling-transition-tool": "2.18.0rc6",
      "description": "Test warming of the authority and authorized accounts for set-code transactions.",
      "url": "https://github.com/ethereum/execution-specs/blob/tests-v5.4.0/tests/prague/eip7702_set_code_tx/test_gas.py#L952",
      "fixture-format": "state_test",
      "reference-spec": "https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md",
      "reference-spec-version": "99f1be49f37c034bdd5c082946f5968710dbfc87"
    },
    "config": {
      "blobSchedule": {
        "Osaka": { "baseFeeUpdateFraction": "0x4c6964", "max": "0x9", "target": "0x6" }
      }
    },
    "env": {
      "currentBaseFee": "0x07",
      "currentBeaconRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
      "currentCoinbase": "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba",
      "currentDifficulty": "0x00",
      "currentExcessBlobGas": "0x00",
      "currentGasLimit": "0x07270e00",
      "currentNumber": "0x01",
      "currentRandom": "0x0000000000000000000000000000000000000000000000000000000000000000",
      "currentTimestamp": "0x03e8",
      "currentWithdrawalsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"
    },
    "post": {
      "Osaka": [{
        "hash": "0xb05aecc0b5912e210061ec749aedc3826aa9398af218b0efd21a227256f65e6f",
        "indexes": { "data": 0, "gas": 0, "value": 0 },
        "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
        "state": {
          "0x8eb73577201595e114480fff692839b6c8b276f6": { "nonce": "0x01", "balance": "0x3635c9adc5de99b84c", "code": "0x", "storage": {} },
          "0xb48d4cfb100b7b36ad0a96296083496d652bd194": { "nonce": "0x01", "balance": "0x00", "code": "0x5a6000600060006000600073ef88e8ab76dffe4d1ad6c4fa617d6bf04a3852546000f15a905090036017900373ef88e8ab76dffe4d1ad6c4fa617d6bf04a385254555a60006000600060006000731091e7be2d59b0e382a714b718600dca726838886000f15a9050900360179003731091e7be2d59b0e382a714b718600dca726838885500", "storage": { "0xef88e8ab76dffe4d1ad6c4fa617d6bf04a385254": "0x64", "0x1091e7be2d59b0e382a714b718600dca72683888": "0x0a28" } }
        }
      }]
    },
    "pre": {
      "0x8eb73577201595e114480fff692839b6c8b276f6": { "nonce": "0x00", "balance": "0x3635c9adc5dea00000", "code": "0x", "storage": {} },
      "0xb48d4cfb100b7b36ad0a96296083496d652bd194": { "nonce": "0x01", "balance": "0x00", "code": "0x5a6000600060006000600073ef88e8ab76dffe4d1ad6c4fa617d6bf04a3852546000f15a905090036017900373ef88e8ab76dffe4d1ad6c4fa617d6bf04a385254555a60006000600060006000731091e7be2d59b0e382a714b718600dca726838886000f15a9050900360179003731091e7be2d59b0e382a714b718600dca726838885500", "storage": { "0xef88e8ab76dffe4d1ad6c4fa617d6bf04a385254": "0xdeadbeef", "0x1091e7be2d59b0e382a714b718600dca72683888": "0xdeadbeef" } }
    },
    "transaction": {
      "accessLists": [ [] ],
      "authorizationList": [{
        "chainId": "0x0", "address": "0x1091e7be2d59b0e382a714b718600dca72683888",
        "nonce": "0x1", "v": "0x0",
        "r": "0xd8634197841d6b0bbde1de28a4e2366d615bc5f23608814d167aef64111514f4",
        "s": "0x49975f6aaa2bb65c9f6a48f64fee71aeb89b988ace04443213f18f6d41bf0866",
        "signer": "0xef88e8ab76dffe4d1ad6c4fa617d6bf04a385254",
        "yParity": "0x00"
      }],
      "data": [ "0x" ],
      "gasLimit": [ "0x0f4240" ],
      "maxFeePerGas": "0x07", "maxPriorityFeePerGas": "0x00",
      "nonce": "0x0",
      "secretKey": "0x7200ffe09b7fa7303964ab95b48d4cfb100b7b36ad0a96296083496d652bd195",
      "sender": "0x8eb73577201595e114480fff692839b6c8b276f6",
      "to": "0xb48d4cfb100b7b36ad0a96296083496d652bd194",
      "value": [ "0x0" ]
    }
  }
}

The test has an authorization with nonce: 1 (invalid — signer's nonce is 0), so the authorization fails. The signer account (0xef88...) is later accessed via CALL. In evmone, the CALL sees a warm account (100 gas); in geth, it sees a cold account (2,600 gas).

Impact

Production: None. No production blockchain uses evmone's state transition layer for consensus. The bugs affect evmone-statetest/t8n testing tooling only.

Scope: EIP-7702 specific, Prague/Pectra hardfork and later. The EVM bytecode interpreter (lib/evmone/) is unaffected.

Found by goevmlab-based differential fuzzer maintained by the EF Protocol Security team.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions