15
15
Alloc ,
16
16
Block ,
17
17
BlockchainTestFiller ,
18
+ Bytecode ,
18
19
Environment ,
19
20
Hash ,
20
21
Transaction ,
36
37
"opcode" ,
37
38
[
38
39
Op .EXTCODESIZE ,
39
- ] ,
40
- )
41
- @ pytest . mark . parametrize (
42
- "attack_gas_limit" ,
43
- [
44
- Environment (). gas_limit ,
40
+ Op . EXTCODEHASH ,
41
+ Op . CALL ,
42
+ Op . CALLCODE ,
43
+ Op . DELEGATECALL ,
44
+ Op . STATICCALL ,
45
+ Op . EXTCODECOPY ,
45
46
],
46
47
)
47
48
@pytest .mark .slow ()
@@ -51,7 +52,6 @@ def test_worst_bytecode_single_opcode(
51
52
pre : Alloc ,
52
53
fork : Fork ,
53
54
opcode : Op ,
54
- attack_gas_limit : int ,
55
55
):
56
56
"""
57
57
Test a block execution where a single opcode execution maxes out the gas limit,
@@ -68,6 +68,7 @@ def test_worst_bytecode_single_opcode(
68
68
# We use 100G gas limit to be able to deploy a large number of contracts in a single block,
69
69
# avoiding bloating the number of preparing blocks in the test.
70
70
env = Environment (gas_limit = 100_000_000_000 )
71
+ attack_gas_limit = Environment ().gas_limit
71
72
72
73
# The initcode will take its address as a starting point to the input to the keccak
73
74
# hash function.
@@ -87,6 +88,10 @@ def test_worst_bytecode_single_opcode(
87
88
),
88
89
condition = Op .LT (Op .MSIZE , MAX_CONTRACT_SIZE ),
89
90
)
91
+ # Despite the whole contract has random bytecode, we make the first opcode be a STOP
92
+ # so CALL-like attacks return as soon as possible, while EXTCODE(HASH|SIZE) work as
93
+ # intended.
94
+ + Op .MSTORE8 (0 , 0x00 )
90
95
+ Op .RETURN (0 , MAX_CONTRACT_SIZE )
91
96
)
92
97
initcode_address = pre .deploy_contract (code = initcode )
@@ -105,7 +110,7 @@ def test_worst_bytecode_single_opcode(
105
110
Op .CREATE2 (
106
111
value = 0 ,
107
112
offset = 0 ,
108
- size = Op .MSIZE ,
113
+ size = Op .EXTCODESIZE ( initcode_address ) ,
109
114
salt = Op .SLOAD (0 ),
110
115
),
111
116
)
@@ -128,40 +133,25 @@ def test_worst_bytecode_single_opcode(
128
133
gas_costs .G_KECCAK_256 # KECCAK static cost
129
134
+ math .ceil (85 / 32 ) * gas_costs .G_KECCAK_256_WORD # KECCAK dynamic cost for CREATE2
130
135
+ gas_costs .G_VERY_LOW * 3 # ~MSTOREs+ADDs
131
- + gas_costs .G_COLD_ACCOUNT_ACCESS # EXTCODESIZE
136
+ + gas_costs .G_COLD_ACCOUNT_ACCESS # Opcode cost
132
137
+ 30 # ~Gluing opcodes
133
138
)
134
- max_number_of_contract_calls = (
139
+ num_contracts = (
135
140
# Base available gas = GAS_LIMIT - intrinsic - (out of loop MSTOREs)
136
141
attack_gas_limit - intrinsic_gas_cost_calc () - gas_costs .G_VERY_LOW * 4
137
142
) // loop_cost
138
143
139
- total_contracts_to_deploy = max_number_of_contract_calls
140
- approximate_gas_per_deployment = 4_970_000 # Obtained from evm tracing
141
- contracts_deployed_per_tx = env .gas_limit // approximate_gas_per_deployment
142
-
143
- deploy_txs = []
144
-
145
- def generate_deploy_tx (contracts_to_deploy : int ):
146
- return Transaction (
147
- to = factory_caller_address ,
148
- gas_limit = env .gas_limit ,
149
- gas_price = 10 ** 9 , # Bump required due to the amount of full blocks
150
- data = Hash (contracts_deployed_per_tx ),
151
- sender = pre .fund_eoa (),
152
- )
153
-
154
- for _ in range (total_contracts_to_deploy // contracts_deployed_per_tx ):
155
- deploy_txs .append (generate_deploy_tx (contracts_deployed_per_tx ))
156
-
157
- if total_contracts_to_deploy % contracts_deployed_per_tx != 0 :
158
- deploy_txs .append (
159
- generate_deploy_tx (total_contracts_to_deploy % contracts_deployed_per_tx )
160
- )
144
+ contracts_deployment_tx = Transaction (
145
+ to = factory_caller_address ,
146
+ gas_limit = env .gas_limit ,
147
+ gas_price = 10 ** 9 ,
148
+ data = Hash (num_contracts ),
149
+ sender = pre .fund_eoa (),
150
+ )
161
151
162
152
post = {}
163
153
deployed_contract_addresses = []
164
- for i in range (total_contracts_to_deploy ):
154
+ for i in range (num_contracts ):
165
155
deployed_contract_address = compute_create2_address (
166
156
address = factory_address ,
167
157
salt = i ,
@@ -170,29 +160,37 @@ def generate_deploy_tx(contracts_to_deploy: int):
170
160
post [deployed_contract_address ] = Account (nonce = 1 )
171
161
deployed_contract_addresses .append (deployed_contract_address )
172
162
173
- opcode_code = (
163
+ attack_call = Bytecode ()
164
+ if opcode == Op .EXTCODECOPY :
165
+ attack_call = Op .EXTCODECOPY (address = Op .SHA3 (32 - 20 - 1 , 85 ), dest_offset = 85 , size = 1000 )
166
+ else :
167
+ # For the rest of the opcodes, we can use the same generic attack call
168
+ # since all only minimally need the `address` of the target.
169
+ attack_call = Op .POP (opcode (address = Op .SHA3 (32 - 20 - 1 , 85 )))
170
+ attack_code = (
174
171
# Setup memory for later CREATE2 address generation loop.
172
+ # 0xFF+[Address(20bytes)]+[seed(32bytes)]+[initcode keccak(32bytes)]
175
173
Op .MSTORE (0 , factory_address )
176
- + Op .MSTORE8 (32 - 20 - 1 , 0xFF ) # 0xFF prefix byte
174
+ + Op .MSTORE8 (32 - 20 - 1 , 0xFF )
177
175
+ Op .MSTORE (32 , 0 )
178
176
+ Op .MSTORE (64 , initcode .keccak256 ())
179
177
# Main loop
180
178
+ While (
181
- body = Op . POP ( opcode ( Op . SHA3 ( 32 - 20 - 1 , 85 ))) + Op .MSTORE (32 , Op .ADD (Op .MLOAD (32 ), 1 )),
179
+ body = attack_call + Op .MSTORE (32 , Op .ADD (Op .MLOAD (32 ), 1 )),
182
180
)
183
181
)
184
182
185
- if len (opcode_code ) > MAX_CONTRACT_SIZE :
183
+ if len (attack_code ) > MAX_CONTRACT_SIZE :
186
184
# TODO: A workaround could be to split the opcode code into multiple contracts
187
185
# and call them in sequence.
188
186
raise ValueError (
189
- f"Code size { len (opcode_code )} exceeds maximum code size { MAX_CONTRACT_SIZE } "
187
+ f"Code size { len (attack_code )} exceeds maximum code size { MAX_CONTRACT_SIZE } "
190
188
)
191
- opcode_address = pre .deploy_contract (code = opcode_code )
189
+ opcode_address = pre .deploy_contract (code = attack_code )
192
190
opcode_tx = Transaction (
193
191
to = opcode_address ,
194
192
gas_limit = attack_gas_limit ,
195
- gas_price = 10 ** 9 , # Bump required due to the amount of full blocks
193
+ gas_price = 10 ** 9 ,
196
194
sender = pre .fund_eoa (),
197
195
)
198
196
@@ -201,7 +199,7 @@ def generate_deploy_tx(contracts_to_deploy: int):
201
199
pre = pre ,
202
200
post = post ,
203
201
blocks = [
204
- * [ Block (txs = [deploy_tx ]) for deploy_tx in deploy_txs ] ,
202
+ Block (txs = [contracts_deployment_tx ]) ,
205
203
Block (txs = [opcode_tx ]),
206
204
],
207
205
exclude_full_post_state_in_output = True ,
0 commit comments