-
Notifications
You must be signed in to change notification settings - Fork 328
EIP-7702: Authority accounts warmed after validation instead of before #1447
Description
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:
- EIP-2929 access list warming — a metadata operation that should NOT mutate the state trie
- 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/warmedWhen 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.