Skip to content

test_self_destructing_initcode()

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

Generate fixtures for these test cases for Bogota with:

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

Test that a contract can self-destruct in its initcode.

Behavior is the same before and after EIP-6780.

Test using
  • Different initial balances for the self-destructing contract
  • Different opcodes: CREATE, CREATE2
  • Different number of calls to the self-destructing contract in the same tx
Source code in tests/cancun/eip6780_selfdestruct/test_selfdestruct.py
@pytest.mark.parametrize("create_opcode", [Op.CREATE, Op.CREATE2])
@pytest.mark.parametrize("call_times", [0, 1, 2])
@pytest.mark.parametrize(
    "selfdestruct_contract_initial_balance",
    [0, 100_000],
)
@pytest.mark.valid_from("Shanghai")
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,
    call_times: int,  # Number of times to call the self-destructing contract
    # in the same tx
    selfdestruct_contract_initial_balance: int,
) -> None:
    """
    Test that a contract can self-destruct in its initcode.

    Behavior is the same before and after EIP-6780.

    Test using:
        - Different initial balances for the self-destructing contract
        - Different opcodes: CREATE, CREATE2
        - Different number of calls to the self-destructing contract in
           the same tx
    """
    initcode_copy_from_address = pre.deploy_contract(selfdestruct_code)
    # Our entry point is an initcode that in turn creates a self-destructing
    # contract
    entry_code_storage = Storage()
    sendall_amount = 0

    # Bytecode used to create the contract, can be CREATE or CREATE2
    create_bytecode = create_opcode(size=len(selfdestruct_code))

    selfdestruct_contract_address = compute_create_address(
        address=compute_create_address(address=sender, nonce=0),
        nonce=1,
        initcode=selfdestruct_code,
        opcode=create_opcode,
    )

    # 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_code),
        )
        # 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(0),
        Op.EXTCODESIZE(selfdestruct_contract_address),
    )

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

    # Call the self-destructing contract multiple times as required, increasing
    # the wei sent each time
    entry_code_balance = 0
    selfdestruct_contract_address_remaining_balance = 0
    for i in range(call_times):
        entry_code += Op.SSTORE(
            entry_code_storage.store_next(1),
            Op.CALL(
                Op.GASLIMIT,  # Gas
                selfdestruct_contract_address,  # Address
                i,  # Value
                0,
                0,
                0,
                0,
            ),
        )
        entry_code_balance += i

        if fork.is_eip_enabled(8246):
            # After the first call, this call value will become stuck since
            # EIP-8246 disables self-destruct burns.
            selfdestruct_contract_address_remaining_balance += i

        entry_code += Op.SSTORE(
            entry_code_storage.store_next(entry_code_balance),
            Op.BALANCE(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_code), 32), 1)

    if selfdestruct_contract_initial_balance > 0:
        # Address where the contract is created already had some balance,
        # which must be included in the send-all operation
        sendall_amount += selfdestruct_contract_initial_balance
        pre.fund_address(
            selfdestruct_contract_address,
            selfdestruct_contract_initial_balance,
        )

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

    entry_code_address = tx.created_contract

    post: Dict[Address, Account] = {
        entry_code_address: Account(
            storage=entry_code_storage,
        ),
        selfdestruct_contract_address: Account.NONEXISTENT
        if selfdestruct_contract_address_remaining_balance == 0
        else Account(balance=selfdestruct_contract_address_remaining_balance),  # type: ignore
        sendall_recipient_addresses[0]: Account(
            balance=sendall_amount, storage={0: 1}
        ),
    }

    expected_logs = []
    if fork.is_eip_enabled(7708):
        # 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
                    )
                )
    tx.expected_receipt = TransactionReceipt(logs=expected_logs)

    state_test(pre=pre, post=post, tx=tx)

Parametrized Test Cases

This test generates 12 parametrized test cases across 6 forks.