@@ -872,11 +872,15 @@ def test_sstore_restoration_sub_frame_revert(
872872 state_test (pre = pre , tx = tx , post = post )
873873
874874
875+ @pytest .mark .with_all_call_opcodes (
876+ selector = lambda call_opcode : call_opcode != Op .STATICCALL
877+ )
875878@pytest .mark .valid_from ("EIP8037" )
876879def test_sstore_restoration_ancestor_revert (
877880 state_test : StateTestFiller ,
878881 pre : Alloc ,
879882 fork : Fork ,
883+ call_opcode : Op ,
880884) -> None :
881885 """
882886 Verify the SSTORE 0 to x to 0 refund returns to the caller when an
@@ -890,6 +894,7 @@ def test_sstore_restoration_ancestor_revert(
890894 replenished by `sstore_state_gas`.
891895 """
892896 gas_costs = fork .gas_costs ()
897+ intrinsic_cost = fork .transaction_intrinsic_cost_calculator ()()
893898 # Probe SSTORE(0, 1): 2 pushes + cold storage write + state gas - 1,
894899 # so it OOGs by 1 when the reservoir is 0 and succeeds otherwise.
895900 probe_gas = (
@@ -899,24 +904,46 @@ def test_sstore_restoration_ancestor_revert(
899904 - 1
900905 )
901906
902- inner = pre .deploy_contract (
903- code = Op .SSTORE (0 , 1 ) + Op .SSTORE (0 , 0 ) + Op .STOP ,
904- )
905- middle = pre .deploy_contract (
906- code = Op .POP (Op .CALL (gas = Op .GAS , address = inner )) + Op .REVERT (0 , 0 ),
907- )
908- probe = pre .deploy_contract (code = Op .SSTORE (0 , 1 ))
907+ set_op = Op .SSTORE .with_metadata (
908+ key_warm = False ,
909+ original_value = 0 ,
910+ current_value = 0 ,
911+ new_value = 1 ,
912+ )(0 , 1 )
913+ clear_op = Op .SSTORE .with_metadata (
914+ key_warm = True ,
915+ original_value = 0 ,
916+ current_value = 1 ,
917+ new_value = 0 ,
918+ )(0 , 0 )
919+ inner_code = set_op + clear_op + Op .STOP
920+ inner = pre .deploy_contract (code = inner_code )
921+
922+ middle_code = Op .POP (Op .CALL (gas = Op .GAS , address = inner )) + Op .REVERT (0 , 0 )
923+ middle = pre .deploy_contract (code = middle_code )
924+
925+ probe_code = Op .SSTORE (0 , 1 )
926+ probe = pre .deploy_contract (code = probe_code )
909927
910928 caller_storage = Storage ()
911- caller = pre .deploy_contract (
912- code = (
913- Op .POP (Op .CALL (gas = Op .GAS , address = middle ))
914- + Op .SSTORE (
915- caller_storage .store_next (1 , "probe_must_succeed" ),
916- Op .CALL (gas = probe_gas , address = probe ),
917- )
918- ),
929+ caller_code = Op .POP (call_opcode (gas = Op .GAS , address = middle )) + Op .SSTORE (
930+ caller_storage .store_next (1 , "probe_must_succeed" ),
931+ Op .CALL (gas = probe_gas , address = probe ),
919932 )
933+ caller = pre .deploy_contract (code = caller_code )
934+
935+ # Block state gas commits: probe's SSTORE-set and caller's outer
936+ # SSTORE-set; inner's set+clear cancel before middle reverts and
937+ # don't propagate. Header gas_used is max(regular, state).
938+ expected_regular = (
939+ intrinsic_cost
940+ + caller_code .regular_cost (fork )
941+ + middle_code .regular_cost (fork )
942+ + inner_code .regular_cost (fork )
943+ + probe_code .regular_cost (fork )
944+ )
945+ expected_state = 2 * fork .sstore_state_gas ()
946+ expected_gas_used = max (expected_regular , expected_state )
920947
921948 # gas_limit at the cap means the caller's reservoir starts at 0.
922949 tx = Transaction (
@@ -925,8 +952,112 @@ def test_sstore_restoration_ancestor_revert(
925952 gas_limit = fork .transaction_gas_limit_cap (),
926953 )
927954
928- post = {caller : Account (storage = caller_storage )}
929- state_test (pre = pre , tx = tx , post = post )
955+ state_test (
956+ pre = pre ,
957+ tx = tx ,
958+ post = {caller : Account (storage = caller_storage )},
959+ blockchain_test_header_verify = Header (gas_used = expected_gas_used ),
960+ )
961+
962+
963+ @pytest .mark .with_all_call_opcodes (
964+ selector = lambda call_opcode : call_opcode in (Op .DELEGATECALL , Op .CALLCODE )
965+ )
966+ @pytest .mark .valid_from ("EIP8037" )
967+ def test_sstore_restoration_charge_in_ancestor_intermediate_revert (
968+ state_test : StateTestFiller ,
969+ pre : Alloc ,
970+ fork : Fork ,
971+ call_opcode : Op ,
972+ ) -> None :
973+ """
974+ Verify a deferred refund applied in an intermediate frame still
975+ flows back to the caller when that frame REVERTs.
976+
977+ Caller's SSTORE charges; the matching clear in inner is deferred
978+ through the chain and lands on middle's own SSTORE-set during
979+ `incorporate_child_on_success`. Middle REVERTs; the applied
980+ amount must reach the caller via `incorporate_child_on_error`.
981+ A probe SSTORE sized to OOG by 1 detects loss.
982+ """
983+ gas_costs = fork .gas_costs ()
984+ sstore_state_gas = fork .sstore_state_gas ()
985+ gas_limit_cap = fork .transaction_gas_limit_cap ()
986+ assert gas_limit_cap is not None
987+ intrinsic_cost = fork .transaction_intrinsic_cost_calculator ()()
988+ # Probe SSTORE(0, 1): 2 pushes + cold storage write + state gas - 1,
989+ # so it OOGs by 1 when the reservoir is 0 and succeeds otherwise.
990+ probe_gas = (
991+ 2 * gas_costs .VERY_LOW
992+ + gas_costs .COLD_STORAGE_WRITE
993+ + sstore_state_gas
994+ - 1
995+ )
996+
997+ inner_code = (
998+ Op .SSTORE .with_metadata (
999+ key_warm = True ,
1000+ original_value = 0 ,
1001+ current_value = 1 ,
1002+ new_value = 0 ,
1003+ )(0 , 0 )
1004+ + Op .STOP
1005+ )
1006+ inner = pre .deploy_contract (code = inner_code )
1007+
1008+ # Middle's own SSTORE on slot 1 supplies the `state_gas_used`
1009+ # that inner's deferred credit lands on, then middle REVERTs.
1010+ middle_code = (
1011+ Op .SSTORE (1 , 1 )
1012+ + Op .POP (call_opcode (gas = Op .GAS , address = inner ))
1013+ + Op .REVERT (0 , 0 )
1014+ )
1015+ middle = pre .deploy_contract (code = middle_code )
1016+
1017+ probe_code = Op .SSTORE (0 , 1 )
1018+ probe = pre .deploy_contract (code = probe_code )
1019+
1020+ caller_storage = Storage ()
1021+ caller_code = (
1022+ Op .SSTORE (caller_storage .store_next (1 , "caller_set_persists" ), 1 )
1023+ + Op .POP (call_opcode (gas = Op .GAS , address = middle ))
1024+ + Op .SSTORE (
1025+ caller_storage .store_next (1 , "probe_must_succeed" ),
1026+ Op .CALL (gas = probe_gas , address = probe ),
1027+ )
1028+ )
1029+ caller = pre .deploy_contract (code = caller_code )
1030+
1031+ # Block state gas commits: caller's slot-0 set + probe's
1032+ # SSTORE-set + caller's outer SSTORE-set on slot 1. Middle's
1033+ # own slot-1 set is washed by inner's deferred credit before
1034+ # middle reverts, so it does not propagate. Header gas_used
1035+ # is max(regular, state).
1036+ expected_regular = (
1037+ intrinsic_cost
1038+ + caller_code .regular_cost (fork )
1039+ + middle_code .regular_cost (fork )
1040+ + inner_code .regular_cost (fork )
1041+ + probe_code .regular_cost (fork )
1042+ )
1043+ expected_state = 3 * sstore_state_gas
1044+ expected_gas_used = max (expected_regular , expected_state )
1045+
1046+ # Reservoir = 2 * sstore_state_gas covers caller's and middle's
1047+ # sets; the deferred credit refills middle by sstore_state_gas,
1048+ # which flows to the caller on revert.
1049+ tx = Transaction (
1050+ sender = pre .fund_eoa (),
1051+ to = caller ,
1052+ gas_limit = gas_limit_cap + 2 * sstore_state_gas ,
1053+ )
1054+
1055+ state_test (
1056+ pre = pre ,
1057+ tx = tx ,
1058+ post = {caller : Account (storage = caller_storage )},
1059+ blockchain_test_header_verify = Header (gas_used = expected_gas_used ),
1060+ )
9301061
9311062
9321063@pytest .mark .with_all_create_opcodes
0 commit comments