Skip to content

Commit 8e958d6

Browse files
bshastryspencer-tb
andauthored
feat(tests): EIP-8037 state-gas refund on failed CREATE2 with init storage (#3011)
Co-authored-by: spencer-tb <spencer.tb@ethereum.org>
1 parent 350e176 commit 8e958d6

2 files changed

Lines changed: 102 additions & 0 deletions

File tree

docs/writing_tests/post_mortems.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,52 @@ None required - the existing framework supported writing these tests.
7272

7373
---
7474

75+
## 2026-06 - CREATE2 Failed Deposit Storage State-Gas Refund - Amsterdam
76+
77+
### Description
78+
79+
A consensus divergence was found via goevmlab differential fuzzing in
80+
go-ethereum's Amsterdam (bal-devnet-7) EIP-8037 implementation: when a `CREATE2`
81+
whose init code writes new storage slots fails its code deposit — either because
82+
the deposited code is rejected by EIP-3541, or because the EIP-8037 code-deposit
83+
state gas cannot be paid — the create frame reverts, but only the new-account
84+
state-creation gas is refunded; the init's storage-slot state-creation gas
85+
(`STATE_BYTES_PER_STORAGE_SET * COST_PER_STATE_BYTE` per slot) is not. The
86+
transaction over-reports gas used (by `num_slots * 97920`), so the sender and
87+
coinbase balances — and the post-state root — diverge from the reference spec
88+
and from revm/nethermind/besu/erigon/ethrex.
89+
90+
### Root Cause Analysis
91+
92+
- State-creation gas charged inside a `CREATE`/`CREATE2` init frame must be fully
93+
reverted when the create fails, for both the new account and any storage slots
94+
the init wrote. The existing `eip8037` suite covered the create-init storage
95+
charge on the success path and same-tx slot-reset refunds, but never isolated
96+
the refund of storage-slot state gas on a create *failure*.
97+
- The new-account state-gas refund on failure was already correct, which masked
98+
the missing storage-slot refund: a failing create with no init storage agrees
99+
across clients, so only the combination "failing create + init storage"
100+
exposes it.
101+
- Differential fuzzing (goevmlab) surfaced it where direct enumeration had not.
102+
103+
### Steps Taken To Avoid Recurrence
104+
105+
- Added a parametric regression test over the failure mechanism (EIP-3541 reject
106+
and code-deposit OOG) and the number of init storage slots (`0`, `1`, `3`). The
107+
`slots=0` case is a negative control (account-creation refund only) that must
108+
not diverge; the `slots>=1` cases isolate the storage-slot state-gas refund on
109+
create failure and scale the discrepancy with the slot count.
110+
111+
### Implemented Test Case
112+
113+
- `tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py::test_create2_failed_deposit_refunds_storage_state_gas`
114+
115+
### Framework/Documentation Changes
116+
117+
None required - the existing framework supported writing this test.
118+
119+
---
120+
75121
## TEMPLATE
76122

77123
## Date - Title - Fork

tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1869,6 +1869,62 @@ def test_create_code_deposit_oog_refunds_state_gas(
18691869
state_test(pre=pre, post={factory: Account(storage=storage)}, tx=tx)
18701870

18711871

1872+
@pytest.mark.parametrize("slots", [0, 1, 3])
1873+
@pytest.mark.parametrize("fail_mode", ["eip3541", "oog_deposit"])
1874+
@pytest.mark.valid_from("EIP8037")
1875+
def test_create2_failed_deposit_refunds_storage_state_gas(
1876+
state_test: StateTestFiller,
1877+
pre: Alloc,
1878+
fork: Fork,
1879+
slots: int,
1880+
fail_mode: str,
1881+
) -> None:
1882+
"""
1883+
Test a failed CREATE2 deposit refunds the init's storage-slot state gas.
1884+
1885+
Total gas used is independent of `slots`, so a client that drops the
1886+
slot refund diverges for `slots >= 1`; `slots == 0` is the negative
1887+
control.
1888+
"""
1889+
gas_limit_cap = fork.transaction_gas_limit_cap()
1890+
assert gas_limit_cap is not None
1891+
1892+
# init: write `slots` new storage slots, then trigger a deposit failure
1893+
init_code = Bytecode()
1894+
for i in range(slots):
1895+
init_code += Op.SSTORE(i, i + 1)
1896+
if fail_mode == "eip3541":
1897+
# return 0xEF -> EIP-3541 rejects the deposited code
1898+
init_code += Op.MSTORE8(0, 0xEF) + Op.RETURN(0, 1)
1899+
else:
1900+
# return max-size code: the code-deposit state gas cannot be paid
1901+
init_code += Op.RETURN(0, fork.max_code_size())
1902+
mstore_value, size = init_code_at_high_bytes(init_code)
1903+
1904+
storage = Storage()
1905+
factory = pre.deploy_contract(
1906+
code=(
1907+
Op.MSTORE(0, mstore_value)
1908+
+ Op.SSTORE(
1909+
storage.store_next(0, "create2_failed"),
1910+
Op.CREATE2(value=0, offset=0, size=size, salt=0),
1911+
)
1912+
),
1913+
)
1914+
1915+
tx = Transaction(
1916+
to=factory,
1917+
gas_limit=gas_limit_cap,
1918+
sender=pre.fund_eoa(),
1919+
)
1920+
1921+
state_test(
1922+
pre=pre,
1923+
post={factory: Account(storage=storage)},
1924+
tx=tx,
1925+
)
1926+
1927+
18721928
@pytest.mark.parametrize(
18731929
"reservoir_covers",
18741930
[

0 commit comments

Comments
 (0)