@@ -313,3 +313,220 @@ def test_bloatnet_balance_extcodecopy(
313
313
blocks = [Block (txs = [attack_tx ])],
314
314
post = post ,
315
315
)
316
+
317
+
318
+ # ERC20 function selectors
319
+ BALANCEOF_SELECTOR = 0x70A08231 # balanceOf(address)
320
+ APPROVE_SELECTOR = 0x095EA7B3 # approve(address,uint256)
321
+
322
+
323
+ @pytest .mark .valid_from ("Prague" )
324
+ @pytest .mark .parametrize (
325
+ "sload_percent,sstore_percent" ,
326
+ [
327
+ pytest .param (50 , 50 , id = "50-50" ),
328
+ pytest .param (70 , 30 , id = "70-30" ),
329
+ pytest .param (90 , 10 , id = "90-10" ),
330
+ ],
331
+ )
332
+ def test_mixed_sload_sstore (
333
+ blockchain_test : BlockchainTestFiller ,
334
+ pre : Alloc ,
335
+ fork : Fork ,
336
+ gas_benchmark_value : int ,
337
+ address_stubs ,
338
+ sload_percent : int ,
339
+ sstore_percent : int ,
340
+ ):
341
+ """
342
+ BloatNet mixed SLOAD/SSTORE benchmark with configurable operation ratios.
343
+
344
+ This test:
345
+ 1. Auto-discovers ERC20 contracts from stubs
346
+ 2. Divides gas budget evenly across all contracts
347
+ 3. For each contract, divides gas into SLOAD and SSTORE portions by
348
+ percentage
349
+ 4. Executes balanceOf (SLOAD) and approve (SSTORE) calls per the ratio
350
+ 5. Stresses clients with combined read/write operations on large
351
+ contracts
352
+ """
353
+ gas_costs = fork .gas_costs ()
354
+
355
+ # Calculate gas costs
356
+ intrinsic_gas = fork .transaction_intrinsic_cost_calculator ()(calldata = b"" )
357
+
358
+ num_contracts = len (address_stubs .root )
359
+
360
+ # Cost per SLOAD iteration (balanceOf call)
361
+ sload_cost_per_iteration = (
362
+ # Attack contract loop overhead
363
+ gas_costs .G_VERY_LOW * 2 # MLOAD counter (3*2)
364
+ + gas_costs .G_VERY_LOW * 2 # MSTORE selector (3*2)
365
+ + gas_costs .G_VERY_LOW * 3 # MLOAD + MSTORE address (3*3)
366
+ + gas_costs .G_BASE # POP (2)
367
+ + gas_costs .G_BASE * 3 # SUB + MLOAD + MSTORE for counter decrement (2*3)
368
+ + gas_costs .G_BASE * 2 # ISZERO * 2 for loop condition (2*2)
369
+ + gas_costs .G_MID # JUMPI (8)
370
+ # CALL to ERC20 contract
371
+ + gas_costs .G_WARM_ACCOUNT_ACCESS # Warm CALL to same contract (100)
372
+ # Inside ERC20 balanceOf
373
+ + gas_costs .G_VERY_LOW # PUSH4 selector (3)
374
+ + gas_costs .G_BASE # EQ selector match (2)
375
+ + gas_costs .G_MID # JUMPI to function (8)
376
+ + gas_costs .G_JUMPDEST # JUMPDEST at function start (1)
377
+ + gas_costs .G_VERY_LOW * 2 # CALLDATALOAD arg (3*2)
378
+ + gas_costs .G_KECCAK_256 # keccak256 static (30)
379
+ + gas_costs .G_KECCAK_256_WORD * 2 # keccak256 dynamic for 64 bytes (2*6)
380
+ + gas_costs .G_COLD_SLOAD # Cold SLOAD (2100)
381
+ + gas_costs .G_VERY_LOW * 3 # MSTORE result + RETURN setup (3*3)
382
+ )
383
+
384
+ # Cost per SSTORE iteration (approve call)
385
+ sstore_cost_per_iteration = (
386
+ # Attack contract loop body operations
387
+ gas_costs .G_VERY_LOW # MSTORE selector at memory[32] (3)
388
+ + gas_costs .G_LOW # MLOAD counter (5)
389
+ + gas_costs .G_VERY_LOW # MSTORE spender at memory[64] (3)
390
+ + gas_costs .G_LOW # MLOAD counter (5)
391
+ + gas_costs .G_VERY_LOW # MSTORE amount at memory[96] (3)
392
+ # CALL to ERC20 contract
393
+ + gas_costs .G_WARM_ACCOUNT_ACCESS # Warm CALL base cost (100)
394
+ + gas_costs .G_BASE # POP call result (2)
395
+ # Counter decrement
396
+ + gas_costs .G_LOW # MLOAD counter (5)
397
+ + gas_costs .G_VERY_LOW # PUSH1 1 (3)
398
+ + gas_costs .G_VERY_LOW # SUB (3)
399
+ + gas_costs .G_VERY_LOW # MSTORE counter back (3)
400
+ # While loop condition check
401
+ + gas_costs .G_LOW # MLOAD counter (5)
402
+ + gas_costs .G_BASE # ISZERO (2)
403
+ + gas_costs .G_BASE # ISZERO (2)
404
+ + gas_costs .G_MID # JUMPI back to loop start (8)
405
+ # Inside ERC20 approve function
406
+ + gas_costs .G_VERY_LOW # PUSH4 selector (3)
407
+ + gas_costs .G_BASE # EQ selector match (2)
408
+ + gas_costs .G_MID # JUMPI to function (8)
409
+ + gas_costs .G_JUMPDEST # JUMPDEST at function start (1)
410
+ + gas_costs .G_VERY_LOW # CALLDATALOAD spender (3)
411
+ + gas_costs .G_VERY_LOW # CALLDATALOAD amount (3)
412
+ + gas_costs .G_KECCAK_256 # keccak256 static (30)
413
+ + gas_costs .G_KECCAK_256_WORD * 2 # keccak256 dynamic for 64 bytes (12)
414
+ + gas_costs .G_STORAGE_SET # SSTORE to zero slot (20000)
415
+ + gas_costs .G_VERY_LOW # PUSH1 1 for return value (3)
416
+ + gas_costs .G_VERY_LOW # MSTORE return value (3)
417
+ + gas_costs .G_VERY_LOW # PUSH1 32 for return size (3)
418
+ + gas_costs .G_VERY_LOW # PUSH1 0 for return offset (3)
419
+ )
420
+
421
+ # Calculate gas budget per contract
422
+ available_gas = gas_benchmark_value - intrinsic_gas
423
+ gas_per_contract = available_gas // num_contracts
424
+
425
+ # For each contract, split gas by percentage
426
+ sload_gas_per_contract = (gas_per_contract * sload_percent ) // 100
427
+ sstore_gas_per_contract = (gas_per_contract * sstore_percent ) // 100
428
+
429
+ # Calculate calls per contract per operation
430
+ sload_calls_per_contract = int (sload_gas_per_contract // sload_cost_per_iteration )
431
+ sstore_calls_per_contract = int (sstore_gas_per_contract // sstore_cost_per_iteration )
432
+
433
+ # Deploy all discovered ERC20 contracts using stubs
434
+ erc20_addresses = []
435
+ for stub_name in address_stubs .root :
436
+ addr = pre .deploy_contract (
437
+ code = Bytecode (),
438
+ stub = stub_name ,
439
+ )
440
+ erc20_addresses .append (addr )
441
+
442
+ # Log test requirements
443
+ print (
444
+ f"Total gas budget: { gas_benchmark_value / 1_000_000 :.1f} M gas. "
445
+ f"~{ gas_per_contract / 1_000_000 :.1f} M gas per contract "
446
+ f"({ sload_percent } % SLOAD, { sstore_percent } % SSTORE). "
447
+ f"Per contract: { sload_calls_per_contract } balanceOf calls, "
448
+ f"{ sstore_calls_per_contract } approve calls."
449
+ )
450
+
451
+ # Build attack code that loops through each contract
452
+ attack_code : Bytecode = Op .JUMPDEST # Entry point
453
+
454
+ for erc20_address in erc20_addresses :
455
+ # For each contract, execute SLOAD operations (balanceOf)
456
+ attack_code += (
457
+ # Initialize counter in memory[0] = number of balanceOf calls
458
+ Op .MSTORE (offset = 0 , value = sload_calls_per_contract )
459
+ # Loop for balanceOf calls
460
+ + While (
461
+ condition = Op .MLOAD (0 ) + Op .ISZERO + Op .ISZERO ,
462
+ body = (
463
+ # Store function selector at memory[32]
464
+ Op .MSTORE (offset = 32 , value = BALANCEOF_SELECTOR )
465
+ # Store address at memory[64] (use counter as address)
466
+ + Op .MSTORE (offset = 64 , value = Op .MLOAD (0 ))
467
+ # Call balanceOf(address) on ERC20 contract
468
+ + Op .CALL (
469
+ address = erc20_address ,
470
+ value = 0 ,
471
+ args_offset = 32 ,
472
+ args_size = 36 ,
473
+ ret_offset = 96 ,
474
+ ret_size = 32 ,
475
+ )
476
+ + Op .POP # Discard result
477
+ # Decrement counter
478
+ + Op .MSTORE (offset = 0 , value = Op .SUB (Op .MLOAD (0 ), 1 ))
479
+ ),
480
+ )
481
+ )
482
+
483
+ # For each contract, execute SSTORE operations (approve)
484
+ attack_code += (
485
+ # Initialize counter in memory[0] = number of approve calls
486
+ Op .MSTORE (offset = 0 , value = sstore_calls_per_contract )
487
+ # Loop for approve calls
488
+ + While (
489
+ condition = Op .MLOAD (0 ) + Op .ISZERO + Op .ISZERO ,
490
+ body = (
491
+ # Store function selector at memory[32]
492
+ Op .MSTORE (offset = 32 , value = APPROVE_SELECTOR )
493
+ # Store spender address at memory[64] (use counter)
494
+ + Op .MSTORE (offset = 64 , value = Op .MLOAD (0 ))
495
+ # Store amount at memory[96] (use counter as amount)
496
+ + Op .MSTORE (offset = 96 , value = Op .MLOAD (0 ))
497
+ # Call approve(spender, amount) on ERC20 contract
498
+ + Op .CALL (
499
+ address = erc20_address ,
500
+ value = 0 ,
501
+ args_offset = 32 ,
502
+ args_size = 68 ,
503
+ ret_offset = 128 ,
504
+ ret_size = 32 ,
505
+ )
506
+ + Op .POP # Discard result
507
+ # Decrement counter
508
+ + Op .MSTORE (offset = 0 , value = Op .SUB (Op .MLOAD (0 ), 1 ))
509
+ ),
510
+ )
511
+ )
512
+
513
+ # Deploy attack contract
514
+ attack_address = pre .deploy_contract (code = attack_code )
515
+
516
+ # Run the attack
517
+ attack_tx = Transaction (
518
+ to = attack_address ,
519
+ gas_limit = gas_benchmark_value ,
520
+ sender = pre .fund_eoa (),
521
+ )
522
+
523
+ # Post-state
524
+ post = {
525
+ attack_address : Account (storage = {}),
526
+ }
527
+
528
+ blockchain_test (
529
+ pre = pre ,
530
+ blocks = [Block (txs = [attack_tx ])],
531
+ post = post ,
532
+ )
0 commit comments