Skip to content

test_selfdestruct_balance_transfer_reverted()

Documentation for tests/cancun/eip6780_selfdestruct/test_journal_revert.py::test_selfdestruct_balance_transfer_reverted@b314d18e.

Generate fixtures for these test cases for Amsterdam with:

fill -v tests/cancun/eip6780_selfdestruct/test_journal_revert.py::test_selfdestruct_balance_transfer_reverted --fork Amsterdam

Test that SELFDESTRUCT balance transfer is reverted on sub-call revert.

Post-Cancun, SELFDESTRUCT does not destroy the contract but still transfers balance. When the sub-call containing SELFDESTRUCT reverts, the balance transfer must also be reverted.

Source code in tests/cancun/eip6780_selfdestruct/test_journal_revert.py
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
@pytest.mark.valid_from("Cancun")
def test_selfdestruct_balance_transfer_reverted(
    state_test: StateTestFiller,
    pre: Alloc,
    fork: Fork,
) -> None:
    """
    Test that SELFDESTRUCT balance transfer is reverted on sub-call revert.

    Post-Cancun, SELFDESTRUCT does not destroy the contract but still
    transfers balance. When the sub-call containing SELFDESTRUCT reverts,
    the balance transfer must also be reverted.
    """
    storage = Storage()

    victim_balance = 1

    beneficiary_balance = 1
    beneficiary = pre.fund_eoa(amount=beneficiary_balance)

    victim = pre.deploy_contract(
        code=Op.SELFDESTRUCT(beneficiary),
        balance=victim_balance,
    )

    # Controller calls victim (triggers SELFDESTRUCT) then reverts.
    controller = pre.deploy_contract(
        Op.POP(Op.CALL(address=victim)) + Op.REVERT(offset=0, size=0)
    )

    # Outer calls controller, then checks beneficiary balance.
    outer = pre.deploy_contract(
        Op.POP(Op.CALL(address=controller))
        + Op.SSTORE(
            storage.store_next(beneficiary_balance, "beneficiary_balance"),
            Op.BALANCE(beneficiary),
        )
        + Op.SSTORE(
            storage.store_next(victim_balance, "victim_balance"),
            Op.BALANCE(victim),
        )
        + Op.STOP
    )

    sender = pre.fund_eoa()

    # Under EIP-7708 the SELFDESTRUCT-triggered Transfer log is emitted inside
    # the reverted sub-call, so it must be discarded together with the rest of
    # the reverted state.
    expected_receipt = (
        TransactionReceipt(logs=[]) if fork.is_eip_enabled(7708) else None
    )

    # Under EIP-7928 (BAL): victim and beneficiary are touched in the
    # reverted sub-call's SELFDESTRUCT and again by outer's BALANCE reads.
    # The balance transfer is undone, so they appear with empty changes
    # (accessed but no net state change).
    expected_bal = (
        BlockAccessListExpectation(
            account_expectations={
                victim: BalAccountExpectation.empty(),
                beneficiary: BalAccountExpectation.empty(),
            }
        )
        if fork.is_eip_enabled(7928)
        else None
    )

    state_test(
        pre=pre,
        post={
            outer: Account(storage=storage),
            # Beneficiary keeps only its initial balance (transfer reverted).
            beneficiary: Account(balance=beneficiary_balance),
            # Victim still has its balance.
            victim: Account(balance=victim_balance),
        },
        tx=Transaction(
            sender=sender,
            to=outer,
            expected_receipt=expected_receipt,
        ),
        expected_block_access_list=expected_bal,
    )

Parametrized Test Cases

This test generates 1 parametrized test case across 4 forks.