Skip to content

EIP-7702: Incorrect refund calculation for warming-only authority accounts #1448

@bshastry

Description

@bshastry

Summary

In process_authorization_list() (test/state/state.cpp), accounts created solely for EIP-2929 warming by a prior failed authorization are incorrectly counted as "existing" for the PER_EMPTY_ACCOUNT_COST - PER_AUTH_BASE_COST (25,000 - 12,500 = 12,500 gas) delegation refund.

This is a consequence of the warming bug described in #1447: because get_or_insert() creates the account in evmone's state even for failed authorizations, subsequent successful authorizations for the same signer see the account as "existing" and incorrectly grant the refund.

The Problem

EIP-7702 step 7 specifies that if the authority account already exists in the state trie, a gas refund should be added. The buggy code uses !authority.is_empty() to determine existence:

// BUGGY CODE
if (!authority.is_empty())
{
    delegation_refund += EXISTING_AUTHORITY_REFUND;  // 12,500 gas
}

This fails in two scenarios:

Scenario A: Failed-then-successful authorization for same signer

When multiple authorization tuples from the same signer are present and the first fails validation:

  1. The first (failed) authorization creates the signer in state via get_or_insert() and warms it (EIP-7702: Authority accounts warmed after validation instead of before #1447)
  2. The second authorization for the same signer finds the account already in m_modified (with erase_if_empty = true, nonce = 0, empty code)
  3. is_empty() returns true for this warming-only entry, so no refund is given in evmone
  4. In geth, Exist() also returns false (the account was only added to the access list, not the trie), so geth also gives no refund -- but for a different reason

The mismatch becomes visible when the warming-only account has non-default state from the pre-state (e.g., non-zero balance).

Scenario B: Empty accounts in pre-state

The refund logic uses !authority.is_empty() but should check whether the account "exists" per geth's Exist(). An empty account (nonce=0, balance=0, no code) that exists in the pre-state receives no refund in evmone (because is_empty() returns true) but may receive one in geth (because Exist() returns true for empty-but-present accounts).

Geth Reference

// geth: applyAuthorization()
if st.state.Exist(authority) {
    st.state.AddRefund(params.CallNewAccountGas - params.TxAuthTupleGas)
}

In geth, Exist() returns true if the account is in the state trie (even if empty). An address only in the access list but not in the trie returns false.

Reproducer

test_set_code_multiple_valid_authorization_tuples_first_invalid_same_signer
{
  "tests/prague/eip7702_set_code_tx/test_set_code_txs.py::test_set_code_multiple_valid_authorization_tuples_first_invalid_same_signer[fork_Osaka-state_test]": {
    "_info": {
      "hash": "0x0f355b515f69be02d980040475782fb9afe042d190a86829fd1277f2a1867fc3",
      "comment": "`execution-specs` generated test",
      "filling-transition-tool": "2.18.0rc6",
      "description": "Test setting the code of an account with multiple authorization tuples from the same signer but the first tuple is invalid.",
      "url": "https://github.com/ethereum/execution-specs/blob/tests-v5.4.0/tests/prague/eip7702_set_code_tx/test_set_code_txs.py#L2066",
      "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": "0x1448edc306e7a2c223c1ceffb7cf2ff52d809431bb0edac25746219c923fd079",
        "indexes": { "data": 0, "gas": 0, "value": 0 },
        "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
        "state": {
          "0x33eec28aef6fb93b01bfaec84c828550b87afeb1": { "nonce": "0x01", "balance": "0x3635c9adc5de80b162", "code": "0x", "storage": {} },
          "0xf921a9faf1a0496e1fbc1a6a87e4622e76aab32f": { "nonce": "0x01", "balance": "0x00", "code": "0xef01006cb62f906b516593f396d2031957902958b32e6a", "storage": { "0x01": "0x01" } }
        }
      }]
    },
    "pre": {
      "0x33eec28aef6fb93b01bfaec84c828550b87afeb1": { "nonce": "0x00", "balance": "0x3635c9adc5dea00000", "code": "0x", "storage": {} }
    },
    "transaction": {
      "nonce": "0x0",
      "maxPriorityFeePerGas": "0x00",
      "maxFeePerGas": "0x07",
      "gasLimit": [ "0x989680" ],
      "to": "0xf921a9faf1a0496e1fbc1a6a87e4622e76aab32f",
      "value": [ "0x00" ],
      "data": [ "0x" ],
      "accessLists": [ [] ],
      "authorizationList": [
        { "chainId": "0x0", "address": "0x6cb62f906b516593f396d2031957902958b32d6a", "nonce": "0x1", "v": "0x1", "r": "0xe4b7bc63365d503c26d2b807c492a09673f5628b72b76c6a7a31ec0cac43efb", "s": "0x78188f342af5ad52b0efbefa561fc9a0457848bef1efb80512652190c612ef8", "signer": "0xf921a9faf1a0496e1fbc1a6a87e4622e76aab32f", "yParity": "0x01" },
        { "chainId": "0x0", "address": "0x6cb62f906b516593f396d2031957902958b32e6a", "nonce": "0x0", "v": "0x0", "r": "0xfcda9c6d6b1273918966d938c378a3de5f9f251af499167d8aeacc20f41b90a0", "s": "0x609d7585801b3807a53ae34f3c203165bc9eb7c05910c39bf7d58abd933fabc4", "signer": "0xf921a9faf1a0496e1fbc1a6a87e4622e76aab32f", "yParity": "0x00" }
      ],
      "sender": "0x33eec28aef6fb93b01bfaec84c828550b87afeb1",
      "secretKey": "0x0819a25b59f9d30da90a34186cb62f906b516593f396d2031957902958b32d6b"
    }
  }
}

The test has 2 authorization tuples from the same signer (0xf921a9...):

  • First auth: nonce: 1 (invalid — signer's nonce is 0) → fails validation
  • Second auth: nonce: 0 (valid) → succeeds

Because the first (failed) auth creates the account in evmone's state via get_or_insert(), the second auth's refund calculation sees a different "existence" state than geth.

Impact

Production: None. No production blockchain uses evmone's state transition layer for consensus. The bug affects 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