Skip to content

Commit ea1822f

Browse files
feat(tests): add approval test for existing slots
1 parent 13231ba commit ea1822f

File tree

1 file changed

+92
-0
lines changed

1 file changed

+92
-0
lines changed

tests/benchmark/mainnet/test_state_xen.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,98 @@ def test_xen_approve(
101101
)
102102

103103

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+
104196
# TODO
105197
# The current test does only claimRank(1) and then waits `SECONDS_IN_DAY = 3_600 * 24;` plus 1
106198
# (see https://etherscan.io/token/0x06450dEe7FD2Fb8E39061434BAbCFC05599a6Fb8#code) and then

0 commit comments

Comments
 (0)