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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176 | @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,
)
|