Skip to content

test_reinvoked_selfdestruct_create_nonce()

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

Generate fixtures for these test cases for Bogota with:

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

A contract created in this transaction self-destructs, is called again and runs a CREATE in a frame that reverts, then is called once more and runs a CREATE that survives. Its nonce keeps accumulating across the SELFDESTRUCT and the reverted frame, so the surviving children sit at CREATE(victim, 1) and CREATE(victim, 2), and no child is ever created at CREATE(victim, 0).

Source code in tests/cancun/eip6780_selfdestruct/test_selfdestruct_finalization.py
def test_reinvoked_selfdestruct_create_nonce(
    state_test: StateTestFiller,
    pre: Alloc,
    fork: Fork,
) -> None:
    """
    A contract created in this transaction self-destructs, is called again
    and runs a CREATE in a frame that reverts, then is called once more and
    runs a CREATE that survives. Its nonce keeps accumulating across the
    SELFDESTRUCT and the reverted frame, so the surviving children sit at
    CREATE(victim, 1) and CREATE(victim, 2), and no child is ever created
    at CREATE(victim, 0).
    """
    sender = pre.fund_eoa()

    victim_code = Op.POP(
        Op.CREATE(value=Op.CALLVALUE, offset=0, size=0)
    ) + Conditional(
        condition=Op.CALLDATASIZE,
        if_true=Op.REVERT(0, 0),
        if_false=Op.SELFDESTRUCT(Op.ADDRESS),
    )
    initcode = Initcode(deploy_code=victim_code)

    entry_code = Om.MSTORE(initcode, 0) + Op.SSTORE(
        0, Op.CREATE2(value=0, offset=0, size=len(initcode), salt=0)
    )
    # Calldata makes the victim revert its frame, undoing that frame's
    # CREATE and its nonce bump; an empty call lets it self-destruct. Each
    # call sends a different value, which the victim passes on to its
    # child, so a child's balance names the call that created it.
    calls = [(1, False), (2, True), (3, False)]  # (value, revert_frame)
    for value, revert_frame in calls:
        entry_code += Op.POP(
            Op.CALL(
                gas=Op.GAS,
                address=Op.SLOAD(0),
                value=value,
                args_size=1 if revert_frame else 0,
            )
        )
    entry = pre.deploy_contract(
        code=entry_code, balance=sum(value for value, _ in calls)
    )
    victim = compute_create_address(
        address=entry, salt=0, initcode=initcode, opcode=Op.CREATE2
    )
    child_nonce0 = compute_create_address(address=victim, nonce=0)
    child_nonce1 = compute_create_address(address=victim, nonce=1)
    child_nonce2 = compute_create_address(address=victim, nonce=2)
    child_nonce3 = compute_create_address(address=victim, nonce=3)

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

    expected_bal = None
    if fork.is_eip_enabled(7928):
        expected_bal = BlockAccessListExpectation(
            account_expectations={
                victim: finalized_bal(fork, 0, []),
                child_nonce1: BalAccountExpectation(
                    nonce_changes=[
                        BalNonceChange(block_access_index=1, post_nonce=1)
                    ],
                    balance_changes=[
                        BalBalanceChange(block_access_index=1, post_balance=1)
                    ],
                ),
                child_nonce2: BalAccountExpectation(
                    nonce_changes=[
                        BalNonceChange(block_access_index=1, post_nonce=1)
                    ],
                    balance_changes=[
                        BalBalanceChange(block_access_index=1, post_balance=3)
                    ],
                ),
                # None asserts these children are absent from the BAL.
                child_nonce0: None,
                child_nonce3: None,
            }
        )

    state_test(
        pre=pre,
        post={
            # The reverted call's value comes back to the entry contract.
            entry: Account(nonce=2, balance=2, storage={0: victim}),
            victim: finalized(fork, 0),
            child_nonce0: Account.NONEXISTENT,
            child_nonce1: Account(nonce=1, code=b"", balance=1),
            child_nonce2: Account(nonce=1, code=b"", balance=3),
            child_nonce3: Account.NONEXISTENT,
        },
        tx=tx,
        expected_block_access_list=expected_bal,
    )

Parametrized Test Cases

This test generates 1 parametrized test case across 5 forks.