Skip to content

Commit 12d6495

Browse files
committed
feat(tests): EIP-8037 drop double refund on incorporate_child_on_error
1 parent b22aa09 commit 12d6495

2 files changed

Lines changed: 91 additions & 49 deletions

File tree

tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_reservoir.py

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1328,9 +1328,7 @@ def test_nested_failure_resets_to_tx_reservoir(
13281328
- `cumulative_gas_used` (receipt) pins `tx.gas - gas_left -
13291329
state_gas_left`, catching bugs in the leftover split.
13301330
- `header.gas_used` pins `max(block_regular, block_state)` via
1331-
the block accumulators. They differ from the receipt by
1332-
exactly `non_top_burns` (inline refunds the user pays for but
1333-
the block doesn't track in either accumulator).
1331+
the block accumulators.
13341332
"""
13351333
gas_limit_cap = fork.transaction_gas_limit_cap()
13361334
assert gas_limit_cap is not None
@@ -1359,41 +1357,25 @@ def test_nested_failure_resets_to_tx_reservoir(
13591357
else:
13601358
top, frame_codes = _build_create_chain(pre, frame_bodies, terminator)
13611359

1362-
# Non-top inline state-gas refunds (body SSTORE x→0 plus CREATE
1363-
# pre-charge credits on child failure) accumulate in each frame's
1364-
# `state_gas_refund` and get subtracted at the parent's
1365-
# `incorporate_child_on_error` boundary so the inflation does not
1366-
# leak across the rolled-back state change. The top frame's
1367-
# refund is preserved by the tx-level error handler.
1368-
non_top_body_refund_burn = sum(
1369-
b.state_refund(fork) for b in frame_bodies[1:]
1370-
)
1371-
non_top_create_credit_burn = max(0, n_creates - 1) * new_account_state_gas
1372-
non_top_burns = non_top_body_refund_burn + non_top_create_credit_burn
1373-
13741360
sum_regular = sum(code.regular_cost(fork) for code in frame_codes)
13751361
spill = max(0, total_state_charges - reservoir)
13761362
if failure_mode == "halt":
13771363
# Policy A (updated EIP): all state-gas — body charges, spilled
13781364
# portions, and CREATE pre-charges (returned via credit) — folds
13791365
# into state_gas_left at tx end. gas_left is zeroed by halt.
1380-
# `non_top_burns` is the inline refund burn at incorporate
1381-
# boundaries that does not return to the user's reservoir.
1382-
state_gas_at_end = max(reservoir, total_state_charges) - non_top_burns
1366+
state_gas_at_end = max(reservoir, total_state_charges)
13831367
expected_cumulative = tx_gas - state_gas_at_end
13841368
# Header: block_regular = gas_limit_cap - spill (spilled
13851369
# state-gas drained gas_left but is no longer reclassified to
13861370
# regular under Policy A); block_state ≈ 0 for plain CALLs.
13871371
expected_header_gas_used = gas_limit_cap - spill
13881372
elif failure_mode == "revert":
13891373
# Revert preserves gas_left; full state-gas refund.
1390-
# User pays only regular costs + intrinsic + non-top burns.
1391-
expected_cumulative = intrinsic_cost + sum_regular + non_top_burns
1374+
# User pays only regular costs + intrinsic.
1375+
expected_cumulative = intrinsic_cost + sum_regular
13921376
# Header reflects the regular-vs-state attribution directly:
13931377
# state_gas_used is zeroed by the tx error handler, so only
1394-
# regular gas usage shows up. The refund burn lives in the
1395-
# `state_gas_left` shortfall (visible in cumulative), not
1396-
# the regular accumulator.
1378+
# regular gas usage shows up.
13971379
expected_header_gas_used = intrinsic_cost + sum_regular
13981380
else:
13991381
raise ValueError("Invariant, unreachable code.")
@@ -1842,3 +1824,62 @@ def test_set_and_clear_pays_no_state_gas(
18421824
# never touched because frame-end byte_delta was zero.
18431825
post = {contract: Account(storage={0: 0})}
18441826
state_test(pre=pre, post=post, tx=tx)
1827+
1828+
1829+
@pytest.mark.valid_from("EIP8037")
1830+
def test_subcall_set_clear_revert_pays_no_state_gas(
1831+
state_test: StateTestFiller,
1832+
pre: Alloc,
1833+
fork: Fork,
1834+
) -> None:
1835+
"""
1836+
A child frame doing SSTORE 0 to x to 0 then REVERT must bill the
1837+
sender only intrinsic + regular costs.
1838+
1839+
Both SSTOREs roll back with the REVERT, so the matching
1840+
state-gas charge and refund cancel cleanly. The receipt's
1841+
`cumulative_gas_used` equals the regular baseline; a leftover
1842+
`sstore_state_gas` would surface a double-charge at the failure
1843+
boundary.
1844+
"""
1845+
intrinsic_cost = fork.transaction_intrinsic_cost_calculator()()
1846+
gas_limit_cap = fork.transaction_gas_limit_cap()
1847+
assert gas_limit_cap is not None
1848+
1849+
set_op = Op.SSTORE.with_metadata(
1850+
key_warm=False,
1851+
original_value=0,
1852+
current_value=0,
1853+
new_value=1,
1854+
)(0, 1)
1855+
clear_op = Op.SSTORE.with_metadata(
1856+
key_warm=True,
1857+
original_value=0,
1858+
current_value=1,
1859+
new_value=0,
1860+
)(0, 0)
1861+
inner_code = set_op + clear_op + Op.REVERT(0, 0)
1862+
inner = pre.deploy_contract(code=inner_code)
1863+
1864+
top_code = Op.POP(Op.CALL(gas=Op.GAS, address=inner)) + Op.STOP
1865+
top = pre.deploy_contract(code=top_code)
1866+
1867+
expected_cumulative = (
1868+
intrinsic_cost
1869+
+ top_code.regular_cost(fork)
1870+
+ inner_code.regular_cost(fork)
1871+
)
1872+
1873+
tx = Transaction(
1874+
to=top,
1875+
gas_limit=gas_limit_cap,
1876+
sender=pre.fund_eoa(),
1877+
expected_receipt=TransactionReceipt(
1878+
cumulative_gas_used=expected_cumulative,
1879+
),
1880+
)
1881+
state_test(
1882+
pre=pre,
1883+
post={top: Account(), inner: Account(storage={0: 0})},
1884+
tx=tx,
1885+
)

tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_sstore.py

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -828,12 +828,15 @@ def test_sstore_restoration_sub_frame_revert(
828828
call_opcode: Op,
829829
) -> None:
830830
"""
831-
Verify 0 to x to 0 reservoir refund unwinds on sub-frame REVERT.
832-
833-
The sub-call performs 0 to x to 0 then REVERTs. If the reservoir
834-
refund is not rolled back with the reverted frame, the reservoir
835-
stays inflated by `sstore_state_gas`. A single-SSTORE probe sized
836-
to OOG by 1 would then succeed; the test asserts it OOGs.
831+
Verify 0 to x to 0 reservoir refund returns to the caller on
832+
sub-frame REVERT.
833+
834+
The sub-call performs 0 to x to 0 then REVERTs. Since both the
835+
set-charge and its refund roll back together, the
836+
`state_gas_used + state_gas_left` sum reflects the unconsumed
837+
reservoir and is returned to the caller via
838+
`incorporate_child_on_error`. A single-SSTORE probe sized to OOG
839+
by 1 succeeds, confirming the caller's reservoir was replenished.
837840
"""
838841
gas_costs = fork.gas_costs()
839842
# Probe SSTORE(0, 1): 2 pushes + cold storage write + state gas - 1,
@@ -853,7 +856,7 @@ def test_sstore_restoration_sub_frame_revert(
853856
# and REVERT without a hard-coded budget.
854857
caller_storage = Storage()
855858
caller_code = Op.POP(call_opcode(gas=Op.GAS, address=child)) + Op.SSTORE(
856-
caller_storage.store_next(0, "probe_must_fail"),
859+
caller_storage.store_next(1, "probe_must_succeed"),
857860
Op.CALL(gas=probe_gas, address=probe),
858861
)
859862
caller = pre.deploy_contract(code=caller_code)
@@ -876,16 +879,15 @@ def test_sstore_restoration_ancestor_revert(
876879
fork: Fork,
877880
) -> None:
878881
"""
879-
Verify the SSTORE 0 to x to 0 refund unwinds when an ancestor frame
880-
(not the applying frame itself) reverts.
882+
Verify the SSTORE 0 to x to 0 refund returns to the caller when an
883+
ancestor frame (not the applying frame itself) reverts.
881884
882885
Inner frame applies the refund and returns successfully; its
883-
refund propagates to middle via `incorporate_child_on_success`.
884-
Middle then REVERTs; its refund must be dropped by the caller's
885-
`incorporate_child_on_error`, rather than propagating up. This
886-
exercises the recursive scope that single-frame revert tests do
887-
not: a bug in the success propagation of `state_gas_refund` would
888-
leak the refund into the caller's reservoir.
886+
`state_gas_left` (inflated by the refund) propagates to middle
887+
via `incorporate_child_on_success`. Middle then REVERTs; the
888+
refunded reservoir flows back to the caller via
889+
`incorporate_child_on_error`, so the caller's reservoir is
890+
replenished by `sstore_state_gas`.
889891
"""
890892
gas_costs = fork.gas_costs()
891893
# Probe SSTORE(0, 1): 2 pushes + cold storage write + state gas - 1,
@@ -910,7 +912,7 @@ def test_sstore_restoration_ancestor_revert(
910912
code=(
911913
Op.POP(Op.CALL(gas=Op.GAS, address=middle))
912914
+ Op.SSTORE(
913-
caller_storage.store_next(0, "probe_must_fail"),
915+
caller_storage.store_next(1, "probe_must_succeed"),
914916
Op.CALL(gas=probe_gas, address=probe),
915917
)
916918
),
@@ -936,16 +938,17 @@ def test_sstore_restoration_create_init_revert(
936938
create_opcode: Op,
937939
) -> None:
938940
"""
939-
Verify reservoir refunds unwind when CREATE init code REVERTs
940-
inside a sub-frame that also REVERTs.
941+
Verify reservoir refunds return to the caller when CREATE init
942+
code REVERTs inside a sub-frame that also REVERTs.
941943
942944
Wrapping the CREATE in an outer reverting frame isolates the
943945
rollback concern from the legitimate CREATE silent-failure refund
944946
(`create_account_state_gas` credited to the frame executing the
945-
CREATE opcode). When the outer frame reverts, every refund that
946-
occurred inside it must unwind, leaving the caller's reservoir at
947-
its pre-call value. A single-SSTORE probe sized to OOG by 1
948-
detects any leaked refund.
947+
CREATE opcode). When the outer frame reverts, the refunded
948+
reservoir flows back to the caller via
949+
`incorporate_child_on_error`, replenishing the caller's
950+
reservoir by at least `sstore_state_gas`. A single-SSTORE probe
951+
sized to OOG by 1 succeeds, confirming the propagation.
949952
"""
950953
gas_costs = fork.gas_costs()
951954
# Probe SSTORE(0, 1): 2 pushes + cold storage write + state gas - 1,
@@ -965,9 +968,7 @@ def test_sstore_restoration_create_init_revert(
965968
else:
966969
create_call = Op.CREATE2(0, 0, len(init_code), 0)
967970

968-
# Inner contract performs the CREATE then REVERTs, so any refunds
969-
# (SSTORE restoration or CREATE silent-failure) applied during its
970-
# execution must unwind with the frame.
971+
# Inner contract performs the CREATE then REVERTs.
971972
inner = pre.deploy_contract(
972973
code=(
973974
Op.MSTORE(
@@ -985,7 +986,7 @@ def test_sstore_restoration_create_init_revert(
985986
code=(
986987
Op.POP(Op.CALL(gas=Op.GAS, address=inner))
987988
+ Op.SSTORE(
988-
caller_storage.store_next(0, "probe_must_fail"),
989+
caller_storage.store_next(1, "probe_must_succeed"),
989990
Op.CALL(gas=probe_gas, address=probe),
990991
)
991992
),

0 commit comments

Comments
 (0)