Skip to content

test_bal_create2_selfdestruct_then_recreate_same_block()

Documentation for tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py::test_bal_create2_selfdestruct_then_recreate_same_block@5c024cbb.

Generate fixtures for these test cases for Amsterdam with:

fill -v tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py::test_bal_create2_selfdestruct_then_recreate_same_block --fork Amsterdam

Tx1 CREATE2+SSTORE+SELFDESTRUCT, Tx2 CREATE2 resurrection at same address.

Two identical txs invoke the same factory with the same initcode (same hash => same CREATE2 address A). The factory branches on its own storage slot 1: on the first tx, the slot is 0 so the factory CREATE2's then CALLs A (runtime SSTOREs to a target slot then SELFDESTRUCTs) and records the CALL's return code in slot 1; on the second tx, slot 1 is non-zero so only CREATE2 runs and A persists with the runtime code (its runtime is never executed).

Per EIP-7928 SELFDESTRUCT-in-tx semantics, Tx1's destructed A has no nonce_changes or code_changes; only balance_changes if it was pre-funded. The SSTORE is demoted to storage_reads because the contract is destroyed in the same tx. Tx2's fresh A has nonce_changes (post=1), code_changes (post=runtime), and empty storage.

Source code in tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py
3798
3799
3800
3801
3802
3803
3804
3805
3806
3807
3808
3809
3810
3811
3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
3823
3824
3825
3826
3827
3828
3829
3830
3831
3832
3833
3834
3835
3836
3837
3838
3839
3840
3841
3842
3843
3844
3845
3846
3847
3848
3849
3850
3851
3852
3853
3854
3855
3856
3857
3858
3859
3860
3861
3862
3863
3864
3865
3866
3867
3868
3869
3870
3871
3872
3873
3874
3875
3876
3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
3913
3914
3915
3916
3917
3918
@pytest.mark.parametrize(
    "pre_balance",
    [0, 100],
    ids=["no_balance", "with_balance"],
)
def test_bal_create2_selfdestruct_then_recreate_same_block(
    pre: Alloc,
    blockchain_test: BlockchainTestFiller,
    pre_balance: int,
) -> None:
    """
    Tx1 CREATE2+SSTORE+SELFDESTRUCT, Tx2 CREATE2 resurrection at same
    address.

    Two identical txs invoke the same factory with the same initcode
    (same hash => same CREATE2 address A). The factory branches on its
    own storage slot 1: on the first tx, the slot is 0 so the factory
    CREATE2's then CALLs A (runtime SSTOREs to a target slot then
    SELFDESTRUCTs) and records the CALL's return code in slot 1; on the
    second tx, slot 1 is non-zero so only CREATE2 runs and A persists
    with the runtime code (its runtime is never executed).

    Per EIP-7928 SELFDESTRUCT-in-tx semantics, Tx1's destructed A has no
    `nonce_changes` or `code_changes`; only `balance_changes` if it was
    pre-funded. The SSTORE is demoted to `storage_reads` because the
    contract is destroyed in the same tx. Tx2's fresh A has
    `nonce_changes` (post=1), `code_changes` (post=runtime), and empty
    storage.
    """
    alice = pre.fund_eoa()
    beneficiary = pre.fund_eoa(amount=0)
    salt = 0
    target_slot = 0x07

    runtime = Op.SSTORE(target_slot, 0xCAFE) + Op.SELFDESTRUCT(beneficiary)
    runtime_bytes = bytes(runtime)
    initcode_bytes = bytes(Initcode(deploy_code=runtime))

    factory_code = (
        Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE)
        + Op.SSTORE(
            0,
            Op.CREATE2(value=0, offset=0, size=Op.CALLDATASIZE, salt=salt),
        )
        + Conditional(
            condition=Op.ISZERO(Op.SLOAD(1)),
            if_true=Op.SSTORE(1, Op.CALL(Op.GAS, Op.SLOAD(0), 0, 0, 0, 0, 0)),
            if_false=Op.STOP,
        )
        + Op.STOP
    )
    factory = pre.deploy_contract(code=factory_code)
    target_a = compute_create_address(
        address=factory,
        salt=salt,
        initcode=initcode_bytes,
        opcode=Op.CREATE2,
    )

    if pre_balance > 0:
        pre.fund_address(target_a, pre_balance)

    tx1 = Transaction(sender=alice, to=factory, data=initcode_bytes)
    tx2 = Transaction(sender=alice, to=factory, data=initcode_bytes)

    target_a_balance_changes = []
    if pre_balance > 0:
        target_a_balance_changes = [
            BalBalanceChange(block_access_index=1, post_balance=0),
        ]
        beneficiary_expectation = BalAccountExpectation(
            balance_changes=[
                BalBalanceChange(
                    block_access_index=1, post_balance=pre_balance
                )
            ],
        )
    else:
        # SELFDESTRUCT touches the beneficiary even with 0 value; no
        # balance change is recorded.
        beneficiary_expectation = BalAccountExpectation.empty()

    block = Block(
        txs=[tx1, tx2],
        expected_block_access_list=BlockAccessListExpectation(
            account_expectations={
                target_a: BalAccountExpectation(
                    # Tx1 destruction (EIP-7928 #165): no nonce/code changes;
                    # the SSTORE is demoted to a storage_read because A is
                    # destroyed same-tx. Tx2 resurrection: fresh contract
                    # with nonce=1, runtime, and untouched storage.
                    nonce_changes=[
                        BalNonceChange(block_access_index=2, post_nonce=1),
                    ],
                    code_changes=[
                        BalCodeChange(
                            block_access_index=2, new_code=runtime_bytes
                        ),
                    ],
                    balance_changes=target_a_balance_changes,
                    storage_changes=[],
                    storage_reads=[target_slot],
                ),
                beneficiary: beneficiary_expectation,
            }
        ),
    )

    blockchain_test(
        pre=pre,
        blocks=[block],
        post={
            target_a: Account(
                nonce=1, balance=0, code=runtime_bytes, storage={}
            ),
            beneficiary: Account(balance=pre_balance)
            if pre_balance > 0
            else Account.NONEXISTENT,
            factory: Account(nonce=3, storage={0: target_a, 1: 1}),
        },
    )

Parametrized Test Cases

This test generates 2 parametrized test cases across 1 fork.