@@ -101,6 +101,98 @@ def test_xen_approve(
101
101
)
102
102
103
103
104
+ @pytest .mark .valid_from ("Frontier" )
105
+ def test_xen_approve_existing_slots (
106
+ blockchain_test : BlockchainTestFiller ,
107
+ pre : Alloc ,
108
+ ):
109
+ """
110
+ Uses the `approve(address,uint256)` method of XEN (ERC20) close to the maximum amount
111
+ of slots which could be edited (as opposed to be created) within a single block/transaction.
112
+ """
113
+ attack_gas_limit = (
114
+ 60_000_000 # TODO: currently hardcoded, should be read from `gas_benchmark_value`
115
+ )
116
+
117
+ # Gas limit: 60M, 2424 SSTOREs, 300 MGas/s
118
+
119
+ xen_contract = 0x06450DEE7FD2FB8E39061434BABCFC05599A6FB8
120
+ gas_threshold = 40_000
121
+
122
+ fn_signature_approve = bytes .fromhex (
123
+ "095EA7B3"
124
+ ) # Function selector of `approve(address,uint256)`
125
+ # This code loops until there is less than threshold_gas left and reads two items from calldata:
126
+ # The first 32 bytes are interpreted as the start address to start approving for
127
+ # The second 32 bytes is the approval amount
128
+ # This can thus be used to initialize the approvals (in multiple txs) to write to the storage
129
+ # Since initializing storage (from zero to nonzero) is more expensive, this thus has
130
+ # to be done over multiple blocks/txs
131
+ # The attack block can then target all of the just initialized storage slots to edit
132
+ # (This should thus yield more dirty trie nodes than the )
133
+ approval_loop_code = (
134
+ Om .MSTORE (fn_signature_approve )
135
+ + Op .MSTORE (4 + 32 , Op .CALLDATALOAD (32 ))
136
+ + Op .CALLDATALOAD (0 )
137
+ + While (
138
+ body = Op .MSTORE (
139
+ 4 , Op .DUP1
140
+ ) # Put a copy of the topmost stack item in memory (this is the target address)
141
+ + Op .CALL (address = xen_contract , args_offset = 0 , args_size = 4 + 32 + 32 )
142
+ + Op .ADD , # Add the status of the CALL
143
+ # (this should always be 1 unless the `gas_threshold` is too low) to the stack item
144
+ # The address and thus target storage slot changes!
145
+ condition = Op .GT (Op .GAS , gas_threshold ),
146
+ )
147
+ )
148
+
149
+ approval_spammer_contract = pre .deploy_contract (code = approval_loop_code )
150
+
151
+ sender = pre .fund_eoa ()
152
+
153
+ blocks = []
154
+
155
+ # TODO: calculate these constants based on the gas limit of the benchmark test
156
+ start_address = 0x01
157
+ current_address = start_address
158
+ address_incr = 2000
159
+
160
+ approval_value_fresh = Hash (0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE )
161
+ approval_value_overwrite = Hash (
162
+ 0xDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDE
163
+ )
164
+
165
+ block_count = 10
166
+
167
+ for _ in range (block_count ):
168
+ setup_calldata = Hash (current_address ) + approval_value_fresh
169
+ setup_tx = Transaction (
170
+ to = approval_spammer_contract ,
171
+ gas_limit = attack_gas_limit ,
172
+ data = setup_calldata ,
173
+ sender = sender ,
174
+ )
175
+ blocks .append (Block (txs = [setup_tx ]))
176
+
177
+ current_address += address_incr
178
+
179
+ attack_calldata = Hash (start_address ) + approval_value_overwrite
180
+
181
+ attack_tx = Transaction (
182
+ to = approval_spammer_contract ,
183
+ gas_limit = attack_gas_limit ,
184
+ data = attack_calldata ,
185
+ sender = sender ,
186
+ )
187
+ blocks .append (Block (txs = [attack_tx ]))
188
+
189
+ blockchain_test (
190
+ pre = pre ,
191
+ post = {}, # TODO: add sanity checks (succesful tx execution and no out-of-gas)
192
+ blocks = blocks ,
193
+ )
194
+
195
+
104
196
# TODO
105
197
# The current test does only claimRank(1) and then waits `SECONDS_IN_DAY = 3_600 * 24;` plus 1
106
198
# (see https://etherscan.io/token/0x06450dEe7FD2Fb8E39061434BAbCFC05599a6Fb8#code) and then
0 commit comments