16
16
17
17
import pytest
18
18
19
- from ethereum_test_forks import Fork
19
+ from ethereum_test_base_types .composite_types import Account
20
+ from ethereum_test_forks .helpers import Fork
20
21
from ethereum_test_tools import (
21
- Account ,
22
22
Alloc ,
23
23
Block ,
24
24
BlockchainTestFiller ,
25
- Environment ,
26
25
Hash ,
27
26
Transaction ,
28
27
While ,
29
- compute_create2_address ,
30
28
)
31
29
from ethereum_test_tools import Macros as Om
32
- from ethereum_test_tools .vm .opcode import Opcodes as Op
30
+ from ethereum_test_types .block_types import Environment
31
+ from ethereum_test_types .helpers import compute_create2_address
32
+ from ethereum_test_vm import Opcodes as Op
33
33
34
34
35
35
# TODO: add test which writes to already existing storage
@@ -102,7 +102,7 @@ def test_xen_approve(
102
102
103
103
104
104
@pytest .mark .valid_from ("Frontier" )
105
- def test_xen_approve_existing_slots (
105
+ def test_xen_approve_change_existing_slots (
106
106
blockchain_test : BlockchainTestFiller ,
107
107
pre : Alloc ,
108
108
):
@@ -114,9 +114,116 @@ def test_xen_approve_existing_slots(
114
114
60_000_000 # TODO: currently hardcoded, should be read from `gas_benchmark_value`
115
115
)
116
116
117
+ # 22 Sep 10:08:22 | Processed 23366464 | 207.4 ms | slot 1,734 ms |⛽ Gas gwei: 1.00 .. 1.00 (1.00) .. 1.00
118
+ # 22 Sep 10:08:22 | Cleared caches: Rlp
119
+ # 22 Sep 10:08:22 | Block 0.0600 ETH 59.96 MGas | 1 txs | calls 7,752 ( 0) | sload 8 | sstore 7,762 | create 0
120
+ # 22 Sep 10:08:22 | Block throughput 289.05 MGas/s | 4.8 tps | 4.82 Blk/s | exec code cache 15,508 | new 0 | ops 1,868,419
121
+
122
+ xen_contract = 0x06450DEE7FD2FB8E39061434BABCFC05599A6FB8
123
+ gas_threshold = 40_000
124
+
125
+ # This test deletes 9599 storage slots from XEN
126
+
127
+ fn_signature_approve = bytes .fromhex (
128
+ "095EA7B3"
129
+ ) # Function selector of `approve(address,uint256)`
130
+ # This code loops until there is less than threshold_gas left and reads two items from calldata:
131
+ # The first 32 bytes are interpreted as the start address to start approving for
132
+ # The second 32 bytes is the approval amount
133
+ # This can thus be used to initialize the approvals (in multiple txs) to write to the storage
134
+ # Since initializing storage (from zero to nonzero) is more expensive, this thus has
135
+ # to be done over multiple blocks/txs
136
+ # The attack block can then target all of the just initialized storage slots to edit
137
+ # (This should thus yield more dirty trie nodes than the )
138
+ approval_loop_code = (
139
+ Om .MSTORE (fn_signature_approve )
140
+ + Op .MSTORE (4 + 32 , Op .CALLDATALOAD (32 ))
141
+ + Op .CALLDATALOAD (0 )
142
+ + While (
143
+ body = Op .MSTORE (
144
+ 4 , Op .DUP1
145
+ ) # Put a copy of the topmost stack item in memory (this is the target address)
146
+ + Op .CALL (address = xen_contract , args_offset = 0 , args_size = 4 + 32 + 32 )
147
+ + Op .ADD # Add the status of the CALL
148
+ # (this should always be 1 unless the `gas_threshold` is too low) to the stack item
149
+ # The address and thus target storage slot changes!
150
+ + Op .MSTORE (4 + 32 , Op .SUB (Op .MLOAD (4 + 32 ), Op .GAS )),
151
+ condition = Op .GT (Op .GAS , gas_threshold ),
152
+ )
153
+ )
154
+
155
+ approval_spammer_contract = pre .deploy_contract (code = approval_loop_code )
156
+
157
+ sender = pre .fund_eoa ()
158
+
159
+ blocks = []
160
+
161
+ # TODO: calculate these constants based on the gas limit of the benchmark test
162
+ start_address = 0x01 # XEN blocks approving the zero address
163
+ current_address = start_address
164
+ address_incr = 2000
165
+
166
+ approval_value_fresh = Hash (0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE )
167
+ approval_value_overwrite = Hash (0 )
168
+
169
+ block_count = 10
170
+
171
+ for _ in range (block_count ):
172
+ setup_calldata = Hash (current_address ) + approval_value_fresh
173
+ setup_tx = Transaction (
174
+ to = approval_spammer_contract ,
175
+ gas_limit = attack_gas_limit ,
176
+ data = setup_calldata ,
177
+ sender = sender ,
178
+ max_priority_fee_per_gas = 100 ,
179
+ max_fee_per_gas = 10000 ,
180
+ )
181
+ blocks .append (Block (txs = [setup_tx ]))
182
+
183
+ current_address += address_incr
184
+
185
+ attack_calldata = Hash (start_address ) + approval_value_overwrite
186
+
187
+ attack_tx = Transaction (
188
+ to = approval_spammer_contract ,
189
+ gas_limit = attack_gas_limit ,
190
+ max_priority_fee_per_gas = 100 ,
191
+ max_fee_per_gas = 10000 ,
192
+ data = attack_calldata ,
193
+ sender = sender ,
194
+ )
195
+ blocks .append (Block (txs = [attack_tx ]))
196
+
197
+ blockchain_test (
198
+ pre = pre ,
199
+ post = {}, # TODO: add sanity checks (succesful tx execution and no out-of-gas)
200
+ blocks = blocks ,
201
+ )
202
+
203
+
204
+ # TODO split this code in all situations: 0->1, 1->2, 1->0
205
+ @pytest .mark .valid_from ("Frontier" )
206
+ def test_xen_approve_delete_existing_slots (
207
+ blockchain_test : BlockchainTestFiller ,
208
+ pre : Alloc ,
209
+ ):
210
+ """
211
+ Uses the `approve(address,uint256)` method of XEN (ERC20) close to the maximum amount
212
+ of slots which could be edited (as opposed to be created) within a single block/transaction.
213
+ """
214
+ attack_gas_limit = (
215
+ 60_000_000 # TODO: currently hardcoded, should be read from `gas_benchmark_value`
216
+ )
217
+
218
+ # 22 Sep 10:08:22 | Processed 23366464 | 207.4 ms | slot 1,734 ms |⛽ Gas gwei: 1.00 .. 1.00 (1.00) .. 1.00
219
+ # 22 Sep 10:08:22 | Cleared caches: Rlp
220
+ # 22 Sep 10:08:22 | Block 0.0600 ETH 59.96 MGas | 1 txs | calls 7,752 ( 0) | sload 8 | sstore 7,762 | create 0
221
+ # 22 Sep 10:08:22 | Block throughput 289.05 MGas/s | 4.8 tps | 4.82 Blk/s | exec code cache 15,508 | new 0 | ops 1,868,419
222
+
117
223
# Gas limit: 60M, 2424 SSTOREs, 300 MGas/s
118
224
119
225
xen_contract = 0x06450DEE7FD2FB8E39061434BABCFC05599A6FB8
226
+ usdt_contract = 0xDAC17F958D2EE523A2206206994597C13D831EC7 # Used in intermediate blocks to attempt to bust te cache
120
227
gas_threshold = 40_000
121
228
122
229
fn_signature_approve = bytes .fromhex (
@@ -142,25 +249,53 @@ def test_xen_approve_existing_slots(
142
249
+ Op .ADD , # Add the status of the CALL
143
250
# (this should always be 1 unless the `gas_threshold` is too low) to the stack item
144
251
# The address and thus target storage slot changes!
252
+ # + Op.MSTORE(4 + 32, Op.SUB(Op.MLOAD(4 + 34), Op.GAS)),
145
253
condition = Op .GT (Op .GAS , gas_threshold ),
146
254
)
147
255
)
148
256
149
257
approval_spammer_contract = pre .deploy_contract (code = approval_loop_code )
150
258
259
+ usdt_approve_spammer_code = (
260
+ Om .MSTORE (fn_signature_approve )
261
+ + Op .MSTORE (4 + 32 , 1 )
262
+ + Op .SLOAD (0 )
263
+ + While (
264
+ body = Op .MSTORE (
265
+ 4 , Op .DUP1
266
+ ) # Put a copy of the topmost stack item in memory (this is the target address)
267
+ + Op .CALL (address = usdt_contract , args_offset = 0 , args_size = 4 + 32 + 32 )
268
+ + Op .ADD , # Add the status of the CALL
269
+ # (this should always be 1 unless the `gas_threshold` is too low) to the stack item
270
+ # The address and thus target storage slot changes!
271
+ # + Op.MSTORE(4 + 32, Op.SUB(Op.MLOAD(4 + 34), Op.GAS)),
272
+ condition = Op .GT (Op .GAS , gas_threshold ),
273
+ )
274
+ + Op .PUSH1 (0 )
275
+ + Op .SSTORE
276
+ )
277
+ # Set storage to value 1 to avoid paying 20k on the update
278
+ usdt_approve_spammer_contract = pre .deploy_contract (
279
+ code = usdt_approve_spammer_code , storage = {0 : 1 }
280
+ )
281
+
151
282
sender = pre .fund_eoa ()
283
+ sender2 = pre .fund_eoa () # More senders to get more chance to get a semi-full block
284
+ sender3 = (
285
+ pre .fund_eoa ()
286
+ ) # If done from one sender, Nethermind's block builder only includes 1 tx
287
+ sender4 = pre .fund_eoa ()
288
+ sender5 = pre .fund_eoa ()
152
289
153
290
blocks = []
154
291
155
292
# TODO: calculate these constants based on the gas limit of the benchmark test
156
- start_address = 0x01
293
+ start_address = 0x01 # Start at address 1, address 0 cannot be approved
157
294
current_address = start_address
158
295
address_incr = 2000
159
296
160
297
approval_value_fresh = Hash (0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE )
161
- approval_value_overwrite = Hash (
162
- 0xDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDE
163
- )
298
+ approval_value_overwrite = Hash (0 )
164
299
165
300
block_count = 10
166
301
@@ -171,20 +306,81 @@ def test_xen_approve_existing_slots(
171
306
gas_limit = attack_gas_limit ,
172
307
data = setup_calldata ,
173
308
sender = sender ,
309
+ max_priority_fee_per_gas = 100 ,
310
+ max_fee_per_gas = 10000 ,
174
311
)
175
312
blocks .append (Block (txs = [setup_tx ]))
176
313
177
314
current_address += address_incr
178
315
316
+ spam_count = 10
317
+
318
+ for _ in range (spam_count ):
319
+ # NOTE: USDC does not allow changing the approval value. It first has to be
320
+ # set to zero before it changes. We therefore flood USDC with approvals in an
321
+ # attempt to bust the cache
322
+ spam_tx = Transaction (
323
+ to = usdt_approve_spammer_contract ,
324
+ gas_limit = attack_gas_limit ,
325
+ sender = sender ,
326
+ max_priority_fee_per_gas = 100 ,
327
+ max_fee_per_gas = 10000 ,
328
+ )
329
+ blocks .append (Block (txs = [spam_tx ]))
330
+
179
331
attack_calldata = Hash (start_address ) + approval_value_overwrite
180
332
181
333
attack_tx = Transaction (
182
334
to = approval_spammer_contract ,
183
335
gas_limit = attack_gas_limit ,
336
+ max_priority_fee_per_gas = 100 ,
337
+ max_fee_per_gas = 10000 ,
184
338
data = attack_calldata ,
185
339
sender = sender ,
186
340
)
187
- blocks .append (Block (txs = [attack_tx ]))
341
+ # Take into account the max refunds (which will be awarded here)
342
+ # The previous tx will also not completely use all gas since it jumps out of the loop early
343
+ # to avoid that the whole tx OOGs
344
+ # TODO: make this attack gas limit dependent
345
+ # It should be sufficient to assume full refund and to send the whole block as gas limit initially
346
+ # The next tx gas limit is thus (if refund is maximally applied) 20% of the original
347
+ # Repeat this until the intrinsic costs cannot be paid
348
+ attack_tx_2 = Transaction (
349
+ to = approval_spammer_contract ,
350
+ gas_limit = attack_gas_limit // 5 ,
351
+ max_priority_fee_per_gas = 90 ,
352
+ max_fee_per_gas = 9000 ,
353
+ data = Hash (8000 ) + approval_value_overwrite ,
354
+ sender = sender2 ,
355
+ )
356
+ attack_tx_3 = Transaction (
357
+ to = approval_spammer_contract ,
358
+ gas_limit = attack_gas_limit // (5 * 5 ),
359
+ max_priority_fee_per_gas = 80 ,
360
+ max_fee_per_gas = 8000 ,
361
+ data = Hash (12000 ) + approval_value_overwrite ,
362
+ sender = sender3 ,
363
+ )
364
+ attack_tx_4 = Transaction (
365
+ to = approval_spammer_contract ,
366
+ gas_limit = attack_gas_limit // (5 * 5 * 5 ),
367
+ max_priority_fee_per_gas = 80 ,
368
+ max_fee_per_gas = 8000 ,
369
+ data = Hash (16000 ) + approval_value_overwrite ,
370
+ sender = sender4 ,
371
+ )
372
+ attack_tx_5 = Transaction (
373
+ to = approval_spammer_contract ,
374
+ gas_limit = attack_gas_limit // (5 * 5 * 5 * 5 ),
375
+ max_priority_fee_per_gas = 80 ,
376
+ max_fee_per_gas = 8000 ,
377
+ data = Hash (18000 ) + approval_value_overwrite ,
378
+ sender = sender5 ,
379
+ )
380
+
381
+ blocks .append (
382
+ Block (txs = [attack_tx , attack_tx_2 , attack_tx_3 , attack_tx_4 , attack_tx_5 ])
383
+ ) # , #attack_tx_2, attack_tx_3]))
188
384
189
385
blockchain_test (
190
386
pre = pre ,
0 commit comments