Skip to content

test_selfdestruct_sequences()

Documentation for tests/cancun/eip6780_selfdestruct/test_selfdestruct_finalization.py::test_selfdestruct_sequences@ffe48572.

Generate fixtures for these test cases for Bogota with:

fill -v tests/cancun/eip6780_selfdestruct/test_selfdestruct_finalization.py::test_selfdestruct_sequences --fork Bogota

Cases 1-12 of the EIP-8246 test list: a contract created in this transaction runs a sequence of self-destructs and value sends, and whatever it still holds at the end is what it keeps.

Source code in tests/cancun/eip6780_selfdestruct/test_selfdestruct_finalization.py
@pytest.mark.parametrize("create_opcode", [Op.CREATE, Op.CREATE2])
@pytest.mark.parametrize(
    "initial_balance",
    [pytest.param(0, id="zero_balance"), pytest.param(3, id="funded")],
)
@pytest.mark.parametrize(
    "steps",
    [
        pytest.param((SD_SELF,), id="case01_self"),
        pytest.param((SD_SELF, SD_SELF), id="case02_self_self"),
        pytest.param((SD_SELF, SD_OTHER), id="case03_self_other"),
        pytest.param((SD_OTHER, SD_SELF), id="case04_other_self"),
        pytest.param((SD_OTHER, SEND), id="case05_other_send"),
        pytest.param((SD_OTHER, SEND, SEND), id="case06_other_send_send"),
        pytest.param((SD_OTHER, SEND, SD_OTHER), id="case07_other_send_other"),
        pytest.param((SD_OTHER, SEND, SD_SELF), id="case08_other_send_self"),
        pytest.param((SD_SELF, SEND), id="case09_self_send"),
        pytest.param((SD_SELF, SEND, SEND), id="case10_self_send_send"),
        pytest.param((SD_SELF, SEND, SD_OTHER), id="case11_self_send_other"),
        pytest.param((SD_SELF, SEND, SD_SELF), id="case12_self_send_self"),
    ],
)
@EIPChecklist.Opcode.Test.ExecutionContext.Call(eip=[8246])
@EIPChecklist.Opcode.Test.GasUsage.ExtraGas(eip=[8246])
@EIPChecklist.Opcode.Test.Terminating.Scenarios.SubLevel(eip=[8246])
def test_selfdestruct_sequences(
    state_test: StateTestFiller,
    pre: Alloc,
    fork: Fork,
    create_opcode: Op,
    steps: tuple[str, ...],
    initial_balance: int,
) -> None:
    """
    Cases 1-12 of the EIP-8246 test list: a contract created in this
    transaction runs a sequence of self-destructs and value sends, and
    whatever it still holds at the end is what it keeps.
    """
    sender = pre.fund_eoa()
    other = pre.fund_eoa(amount=OTHER_BALANCE)

    # The victim self-destructs to the address in calldata, or just takes
    # the value when calldata is zero.
    victim_code = (
        Conditional(
            condition=Op.CALLDATALOAD(0),
            if_true=Op.SELFDESTRUCT(Op.CALLDATALOAD(0)),
        )
        + Op.STOP
    )
    initcode = Initcode(deploy_code=victim_code)

    if create_opcode == Op.CREATE:
        create = Op.CREATE(value=initial_balance, offset=0, size=len(initcode))
    elif create_opcode == Op.CREATE2:
        create = Op.CREATE2(
            value=initial_balance, offset=0, size=len(initcode), salt=0
        )
    else:
        raise ValueError(f"unhandled create opcode {create_opcode}")

    entry_code = Om.MSTORE(initcode, 0) + Op.SSTORE(0, create)
    for slot, step in enumerate(steps, 1):
        if step == SD_SELF:
            calldata: Address | Bytecode | int = Op.SLOAD(0)
            value = 0
        elif step == SD_OTHER:
            calldata = other
            value = 0
        elif step == SEND:
            calldata = 0
            value = SEND_AMOUNT
        else:
            raise ValueError(f"unhandled step {step}")
        entry_code += Op.MSTORE(0, calldata) + Op.SSTORE(
            slot,
            Op.CALL(
                gas=Op.GAS,
                address=Op.SLOAD(0),
                value=value,
                args_offset=0,
                args_size=32,
            ),
        )
    entry = pre.deploy_contract(code=entry_code + Op.STOP)
    victim = compute_create_address(
        address=entry,
        nonce=1,
        salt=0,
        initcode=initcode,
        opcode=create_opcode,
    )

    tx_value = initial_balance + SEND_AMOUNT * steps.count(SEND)
    tx = Transaction(sender=sender, to=entry, value=tx_value)

    victim_balance = initial_balance
    other_balance = OTHER_BALANCE
    logs: list[TransactionLog] = []
    if tx_value > 0:
        logs.append(transfer_log(sender, entry, tx_value))
    if initial_balance > 0:
        logs.append(transfer_log(entry, victim, initial_balance))
    for step in steps:
        if step == SD_SELF:
            # Before EIP-8246 the opcode burns a same-tx contract's balance.
            if not fork.is_eip_enabled(8246):
                victim_balance = 0
        elif step == SD_OTHER:
            if victim_balance > 0:
                logs.append(transfer_log(victim, other, victim_balance))
            other_balance += victim_balance
            victim_balance = 0
        elif step == SEND:
            logs.append(transfer_log(entry, victim, SEND_AMOUNT))
            victim_balance += SEND_AMOUNT
        else:
            raise ValueError(f"unhandled step {step}")
    if fork.is_eip_enabled(7708):
        tx.expected_receipt = TransactionReceipt(logs=logs)

    expected_bal = None
    if fork.is_eip_enabled(7928):
        expected_bal = BlockAccessListExpectation(
            account_expectations={
                victim: finalized_bal(fork, victim_balance, [])
            }
        )

    entry_storage: dict[int, Address | int] = {0: victim}
    for slot in range(1, len(steps) + 1):
        entry_storage[slot] = 1
    state_test(
        pre=pre,
        post={
            entry: Account(balance=0, storage=entry_storage),
            victim: finalized(fork, victim_balance),
            other: Account(balance=other_balance),
        },
        tx=tx,
        expected_block_access_list=expected_bal,
    )

Parametrized Test Cases

This test generates 48 parametrized test cases across 5 forks.