@@ -465,3 +465,220 @@ def test_bloatnet_balance_extcodehash(
465
465
blocks = [Block (txs = [attack_tx ])],
466
466
post = post ,
467
467
)
468
+
469
+
470
+ # ERC20 function selectors
471
+ BALANCEOF_SELECTOR = 0x70A08231 # balanceOf(address)
472
+ APPROVE_SELECTOR = 0x095EA7B3 # approve(address,uint256)
473
+
474
+
475
+ @pytest .mark .valid_from ("Prague" )
476
+ @pytest .mark .parametrize (
477
+ "sload_percent,sstore_percent" ,
478
+ [
479
+ pytest .param (50 , 50 , id = "50-50" ),
480
+ pytest .param (70 , 30 , id = "70-30" ),
481
+ pytest .param (90 , 10 , id = "90-10" ),
482
+ ],
483
+ )
484
+ def test_mixed_sload_sstore (
485
+ blockchain_test : BlockchainTestFiller ,
486
+ pre : Alloc ,
487
+ fork : Fork ,
488
+ gas_benchmark_value : int ,
489
+ address_stubs ,
490
+ sload_percent : int ,
491
+ sstore_percent : int ,
492
+ ):
493
+ """
494
+ BloatNet mixed SLOAD/SSTORE benchmark with configurable operation ratios.
495
+
496
+ This test:
497
+ 1. Auto-discovers ERC20 contracts from stubs
498
+ 2. Divides gas budget evenly across all contracts
499
+ 3. For each contract, divides gas into SLOAD and SSTORE portions by
500
+ percentage
501
+ 4. Executes balanceOf (SLOAD) and approve (SSTORE) calls per the ratio
502
+ 5. Stresses clients with combined read/write operations on large
503
+ contracts
504
+ """
505
+ gas_costs = fork .gas_costs ()
506
+
507
+ # Calculate gas costs
508
+ intrinsic_gas = fork .transaction_intrinsic_cost_calculator ()(calldata = b"" )
509
+
510
+ num_contracts = len (address_stubs .root )
511
+
512
+ # Cost per SLOAD iteration (balanceOf call)
513
+ sload_cost_per_iteration = (
514
+ # Attack contract loop overhead
515
+ gas_costs .G_VERY_LOW * 2 # MLOAD counter (3*2)
516
+ + gas_costs .G_VERY_LOW * 2 # MSTORE selector (3*2)
517
+ + gas_costs .G_VERY_LOW * 3 # MLOAD + MSTORE address (3*3)
518
+ + gas_costs .G_BASE # POP (2)
519
+ + gas_costs .G_BASE * 3 # SUB + MLOAD + MSTORE for counter decrement (2*3)
520
+ + gas_costs .G_BASE * 2 # ISZERO * 2 for loop condition (2*2)
521
+ + gas_costs .G_MID # JUMPI (8)
522
+ # CALL to ERC20 contract
523
+ + gas_costs .G_WARM_ACCOUNT_ACCESS # Warm CALL to same contract (100)
524
+ # Inside ERC20 balanceOf
525
+ + gas_costs .G_VERY_LOW # PUSH4 selector (3)
526
+ + gas_costs .G_BASE # EQ selector match (2)
527
+ + gas_costs .G_MID # JUMPI to function (8)
528
+ + gas_costs .G_JUMPDEST # JUMPDEST at function start (1)
529
+ + gas_costs .G_VERY_LOW * 2 # CALLDATALOAD arg (3*2)
530
+ + gas_costs .G_KECCAK_256 # keccak256 static (30)
531
+ + gas_costs .G_KECCAK_256_WORD * 2 # keccak256 dynamic for 64 bytes (2*6)
532
+ + gas_costs .G_COLD_SLOAD # Cold SLOAD (2100)
533
+ + gas_costs .G_VERY_LOW * 3 # MSTORE result + RETURN setup (3*3)
534
+ )
535
+
536
+ # Cost per SSTORE iteration (approve call)
537
+ sstore_cost_per_iteration = (
538
+ # Attack contract loop body operations
539
+ gas_costs .G_VERY_LOW # MSTORE selector at memory[32] (3)
540
+ + gas_costs .G_LOW # MLOAD counter (5)
541
+ + gas_costs .G_VERY_LOW # MSTORE spender at memory[64] (3)
542
+ + gas_costs .G_LOW # MLOAD counter (5)
543
+ + gas_costs .G_VERY_LOW # MSTORE amount at memory[96] (3)
544
+ # CALL to ERC20 contract
545
+ + gas_costs .G_WARM_ACCOUNT_ACCESS # Warm CALL base cost (100)
546
+ + gas_costs .G_BASE # POP call result (2)
547
+ # Counter decrement
548
+ + gas_costs .G_LOW # MLOAD counter (5)
549
+ + gas_costs .G_VERY_LOW # PUSH1 1 (3)
550
+ + gas_costs .G_VERY_LOW # SUB (3)
551
+ + gas_costs .G_VERY_LOW # MSTORE counter back (3)
552
+ # While loop condition check
553
+ + gas_costs .G_LOW # MLOAD counter (5)
554
+ + gas_costs .G_BASE # ISZERO (2)
555
+ + gas_costs .G_BASE # ISZERO (2)
556
+ + gas_costs .G_MID # JUMPI back to loop start (8)
557
+ # Inside ERC20 approve function
558
+ + gas_costs .G_VERY_LOW # PUSH4 selector (3)
559
+ + gas_costs .G_BASE # EQ selector match (2)
560
+ + gas_costs .G_MID # JUMPI to function (8)
561
+ + gas_costs .G_JUMPDEST # JUMPDEST at function start (1)
562
+ + gas_costs .G_VERY_LOW # CALLDATALOAD spender (3)
563
+ + gas_costs .G_VERY_LOW # CALLDATALOAD amount (3)
564
+ + gas_costs .G_KECCAK_256 # keccak256 static (30)
565
+ + gas_costs .G_KECCAK_256_WORD * 2 # keccak256 dynamic for 64 bytes (12)
566
+ + gas_costs .G_STORAGE_SET # SSTORE to zero slot (20000)
567
+ + gas_costs .G_VERY_LOW # PUSH1 1 for return value (3)
568
+ + gas_costs .G_VERY_LOW # MSTORE return value (3)
569
+ + gas_costs .G_VERY_LOW # PUSH1 32 for return size (3)
570
+ + gas_costs .G_VERY_LOW # PUSH1 0 for return offset (3)
571
+ )
572
+
573
+ # Calculate gas budget per contract
574
+ available_gas = gas_benchmark_value - intrinsic_gas
575
+ gas_per_contract = available_gas // num_contracts
576
+
577
+ # For each contract, split gas by percentage
578
+ sload_gas_per_contract = (gas_per_contract * sload_percent ) // 100
579
+ sstore_gas_per_contract = (gas_per_contract * sstore_percent ) // 100
580
+
581
+ # Calculate calls per contract per operation
582
+ sload_calls_per_contract = int (sload_gas_per_contract // sload_cost_per_iteration )
583
+ sstore_calls_per_contract = int (sstore_gas_per_contract // sstore_cost_per_iteration )
584
+
585
+ # Deploy all discovered ERC20 contracts using stubs
586
+ erc20_addresses = []
587
+ for stub_name in address_stubs .root :
588
+ addr = pre .deploy_contract (
589
+ code = Bytecode (),
590
+ stub = stub_name ,
591
+ )
592
+ erc20_addresses .append (addr )
593
+
594
+ # Log test requirements
595
+ print (
596
+ f"Total gas budget: { gas_benchmark_value / 1_000_000 :.1f} M gas. "
597
+ f"~{ gas_per_contract / 1_000_000 :.1f} M gas per contract "
598
+ f"({ sload_percent } % SLOAD, { sstore_percent } % SSTORE). "
599
+ f"Per contract: { sload_calls_per_contract } balanceOf calls, "
600
+ f"{ sstore_calls_per_contract } approve calls."
601
+ )
602
+
603
+ # Build attack code that loops through each contract
604
+ attack_code : Bytecode = Op .JUMPDEST # Entry point
605
+
606
+ for erc20_address in erc20_addresses :
607
+ # For each contract, execute SLOAD operations (balanceOf)
608
+ attack_code += (
609
+ # Store function selector at memory[32] (once per contract)
610
+ Op .MSTORE (offset = 32 , value = BALANCEOF_SELECTOR )
611
+ # Initialize counter in memory[0] = number of balanceOf calls
612
+ + Op .MSTORE (offset = 0 , value = sload_calls_per_contract )
613
+ # Loop for balanceOf calls
614
+ + While (
615
+ condition = Op .MLOAD (0 ) + Op .ISZERO + Op .ISZERO ,
616
+ body = (
617
+ # Store address at memory[64] (use counter as address)
618
+ Op .MSTORE (offset = 64 , value = Op .MLOAD (0 ))
619
+ # Call balanceOf(address) on ERC20 contract
620
+ + Op .CALL (
621
+ address = erc20_address ,
622
+ value = 0 ,
623
+ args_offset = 32 ,
624
+ args_size = 36 ,
625
+ ret_offset = 0 ,
626
+ ret_size = 0 ,
627
+ )
628
+ + Op .POP # Discard result
629
+ # Decrement counter
630
+ + Op .MSTORE (offset = 0 , value = Op .SUB (Op .MLOAD (0 ), 1 ))
631
+ ),
632
+ )
633
+ )
634
+
635
+ # For each contract, execute SSTORE operations (approve)
636
+ attack_code += (
637
+ # Store function selector at memory[32] (once per contract)
638
+ Op .MSTORE (offset = 32 , value = APPROVE_SELECTOR )
639
+ # Initialize counter in memory[0] = number of approve calls
640
+ + Op .MSTORE (offset = 0 , value = sstore_calls_per_contract )
641
+ # Loop for approve calls
642
+ + While (
643
+ condition = Op .MLOAD (0 ) + Op .ISZERO + Op .ISZERO ,
644
+ body = (
645
+ # Store spender address at memory[64] (use counter)
646
+ Op .MSTORE (offset = 64 , value = Op .MLOAD (0 ))
647
+ # Store amount at memory[96] (use counter as amount)
648
+ + Op .MSTORE (offset = 96 , value = Op .MLOAD (0 ))
649
+ # Call approve(spender, amount) on ERC20 contract
650
+ + Op .CALL (
651
+ address = erc20_address ,
652
+ value = 0 ,
653
+ args_offset = 32 ,
654
+ args_size = 68 ,
655
+ ret_offset = 0 ,
656
+ ret_size = 0 ,
657
+ )
658
+ + Op .POP # Discard result
659
+ # Decrement counter
660
+ + Op .MSTORE (offset = 0 , value = Op .SUB (Op .MLOAD (0 ), 1 ))
661
+ ),
662
+ )
663
+ )
664
+
665
+ # Deploy attack contract
666
+ attack_address = pre .deploy_contract (code = attack_code )
667
+
668
+ # Run the attack
669
+ attack_tx = Transaction (
670
+ to = attack_address ,
671
+ gas_limit = gas_benchmark_value ,
672
+ sender = pre .fund_eoa (),
673
+ )
674
+
675
+ # Post-state
676
+ post = {
677
+ attack_address : Account (storage = {}),
678
+ }
679
+
680
+ blockchain_test (
681
+ pre = pre ,
682
+ blocks = [Block (txs = [attack_tx ])],
683
+ post = post ,
684
+ )
0 commit comments