1616
1717import pytest
1818
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
2021from ethereum_test_tools import (
21- Account ,
2222 Alloc ,
2323 Block ,
2424 BlockchainTestFiller ,
25- Environment ,
2625 Hash ,
2726 Transaction ,
2827 While ,
29- compute_create2_address ,
3028)
3129from 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
3333
3434
3535# TODO: add test which writes to already existing storage
@@ -102,7 +102,7 @@ def test_xen_approve(
102102
103103
104104@pytest .mark .valid_from ("Frontier" )
105- def test_xen_approve_existing_slots (
105+ def test_xen_approve_change_existing_slots (
106106 blockchain_test : BlockchainTestFiller ,
107107 pre : Alloc ,
108108):
@@ -114,9 +114,116 @@ def test_xen_approve_existing_slots(
114114 60_000_000 # TODO: currently hardcoded, should be read from `gas_benchmark_value`
115115 )
116116
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+
117223 # Gas limit: 60M, 2424 SSTOREs, 300 MGas/s
118224
119225 xen_contract = 0x06450DEE7FD2FB8E39061434BABCFC05599A6FB8
226+ usdt_contract = 0xDAC17F958D2EE523A2206206994597C13D831EC7 # Used in intermediate blocks to attempt to bust te cache
120227 gas_threshold = 40_000
121228
122229 fn_signature_approve = bytes .fromhex (
@@ -142,25 +249,53 @@ def test_xen_approve_existing_slots(
142249 + Op .ADD , # Add the status of the CALL
143250 # (this should always be 1 unless the `gas_threshold` is too low) to the stack item
144251 # The address and thus target storage slot changes!
252+ # + Op.MSTORE(4 + 32, Op.SUB(Op.MLOAD(4 + 34), Op.GAS)),
145253 condition = Op .GT (Op .GAS , gas_threshold ),
146254 )
147255 )
148256
149257 approval_spammer_contract = pre .deploy_contract (code = approval_loop_code )
150258
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+
151282 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 ()
152289
153290 blocks = []
154291
155292 # 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
157294 current_address = start_address
158295 address_incr = 2000
159296
160297 approval_value_fresh = Hash (0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE )
161- approval_value_overwrite = Hash (
162- 0xDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDE
163- )
298+ approval_value_overwrite = Hash (0 )
164299
165300 block_count = 10
166301
@@ -171,20 +306,81 @@ def test_xen_approve_existing_slots(
171306 gas_limit = attack_gas_limit ,
172307 data = setup_calldata ,
173308 sender = sender ,
309+ max_priority_fee_per_gas = 100 ,
310+ max_fee_per_gas = 10000 ,
174311 )
175312 blocks .append (Block (txs = [setup_tx ]))
176313
177314 current_address += address_incr
178315
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+
179331 attack_calldata = Hash (start_address ) + approval_value_overwrite
180332
181333 attack_tx = Transaction (
182334 to = approval_spammer_contract ,
183335 gas_limit = attack_gas_limit ,
336+ max_priority_fee_per_gas = 100 ,
337+ max_fee_per_gas = 10000 ,
184338 data = attack_calldata ,
185339 sender = sender ,
186340 )
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]))
188384
189385 blockchain_test (
190386 pre = pre ,
0 commit comments