-
Notifications
You must be signed in to change notification settings - Fork 330
Description
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:
- 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) - The second authorization for the same signer finds the account already in
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 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).
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.