You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
The second authorization for the same signer finds the account already in m_modified (with erase_if_empty = true, nonce = 0, empty code)
is_empty() returns true for this warming-only entry, so no refund is given in evmone
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).
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.
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.
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 thePER_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: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:
get_or_insert()and warms it (EIP-7702: Authority accounts warmed after validation instead of before #1447)m_modified(witherase_if_empty = true, nonce = 0, empty code)is_empty()returns true for this warming-only entry, so no refund is given in evmoneExist()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 reasonThe 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'sExist(). An empty account (nonce=0, balance=0, no code) that exists in the pre-state receives no refund in evmone (becauseis_empty()returns true) but may receive one in geth (becauseExist()returns true for empty-but-present accounts).Geth Reference
In geth,
Exist()returnstrueif the account is in the state trie (even if empty). An address only in the access list but not in the trie returnsfalse.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...):nonce: 1(invalid — signer's nonce is 0) → fails validationnonce: 0(valid) → succeedsBecause 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/t8ntesting 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.