Skip to content

test_parent_creates_child_selfdestruct_one()

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

Generate fixtures for these test cases for Bogota with:

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

Test a parent contract that creates a child contract, then only one of them self-destructs (either parent or child, based on parameter).

Both contracts are created in the same transaction: - If destroy_parent=True: Parent self-destructs, child survives - If destroy_parent=False: Child self-destructs, parent survives

Since both are created in the same tx, whichever self-destructs should be deleted.

Source code in tests/cancun/eip6780_selfdestruct/test_selfdestruct.py
@pytest.mark.parametrize("destroy_parent", [True, False])
@pytest.mark.parametrize("selfdestruct_contract_initial_balance", [0, 100_000])
@pytest.mark.valid_from("Shanghai")
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:
    """
    Test a parent contract that creates a child contract, then only one
    of them self-destructs (either parent or child, based on parameter).

    Both contracts are created in the same transaction:
    - If destroy_parent=True: Parent self-destructs, child survives
    - If destroy_parent=False: Child self-destructs, parent survives

    Since both are created in the same tx, whichever self-destructs should be
    deleted.
    """
    entry_code_storage = Storage()

    sendall_recipient = pre.deploy_contract(
        code=Op.SSTORE(0, 0),
        storage={0: 1},
    )

    # Child contract: just has code, self-destructs when called
    child_code = Op.SSTORE(0, Op.ADD(Op.SLOAD(0), 1)) + Op.SELFDESTRUCT(
        sendall_recipient
    )
    child_initcode = Initcode(deploy_code=child_code)

    # Parent contract: creates child, then either self-destructs or calls child
    # to self-destruct based on calldata[0]: 1 = destroy parent, 0 = destroy
    # child
    # For simplicity, use pre-deployed child initcode
    child_initcode_address = pre.deploy_contract(child_initcode)

    parent_code = (
        Op.SSTORE(0, Op.ADD(Op.SLOAD(0), 1))
        + Op.EXTCODECOPY(child_initcode_address, 0, 0, len(child_initcode))
        + Op.SSTORE(1, Op.CREATE(value=0, offset=0, size=len(child_initcode)))
        + Conditional(
            condition=Op.EQ(Op.CALLDATALOAD(0), 1),
            if_true=Op.SELFDESTRUCT(sendall_recipient),
            if_false=Op.CALL(Op.GASLIMIT, Op.SLOAD(1), 0, 0, 0, 0, 0),
        )
        + Op.STOP
    )
    parent_initcode = Initcode(deploy_code=parent_code)
    parent_initcode_address = pre.deploy_contract(parent_initcode)

    entry_code_address = compute_create_address(address=sender, nonce=0)
    parent_address = compute_create_address(
        address=entry_code_address,
        nonce=1,
        initcode=parent_initcode,
        opcode=Op.CREATE,
    )
    child_address = compute_create_address(address=parent_address, nonce=1)

    if selfdestruct_contract_initial_balance > 0:
        pre.fund_address(parent_address, selfdestruct_contract_initial_balance)
        pre.fund_address(child_address, selfdestruct_contract_initial_balance)

    # Entry code: create parent and call it with appropriate flag
    entry_code = Op.EXTCODECOPY(
        parent_initcode_address,
        0,
        0,
        len(parent_initcode),
    )

    entry_code += Op.SSTORE(
        entry_code_storage.store_next(parent_address),
        Op.CREATE(value=0, offset=0, size=len(parent_initcode)),
    )

    flag = 1 if destroy_parent else 0
    entry_code += Op.MSTORE(0, flag)
    entry_code += Op.SSTORE(
        entry_code_storage.store_next(1),
        Op.CALL(Op.GASLIMIT, parent_address, 0, 0, 32, 0, 0),
    )

    entry_code += Op.RETURN(32, 1)

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

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

    if destroy_parent:
        post[parent_address] = Account.NONEXISTENT  # type: ignore
        post[child_address] = Account(
            storage={0: 0},
            balance=selfdestruct_contract_initial_balance,
        )
        post[sendall_recipient] = Account(
            balance=selfdestruct_contract_initial_balance,
            storage={0: 1},
        )
    else:
        post[child_address] = Account.NONEXISTENT  # type: ignore
        post[parent_address] = Account(
            storage={0: 1, 1: child_address},
            balance=selfdestruct_contract_initial_balance,
        )
        post[sendall_recipient] = Account(
            balance=selfdestruct_contract_initial_balance,
            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)

Parametrized Test Cases

This test generates 4 parametrized test cases across 6 forks.