Skip to content

test_create_selfdestruct_same_tx()

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

Generate fixtures for these test cases for Bogota with:

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

Use CREATE or CREATE2 to create a self-destructing contract, and call it in the same transaction.

Behavior should be the same before and after EIP-6780.

Test using
  • Different send-all recipient addresses: single, multiple, including self
  • Different initial balances for the self-destructing contract
  • Different opcodes: CREATE, CREATE2
Source code in tests/cancun/eip6780_selfdestruct/test_selfdestruct.py
@pytest.mark.parametrize("create_opcode", [Op.CREATE, Op.CREATE2])
@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_create_selfdestruct_same_tx(
    state_test: StateTestFiller,
    pre: Alloc,
    sender: EOA,
    fork: Fork,
    selfdestruct_code: Bytecode,
    sendall_recipient_addresses: List[Address],
    create_opcode: Op,
    call_times: int,
    selfdestruct_contract_initial_balance: int,
) -> None:
    """
    Use CREATE or CREATE2 to create a self-destructing contract, and call it in
    the same transaction.

    Behavior should be the same before and after EIP-6780.

    Test using:
        - Different send-all recipient addresses: single, multiple,
           including self
        - Different initial balances for the self-destructing contract
        - Different opcodes: CREATE, CREATE2
    """
    selfdestruct_contract_initcode = Initcode(deploy_code=selfdestruct_code)
    initcode_copy_from_address = pre.deploy_contract(
        selfdestruct_contract_initcode
    )
    # Our entry point is an initcode that in turn creates a self-destructing
    # contract
    entry_code_storage = Storage()

    # Bytecode used to create the contract, can be CREATE or CREATE2
    create_bytecode = create_opcode(size=len(selfdestruct_contract_initcode))
    selfdestruct_contract_address = compute_create_address(
        address=compute_create_address(address=sender, nonce=0),
        nonce=1,
        initcode=selfdestruct_contract_initcode,
        opcode=create_opcode,
    )
    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(
            selfdestruct_contract_address,
            selfdestruct_contract_initial_balance,
        )

    # 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 that will be executed, creates the contract and then calls it
    # in the same tx
    entry_code = (
        # Initcode is already deployed at `initcode_copy_from_address`, so just
        # copy it
        Op.EXTCODECOPY(
            initcode_copy_from_address,
            0,
            0,
            len(selfdestruct_contract_initcode),
        )
        # And we store the created address for verification purposes
        + Op.SSTORE(
            entry_code_storage.store_next(selfdestruct_contract_address),
            create_bytecode,
        )
    )

    # Store the EXTCODE* properties of the created address
    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),
    )

    # 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. 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)
    ):
        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 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(
                    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
            )
            selfdestruct_contract_current_balance = 0
        elif not fork.is_eip_enabled(8246):
            # per EIP-8246
            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 again
    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(max(len(selfdestruct_contract_initcode), 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():
        post[address] = Account(balance=balance, storage={0: 1})

    if fork.is_eip_enabled(8246) and selfdestruct_contract_current_balance > 0:
        # per EIP-8246
        post[selfdestruct_contract_address] = Account(
            balance=selfdestruct_contract_current_balance,
            nonce=0,
            code=b"",
            storage={},
        )
    else:
        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)

Parametrized Test Cases

This test generates 36 parametrized test cases across 6 forks.