Skip to content

test_selfdestruct_in_delegate_context()

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

Generate fixtures for these test cases for Bogota with:

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

The account destroyed is the one whose context runs SELFDESTRUCT, not the library holding the code.

Source code in tests/cancun/eip6780_selfdestruct/test_selfdestruct_finalization.py
@pytest.mark.parametrize("call_opcode", [Op.DELEGATECALL, Op.CALLCODE])
@pytest.mark.parametrize(
    "beneficiary",
    [pytest.param("self", id="self"), pytest.param("other", id="other")],
)
@pytest.mark.parametrize(
    "initial_balance",
    [pytest.param(0, id="zero_balance"), pytest.param(3, id="funded")],
)
@EIPChecklist.Opcode.Test.ExecutionContext.Delegatecall(eip=[8246])
@EIPChecklist.Opcode.Test.ExecutionContext.Delegatecall.Balance(eip=[8246])
@EIPChecklist.Opcode.Test.ExecutionContext.Delegatecall.Code(eip=[8246])
@EIPChecklist.Opcode.Test.ExecutionContext.Delegatecall.Storage(eip=[8246])
@EIPChecklist.Opcode.Test.ExecutionContext.Callcode(eip=[8246])
def test_selfdestruct_in_delegate_context(
    state_test: StateTestFiller,
    pre: Alloc,
    fork: Fork,
    call_opcode: Op,
    beneficiary: str,
    initial_balance: int,
) -> None:
    """
    The account destroyed is the one whose context runs SELFDESTRUCT, not
    the library holding the code.
    """
    sender = pre.fund_eoa()
    other = pre.fund_eoa(amount=OTHER_BALANCE)

    if beneficiary == "self":
        target: Address | Bytecode = Op.ADDRESS
    elif beneficiary == "other":
        target = other
    else:
        raise ValueError(f"unhandled beneficiary {beneficiary}")
    library_code = Op.SSTORE(0, 1) + Op.SELFDESTRUCT(target)
    library = pre.deploy_contract(code=library_code, balance=7)

    victim_code = Op.POP(call_opcode(gas=Op.GAS, address=library)) + Op.STOP
    initcode = Initcode(deploy_code=victim_code)
    entry = pre.deploy_contract(
        code=Om.MSTORE(initcode, 0)
        + Op.SSTORE(
            0, Op.CREATE(value=initial_balance, offset=0, size=len(initcode))
        )
        + Op.SSTORE(1, Op.CALL(gas=Op.GAS, address=Op.SLOAD(0)))
        + Op.STOP
    )
    victim = compute_create_address(address=entry, nonce=1)

    tx = Transaction(sender=sender, to=entry, value=initial_balance)

    if beneficiary == "self":
        victim_balance = initial_balance
        other_balance = OTHER_BALANCE
    elif beneficiary == "other":
        victim_balance = 0
        other_balance = OTHER_BALANCE + initial_balance
    else:
        raise ValueError(f"unhandled beneficiary {beneficiary}")

    logs: list[TransactionLog] = []
    if initial_balance > 0:
        logs.append(transfer_log(sender, entry, initial_balance))
        logs.append(transfer_log(entry, victim, initial_balance))
        if beneficiary == "other":
            logs.append(transfer_log(victim, other, initial_balance))
    if fork.is_eip_enabled(7708):
        tx.expected_receipt = TransactionReceipt(logs=logs)

    expected_bal = None
    if fork.is_eip_enabled(7928):
        # The library's SSTORE runs in the victim's context, so the BAL
        # attributes the (then cleared) slot to the victim, not the library.
        expected_bal = BlockAccessListExpectation(
            account_expectations={
                victim: finalized_bal(fork, victim_balance, [0]),
                library: BalAccountExpectation.empty(),
            }
        )

    state_test(
        pre=pre,
        post={
            entry: Account(storage={0: victim, 1: 1}),
            victim: finalized(fork, victim_balance),
            library: Account(balance=7, code=library_code, storage={}),
            other: Account(balance=other_balance),
        },
        tx=tx,
        expected_block_access_list=expected_bal,
    )

Parametrized Test Cases

This test generates 8 parametrized test cases across 5 forks.