Author: Jiujiu & Lisa
Editor: Sherry
background
On March 30, 2025, according to the monitoring of SlowMist MistEye security monitoring system, the leveraged trading project SIR.trading (@leveragesir) on the Ethereum chain was attacked, losing assets worth more than $300,000. The SlowMist security team analyzed the incident and shared the results as follows:
(https://x.com/SlowMist_Team/status/1906245980770746449)
Related information
Attacker Address:
https://etherscan.io/address/0x27defcfa6498f957918f407ed8a58eba2884768c
The address of the vulnerable contract:
https://etherscan.io/address/0xb91ae2c8365fd45030aba84a4666c4db074e53e7#code
Attack Transaction:
https://etherscan.io/tx/0xa05f047ddfdad9126624c4496b5d4a59f961ee7c091e7b4e38cee86f1335736f
Prerequisites
Solidity version 0.8.24 (released in January 2024) introduces the transient storage feature based on EIP-1153. This is a new data storage location designed to provide developers with a low-cost, efficient temporary storage method during transactions.
Transient storage is a new data location that is parallel to storage, memory, and call data. Its core feature is that the data is only valid during the current transaction execution and will be automatically cleared after the transaction ends. Accessing and modifying transient storage is achieved through two new EVM instructions:
- TSTORE(key, value): Stores the 256-bit value value into the memory corresponding to the specified key key in transient storage.
- TLOAD(key): Reads a 256-bit value from the memory corresponding to the specified key key in transient storage.
This feature has the following main features:
- Low gas cost: TSTORE and TLOAD have a fixed gas cost of 100, equivalent to warm storage access. In contrast, regular storage operations (SSTORE) can cost up to 20,000 gas for the first write (from 0 to non-0), and at least 5,000 gas for updates.
- In-transaction persistence: Transiently stored data remains valid throughout the transaction, including all function calls and sub-calls, which is suitable for scenarios where temporary state needs to be shared across calls.
- Automatic clearing: After the transaction is completed, transient storage is automatically reset to zero, eliminating the need for manual cleanup and reducing developer maintenance costs.
root cause
The root cause of this hack is that the value of transient storage in tstore is not cleared after the function call ends, which allows attackers to use this feature to construct specific malicious addresses to bypass permission checks and transfer tokens.
Attack steps
1. The attacker first creates two malicious tokens A and B, then creates a pool for these two tokens on UniswapV3 and injects liquidity, where token A is the attack contract.
2. The attacker then calls the initialize function of the Vault contract to create a leveraged trading market APE-21 with A token as the collateral token and B token as the debt token.
3. Immediately afterwards, the attacker calls the mint function of the Vault contract and deposits the debt token B to mint the leveraged token APE.
Following up in the mint function, we found that when the debt token B needs to be deposited to mint leveraged tokens, the value of the collateralToDepositMin parameter that needs to be passed in cannot be equal to 0. After that, the B token will be exchanged for the collateral token A through UniswapV3 and transferred to the Vault, where the address of the UniswapV3 pool previously created by the attacker will be transiently stored for the first time.
When the UniswapV3 pool performs a swap operation, the uniswapV3SwapCallback function of the Vault contract will be called back. It can be seen that the function first uses tload to retrieve the value from the memory corresponding to the specified key 1 previously stored transiently to verify whether the caller is the UniswapV3 pool, then transfers the debt token B from the minter's address and mints the leveraged token APE, and finally stores the minted amount amount for a second transient storage, saves it in the memory corresponding to the specified key 1, and uses it as the return value of the mint function. The amount that needs to be minted here is calculated and controlled in advance by the attacker, and its value is 95759995883742311247042417521410689.
4. The attacker then calls the safeCreate2 function of the Keyless CREATE2 Factory contract to create a malicious contract with the contract address 0x00000000001271551295307acc16ba1e7e0d4281, which is the same as the value of the second transient storage.
5. The attacker then uses the malicious contract to directly call the uniswapV3SwapCallback function of the Vault contract to transfer tokens.
Because the uniswapV3SwapCallback function uses tload(1) to verify whether the caller is the UniswapV3 pool. However, in the previous minting operation, the value in the memory corresponding to the specified key 1 is saved as the minted amount 95759995883742311247042417521410689, and the value in the memory is not cleared after the mint function is called. Therefore, the address of uniswapPool is obtained as 0x00000000001271551295307acc16ba1e7e0d4281 at this moment, causing the identity check of the caller to be erroneously passed.
And the attacker has calculated the number of tokens to be transferred in advance, and constructed the final minted amount as the specified value: 1337821702718000008706643092967756684847623606640. Similarly, at the end of this call to the uniswapV3SwapCallback function, a third transient storage will be performed to save the value to the memory corresponding to the specified key 1. This requires that the value be the same as the value of the address of the attack contract (A token) 0xea55fffae1937e47eba2d854ab7bd29a9cc29170, so that the subsequent check on the caller can pass.
6. Finally, the attacker can directly call the uniswapV3SwapCallback function of the Vault contract through the attack contract (token A) to transfer other tokens (WBTC, WETH) in the Vault contract for profit.
MistTrack Analysis
According to the analysis of the on-chain anti-money laundering and tracking tool MistTrack, the attacker (0x27defcfa6498f957918f407ed8a58eba2884768c) stole approximately $300,000 in assets, including 17,814.8626 USDC, 1.4085 WBTC and 119.871 WETH.
WBTC was exchanged for 63.5596 WETH, and USDC was exchanged for 9.7122 WETH:
Then, a total of 193.1428 WETH was transferred to Railgun:
In addition, the attacker’s initial funding came from 0.3 ETH transferred from Railgun:
Summarize
The core of this attack is that the attacker takes advantage of the fact that the transient storage in the project will not clear the saved value immediately after the function call, but will be saved throughout the transaction period, thereby bypassing the permission verification of the callback function to make a profit. The SlowMist security team recommends that the project party should use tstore(key, 0) to clear the value in the transient storage immediately after the function call according to the corresponding business logic. In addition, the contract code of the project should be audited and security tested to avoid similar situations.