1212 Account ,
1313 Address ,
1414 Alloc ,
15+ Block ,
16+ BlockchainTestFiller ,
1517 Bytecode ,
1618 Environment ,
19+ Fork ,
20+ Header ,
1721 Initcode ,
1822 Op ,
1923 Opcodes ,
@@ -74,12 +78,14 @@ def test_selfdestruct_to_self_pre_existing_no_log(
7478 pytest .param (0 , id = "zero_balance" ),
7579 ],
7680)
81+ @pytest .mark .with_all_create_opcodes
7782def test_selfdestruct_to_self_same_tx (
7883 state_test : StateTestFiller ,
7984 env : Environment ,
8085 pre : Alloc ,
8186 sender : EOA ,
8287 contract_balance : int ,
88+ create_opcode : Op ,
8389) -> None :
8490 """
8591 Test selfdestruct-to-self for same-tx created contracts.
@@ -93,12 +99,18 @@ def test_selfdestruct_to_self_same_tx(
9399
94100 factory_code = Op .MSTORE (
95101 0 , Op .PUSH32 (initcode_bytes .rjust (32 , b"\x00 " ))
96- ) + Op . CREATE (
102+ ) + create_opcode (
97103 value = Op .CALLVALUE , offset = 32 - initcode_len , size = initcode_len
98104 )
99105
100106 factory = pre .deploy_contract (factory_code )
101- created_address = compute_create_address (address = factory , nonce = 1 )
107+ created_address = compute_create_address (
108+ address = factory ,
109+ nonce = 1 ,
110+ salt = 0 ,
111+ initcode = initcode_bytes ,
112+ opcode = create_opcode ,
113+ )
102114
103115 if contract_balance > 0 :
104116 expected_logs = [
@@ -127,12 +139,14 @@ def test_selfdestruct_to_self_same_tx(
127139 pytest .param (0 , id = "zero_balance" ),
128140 ],
129141)
142+ @pytest .mark .with_all_create_opcodes
130143def test_selfdestruct_to_different_address_same_tx (
131144 state_test : StateTestFiller ,
132145 env : Environment ,
133146 pre : Alloc ,
134147 sender : EOA ,
135148 contract_balance : int ,
149+ create_opcode : Op ,
136150) -> None :
137151 """
138152 Test same-tx selfdestruct to different address.
@@ -147,12 +161,18 @@ def test_selfdestruct_to_different_address_same_tx(
147161
148162 factory_code = Op .MSTORE (
149163 0 , Op .PUSH32 (initcode_bytes .rjust (32 , b"\x00 " ))
150- ) + Op . CREATE (
164+ ) + create_opcode (
151165 value = Op .CALLVALUE , offset = 32 - initcode_len , size = initcode_len
152166 )
153167
154168 factory = pre .deploy_contract (factory_code )
155- created_address = compute_create_address (address = factory , nonce = 1 )
169+ created_address = compute_create_address (
170+ address = factory ,
171+ nonce = 1 ,
172+ salt = 0 ,
173+ initcode = initcode_bytes ,
174+ opcode = create_opcode ,
175+ )
156176
157177 if contract_balance > 0 :
158178 expected_logs = [
@@ -364,11 +384,7 @@ def test_finalization_selfdestruct_logs(
364384 reverse_sorted = list (reversed (sorted_addrs ))
365385
366386 # Runtime: selfdestruct on first call, STOP on subsequent calls
367- selfdestruct_target : Address | Opcodes
368- if to_self :
369- selfdestruct_target = Op .ADDRESS
370- else :
371- selfdestruct_target = beneficiary
387+ target : Address | Opcodes = Op .ADDRESS if to_self else beneficiary
372388 runtime = (
373389 Op .TLOAD (0 )
374390 + Op .ISZERO
@@ -377,7 +393,7 @@ def test_finalization_selfdestruct_logs(
377393 + Op .STOP
378394 + Op .JUMPDEST
379395 + Op .TSTORE (0 , 1 )
380- + Op .SELFDESTRUCT (selfdestruct_target )
396+ + Op .SELFDESTRUCT (target )
381397 )
382398 initcode = Initcode (deploy_code = runtime )
383399 initcode_len = len (initcode )
@@ -497,3 +513,155 @@ def test_finalization_selfdestruct_logs(
497513 )
498514
499515 state_test (env = env , pre = pre , post = post , tx = tx )
516+
517+
518+ @pytest .mark .parametrize (
519+ "funded_after_selfdestruct" ,
520+ [
521+ pytest .param (True , id = "funded_after_selfdestruct" ),
522+ pytest .param (False , id = "miner_fee_only" ),
523+ ],
524+ )
525+ def test_selfdestruct_finalization_after_priority_fee (
526+ blockchain_test : BlockchainTestFiller ,
527+ pre : Alloc ,
528+ fork : Fork ,
529+ funded_after_selfdestruct : bool ,
530+ ) -> None :
531+ """
532+ Verify finalization burn logs are emitted after priority fee payment.
533+
534+ Sets coinbase to a contract that self-destructs in the same tx. The
535+ finalization burn log includes the priority fee, proving finalization
536+ happens after fee payment per EIP-7708.
537+
538+ funded_after_selfdestruct:
539+ - if True: payer sends ETH, finalization = funding + priority_fee
540+ - if False: no payer, finalization = priority_fee only
541+ """
542+ contract_balance = 1000
543+ funding_amount = 10_000 if funded_after_selfdestruct else 0
544+
545+ sender = pre .fund_eoa ()
546+
547+ factory_address = compute_create_address (address = sender , nonce = 0 )
548+ created_address = compute_create_address (address = factory_address , nonce = 1 )
549+ coinbase = created_address # coinbase == self-destructed contract
550+
551+ # inner contract: simple SELFDESTRUCT to self
552+ runtime_code = Op .SELFDESTRUCT (Op .ADDRESS )
553+ initcode = Initcode (deploy_code = runtime_code )
554+ initcode_len = len (initcode )
555+
556+ gas_costs = fork .gas_costs ()
557+ mem_after_mstore = ((initcode_len + 31 ) // 32 ) * 32
558+
559+ # The base factory code: CREATE + CALL to trigger selfdestruct
560+ factory_code = Om .MSTORE (
561+ initcode , 0 , new_memory_size = mem_after_mstore
562+ ) + Op .CALL (
563+ gas = 100_000 ,
564+ address = Op .CREATE (
565+ value = contract_balance ,
566+ offset = 0 ,
567+ size = initcode_len ,
568+ init_code_size = initcode_len ,
569+ ),
570+ address_warm = True ,
571+ )
572+
573+ # optionally add payer call to fund coinbase after selfdestruct
574+ payer = None
575+ payer_runtime_gas = 0
576+ if funded_after_selfdestruct :
577+ payer_code = Op .SELFDESTRUCT (Op .CALLDATALOAD (0 ))
578+ payer = pre .deploy_contract (payer_code , balance = funding_amount )
579+ factory_code += Op .MSTORE (0 , created_address )
580+ factory_code += Op .CALL (
581+ gas = 100_000 , address = payer , args_offset = 0 , args_size = 32
582+ )
583+ payer_runtime_gas = Op .SELFDESTRUCT (
584+ Op .CALLDATALOAD (0 ), address_warm = True , account_new = False
585+ ).gas_cost (fork )
586+
587+ pre .fund_address (factory_address , contract_balance )
588+
589+ # prio fee calc
590+ genesis_base_fee = 7
591+ gas_price = 10
592+ base_fee = fork .base_fee_per_gas_calculator ()(
593+ parent_base_fee_per_gas = genesis_base_fee ,
594+ parent_gas_used = 0 ,
595+ parent_gas_limit = Environment ().gas_limit ,
596+ )
597+ priority_fee_per_gas = gas_price - base_fee
598+
599+ intrinsic_gas = fork .transaction_intrinsic_cost_calculator ()(
600+ calldata = bytes (factory_code ),
601+ contract_creation = True ,
602+ )
603+ factory_gas = factory_code .gas_cost (fork )
604+ initcode_exec_gas = initcode .execution_gas
605+ code_deposit_gas = len (runtime_code ) * gas_costs .G_CODE_DEPOSIT_BYTE
606+ inner_runtime_gas = Op .SELFDESTRUCT (
607+ Op .ADDRESS , address_warm = True , account_new = False
608+ ).gas_cost (fork )
609+
610+ gas_used = (
611+ intrinsic_gas
612+ + factory_gas
613+ + initcode_exec_gas
614+ + code_deposit_gas
615+ + inner_runtime_gas
616+ + payer_runtime_gas
617+ )
618+ priority_fee = priority_fee_per_gas * gas_used
619+
620+ # Finalization burn log proves coinbase received priority fee before log
621+ finalization_balance = funding_amount + priority_fee
622+
623+ expected_logs = [
624+ transfer_log (factory_address , created_address , contract_balance ),
625+ selfdestruct_log (created_address , contract_balance ),
626+ ]
627+
628+ # if funded after selfdestruct, expect transfer log from payer
629+ if funded_after_selfdestruct :
630+ assert payer is not None
631+ expected_logs .append (
632+ transfer_log (payer , created_address , funding_amount )
633+ )
634+
635+ # finalization selfdestruct log
636+ expected_logs .append (
637+ selfdestruct_log (created_address , finalization_balance )
638+ )
639+
640+ tx = Transaction (
641+ sender = sender ,
642+ to = None ,
643+ value = 0 ,
644+ data = factory_code ,
645+ gas_limit = 500_000 ,
646+ gas_price = gas_price ,
647+ expected_receipt = TransactionReceipt (logs = expected_logs ),
648+ )
649+
650+ post : dict [Address , Account | None ] = {
651+ created_address : Account .NONEXISTENT ,
652+ }
653+ if payer is not None :
654+ post [payer ] = Account (balance = 0 )
655+
656+ blockchain_test (
657+ pre = pre ,
658+ blocks = [
659+ Block (
660+ txs = [tx ],
661+ fee_recipient = coinbase ,
662+ header_verify = Header (base_fee_per_gas = base_fee ),
663+ )
664+ ],
665+ post = post ,
666+ genesis_environment = Environment (base_fee_per_gas = genesis_base_fee ),
667+ )
0 commit comments