Skip to content

test_create_insufficient_balance()

Documentation for tests/berlin/eip2929_gas_cost_increases/test_create.py::test_create_insufficient_balance@ffe48572.

Generate fixtures for these test cases for Bogota with:

fill -v tests/berlin/eip2929_gas_cost_increases/test_create.py::test_create_insufficient_balance --fork Bogota

Test that a failed CREATE/CREATE2 due to insufficient balance does not warm the contract address.

A creator contract with zero balance attempts to create with value=1. The create aborts, and a subsequent BALANCE check on the would-be contract address verifies it remains cold (costs COLD_ACCOUNT_ACCESS instead of WARM_STORAGE_READ).

Source code in tests/berlin/eip2929_gas_cost_increases/test_create.py
@pytest.mark.valid_from("Berlin")
@pytest.mark.parametrize(
    "create_opcode",
    [
        pytest.param(Op.CREATE, id="CREATE"),
        pytest.param(Op.CREATE2, id="CREATE2"),
    ],
)
def test_create_insufficient_balance(
    state_test: StateTestFiller,
    pre: Alloc,
    env: Environment,
    fork: Fork,
    create_opcode: Op,
) -> None:
    """
    Test that a failed CREATE/CREATE2 due to insufficient balance does not
    warm the contract address.

    A creator contract with zero balance attempts to create with value=1.
    The create aborts, and a subsequent BALANCE check on the would-be
    contract address verifies it remains cold (costs COLD_ACCOUNT_ACCESS
    instead of WARM_STORAGE_READ).
    """
    initcode = Op.STOP

    creator_code = Op.MSTORE(
        0, Op.PUSH32(bytes(initcode).ljust(32, b"\0"))
    ) + Op.SSTORE(
        0,
        create_opcode(value=1, offset=0, size=len(initcode)),
    )

    # Creator has zero balance, so CREATE with value=1 will abort
    creator_address = pre.deploy_contract(
        creator_code, balance=0, storage={0: 1}
    )

    # Pre-compute the address that would have been created
    contract_address = compute_create_address(
        address=creator_address,
        nonce=1,
        salt=0,
        initcode=initcode,
        opcode=create_opcode,
    )

    # Measure gas cost of BALANCE on the would-be contract address;
    # cold access proves the address was not warmed by the failed create
    cold_balance = Op.BALANCE(contract_address, address_warm=False)
    checker_address = pre.deploy_contract(
        CodeGasMeasure(
            code=cold_balance,
            extra_stack_items=1,
            sstore_key=1,
        )
    )

    entry_address = pre.deploy_contract(
        Op.CALL(gas=Op.GAS, address=creator_address)
        + Op.CALL(gas=Op.GAS, address=checker_address)
        + Op.STOP
    )

    sender = pre.fund_eoa()
    tx = Transaction(
        to=entry_address,
        gas_limit=1_000_000,
        sender=sender,
    )

    post = {
        # CREATE returned 0 (failed)
        creator_address: Account(storage={0: 0}),
        # BALANCE gas cost matches cold access
        checker_address: Account(storage={1: cold_balance.gas_cost(fork)}),
        # Fail-proofing: confirm CREATE never deposited a contract.
        contract_address: Account.NONEXISTENT,
    }

    # Under EIP-7928 (BAL): the failed CREATE itself does NOT add the
    # would-be address to BAL (failure precedes `track_address`). The
    # subsequent BALANCE call in `checker_address` is what brings it in,
    # so it appears with empty changes. Creator/checker have real
    # storage writes; entry is just a passthrough.
    expected_bal = (
        BlockAccessListExpectation(
            account_expectations={
                sender: BalAccountExpectation(
                    nonce_changes=[
                        BalNonceChange(block_access_index=1, post_nonce=1)
                    ],
                ),
                entry_address: BalAccountExpectation.empty(),
                creator_address: BalAccountExpectation(
                    storage_changes=[
                        BalStorageSlot(
                            slot=0,
                            slot_changes=[
                                BalStorageChange(
                                    block_access_index=1, post_value=0
                                )
                            ],
                        )
                    ],
                ),
                checker_address: BalAccountExpectation(
                    storage_changes=[
                        BalStorageSlot(
                            slot=1,
                            slot_changes=[
                                BalStorageChange(
                                    block_access_index=1,
                                    post_value=cold_balance.gas_cost(fork),
                                )
                            ],
                        )
                    ],
                ),
                contract_address: BalAccountExpectation.empty(),
            }
        )
        if fork.is_eip_enabled(7928)
        else None
    )

    state_test(
        env=env,
        pre=pre,
        post=post,
        tx=tx,
        expected_block_access_list=expected_bal,
    )

Parametrized Test Cases

This test generates 2 parametrized test cases across 9 forks.