From 3f01d328da2b86cb812298d9012c28972c3d7f35 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Fri, 4 Sep 2026 15:48:57 +0200 Subject: [PATCH] feat(tests): EIP-8037 - no refill for failed creates onto alive accounts Add a regression test proving that a create frame failing on an alive destination refills no NEW_ACCOUNT state gas: the factory's gas is tuned so its probe SSTORE only fits when the reservoir funds it, and removing the refill guard fails all twelve no_reservoir fixtures. Correct the collision-test docstring (an alive target is never charged, so nothing is refunded) and record the EELS line-coverage evidence for the checklist: every EIP-8037 line is executed, with the remaining misses classified as pre-existing code. --- .../eip_checklist_external_coverage.txt | 2 + .../test_state_gas_create.py | 122 +++++++++++++++++- 2 files changed, 117 insertions(+), 7 deletions(-) diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/eip_checklist_external_coverage.txt b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/eip_checklist_external_coverage.txt index 28f2ca9a555..c7b80d8f8cc 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/eip_checklist_external_coverage.txt +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/eip_checklist_external_coverage.txt @@ -1 +1,3 @@ general/code_coverage/test_coverage = Measured on 2026-09-10 with #3524 (e330e06f60) rebased onto forks/amsterdam (c998b45fe8): 4936/4944 test statements and 452/462 branches covered by 2116 passing fixture variants. The eight missed statements are six defensive invalid-parameter raises and two lines adapting SSTORE counts to future gas repricing. Reproduce with --cov=tests/amsterdam/eip8037_state_creation_gas_cost_increase --cov-branch; see #3524 for scope and validation limits. +general/code_coverage/eels = Measured on 2026-09-21 at forks/amsterdam c335bc4e9e with `uv run fill --fork=Amsterdam --cov=ethereum.forks.amsterdam --cov-branch tests/amsterdam/eip8037_state_creation_gas_cost_increase` (2179 fixtures through the in-process t8n). Every EIP-8037 line is executed: vm/__init__.py 67/67 statements, vm/gas.py 338/351, vm/interpreter.py 146/149, vm/instructions/system.py 278/293, vm/instructions/storage.py 66/67, transactions.py 256/292 and fork.py 192/298, where every miss is pre-existing code classified under missed_lines. The one EIP-8037 path this directory does not reach, the top-frame NEW_ACCOUNT charge for a value transfer to a non-alive recipient (charge_value_transfer_to_non_alive_account), is exercised by tests/amsterdam/eip2780_reduce_intrinsic_tx_gas/test_top_frame_charges.py::test_top_frame_new_account_charged_as_state_gas. +general/code_coverage/missed_lines = All misses in the files EIP-8037 touches are pre-existing branches: the static-context and delegated-code arms of CALL, CALLCODE, DELEGATECALL, STATICCALL, SELFDESTRUCT and TSTORE; EIP-7702 warm delegation access, authority recovery and authorization validation errors; transaction decoding, signature recovery and validity errors (nonce overflow, priority fee above max fee, blob count and versioned-hash checks, chain id, insufficient balance, non-EOA sender, missing system contract code); the memory-expansion overflow and gas_left shortfall branches of vm/gas.py together with excess-blob-gas arithmetic and the blob-gas capacity check; and block-level functions the t8n never runs (state_transition, execute_block, apply_body, validate_header, check_gas_limit, get_last_256_block_hashes, process_general_purpose_requests, process_withdrawals). The two-dimensional admission checks in check_block_gas_capacity and the gas split and accounting in check_transaction and process_transaction are fully executed. diff --git a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py index 9949dc89b05..23d9cbb0dfd 100644 --- a/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py +++ b/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py @@ -2284,14 +2284,15 @@ def test_create_collision_refunds_state_gas( create_opcode: Op, ) -> None: """ - Verify CREATE/CREATE2 address collision refunds account state gas. + Verify a CREATE/CREATE2 address collision leaves the reservoir intact. - The collision path increments the factory nonce and burns the - forwarded execution gas (consumed by the never-spawned child), but - still refunds `GAS_NEW_ACCOUNT` to the reservoir. Tight gas - tuning limits the factory's post-collision `gas_left` so the - probe SSTORE can only succeed via the refunded reservoir, not - by spilling state gas from `gas_left`. + The collision target has code, so it is alive and no + `GAS_NEW_ACCOUNT` charge is made for it. The collision path + increments the factory nonce and burns the forwarded execution gas + (consumed by the never-spawned child) while the reservoir returns + untouched. Tight gas tuning limits the factory's post-collision + `gas_left` so the probe SSTORE can only succeed via the intact + reservoir, not by spilling state gas from `gas_left`. """ sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) @@ -3610,3 +3611,110 @@ def test_no_account_charge_on_existing_account( ), } state_test(pre=pre, post=post, tx=tx) + + +@pytest.mark.with_all_create_opcodes() +@pytest.mark.parametrize( + ("failure_op", "halts"), + [ + pytest.param(Op.REVERT(0, 0), False, id="revert"), + pytest.param(Op.INVALID, True, id="halt"), + ], +) +@pytest.mark.parametrize( + "reservoir_covers", + [ + pytest.param(False, id="no_reservoir"), + pytest.param(True, id="reservoir_covers"), + ], +) +@pytest.mark.valid_from("EIP8037") +def test_failed_create_on_existing_account_refills_nothing( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + create_opcode: Op, + failure_op: Bytecode, + halts: bool, + reservoir_covers: bool, +) -> None: + """ + Verify a create frame failing on an alive destination refills nothing. + + The pre-funded target is never charged NEW_ACCOUNT, so the child + failure has no charge to return. The factory's gas is tuned so its + probe SSTORE only fits when the reservoir funds the state gas: a + phantom refill would let the `no_reservoir` arms succeed. + """ + sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork) + + init_code = failure_op + mstore_value, size = init_code_at_high_bytes(init_code) + salt = 0 + create_call = ( + create_opcode( + value=0, + offset=0, + size=size, + salt=salt, + init_code_size=size, + account_new=False, + ) + if create_opcode == Op.CREATE2 + else create_opcode( + value=0, + offset=0, + size=size, + init_code_size=size, + account_new=False, + ) + ) + + probe_slot = 0 + setup_code = Op.MSTORE(0, mstore_value) + Op.POP(create_call) + factory_code = setup_code + Op.SSTORE(probe_slot, 1) + factory = pre.deploy_contract(code=factory_code) + + target = compute_create_address( + address=factory, + nonce=1, + salt=salt, + initcode=bytes(init_code), + opcode=create_opcode, + ) + pre.fund_address(target, amount=1) + + # Leave the factory a window where the probe's execution fits but + # spilling its state gas from `gas_left` does not. + probe_execution = Op.SSTORE(probe_slot, 1).execution_cost(fork) + retained = probe_execution + sstore_state_gas // 2 + if halts: + # The halt burns the child's grant; the withheld 1/64 remains. + remaining = retained * 64 + else: + # The revert returns the grant less what the init code spent. + remaining = retained + init_code.execution_cost(fork) + forwarded_gas = remaining + setup_code.execution_cost(fork) + + caller_storage = Storage() + caller = pre.deploy_contract( + code=Op.SSTORE( + caller_storage.store_next(reservoir_covers, "factory_succeeds"), + Op.CALL(gas=forwarded_gas, address=factory), + ) + ) + + tx = Transaction( + to=caller, + state_gas_reservoir=sstore_state_gas if reservoir_covers else 0, + sender=pre.fund_eoa(), + ) + + post = { + caller: Account(storage=caller_storage), + factory: Account(nonce=2, storage={probe_slot: 1}) + if reservoir_covers + else Account(nonce=1, storage={}), + target: Account(nonce=0, balance=1, code=b""), + } + state_test(pre=pre, post=post, tx=tx)