49
49
# 4. Attack rapidly accesses all contracts, stressing client's state handling
50
50
51
51
52
+ @pytest .mark .parametrize (
53
+ "balance_first" ,
54
+ [True , False ],
55
+ ids = ["balance_extcodesize" , "extcodesize_balance" ],
56
+ )
52
57
@pytest .mark .valid_from ("Prague" )
53
58
def test_bloatnet_balance_extcodesize (
54
59
blockchain_test : BlockchainTestFiller ,
55
60
pre : Alloc ,
56
61
fork : Fork ,
57
62
gas_benchmark_value : int ,
63
+ balance_first : bool ,
58
64
):
59
65
"""
60
66
BloatNet test using BALANCE + EXTCODESIZE with "on-the-fly" CREATE2
@@ -63,7 +69,7 @@ def test_bloatnet_balance_extcodesize(
63
69
This test:
64
70
1. Assumes contracts are already deployed via the factory (salt 0 to N-1)
65
71
2. Generates CREATE2 addresses dynamically during execution
66
- 3. Calls BALANCE (cold) then EXTCODESIZE (warm) on each
72
+ 3. Calls BALANCE and EXTCODESIZE (order controlled by balance_first param)
67
73
4. Maximizes cache eviction by accessing many contracts
68
74
"""
69
75
gas_costs = fork .gas_costs ()
@@ -75,11 +81,11 @@ def test_bloatnet_balance_extcodesize(
75
81
cost_per_contract = (
76
82
gas_costs .G_KECCAK_256 # SHA3 static cost for address generation (30)
77
83
+ gas_costs .G_KECCAK_256_WORD * 3 # SHA3 dynamic cost (85 bytes = 3 words * 6)
78
- + gas_costs .G_COLD_ACCOUNT_ACCESS # Cold BALANCE (2600)
79
- + gas_costs .G_BASE # POP balance (2)
80
- + gas_costs .G_WARM_ACCOUNT_ACCESS # Warm EXTCODESIZE (100)
81
- + gas_costs .G_BASE # POP code size (2)
82
- + gas_costs .G_BASE # DUP1 before BALANCE (3)
84
+ + gas_costs .G_COLD_ACCOUNT_ACCESS # Cold access (2600)
85
+ + gas_costs .G_BASE # POP first result (2)
86
+ + gas_costs .G_WARM_ACCOUNT_ACCESS # Warm access (100)
87
+ + gas_costs .G_BASE # POP second result (2)
88
+ + gas_costs .G_BASE # DUP1 before first op (3)
83
89
+ gas_costs .G_VERY_LOW * 4 # PUSH1 operations (4 * 3)
84
90
+ gas_costs .G_LOW # MLOAD for salt (3)
85
91
+ gas_costs .G_VERY_LOW # ADD for increment (3)
@@ -108,6 +114,13 @@ def test_bloatnet_balance_extcodesize(
108
114
f"Factory storage will be checked during execution."
109
115
)
110
116
117
+ # Define operations that differ based on parameter
118
+ balance_op = Op .POP (Op .BALANCE )
119
+ extcodesize_op = Op .POP (Op .EXTCODESIZE )
120
+ benchmark_ops = (
121
+ (balance_op + extcodesize_op ) if balance_first else (extcodesize_op + balance_op )
122
+ )
123
+
111
124
# Build attack contract that reads config from factory and performs attack
112
125
attack_code = (
113
126
# Call getConfig() on factory to get num_deployed and init_code_hash
@@ -143,9 +156,8 @@ def test_bloatnet_balance_extcodesize(
143
156
# Generate CREATE2 addr: keccak256(0xFF+factory+salt+hash)
144
157
Op .SHA3 (11 , 85 ) # Generate CREATE2 address from memory[11:96]
145
158
# The address is now on the stack
146
- + Op .DUP1 # Duplicate for EXTCODESIZE
147
- + Op .POP (Op .BALANCE ) # Cold access
148
- + Op .POP (Op .EXTCODESIZE ) # Warm access
159
+ + Op .DUP1 # Duplicate for second operation
160
+ + benchmark_ops # Execute operations in specified order
149
161
# Increment salt for next iteration
150
162
+ Op .MSTORE (32 , Op .ADD (Op .MLOAD (32 ), 1 )) # Increment and store salt
151
163
),
@@ -177,12 +189,18 @@ def test_bloatnet_balance_extcodesize(
177
189
)
178
190
179
191
192
+ @pytest .mark .parametrize (
193
+ "balance_first" ,
194
+ [True , False ],
195
+ ids = ["balance_extcodecopy" , "extcodecopy_balance" ],
196
+ )
180
197
@pytest .mark .valid_from ("Prague" )
181
198
def test_bloatnet_balance_extcodecopy (
182
199
blockchain_test : BlockchainTestFiller ,
183
200
pre : Alloc ,
184
201
fork : Fork ,
185
202
gas_benchmark_value : int ,
203
+ balance_first : bool ,
186
204
):
187
205
"""
188
206
BloatNet test using BALANCE + EXTCODECOPY with on-the-fly CREATE2
@@ -191,8 +209,8 @@ def test_bloatnet_balance_extcodecopy(
191
209
This test forces actual bytecode reads from disk by:
192
210
1. Assumes contracts are already deployed via the factory
193
211
2. Generating CREATE2 addresses dynamically during execution
194
- 3. Using BALANCE (cold) to warm the account
195
- 4. Using EXTCODECOPY (warm) to read 1 byte from the END of the bytecode
212
+ 3. Using BALANCE and EXTCODECOPY (order controlled by balance_first param)
213
+ 4. Reading 1 byte from the END of the bytecode to force full contract load
196
214
"""
197
215
gas_costs = fork .gas_costs ()
198
216
max_contract_size = fork .max_code_size ()
@@ -204,16 +222,16 @@ def test_bloatnet_balance_extcodecopy(
204
222
cost_per_contract = (
205
223
gas_costs .G_KECCAK_256 # SHA3 static cost for address generation (30)
206
224
+ gas_costs .G_KECCAK_256_WORD * 3 # SHA3 dynamic cost (85 bytes = 3 words * 6)
207
- + gas_costs .G_COLD_ACCOUNT_ACCESS # Cold BALANCE (2600)
208
- + gas_costs .G_BASE # POP balance (2)
209
- + gas_costs .G_WARM_ACCOUNT_ACCESS # Warm EXTCODECOPY base (100)
225
+ + gas_costs .G_COLD_ACCOUNT_ACCESS # Cold access (2600)
226
+ + gas_costs .G_BASE # POP first result (2)
227
+ + gas_costs .G_WARM_ACCOUNT_ACCESS # Warm access base (100)
210
228
+ gas_costs .G_COPY * 1 # Copy cost for 1 byte (3)
211
- + gas_costs .G_BASE * 2 # DUP1 before BALANCE , DUP4 for address (6)
229
+ + gas_costs .G_BASE * 2 # DUP1 before first op , DUP4 for address (6)
212
230
+ gas_costs .G_VERY_LOW * 8 # PUSH operations (8 * 3 = 24)
213
231
+ gas_costs .G_LOW * 2 # MLOAD for salt twice (6)
214
232
+ gas_costs .G_VERY_LOW * 2 # ADD operations (6)
215
233
+ gas_costs .G_LOW # MSTORE salt back (3)
216
- + gas_costs .G_BASE # POP after EXTCODECOPY (2)
234
+ + gas_costs .G_BASE # POP after second op (2)
217
235
+ 10 # While loop overhead
218
236
)
219
237
@@ -238,6 +256,20 @@ def test_bloatnet_balance_extcodecopy(
238
256
f"Factory storage will be checked during execution."
239
257
)
240
258
259
+ # Define operations that differ based on parameter
260
+ balance_op = Op .POP (Op .BALANCE )
261
+ extcodecopy_op = (
262
+ Op .PUSH1 (1 ) # size (1 byte)
263
+ + Op .PUSH2 (max_contract_size - 1 ) # code offset (last byte)
264
+ + Op .ADD (Op .MLOAD (32 ), 96 ) # unique memory offset
265
+ + Op .DUP4 # address (duplicated earlier)
266
+ + Op .EXTCODECOPY
267
+ + Op .POP # clean up address
268
+ )
269
+ benchmark_ops = (
270
+ (balance_op + extcodecopy_op ) if balance_first else (extcodecopy_op + balance_op )
271
+ )
272
+
241
273
# Build attack contract that reads config from factory and performs attack
242
274
attack_code = (
243
275
# Call getConfig() on factory to get num_deployed and init_code_hash
@@ -274,16 +306,7 @@ def test_bloatnet_balance_extcodecopy(
274
306
Op .SHA3 (11 , 85 ) # Generate CREATE2 address from memory[11:96]
275
307
# The address is now on the stack
276
308
+ Op .DUP1 # Duplicate for later operations
277
- + Op .POP (Op .BALANCE ) # Cold access
278
- # EXTCODECOPY(addr, mem_offset, last_byte_offset, 1)
279
- # Read the LAST byte to force full contract load
280
- + Op .PUSH1 (1 ) # size (1 byte)
281
- + Op .PUSH2 (max_contract_size - 1 ) # code offset (last byte)
282
- # Use salt as memory offset to avoid overlap
283
- + Op .ADD (Op .MLOAD (32 ), 96 ) # Add base memory offset for unique position
284
- + Op .DUP4 # address (duplicated earlier)
285
- + Op .EXTCODECOPY
286
- + Op .POP # Clean up address
309
+ + benchmark_ops # Execute operations in specified order
287
310
# Increment salt for next iteration
288
311
+ Op .MSTORE (32 , Op .ADD (Op .MLOAD (32 ), 1 )) # Increment and store salt
289
312
),
@@ -313,3 +336,132 @@ def test_bloatnet_balance_extcodecopy(
313
336
blocks = [Block (txs = [attack_tx ])],
314
337
post = post ,
315
338
)
339
+
340
+
341
+ @pytest .mark .parametrize (
342
+ "balance_first" ,
343
+ [True , False ],
344
+ ids = ["balance_extcodehash" , "extcodehash_balance" ],
345
+ )
346
+ @pytest .mark .valid_from ("Prague" )
347
+ def test_bloatnet_balance_extcodehash (
348
+ blockchain_test : BlockchainTestFiller ,
349
+ pre : Alloc ,
350
+ fork : Fork ,
351
+ gas_benchmark_value : int ,
352
+ balance_first : bool ,
353
+ ):
354
+ """
355
+ BloatNet test using BALANCE + EXTCODEHASH with on-the-fly CREATE2
356
+ address generation.
357
+
358
+ This test:
359
+ 1. Assumes contracts are already deployed via the factory
360
+ 2. Generates CREATE2 addresses dynamically during execution
361
+ 3. Calls BALANCE and EXTCODEHASH (order controlled by balance_first param)
362
+ 4. Forces client to compute code hash for 24KB bytecode
363
+ """
364
+ gas_costs = fork .gas_costs ()
365
+
366
+ # Calculate gas costs
367
+ intrinsic_gas = fork .transaction_intrinsic_cost_calculator ()(calldata = b"" )
368
+
369
+ # Cost per contract access with CREATE2 address generation
370
+ cost_per_contract = (
371
+ gas_costs .G_KECCAK_256 # SHA3 static cost for address generation (30)
372
+ + gas_costs .G_KECCAK_256_WORD * 3 # SHA3 dynamic cost (85 bytes = 3 words * 6)
373
+ + gas_costs .G_COLD_ACCOUNT_ACCESS # Cold access (2600)
374
+ + gas_costs .G_BASE # POP first result (2)
375
+ + gas_costs .G_WARM_ACCOUNT_ACCESS # Warm access (100)
376
+ + gas_costs .G_BASE # POP second result (2)
377
+ + gas_costs .G_BASE # DUP1 before first op (3)
378
+ + gas_costs .G_VERY_LOW * 4 # PUSH1 operations (4 * 3)
379
+ + gas_costs .G_LOW # MLOAD for salt (3)
380
+ + gas_costs .G_VERY_LOW # ADD for increment (3)
381
+ + gas_costs .G_LOW # MSTORE salt back (3)
382
+ + 10 # While loop overhead
383
+ )
384
+
385
+ # Calculate how many contracts to access based on available gas
386
+ available_gas = gas_benchmark_value - intrinsic_gas - 1000 # Reserve for cleanup
387
+ contracts_needed = int (available_gas // cost_per_contract )
388
+
389
+ # Deploy factory using stub contract
390
+ factory_address = pre .deploy_contract (
391
+ code = Bytecode (),
392
+ stub = "bloatnet_factory" ,
393
+ )
394
+
395
+ # Log test requirements
396
+ print (
397
+ f"Test needs { contracts_needed } contracts for "
398
+ f"{ gas_benchmark_value / 1_000_000 :.1f} M gas. "
399
+ f"Factory storage will be checked during execution."
400
+ )
401
+
402
+ # Define operations that differ based on parameter
403
+ balance_op = Op .POP (Op .BALANCE )
404
+ extcodehash_op = Op .POP (Op .EXTCODEHASH )
405
+ benchmark_ops = (
406
+ (balance_op + extcodehash_op ) if balance_first else (extcodehash_op + balance_op )
407
+ )
408
+
409
+ # Build attack contract that reads config from factory and performs attack
410
+ attack_code = (
411
+ # Call getConfig() on factory to get num_deployed and init_code_hash
412
+ Op .STATICCALL (
413
+ gas = Op .GAS ,
414
+ address = factory_address ,
415
+ args_offset = 0 ,
416
+ args_size = 0 ,
417
+ ret_offset = 96 ,
418
+ ret_size = 64 ,
419
+ )
420
+ # Check if call succeeded
421
+ + Op .ISZERO
422
+ + Op .PUSH2 (0x1000 ) # Jump to error handler if failed
423
+ + Op .JUMPI
424
+ # Load results from memory
425
+ + Op .MLOAD (96 ) # Load num_deployed_contracts
426
+ + Op .MLOAD (128 ) # Load init_code_hash
427
+ # Setup memory for CREATE2 address generation
428
+ + Op .MSTORE (0 , factory_address )
429
+ + Op .MSTORE8 (11 , 0xFF )
430
+ + Op .MSTORE (32 , 0 ) # Initial salt
431
+ + Op .PUSH1 (64 )
432
+ + Op .MSTORE # Store init_code_hash
433
+ # Main attack loop
434
+ + While (
435
+ body = (
436
+ # Generate CREATE2 address
437
+ Op .SHA3 (11 , 85 )
438
+ + Op .DUP1 # Duplicate for second operation
439
+ + benchmark_ops # Execute operations in specified order
440
+ # Increment salt
441
+ + Op .MSTORE (32 , Op .ADD (Op .MLOAD (32 ), 1 ))
442
+ ),
443
+ condition = Op .DUP1 + Op .PUSH1 (1 ) + Op .SWAP1 + Op .SUB + Op .DUP1 + Op .ISZERO + Op .ISZERO ,
444
+ )
445
+ + Op .POP # Clean up counter
446
+ )
447
+
448
+ # Deploy attack contract
449
+ attack_address = pre .deploy_contract (code = attack_code )
450
+
451
+ # Run the attack
452
+ attack_tx = Transaction (
453
+ to = attack_address ,
454
+ gas_limit = gas_benchmark_value ,
455
+ sender = pre .fund_eoa (),
456
+ )
457
+
458
+ # Post-state
459
+ post = {
460
+ attack_address : Account (storage = {}),
461
+ }
462
+
463
+ blockchain_test (
464
+ pre = pre ,
465
+ blocks = [Block (txs = [attack_tx ])],
466
+ post = post ,
467
+ )
0 commit comments