diff --git a/tests/cancun/eip6780_selfdestruct/test_journal_revert.py b/tests/cancun/eip6780_selfdestruct/test_journal_revert.py index 9168e1e6408..1ed5f267e5a 100644 --- a/tests/cancun/eip6780_selfdestruct/test_journal_revert.py +++ b/tests/cancun/eip6780_selfdestruct/test_journal_revert.py @@ -7,10 +7,12 @@ Account, Alloc, Environment, + Fork, Op, StateTestFiller, Storage, Transaction, + TransactionReceipt, ) REFERENCE_SPEC_GIT_PATH = "EIPS/eip-6780.md" @@ -22,6 +24,7 @@ def test_selfdestruct_balance_transfer_reverted( state_test: StateTestFiller, env: Environment, pre: Alloc, + fork: Fork, ) -> None: """ Test that SELFDESTRUCT balance transfer is reverted on sub-call revert. @@ -64,6 +67,13 @@ def test_selfdestruct_balance_transfer_reverted( sender = pre.fund_eoa() + # Under EIP-7708 the SELFDESTRUCT-triggered Transfer log is emitted inside + # the reverted sub-call, so it must be discarded together with the rest of + # the reverted state. + expected_receipt = ( + TransactionReceipt(logs=[]) if fork.is_eip_enabled(7708) else None + ) + state_test( env=env, pre=pre, @@ -78,5 +88,6 @@ def test_selfdestruct_balance_transfer_reverted( sender=sender, to=outer, gas_limit=1_000_000, + expected_receipt=expected_receipt, ), ) diff --git a/tests/cancun/eip6780_selfdestruct/test_reentrancy_selfdestruct_revert.py b/tests/cancun/eip6780_selfdestruct/test_reentrancy_selfdestruct_revert.py index 7848ea91ac6..6179045a240 100644 --- a/tests/cancun/eip6780_selfdestruct/test_reentrancy_selfdestruct_revert.py +++ b/tests/cancun/eip6780_selfdestruct/test_reentrancy_selfdestruct_revert.py @@ -1,4 +1,7 @@ -"""Suicide scenario requested test https://github.com/ethereum/tests/issues/1325.""" +""" +Self-destruct scenario requested test +https://github.com/ethereum/tests/issues/1325. +""" from typing import SupportsBytes @@ -14,9 +17,12 @@ Op, StateTestFiller, Transaction, + TransactionReceipt, ) from execution_testing.forks import Cancun +from tests.amsterdam.eip7708_eth_transfer_logs.spec import transfer_log + REFERENCE_SPEC_GIT_PATH = "EIPS/eip-6780.md" REFERENCE_SPEC_VERSION = "1b6a0e94cc47e859b9866e570391cf37dc55059a" @@ -49,7 +55,7 @@ def selfdestruct_contract_address( @pytest.fixture def executor_contract_bytecode( - first_suicide: Op, + first_selfdestruct: Op, revert_contract_address: Address, selfdestruct_contract_address: Address, ) -> Bytecode: @@ -58,9 +64,11 @@ def executor_contract_bytecode( Op.SSTORE( 1, ( - first_suicide(address=selfdestruct_contract_address, value=0) - if first_suicide in [Op.CALL, Op.CALLCODE] - else first_suicide(address=selfdestruct_contract_address) + first_selfdestruct( + address=selfdestruct_contract_address, value=0 + ) + if first_selfdestruct in [Op.CALL, Op.CALLCODE] + else first_selfdestruct(address=selfdestruct_contract_address) ), ) + Op.SSTORE(2, Op.CALL(address=revert_contract_address)) @@ -100,14 +108,14 @@ def executor_contract_address( @pytest.fixture def revert_contract_bytecode( - second_suicide: Op, + second_selfdestruct: Op, selfdestruct_contract_address: Address, ) -> Bytecode: """Contract code that performs a call and then reverts.""" call_op = ( - second_suicide(address=selfdestruct_contract_address, value=100) - if second_suicide in [Op.CALL, Op.CALLCODE] - else second_suicide(address=selfdestruct_contract_address) + second_selfdestruct(address=selfdestruct_contract_address, value=100) + if second_selfdestruct in [Op.CALL, Op.CALLCODE] + else second_selfdestruct(address=selfdestruct_contract_address) ) return Op.MSTORE(0, Op.ADD(15, call_op)) + Op.REVERT(0, 32) @@ -131,18 +139,18 @@ def revert_contract_address( @pytest.mark.valid_from("Paris") @pytest.mark.parametrize( - "first_suicide", [Op.CALL, Op.CALLCODE, Op.DELEGATECALL] + "first_selfdestruct", [Op.CALL, Op.CALLCODE, Op.DELEGATECALL] ) @pytest.mark.parametrize( - "second_suicide", [Op.CALL, Op.CALLCODE, Op.DELEGATECALL] + "second_selfdestruct", [Op.CALL, Op.CALLCODE, Op.DELEGATECALL] ) def test_reentrancy_selfdestruct_revert( pre: Alloc, env: Environment, sender: EOA, fork: Fork, - first_suicide: Op, - second_suicide: Op, + first_selfdestruct: Op, + second_selfdestruct: Op, state_test: StateTestFiller, selfdestruct_contract_bytecode: Bytecode, selfdestruct_contract_address: Address, @@ -171,23 +179,24 @@ def test_reentrancy_selfdestruct_revert( ), } - if first_suicide in [Op.CALLCODE, Op.DELEGATECALL]: + if first_selfdestruct in [Op.CALLCODE, Op.DELEGATECALL]: if fork >= Cancun: # On Cancun even callcode/delegatecall does not remove the account, # so the value remain post[executor_contract_address] = Account( storage={ - 0x01: 0x01, # First call to contract S->suicide success - 0x02: 0x00, # Second call to contract S->suicide reverted + 0x01: 0x01, # 1st call to contract S->selfdestruct success + 0x02: 0x00, # 2nd call to contract S->selfdestruct revert 0x03: 16, # Reverted value to check that revert really # worked }, ) else: - # Callcode executed first suicide from sender. sender is deleted + # Callcode executed first selfdestruct from sender. + # Sender is deleted. post[executor_contract_address] = Account.NONEXISTENT # type: ignore - # Original suicide account remains in state + # Original selfdestruct account remains in state post[selfdestruct_contract_address] = Account( balance=selfdestruct_contract_init_balance, storage={} ) @@ -196,19 +205,19 @@ def test_reentrancy_selfdestruct_revert( balance=executor_contract_init_balance, ) - # On Cancun suicide no longer destroys the account from state, just cleans - # the balance - if first_suicide in [Op.CALL]: + # On Cancun selfdestruct no longer destroys the account from state, just + # cleans the balance + if first_selfdestruct in [Op.CALL]: post[executor_contract_address] = Account( storage={ - 0x01: 0x01, # First call to contract S->suicide success - 0x02: 0x00, # Second call to contract S->suicide reverted + 0x01: 0x01, # First call to contract S->selfdestruct success + 0x02: 0x00, # Second call to contract S->selfdestruct reverted 0x03: 16, # Reverted value to check that revert really worked }, ) if fork >= Cancun: - # On Cancun suicide does not remove the account, just sends the - # balance + # On Cancun selfdestruct does not remove the account, just sends + # the balance post[selfdestruct_contract_address] = Account( balance=0, code=selfdestruct_contract_bytecode, storage={} ) @@ -220,11 +229,41 @@ def test_reentrancy_selfdestruct_revert( balance=selfdestruct_contract_init_balance, ) + # Under EIP-7708 the first SELFDESTRUCT emits a Transfer log to the + # recipient; the second SELFDESTRUCT happens inside the reverted frame so + # its logs are discarded. For CALL the transfer is from S; for + # CALLCODE/DELEGATECALL the code runs in executor's context, so the + # transfer is from executor. + expected_receipt = None + if fork.is_eip_enabled(7708): + if first_selfdestruct == Op.CALL: + expected_logs = [ + transfer_log( + selfdestruct_contract_address, + selfdestruct_recipient_address, + selfdestruct_contract_init_balance, + ) + ] + elif first_selfdestruct in [Op.CALLCODE, Op.DELEGATECALL]: + expected_logs = [ + transfer_log( + executor_contract_address, + selfdestruct_recipient_address, + executor_contract_init_balance, + ) + ] + else: + raise RuntimeError( + f"Unexpected opcode for test: {first_selfdestruct}" + ) + expected_receipt = TransactionReceipt(logs=expected_logs) + tx = Transaction( sender=sender, to=executor_contract_address, gas_limit=500_000, value=0, + expected_receipt=expected_receipt, ) state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py b/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py index 37b589fd3f3..07a42652b2d 100644 --- a/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py +++ b/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py @@ -24,10 +24,16 @@ StateTestFiller, Storage, Transaction, + TransactionReceipt, compute_create_address, ) from execution_testing.forks import Cancun +from tests.amsterdam.eip7708_eth_transfer_logs.spec import ( + burn_log, + transfer_log, +) + REFERENCE_SPEC_GIT_PATH = "EIPS/eip-6780.md" REFERENCE_SPEC_VERSION = "1b6a0e94cc47e859b9866e570391cf37dc55059a" @@ -197,6 +203,7 @@ def test_create_selfdestruct_same_tx( state_test: StateTestFiller, pre: Alloc, sender: EOA, + fork: Fork, selfdestruct_code: Bytecode, sendall_recipient_addresses: List[Address], create_opcode: Op, @@ -231,8 +238,8 @@ def test_create_selfdestruct_same_tx( initcode=selfdestruct_contract_initcode, opcode=create_opcode, ) - for i in range(len(sendall_recipient_addresses)): - if sendall_recipient_addresses[i] == SELF_ADDRESS: + for i, addr in enumerate(sendall_recipient_addresses): + if addr == SELF_ADDRESS: sendall_recipient_addresses[i] = selfdestruct_contract_address if selfdestruct_contract_initial_balance > 0: pre.fund_address( @@ -281,8 +288,13 @@ def test_create_selfdestruct_same_tx( Op.EXTCODEHASH(selfdestruct_contract_address), ) + # Precompute entry_code_address (for Transfer log sender attribution) + entry_code_address = compute_create_address(address=sender, nonce=0) + # Call the self-destructing contract multiple times as required, increasing - # the wei sent each time + # the wei sent each time. Also track the sequence of EIP-7708 logs so they + # can be asserted as expected receipt logs. + expected_logs_after_tx_value: list = [] entry_code_balance = 0 for i, sendall_recipient in zip( range(call_times), cycle(sendall_recipient_addresses) @@ -303,6 +315,33 @@ def test_create_selfdestruct_same_tx( entry_code_balance += i selfdestruct_contract_current_balance += i + # CALL with value > 0 emits a Transfer log (entry_code -> contract) + if i > 0: + expected_logs_after_tx_value.append( + transfer_log( + entry_code_address, selfdestruct_contract_address, i + ) + ) + + # SELFDESTRUCT emits a Transfer log to a different address, or a Burn + # log when sending to self (contract was created in this tx). + if selfdestruct_contract_current_balance > 0: + if sendall_recipient == selfdestruct_contract_address: + expected_logs_after_tx_value.append( + burn_log( + selfdestruct_contract_address, + selfdestruct_contract_current_balance, + ) + ) + else: + expected_logs_after_tx_value.append( + transfer_log( + selfdestruct_contract_address, + sendall_recipient, + selfdestruct_contract_current_balance, + ) + ) + # Balance is always sent to other contracts if sendall_recipient != selfdestruct_contract_address: sendall_final_balances[sendall_recipient] += ( @@ -342,7 +381,7 @@ def test_create_selfdestruct_same_tx( gas_limit=500_000, ) - entry_code_address = tx.created_contract + assert tx.created_contract == entry_code_address post: Dict[Address, Account] = { entry_code_address: Account( @@ -356,11 +395,21 @@ def test_create_selfdestruct_same_tx( post[selfdestruct_contract_address] = Account.NONEXISTENT # type: ignore + if fork.is_eip_enabled(7708): + expected_logs = [] + if entry_code_balance > 0: + # tx value transfer: sender -> entry_code_address + expected_logs.append( + transfer_log(sender, entry_code_address, entry_code_balance) + ) + expected_logs.extend(expected_logs_after_tx_value) + tx.expected_receipt = TransactionReceipt(logs=expected_logs) + state_test(pre=pre, post=post, tx=tx) @pytest.mark.parametrize("create_opcode", [Op.CREATE, Op.CREATE2]) -@pytest.mark.parametrize("call_times", [0, 1]) +@pytest.mark.parametrize("call_times", [0, 1, 2]) @pytest.mark.parametrize( "selfdestruct_contract_initial_balance", [0, 100_000], @@ -370,6 +419,7 @@ def test_self_destructing_initcode( state_test: StateTestFiller, pre: Alloc, sender: EOA, + fork: Fork, selfdestruct_code: Bytecode, sendall_recipient_addresses: List[Address], create_opcode: Op, @@ -452,7 +502,7 @@ def test_self_destructing_initcode( entry_code_balance += i entry_code += Op.SSTORE( - entry_code_storage.store_next(0), + entry_code_storage.store_next(entry_code_balance), Op.BALANCE(selfdestruct_contract_address), ) @@ -489,6 +539,38 @@ def test_self_destructing_initcode( ), } + if fork.is_eip_enabled(7708): + expected_logs = [] + # tx value transfer: sender -> entry_code_address (created contract) + if entry_code_balance > 0: + expected_logs.append( + transfer_log(sender, entry_code_address, entry_code_balance) + ) + # Initcode SELFDESTRUCT sends pre-existing balance to the recipient. + if selfdestruct_contract_initial_balance > 0: + expected_logs.append( + transfer_log( + selfdestruct_contract_address, + sendall_recipient_addresses[0], + selfdestruct_contract_initial_balance, + ) + ) + # CALLs to the destroyed contract transfer ETH to it. + for i in range(call_times): + if i > 0: + expected_logs.append( + transfer_log( + entry_code_address, selfdestruct_contract_address, i + ) + ) + # At finalization the (destroyed) contract has the accumulated + # post-SELFDESTRUCT balance, which is burned. + if entry_code_balance > 0: + expected_logs.append( + burn_log(selfdestruct_contract_address, entry_code_balance) + ) + tx.expected_receipt = TransactionReceipt(logs=expected_logs) + state_test(pre=pre, post=post, tx=tx) @@ -502,6 +584,7 @@ def test_self_destructing_initcode_create_tx( state_test: StateTestFiller, pre: Alloc, sender: EOA, + fork: Fork, tx_value: int, selfdestruct_code: Bytecode, sendall_recipient_addresses: List[Address], @@ -541,6 +624,22 @@ def test_self_destructing_initcode_create_tx( ), } + if fork.is_eip_enabled(7708): + expected_logs = [] + if tx_value > 0: + expected_logs.append( + transfer_log(sender, selfdestruct_contract_address, tx_value) + ) + if sendall_amount > 0: + expected_logs.append( + transfer_log( + selfdestruct_contract_address, + sendall_recipient_addresses[0], + sendall_amount, + ) + ) + tx.expected_receipt = TransactionReceipt(logs=expected_logs) + state_test(pre=pre, post=post, tx=tx) @@ -571,6 +670,7 @@ def test_recreate_self_destructed_contract_different_txs( blockchain_test: BlockchainTestFiller, pre: Alloc, sender: EOA, + fork: Fork, selfdestruct_code: Bytecode, selfdestruct_contract_initial_balance: int, sendall_recipient_addresses: List[Address], @@ -597,7 +697,7 @@ def test_recreate_self_destructed_contract_different_txs( entry_code_storage = Storage() sendall_amount = selfdestruct_contract_initial_balance - # Bytecode used to create the contract + # Validate bytecode used to create the contract assert create_opcode != Op.CREATE, ( "cannot recreate contract using CREATE opcode" ) @@ -645,18 +745,46 @@ def test_recreate_self_destructed_contract_different_txs( selfdestruct_contract_address, selfdestruct_contract_initial_balance, ) - for i in range(len(sendall_recipient_addresses)): - if sendall_recipient_addresses[i] == SELF_ADDRESS: + for i, addr in enumerate(sendall_recipient_addresses): + if addr == SELF_ADDRESS: sendall_recipient_addresses[i] = selfdestruct_contract_address txs: List[Transaction] = [] for i in range(recreate_times + 1): + expected_receipt = None + if fork.is_eip_enabled(7708): + # First tx: contract is recreated at the pre-funded address, then + # SELFDESTRUCTs transferring initial_balance to the recipient + # (or emitting a Burn log when SD to self). Subsequent txs see + # address with 0 balance (destroyed+cleared), so no log. + tx_logs: list = [] + if i == 0 and selfdestruct_contract_initial_balance > 0: + if ( + sendall_recipient_addresses[0] + == selfdestruct_contract_address + ): + tx_logs.append( + burn_log( + selfdestruct_contract_address, + selfdestruct_contract_initial_balance, + ) + ) + else: + tx_logs.append( + transfer_log( + selfdestruct_contract_address, + sendall_recipient_addresses[0], + selfdestruct_contract_initial_balance, + ) + ) + expected_receipt = TransactionReceipt(logs=tx_logs) txs.append( Transaction( data=Hash(i), sender=sender, to=entry_code_address, gas_limit=500_000, + expected_receipt=expected_receipt, ) ) entry_code_storage[i] = selfdestruct_contract_address @@ -737,6 +865,7 @@ def test_selfdestruct_pre_existing( eip_enabled: bool, pre: Alloc, sender: EOA, + fork: Fork, selfdestruct_code: Bytecode, selfdestruct_contract_initial_balance: int, sendall_recipient_addresses: List[Address], @@ -760,8 +889,8 @@ def test_selfdestruct_pre_existing( ) entry_code_storage = Storage() - for i in range(len(sendall_recipient_addresses)): - if sendall_recipient_addresses[i] == SELF_ADDRESS: + for i, addr in enumerate(sendall_recipient_addresses): + if addr == SELF_ADDRESS: sendall_recipient_addresses[i] = selfdestruct_contract_address # Create a dict to record the expected final balances @@ -780,8 +909,12 @@ def test_selfdestruct_pre_existing( # destructing contract, as many times as required entry_code = Bytecode() + # Pre-compute the entry_code_address to use for Transfer log attribution. + entry_code_address = compute_create_address(address=sender, nonce=0) + # Call the self-destructing contract multiple times as required, increasing # the wei sent each time + expected_logs_after_tx_value: list = [] entry_code_balance = 0 for i, sendall_recipient in zip( range(call_times), cycle(sendall_recipient_addresses) @@ -802,6 +935,31 @@ def test_selfdestruct_pre_existing( entry_code_balance += i selfdestruct_contract_current_balance += i + # CALL with nonzero value emits Transfer(entry_code -> contract). + if i > 0: + expected_logs_after_tx_value.append( + transfer_log( + entry_code_address, selfdestruct_contract_address, i + ) + ) + + # SELFDESTRUCT emits Transfer to a different recipient; for a + # pre-existing contract sending to itself, no log is emitted (balance + # stays). Pre-Cancun, SD also burns on self, but EIP-7708 is + # Amsterdam+, long after EIP-6780 is enabled, so the self-keep path + # applies here. + if ( + sendall_recipient != selfdestruct_contract_address + and selfdestruct_contract_current_balance > 0 + ): + expected_logs_after_tx_value.append( + transfer_log( + selfdestruct_contract_address, + sendall_recipient, + selfdestruct_contract_current_balance, + ) + ) + # Balance is always sent to other contracts if sendall_recipient != selfdestruct_contract_address: sendall_final_balances[sendall_recipient] += ( @@ -847,7 +1005,7 @@ def test_selfdestruct_pre_existing( gas_limit=500_000, ) - entry_code_address = tx.created_contract + assert tx.created_contract == entry_code_address post: Dict[Address, Account] = { entry_code_address: Account( @@ -869,6 +1027,15 @@ def test_selfdestruct_pre_existing( else: post[selfdestruct_contract_address] = Account.NONEXISTENT # type: ignore + if fork.is_eip_enabled(7708): + expected_logs = [] + if entry_code_balance > 0: + expected_logs.append( + transfer_log(sender, entry_code_address, entry_code_balance) + ) + expected_logs.extend(expected_logs_after_tx_value) + tx.expected_receipt = TransactionReceipt(logs=expected_logs) + state_test(pre=pre, post=post, tx=tx) @@ -880,6 +1047,7 @@ def test_selfdestruct_created_same_block_different_tx( eip_enabled: bool, pre: Alloc, sender: EOA, + fork: Fork, selfdestruct_contract_initial_balance: int, sendall_recipient_addresses: List[Address], call_times: int, @@ -959,6 +1127,45 @@ def test_selfdestruct_created_same_block_different_tx( else: post[selfdestruct_contract_address] = Account.NONEXISTENT # type: ignore + tx1_receipt = None + tx2_receipt = None + if fork.is_eip_enabled(7708): + tx1_logs = [] + if selfdestruct_contract_initial_balance > 0: + tx1_logs.append( + transfer_log( + sender, + selfdestruct_contract_address, + selfdestruct_contract_initial_balance, + ) + ) + tx1_receipt = TransactionReceipt(logs=tx1_logs) + + tx2_logs = [] + if entry_code_balance > 0: + tx2_logs.append( + transfer_log(sender, entry_code_address, entry_code_balance) + ) + running_balance = selfdestruct_contract_initial_balance + for i in range(call_times): + if i > 0: + tx2_logs.append( + transfer_log( + entry_code_address, selfdestruct_contract_address, i + ) + ) + running_balance += i + if running_balance > 0: + tx2_logs.append( + transfer_log( + selfdestruct_contract_address, + sendall_recipient_addresses[0], + running_balance, + ) + ) + running_balance = 0 + tx2_receipt = TransactionReceipt(logs=tx2_logs) + txs = [ Transaction( value=selfdestruct_contract_initial_balance, @@ -966,6 +1173,7 @@ def test_selfdestruct_created_same_block_different_tx( sender=sender, to=None, gas_limit=500_000, + expected_receipt=tx1_receipt, ), Transaction( value=entry_code_balance, @@ -973,6 +1181,7 @@ def test_selfdestruct_created_same_block_different_tx( sender=sender, to=None, gas_limit=500_000, + expected_receipt=tx2_receipt, ), ] @@ -988,6 +1197,7 @@ def test_calling_from_new_contract_to_pre_existing_contract( state_test: StateTestFiller, pre: Alloc, sender: EOA, + fork: Fork, sendall_recipient_addresses: List[Address], create_opcode: Op, call_opcode: Op, @@ -1121,6 +1331,35 @@ def test_calling_from_new_contract_to_pre_existing_contract( gas_limit=500_000, ) + if fork.is_eip_enabled(7708): + # The new contract's body is a DELEGATECALL/CALLCODE to the pre- + # existing selfdestruct code, so SELFDESTRUCT runs in the NEW + # contract's context and transfers its balance to the recipient. + expected_logs = [] + if entry_code_balance > 0: + expected_logs.append( + transfer_log(sender, entry_code_address, entry_code_balance) + ) + running_balance = selfdestruct_contract_initial_balance + for i in range(call_times): + if i > 0: + expected_logs.append( + transfer_log( + entry_code_address, selfdestruct_contract_address, i + ) + ) + running_balance += i + if running_balance > 0: + expected_logs.append( + transfer_log( + selfdestruct_contract_address, + sendall_recipient_addresses[0], + running_balance, + ) + ) + running_balance = 0 + tx.expected_receipt = TransactionReceipt(logs=expected_logs) + state_test(pre=pre, post=post, tx=tx) @@ -1135,6 +1374,7 @@ def test_calling_from_pre_existing_contract_to_new_contract( eip_enabled: bool, pre: Alloc, sender: EOA, + fork: Fork, selfdestruct_code: Bytecode, sendall_recipient_addresses: List[Address], call_opcode: Op, @@ -1277,6 +1517,42 @@ def test_calling_from_pre_existing_contract_to_new_contract( else: post[caller_address] = Account.NONEXISTENT # type: ignore + if fork.is_eip_enabled(7708): + # SELFDESTRUCT runs in the caller (pre-existing) contract's context + # via DELEGATECALL/CALLCODE, so the Transfer log attributes the + # balance flow from the caller to the recipient. + expected_logs = [] + if entry_code_balance > 0: + expected_logs.append( + transfer_log(sender, entry_code_address, entry_code_balance) + ) + if selfdestruct_contract_initial_balance > 0: + # CREATE with value: entry_code -> new selfdestruct_contract + expected_logs.append( + transfer_log( + entry_code_address, + selfdestruct_contract_address, + selfdestruct_contract_initial_balance, + ) + ) + caller_running_balance = pre_existing_contract_initial_balance + for i in range(call_times): + if i > 0: + expected_logs.append( + transfer_log(entry_code_address, caller_address, i) + ) + caller_running_balance += i + if caller_running_balance > 0: + expected_logs.append( + transfer_log( + caller_address, + sendall_recipient_addresses[0], + caller_running_balance, + ) + ) + caller_running_balance = 0 + tx.expected_receipt = TransactionReceipt(logs=expected_logs) + state_test(pre=pre, post=post, tx=tx) @@ -1297,6 +1573,7 @@ def test_create_selfdestruct_same_tx_increased_nonce( state_test: StateTestFiller, pre: Alloc, sender: EOA, + fork: Fork, selfdestruct_code: Bytecode, sendall_recipient_addresses: List[Address], create_opcode: Op, @@ -1377,8 +1654,11 @@ def test_create_selfdestruct_same_tx_increased_nonce( Op.EXTCODEHASH(selfdestruct_contract_address), ) + entry_code_address = compute_create_address(address=sender, nonce=0) + # Call the self-destructing contract multiple times as required, increasing # the wei sent each time + expected_logs_after_tx_value: list = [] entry_code_balance = 0 for i, sendall_recipient in zip( range(call_times), cycle(sendall_recipient_addresses) @@ -1399,6 +1679,28 @@ def test_create_selfdestruct_same_tx_increased_nonce( entry_code_balance += i selfdestruct_contract_current_balance += i + # CALL with value > 0 emits a Transfer log (entry_code -> contract). + # The inner CREATE(value=0) prepended to selfdestruct_code does not + # emit a log (zero value). + if i > 0: + expected_logs_after_tx_value.append( + transfer_log( + entry_code_address, selfdestruct_contract_address, i + ) + ) + + # SELFDESTRUCT always sends to a pre-deployed recipient in this test + # (SELF_ADDRESS is not parametrized here), so a Transfer log is + # emitted whenever the contract has a nonzero balance. + if selfdestruct_contract_current_balance > 0: + expected_logs_after_tx_value.append( + transfer_log( + selfdestruct_contract_address, + sendall_recipient, + selfdestruct_contract_current_balance, + ) + ) + # Balance is always sent to other contracts if sendall_recipient != selfdestruct_contract_address: sendall_final_balances[sendall_recipient] += ( @@ -1438,7 +1740,7 @@ def test_create_selfdestruct_same_tx_increased_nonce( gas_limit=1_000_000, ) - entry_code_address = tx.created_contract + assert tx.created_contract == entry_code_address post: Dict[Address, Account] = { entry_code_address: Account( @@ -1468,6 +1770,15 @@ def test_create_selfdestruct_same_tx_increased_nonce( post[selfdestruct_contract_address] = Account.NONEXISTENT # type: ignore + if fork.is_eip_enabled(7708): + expected_logs = [] + if entry_code_balance > 0: + expected_logs.append( + transfer_log(sender, entry_code_address, entry_code_balance) + ) + expected_logs.extend(expected_logs_after_tx_value) + tx.expected_receipt = TransactionReceipt(logs=expected_logs) + state_test(pre=pre, post=post, tx=tx) @@ -1478,6 +1789,7 @@ def test_create_and_destroy_multiple_contracts_same_tx( state_test: StateTestFiller, pre: Alloc, sender: EOA, + fork: Fork, num_contracts: int, selfdestruct_contract_initial_balance: int, ) -> None: @@ -1583,6 +1895,23 @@ def test_create_and_destroy_multiple_contracts_same_tx( for addr in contract_addresses: post[addr] = Account.NONEXISTENT # type: ignore + if fork.is_eip_enabled(7708): + # Each contract SELFDESTRUCTs to a shared recipient after being created + # in the same tx. CREATE2 uses value=0 and CALLs use value=0, so the + # only Transfer logs are emitted when each contract's pre-funded + # balance is sent to the recipient via SELFDESTRUCT. + expected_logs = [] + if selfdestruct_contract_initial_balance > 0: + for addr in contract_addresses: + expected_logs.append( + transfer_log( + addr, + sendall_recipient, + selfdestruct_contract_initial_balance, + ) + ) + tx.expected_receipt = TransactionReceipt(logs=expected_logs) + state_test(pre=pre, post=post, tx=tx) @@ -1593,6 +1922,7 @@ def test_create_multiple_contracts_destroy_one_then_destroy_other_next_tx( eip_enabled: bool, pre: Alloc, sender: EOA, + fork: Fork, selfdestruct_contract_initial_balance: int, ) -> None: """ @@ -1694,16 +2024,48 @@ def test_create_multiple_contracts_destroy_one_then_destroy_other_next_tx( + Op.STOP ) + tx1_receipt = None + tx2_receipt = None + if fork.is_eip_enabled(7708): + # Tx1: only A SELFDESTRUCTs (flag=1), transferring its pre-funded + # balance to the shared recipient. B gets called with flag=0 and + # returns without emitting any log. + tx1_logs = [] + if selfdestruct_contract_initial_balance > 0: + tx1_logs.append( + transfer_log( + contract_a_address, + sendall_recipient, + selfdestruct_contract_initial_balance, + ) + ) + tx1_receipt = TransactionReceipt(logs=tx1_logs) + + # Tx2: B (now pre-existing) SELFDESTRUCTs transferring its + # pre-funded balance to the recipient. + tx2_logs = [] + if selfdestruct_contract_initial_balance > 0: + tx2_logs.append( + transfer_log( + contract_b_address, + sendall_recipient, + selfdestruct_contract_initial_balance, + ) + ) + tx2_receipt = TransactionReceipt(logs=tx2_logs) + txs = [ Transaction( sender=sender, to=entry_code_address, gas_limit=1_000_000, + expected_receipt=tx1_receipt, ), Transaction( sender=sender, to=tx2_caller, gas_limit=500_000, + expected_receipt=tx2_receipt, ), ] @@ -1742,6 +2104,7 @@ def test_parent_creates_child_selfdestruct_one( state_test: StateTestFiller, pre: Alloc, sender: EOA, + fork: Fork, destroy_parent: bool, selfdestruct_contract_initial_balance: int, ) -> None: @@ -1857,6 +2220,22 @@ def test_parent_creates_child_selfdestruct_one( storage={0: 1}, ) + if fork.is_eip_enabled(7708): + # Only the SELFDESTRUCT that actually runs emits a log. Both parent + # and child are pre-funded via pre.fund_address, so whichever + # SELFDESTRUCTs to the shared recipient transfers its initial_balance. + expected_logs = [] + if selfdestruct_contract_initial_balance > 0: + source = parent_address if destroy_parent else child_address + expected_logs.append( + transfer_log( + source, + sendall_recipient, + selfdestruct_contract_initial_balance, + ) + ) + tx.expected_receipt = TransactionReceipt(logs=expected_logs) + state_test(pre=pre, post=post, tx=tx) @@ -1868,6 +2247,7 @@ def test_recursive_contract_creation_and_selfdestruct( state_test: StateTestFiller, pre: Alloc, sender: EOA, + fork: Fork, recursion_depth: int, selfdestruct_on_unwind: bool, selfdestruct_contract_initial_balance: int, @@ -2022,4 +2402,25 @@ def test_recursive_contract_creation_and_selfdestruct( storage={0: 1}, ) + if fork.is_eip_enabled(7708): + # CREATE/CALL all use value=0, so the only Transfer logs come from + # each SELFDESTRUCT that runs. On unwind every contract SDs, starting + # from the deepest; otherwise only the deepest SDs. + expected_logs = [] + if selfdestruct_contract_initial_balance > 0: + sd_sources = ( + list(reversed(contract_addresses)) + if selfdestruct_on_unwind + else [contract_addresses[-1]] + ) + for addr in sd_sources: + expected_logs.append( + transfer_log( + addr, + sendall_recipient, + selfdestruct_contract_initial_balance, + ) + ) + tx.expected_receipt = TransactionReceipt(logs=expected_logs) + state_test(pre=pre, post=post, tx=tx)