53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190 | @pytest.mark.parametrize(
"balance_first",
[True, False],
ids=["balance_extcodesize", "extcodesize_balance"],
)
@pytest.mark.valid_from("Prague")
def test_bloatnet_balance_extcodesize(
blockchain_test: BlockchainTestFiller,
pre: Alloc,
fork: Fork,
gas_benchmark_value: int,
balance_first: bool,
) -> None:
"""
BloatNet test using BALANCE + EXTCODESIZE with "on-the-fly" CREATE2
address generation.
This test:
1. Assumes contracts are already deployed via the factory (salt 0 to N-1)
2. Generates CREATE2 addresses dynamically during execution
3. Calls BALANCE and EXTCODESIZE (order controlled by balance_first param)
4. Maximizes cache eviction by accessing many contracts
"""
gas_costs = fork.gas_costs()
# Calculate gas costs
intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(calldata=b"")
# Cost per contract access with 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 (100)
+ gas_costs.G_BASE # POP second result (2)
+ gas_costs.G_BASE # DUP1 before first op (3)
+ gas_costs.G_VERY_LOW * 4 # PUSH1 operations (4 * 3)
+ gas_costs.G_LOW # MLOAD for salt (3)
+ gas_costs.G_VERY_LOW # ADD for increment (3)
+ gas_costs.G_LOW # MSTORE salt back (3)
+ 10 # While loop overhead
)
# Calculate how many contracts to access based on available gas
available_gas = gas_benchmark_value - intrinsic_gas - 1000 # Reserve for cleanup
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)
extcodesize_op = Op.POP(Op.EXTCODESIZE)
benchmark_ops = (
(balance_op + extcodesize_op) if balance_first else (extcodesize_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 addr: keccak256(0xFF+factory+salt+hash)
Op.SHA3(11, 85) # Generate CREATE2 address from memory[11:96]
# The address is now on the stack
+ Op.DUP1 # Duplicate for second operation
+ 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 we haven't reached the limit
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: just verify attack contract exists
post = {
attack_address: Account(storage={}),
}
blockchain_test(
pre=pre,
blocks=[Block(txs=[attack_tx])],
post=post,
)
|