@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,
)