@@ -528,3 +528,109 @@ def test_selfdestruct_pre_existing_account_no_refund(
528528 blocks = [Block (txs = [tx ], header_verify = Header (gas_used = tx_regular ))],
529529 post = {victim : Account (code = victim_code )},
530530 )
531+
532+
533+ @pytest .mark .parametrize (
534+ "num_hops" ,
535+ [
536+ pytest .param (1 , id = "single_hop" ),
537+ pytest .param (2 , id = "two_hops" ),
538+ ],
539+ )
540+ @pytest .mark .with_all_call_opcodes (
541+ selector = lambda call_opcode : call_opcode in (Op .DELEGATECALL , Op .CALLCODE )
542+ )
543+ @pytest .mark .valid_from ("EIP8037" )
544+ def test_selfdestruct_via_delegatecall_chain (
545+ blockchain_test : BlockchainTestFiller ,
546+ pre : Alloc ,
547+ fork : Fork ,
548+ num_hops : int ,
549+ call_opcode : Op ,
550+ ) -> None :
551+ """
552+ Verify SELFDESTRUCT refund when the opcode executes in a nested
553+ DELEGATECALL/CALLCODE frame below a same-tx-created contract.
554+
555+ A factory CREATEs contract A; A delegates down `num_hops` frames
556+ into a helper that runs SELFDESTRUCT(Op.ADDRESS). `current_target`
557+ is preserved by DELEGATECALL/CALLCODE, so A is queued for deletion
558+ and its account + code-deposit state gas is refunded at tx end.
559+ Exercises `accounts_to_delete` propagation across multiple
560+ `incorporate_child_on_success` hops.
561+ """
562+ gas_limit_cap = fork .transaction_gas_limit_cap ()
563+ assert gas_limit_cap is not None
564+ new_account_state_gas = fork .gas_costs ().GAS_NEW_ACCOUNT
565+ sstore_state_gas = fork .sstore_state_gas ()
566+
567+ # Bottom of the chain does the SELFDESTRUCT; intermediate helpers
568+ # just delegate further down.
569+ delegate_target = pre .deploy_contract (code = Op .SELFDESTRUCT (Op .ADDRESS ))
570+ for _ in range (num_hops - 1 ):
571+ delegate_target = pre .deploy_contract (
572+ code = Op .POP (call_opcode (gas = Op .GAS , address = delegate_target ))
573+ + Op .STOP ,
574+ )
575+
576+ # A's deployed runtime: one delegation into the top of the chain.
577+ deployed = bytes (
578+ Op .POP (call_opcode (gas = Op .GAS , address = delegate_target )) + Op .STOP
579+ )
580+ code_deposit_state_gas = fork .code_deposit_state_gas (
581+ code_size = len (deployed )
582+ )
583+ initcode = Initcode (deploy_code = deployed )
584+ initcode_len = len (initcode )
585+
586+ # Slots 0 and 1 guard against a vacuously-NONEXISTENT A: slot 0
587+ # fails if CREATE silently returned 0, slot 1 fails if the factory
588+ # OOGed before completing the nested CALL. TSTORE caches the
589+ # CREATE return so both can reuse it.
590+ factory_storage = Storage ()
591+ factory_code = (
592+ Op .CALLDATACOPY (
593+ 0 ,
594+ 0 ,
595+ Op .CALLDATASIZE ,
596+ data_size = initcode_len ,
597+ new_memory_size = initcode_len ,
598+ )
599+ + Op .TSTORE (
600+ 0 ,
601+ Op .CREATE (
602+ value = 0 ,
603+ offset = 0 ,
604+ size = Op .CALLDATASIZE ,
605+ init_code_size = initcode_len ,
606+ ),
607+ )
608+ + Op .SSTORE (
609+ factory_storage .store_next (1 , "create_returned_nonzero" ),
610+ Op .ISZERO (Op .ISZERO (Op .TLOAD (0 ))),
611+ )
612+ + Op .SSTORE (
613+ factory_storage .store_next (1 , "call_returned_success" ),
614+ Op .CALL (gas = Op .GAS , address = Op .TLOAD (0 )),
615+ )
616+ )
617+ factory = pre .deploy_contract (code = factory_code )
618+ created_address = compute_create_address (address = factory , nonce = 1 )
619+
620+ # Reservoir must also cover the two fresh SSTORE markers.
621+ total_state_refund = new_account_state_gas + code_deposit_state_gas
622+ tx = Transaction (
623+ to = factory ,
624+ data = bytes (initcode ),
625+ gas_limit = gas_limit_cap + total_state_refund + 2 * sstore_state_gas ,
626+ sender = pre .fund_eoa (),
627+ )
628+
629+ blockchain_test (
630+ pre = pre ,
631+ blocks = [Block (txs = [tx ])],
632+ post = {
633+ created_address : Account .NONEXISTENT ,
634+ factory : Account (storage = factory_storage ),
635+ },
636+ )
0 commit comments