Deadly Remains: A $300,000 On-Chain Heist Triggered by Transient Storage

Blockchain editor
01 Apr 2025 11:01:01 AM
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 a specific malicious address to bypass permission checks and t
Deadly Remains: A $300,000 On-Chain Heist Triggered by Transient Storage

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 a specific malicious address to bypass permission checks and transfer tokens.

Background

On March 30, 2025, according to the monitoring of the SlowMist MistEye security monitoring system, the leveraged trading project SIR.trading on the Ethereum chain was attacked and lost assets worth more than $300,000. The SlowMist security team analyzed the incident and shared the results as follows:

Prerequisites

Solidity version 0.8.24 (released in January 2024) introduced the transient storage feature based on EIP-1153. This is a new data storage location designed to provide developers with a low-cost, effective temporary storage method during transactions.

Transient storage is a new data location that is parallel to storage, memory, and calldata. 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 implemented through two new EVM instructions:

TSTORE(key, value): Store the 256-bit value value to the memory corresponding to the specified key key of the transient storage.

TLOAD(key): Read the 256-bit value from the memory corresponding to the specified key key of the transient storage.

This feature has the following main features:

Low gas cost: The gas cost of TSTORE and TLOAD is fixed at 100, which is equivalent to warm storage access. In contrast, regular storage operations (SSTORE) may be as high as 20,000 gas when first written (from 0 to non-0), and at least 5,000 gas is required for updates.

In-transaction persistence: Data in transient storage remains valid throughout the transaction, including all function calls and subcalls, suitable for scenarios where temporary states need to be shared across calls.

Automatic clearing: After the transaction is completed, transient storage is automatically reset to zero, without manual cleanup, reducing developer maintenance costs.

Root cause

The root cause of this hack is that the value of transient storage called by tstore in the function is not cleared after the function call ends, which allows attackers to use this feature to construct a specific malicious address 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 A token is the attack contract.

2. Then the attacker calls the initialize function of the Vault contract, creates 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 debt token B to cast 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 the exchange 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 of the previous transient storage to verify whether the caller is the UniswapV3 pool, then transfers the debt token B from the minter address and mints the leveraged token APE, and finally stores the minted amount amount for the second time transiently, saves it in the memory corresponding to the specified key 1, and uses it as the return value of the mint function. The amount of tokens 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 a contract address of 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 out tokens.

Because the uniswapV3SwapCallback function uses tload(1) to verify whether the caller is a UniswapV3 pool. However, in the previous minting operation, the value in the memory corresponding to the specified key 1 is saved as the minting amount 95759995883742311247042417521410689, and the value in the memory is not cleared after the mint function is called, so the address of uniswapPool is obtained as 0x00000000001271551295307acc16ba1e7e0d4281 at this moment, causing the identity check of the caller to be passed incorrectly.

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 calling the uniswapV3SwapCallback function this time, 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 (A token) to transfer other tokens (WBTC, WETH) in the Vault contract for profit.

MistTrack analysis

According to the analysis of MistTrack, an on-chain anti-money laundering and tracking tool, the attacker (0x27defcfa6498f957918f407ed8a58eba2884768c) stole about $300,000 in assets, including 17,814.8626 USDC, 1.4085 WBTC and 119.871 WETH.

Among them, 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 funds came from 0.3 ETH transferred by Railgun:

Summary:

The core of this attack is that the attacker took advantage of the fact that 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 ends according to the corresponding business logic. In addition, the audit and security testing of the project's contract code should be strengthened to avoid similar situations.