Skip to content

test_selfdestruct_pre_existing()

Documentation for tests/cancun/eip6780_selfdestruct/test_selfdestruct.py::test_selfdestruct_pre_existing@ffe48572.

Generate fixtures for these test cases for Bogota with:

fill -v tests/cancun/eip6780_selfdestruct/test_selfdestruct.py::test_selfdestruct_pre_existing --fork Bogota

Test calling a previously created account that contains a selfdestruct, and verify its balance is sent to the destination address.

After EIP-6780, the balance should be sent to the send-all recipient address, similar to the behavior before the EIP, but the account is not deleted.

Test using: - Different send-all recipient addresses: single, multiple, including self - Different initial balances for the self-destructing contract

Source code in tests/cancun/eip6780_selfdestruct/test_selfdestruct.py
@pytest.mark.parametrize(
    "call_times,sendall_recipient_addresses",
    [
        pytest.param(
            1,
            [PRE_DEPLOY_CONTRACT_1],
            id="single_call",
        ),
        pytest.param(
            1,
            [SELF_ADDRESS],
            id="single_call_self",
        ),
        pytest.param(
            2,
            [PRE_DEPLOY_CONTRACT_1],
            id="multiple_calls_single_sendall_recipient",
        ),
        pytest.param(
            2,
            [SELF_ADDRESS],
            id="multiple_calls_single_self_recipient",
        ),
        pytest.param(
            3,
            [
                PRE_DEPLOY_CONTRACT_1,
                PRE_DEPLOY_CONTRACT_2,
                PRE_DEPLOY_CONTRACT_3,
            ],
            id="multiple_calls_multiple_sendall_recipients",
        ),
        pytest.param(
            3,
            [SELF_ADDRESS, PRE_DEPLOY_CONTRACT_2, PRE_DEPLOY_CONTRACT_3],
            id="multiple_calls_multiple_sendall_recipients_including_self",
        ),
        pytest.param(
            3,
            [PRE_DEPLOY_CONTRACT_1, PRE_DEPLOY_CONTRACT_2, SELF_ADDRESS],
            id="multiple_calls_multiple_sendall_recipients_including_self_last",
        ),
        pytest.param(
            6,
            [SELF_ADDRESS, PRE_DEPLOY_CONTRACT_2, PRE_DEPLOY_CONTRACT_3],
            id="multiple_calls_multiple_repeating_sendall_recipients_including_self",
        ),
        pytest.param(
            6,
            [PRE_DEPLOY_CONTRACT_1, PRE_DEPLOY_CONTRACT_2, SELF_ADDRESS],
            id="multiple_calls_multiple_repeating_sendall_recipients_including_self_last",
        ),
    ],
    indirect=["sendall_recipient_addresses"],
)
@pytest.mark.parametrize("selfdestruct_contract_initial_balance", [0, 100_000])
@pytest.mark.valid_from("Shanghai")
def test_selfdestruct_pre_existing(
    state_test: StateTestFiller,
    eip_enabled: bool,
    pre: Alloc,
    sender: EOA,
    fork: Fork,
    selfdestruct_code: Bytecode,
    selfdestruct_contract_initial_balance: int,
    sendall_recipient_addresses: List[Address],
    call_times: int,
) -> None:
    """
    Test calling a previously created account that contains a selfdestruct, and
    verify its balance is sent to the destination address.

    After EIP-6780, the balance should be sent to the send-all recipient
    address, similar to the behavior before the EIP, but the account is not
    deleted.

    Test using:
    - Different send-all recipient addresses: single, multiple,
       including self
    - Different initial balances for the self-destructing contract
    """
    selfdestruct_contract_address = pre.deploy_contract(
        selfdestruct_code, balance=selfdestruct_contract_initial_balance
    )
    entry_code_storage = Storage()

    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
    sendall_final_balances = dict(
        zip(
            sendall_recipient_addresses,
            [0] * len(sendall_recipient_addresses),
            strict=False,
        )
    )
    selfdestruct_contract_current_balance = (
        selfdestruct_contract_initial_balance
    )

    # Entry code in this case will simply call the pre-existing self-
    # 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)
    ):
        entry_code += Op.MSTORE(0, sendall_recipient)
        entry_code += Op.SSTORE(
            entry_code_storage.store_next(1),
            Op.CALL(
                Op.GASLIMIT,  # Gas
                selfdestruct_contract_address,  # Address
                i,  # Value
                0,
                32,
                0,
                0,
            ),
        )
        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] += (
                selfdestruct_contract_current_balance
            )

        # Balance is only kept by the self-destructing contract if we are
        # sending to self and the EIP is activated, otherwise the balance is
        # destroyed
        if (
            sendall_recipient != selfdestruct_contract_address
            or not eip_enabled
        ):
            selfdestruct_contract_current_balance = 0

        entry_code += Op.SSTORE(
            entry_code_storage.store_next(
                selfdestruct_contract_current_balance
            ),
            Op.BALANCE(selfdestruct_contract_address),
        )

    # Check the EXTCODE* properties of the self-destructing contract
    entry_code += Op.SSTORE(
        entry_code_storage.store_next(len(selfdestruct_code)),
        Op.EXTCODESIZE(selfdestruct_contract_address),
    )

    entry_code += Op.SSTORE(
        entry_code_storage.store_next(selfdestruct_code.keccak256()),
        Op.EXTCODEHASH(selfdestruct_contract_address),
    )

    # Lastly return zero so the entry point contract is created and we can
    # retain the stored values for verification.
    entry_code += Op.RETURN(32, 1)

    tx = Transaction(
        value=entry_code_balance,
        data=entry_code,
        sender=sender,
        to=None,
    )

    assert tx.created_contract == entry_code_address

    post: Dict[Address, Account] = {
        entry_code_address: Account(
            storage=entry_code_storage,
        ),
    }

    # Check the balances of the sendall recipients
    for address, balance in sendall_final_balances.items():
        if address != selfdestruct_contract_address:
            post[address] = Account(balance=balance, storage={0: 1})

    if eip_enabled:
        balance = selfdestruct_contract_current_balance
        post[selfdestruct_contract_address] = Account(
            balance=balance,
            storage={0: call_times},
        )
    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)

Parametrized Test Cases

This test generates 18 parametrized test cases across 6 forks.