193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339 | @pytest.mark.parametrize(
"balance_first",
[True, False],
ids=["balance_extcodecopy", "extcodecopy_balance"],
)
@pytest.mark.valid_from("Prague")
def test_bloatnet_balance_extcodecopy(
blockchain_test: BlockchainTestFiller,
pre: Alloc,
fork: Fork,
gas_benchmark_value: int,
balance_first: bool,
) -> None:
"""
BloatNet test using BALANCE + EXTCODECOPY with on-the-fly CREATE2
address generation.
This test forces actual bytecode reads from disk by:
1. Assumes contracts are already deployed via the factory
2. Generating CREATE2 addresses dynamically during execution
3. Using BALANCE and EXTCODECOPY (order controlled by balance_first param)
4. Reading 1 byte from the END of the bytecode to force full contract load
"""
gas_costs = fork.gas_costs()
max_contract_size = fork.max_code_size()
# Calculate costs
intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(calldata=b"")
# Cost per contract with EXTCODECOPY and CREATE2 address generation
cost_per_contract = (
gas_costs.G_KECCAK_256 # SHA3 static cost for address generation (30)
+ gas_costs.G_KECCAK_256_WORD * 3 # SHA3 dynamic cost (85 bytes = 3 words * 6)
+ gas_costs.G_COLD_ACCOUNT_ACCESS # Cold access (2600)
+ gas_costs.G_BASE # POP first result (2)
+ gas_costs.G_WARM_ACCOUNT_ACCESS # Warm access base (100)
+ gas_costs.G_COPY * 1 # Copy cost for 1 byte (3)
+ gas_costs.G_BASE * 2 # DUP1 before first op, DUP4 for address (6)
+ gas_costs.G_VERY_LOW * 8 # PUSH operations (8 * 3 = 24)
+ gas_costs.G_LOW * 2 # MLOAD for salt twice (6)
+ gas_costs.G_VERY_LOW * 2 # ADD operations (6)
+ gas_costs.G_LOW # MSTORE salt back (3)
+ gas_costs.G_BASE # POP after second op (2)
+ 10 # While loop overhead
)
# Calculate how many contracts to access
available_gas = gas_benchmark_value - intrinsic_gas - 1000
contracts_needed = int(available_gas // cost_per_contract)
# Deploy factory using stub contract - NO HARDCODED VALUES
# The stub "bloatnet_factory" must be provided via --address-stubs flag
# The factory at that address MUST have:
# - Slot 0: Number of deployed contracts
# - Slot 1: Init code hash for CREATE2 address calculation
factory_address = pre.deploy_contract(
code=Bytecode(), # Required parameter, but will be ignored for stubs
stub="bloatnet_factory",
)
# Log test requirements - deployed count read from factory storage
print(
f"Test needs {contracts_needed} contracts for "
f"{gas_benchmark_value / 1_000_000:.1f}M gas. "
f"Factory storage will be checked during execution."
)
# Define operations that differ based on parameter
balance_op = Op.POP(Op.BALANCE)
extcodecopy_op = (
Op.PUSH1(1) # size (1 byte)
+ Op.PUSH2(max_contract_size - 1) # code offset (last byte)
+ Op.ADD(Op.MLOAD(32), 96) # unique memory offset
+ Op.DUP4 # address (duplicated earlier)
+ Op.EXTCODECOPY
+ Op.POP # clean up address
)
benchmark_ops = (
(balance_op + extcodecopy_op) if balance_first else (extcodecopy_op + balance_op)
)
# Build attack contract that reads config from factory and performs attack
attack_code = (
# Call getConfig() on factory to get num_deployed and init_code_hash
Op.STATICCALL(
gas=Op.GAS,
address=factory_address,
args_offset=0,
args_size=0,
ret_offset=96,
ret_size=64,
)
# Check if call succeeded
+ Op.ISZERO
+ Op.PUSH2(0x1000) # Jump to error handler if failed (far jump)
+ Op.JUMPI
# Load results from memory
# Memory[96:128] = num_deployed_contracts
# Memory[128:160] = init_code_hash
+ Op.MLOAD(96) # Load num_deployed_contracts
+ Op.MLOAD(128) # Load init_code_hash
# Setup memory for CREATE2 address generation
# Memory layout at 0: 0xFF + factory_addr(20) + salt(32) + hash(32)
+ Op.MSTORE(0, factory_address) # Store factory address at memory position 0
+ Op.MSTORE8(11, 0xFF) # Store 0xFF prefix at position (32 - 20 - 1)
+ Op.MSTORE(32, 0) # Store salt at position 32
# Stack now has: [num_contracts, init_code_hash]
+ Op.PUSH1(64) # Push memory position
+ Op.MSTORE # Store init_code_hash at memory[64]
# Stack now has: [num_contracts]
# Main attack loop - iterate through all deployed contracts
+ While(
body=(
# Generate CREATE2 address
Op.SHA3(11, 85) # Generate CREATE2 address from memory[11:96]
# The address is now on the stack
+ Op.DUP1 # Duplicate for later operations
+ benchmark_ops # Execute operations in specified order
# Increment salt for next iteration
+ Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)) # Increment and store salt
),
# Continue while counter > 0
condition=Op.DUP1 + Op.PUSH1(1) + Op.SWAP1 + Op.SUB + Op.DUP1 + Op.ISZERO + Op.ISZERO,
)
+ Op.POP # Clean up counter
)
# Deploy attack contract
attack_address = pre.deploy_contract(code=attack_code)
# Run the attack
attack_tx = Transaction(
to=attack_address,
gas_limit=gas_benchmark_value,
sender=pre.fund_eoa(),
)
# Post-state
post = {
attack_address: Account(storage={}),
}
blockchain_test(
pre=pre,
blocks=[Block(txs=[attack_tx])],
post=post,
)
|