Get a Quote
Back To BlogSIR Exploit ~355k loss | Vulnerability Analysis

SIR Exploit ~355k loss | Vulnerability Analysis

Summary

On March 30th 2025, an attacker exploited a logic error in SIR's Vault resulting in a ~$355k loss. The vulnerability allowed the attacker to exploit the incorrect use of transient storage by leveraging CREATE2 vanity addresses to spoof legitimate Uniswap V3 pools and bypass access controls.

What is SIR?

SIR is a DeFi protocol that aims to solve leveraged trading core problems through its novel vault system where users can permissionlessly create leverage trading vaults.

Each vault represents a trading pair and issues both APE tokens (for leveraged positions with upfront fees) and TEA tokens (for LP positions). This design eliminates recurring funding fees while allowing LPs to earn from trading activity without liquidation risk.

SIR's Vault operates as singleton contract that houses all vaults within a single smart contract, using Uniswap V3's TWAP oracles as its price source of truth.

Vulnerability Analysis

The attacker began the exploit by deploying two dummy ERC-20 tokens (TokenA and TokenB) and immediately minting a large supply of both tokens to his own address. Using these tokens, the attacker created a new Uniswap V3 pool and provided the initial liquidity. The attacker then executed a swap between TokenA and TokenB in his newly created pool, deliberately initializing a price ratio that would later enable the new vault to be properly initialized.

SIR's Vault architecture allows any user to create new vaults by simply specifying a collateral token, debt token and leverage tier through the initialize() function.

1function initialize(SirStructs.VaultParameters memory vaultParams) external {
2    VaultExternal.deploy(
3        _ORACLE,
4        _vaultStates[vaultParams.debtToken][vaultParams.collateralToken][vaultParams.leverageTier],
5        _paramsById,
6        vaultParams,
7        _APE_IMPLEMENTATION
8    );
9}

While intentionally permissionless, this design enabled the attacker to create a vault with his own controlled tokens, setting the stage for the subsequent manipulation of the Vault's transient storage.

1function deploy(
2    Oracle oracle,
3    SirStructs.VaultState storage vaultState,
4    SirStructs.VaultParameters[] storage paramsById,
5    SirStructs.VaultParameters calldata vaultParams,
6    address implementationOfAPE
7) external {
8    if (
9        vaultParams.leverageTier > SystemConstants.MAX_LEVERAGE_TIER ||
10        vaultParams.leverageTier < SystemConstants.MIN_LEVERAGE_TIER
11    ) revert LeverageTierOutOfRange();
12
13    oracle.initialize(vaultParams.debtToken, vaultParams.collateralToken);
14
15    ...
16
17    emit VaultInitialized(
18        vaultParams.debtToken,
19        vaultParams.collateralToken,
20        vaultParams.leverageTier,
21        vaultId,
22        ape
23    );
24}

The attacker used the newly created TokenA as collateral and TokenB as the debt token to mint APE through the Vault's mint() function. During this process, the protocol temporarily stored the Uniswap pool address in transient storage slot 1, with the intention of later verifying that only the legitimate pool could trigger the swap callback function.

1function mint(
2    bool isAPE,
3    SirStructs.VaultParameters memory vaultParams,
4    uint256 amountToDeposit, 
5    uint144 collateralToDepositMin
6) external payable nonReentrant returns (uint256 amount) {
7    ...
8
9    if (collateralToDepositMin == 0) {
10        ...
11    } else {
12        // Store Uniswap v3 pool in transient storage so we can use it in the callback function
13        assembly {
14            tstore(1, uniswapPool)
15        }
16
17        ...
18
19        // Encode data for swap callback
20        bool zeroForOne = vaultParams.collateralToken > vaultParams.debtToken;
21        bytes memory data = abi.encode(msg.sender, ape, vaultParams, vaultState, reserves, zeroForOne, isETH);
22
23        // Swap 
24        (int256 amount0, int256 amount1) = IUniswapV3Pool(uniswapPool).swap(
25            address(this),
26            zeroForOne,
27            int256(amountToDeposit),
28            zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1,
29            data
30        );
31
32        ...
33    }
34}

When Uniswap executed the callback to the Vault contract, the system properly verified that the caller matched the pool address stored in transient storage slot 1. However, a critical oversight occurred when the protocol later stored the total amount of minted APE tokens to the same transient storage slot 1 at the end of the callback execution.

1function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external {
2    // Check caller is the legit Uniswap pool
3    address uniswapPool;
4    assembly {
5        uniswapPool := tload(1)
6    }
7    require(msg.sender == uniswapPool);
8
9    ...
10
11    // Use the transient storage to return amount of tokens minted to the mint function
12    assembly {
13        tstore(1, amount)
14    }
15}

Because transient storage persists only for a transaction's lifetime, the attacker was able to strategically manipulate the system by precisely crafting a mint amount that when casted from uint256 to an address would result in address of a contract with multiple leading zeros controlled.

1chisel
2Welcome to Chisel! Type `!help` to show available commands.
3➜ uint256 amount = 83257840921570807743408402243071
4➜ address casted = address(uint160(amount))
5➜ casted
6Type: address
7└ Data: 0x000000000000041adc97DA74FAE8f30a41C29DfF

With the manipulated transient storage value still active (as the transaction had not yet finished), the attacker deployed a malicious contract to the precomputed address (derived from the amount to mint) using CREATE2. With all pre-conditions satisfied, the malicious contract directly called the uniswapV3SwapCallback() function to drain the entire protocol.

Proof of Concept

The coded proof of concept for this exploit can be found here.

Conclusion

This was one of the first exploits caused by the misuse of transient storage (EIP-1153), where the attacker cleverly combined it with vanity addresses to spoof Uniswap V3 pools. As transient storage is a recent feature with limited educational content, more similar exploits are likely to occur until developers fully understand its risks.

Resources

Previous Article