4949# 4. Attack rapidly accesses all contracts, stressing client's state handling
5050
5151
52+ @pytest .mark .parametrize (
53+ "balance_first" ,
54+ [True , False ],
55+ ids = ["balance_extcodesize" , "extcodesize_balance" ],
56+ )
5257@pytest .mark .valid_from ("Prague" )
5358def test_bloatnet_balance_extcodesize (
5459 blockchain_test : BlockchainTestFiller ,
5560 pre : Alloc ,
5661 fork : Fork ,
5762 gas_benchmark_value : int ,
63+ balance_first : bool ,
5864):
5965 """
6066 BloatNet test using BALANCE + EXTCODESIZE with "on-the-fly" CREATE2
@@ -63,7 +69,7 @@ def test_bloatnet_balance_extcodesize(
6369 This test:
6470 1. Assumes contracts are already deployed via the factory (salt 0 to N-1)
6571 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)
6773 4. Maximizes cache eviction by accessing many contracts
6874 """
6975 gas_costs = fork .gas_costs ()
@@ -75,11 +81,11 @@ def test_bloatnet_balance_extcodesize(
7581 cost_per_contract = (
7682 gas_costs .G_KECCAK_256 # SHA3 static cost for address generation (30)
7783 + 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)
8389 + gas_costs .G_VERY_LOW * 4 # PUSH1 operations (4 * 3)
8490 + gas_costs .G_LOW # MLOAD for salt (3)
8591 + gas_costs .G_VERY_LOW # ADD for increment (3)
@@ -108,6 +114,13 @@ def test_bloatnet_balance_extcodesize(
108114 f"Factory storage will be checked during execution."
109115 )
110116
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+
111124 # Build attack contract that reads config from factory and performs attack
112125 attack_code = (
113126 # Call getConfig() on factory to get num_deployed and init_code_hash
@@ -143,9 +156,8 @@ def test_bloatnet_balance_extcodesize(
143156 # Generate CREATE2 addr: keccak256(0xFF+factory+salt+hash)
144157 Op .SHA3 (11 , 85 ) # Generate CREATE2 address from memory[11:96]
145158 # 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
149161 # Increment salt for next iteration
150162 + Op .MSTORE (32 , Op .ADD (Op .MLOAD (32 ), 1 )) # Increment and store salt
151163 ),
@@ -177,12 +189,18 @@ def test_bloatnet_balance_extcodesize(
177189 )
178190
179191
192+ @pytest .mark .parametrize (
193+ "balance_first" ,
194+ [True , False ],
195+ ids = ["balance_extcodecopy" , "extcodecopy_balance" ],
196+ )
180197@pytest .mark .valid_from ("Prague" )
181198def test_bloatnet_balance_extcodecopy (
182199 blockchain_test : BlockchainTestFiller ,
183200 pre : Alloc ,
184201 fork : Fork ,
185202 gas_benchmark_value : int ,
203+ balance_first : bool ,
186204):
187205 """
188206 BloatNet test using BALANCE + EXTCODECOPY with on-the-fly CREATE2
@@ -191,8 +209,8 @@ def test_bloatnet_balance_extcodecopy(
191209 This test forces actual bytecode reads from disk by:
192210 1. Assumes contracts are already deployed via the factory
193211 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
196214 """
197215 gas_costs = fork .gas_costs ()
198216 max_contract_size = fork .max_code_size ()
@@ -204,16 +222,16 @@ def test_bloatnet_balance_extcodecopy(
204222 cost_per_contract = (
205223 gas_costs .G_KECCAK_256 # SHA3 static cost for address generation (30)
206224 + 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)
210228 + 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)
212230 + gas_costs .G_VERY_LOW * 8 # PUSH operations (8 * 3 = 24)
213231 + gas_costs .G_LOW * 2 # MLOAD for salt twice (6)
214232 + gas_costs .G_VERY_LOW * 2 # ADD operations (6)
215233 + 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)
217235 + 10 # While loop overhead
218236 )
219237
@@ -238,6 +256,20 @@ def test_bloatnet_balance_extcodecopy(
238256 f"Factory storage will be checked during execution."
239257 )
240258
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+
241273 # Build attack contract that reads config from factory and performs attack
242274 attack_code = (
243275 # Call getConfig() on factory to get num_deployed and init_code_hash
@@ -274,16 +306,7 @@ def test_bloatnet_balance_extcodecopy(
274306 Op .SHA3 (11 , 85 ) # Generate CREATE2 address from memory[11:96]
275307 # The address is now on the stack
276308 + 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
287310 # Increment salt for next iteration
288311 + Op .MSTORE (32 , Op .ADD (Op .MLOAD (32 ), 1 )) # Increment and store salt
289312 ),
@@ -313,3 +336,132 @@ def test_bloatnet_balance_extcodecopy(
313336 blocks = [Block (txs = [attack_tx ])],
314337 post = post ,
315338 )
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