|
| 1 | ++++ |
| 2 | +title = "Transient Heist Revenge" |
| 3 | +date = 2025-07-15 |
| 4 | +authors = ["Vrishab"] |
| 5 | ++++ |
| 6 | + |
| 7 | +## Overview |
| 8 | + |
| 9 | +The main goal of the challenge is to trick the function `isSolved` from Setup.sol into completing. There are 2 main entities here: |
| 10 | + |
| 11 | +- **Bank Vault** (USDSEngine): This is a very secure vault that stores collateral and allows minting of USDS stablecoin. It uses transient storage (tstore/tload) to verify only the correct Bi0sSwapPair can call certain functions. |
| 12 | +- **Currency Exchange** (Bi0sSwapPair): This booth swaps one type of token for another. |
| 13 | + |
| 14 | +We need to trick the Vault into thinking we’ve deposited a huge amount as collateral - it has to be more than a very large hash value. |
| 15 | + |
| 16 | +## How Collateral Deposits Work |
| 17 | + |
| 18 | +Collateral Deposits normally work like this: |
| 19 | + |
| 20 | +1. A user calls `depositCollateralThroughSwap` to deposit collateral via an automated token swap. |
| 21 | +2. The vault transfers tokens to the Bi0sSwapPair, writes the swap pair’s address into transient storage (`tstore(1, bi0sSwapPair)`). |
| 22 | +3. The swap is performed, and upon completion, the Bi0sSwapPair calls back into `bi0sSwapv1Call` to deposit the collateral. |
| 23 | +4. In `bi0sSwapv1Call`, the vault checks that `msg.sender` matches the stored address from transient storage, ensures the requested collateral deposit is not greater than the amount of tokens received (`collateralDepositAmount <= amountOut`), and updates internal collateral balances. |
| 24 | + |
| 25 | +## The Vulnerability |
| 26 | + |
| 27 | +Now, the only check on who can call `bi0sSwapv1Call` is the transient storage slot. This means that if an attacker can use the value written into transient storage, they can then call `bi0sSwapv1Call` from their own contract address. The vault only checks `msg.sender == tload(1)`, but `tload(1)` gets overwritten during the callback with `tokensSentToUserVault`, allowing us to control this value. |
| 28 | + |
| 29 | +## Exploit Steps |
| 30 | + |
| 31 | +**1. Deploying a malicious contract at a controlled address** - we use CREATE2 to deploy an attacker contract at an address that can be precomputed. This contract must implement the `bi0sSwapv1Call` function. A contract is used instead of a normal wallet since only contracts can call `bi0sSwapv1Call` and pass the transient storage check while being deployed at a known address. We should ensure we get a contract address with sufficient leading zeros for the arithmetic manipulation. |
| 32 | + |
| 33 | +**2. Initiating a Legitimate Swap** - we now call `depositCollateralThroughSwap` with 80,000 WETH which we want to swap for SafeMoon. This triggers `tstore(1, bi0sSwapPair)` - usage of transient storage which survives for the entire transaction, not just the swap call. |
| 34 | + |
| 35 | +**3. Overwrite Transient Storage During First Callback**: During the legitimate `bi0sSwapv1Call` callback, we set `collateralDepositAmount = amountOut - vanity_contract_address`, making `tokensSentToUserVault = vanity_contract_address`. The line `tstore(1, tokensSentToUserVault)` then overwrites the original swap pair address with our contract address. |
| 36 | + |
| 37 | +**4. Second Call to bi0sSwapv1Call**: Within the same transaction, we call `bi0sSwapv1Call` directly from our vanity contract. The check `msg.sender == tload(1)` now passes because `tload(1)` contains our contract address from step 3. |
| 38 | + |
| 39 | +**5. Set Arbitrary Collateral Amounts**: In the second call, we can supply ANY `amountOut` and `collateralDepositAmount > FLAG_HASH` (ensuring `collateralDepositAmount <= amountOut`). These don't need to be realistic token amounts - just arbitrary large numbers. |
| 40 | + |
| 41 | +**6. Repeat for Second Token**: The `isSolved()` function requires BOTH `collateralTokens[0]` (WETH) AND `collateralTokens[1]` (SafeMoon) to exceed `FLAG_HASH`. Since transient storage persists for the entire transaction, you can make a second direct call to `bi0sSwapv1Call` with the other token type using the same poisoned transient storage. |
| 42 | + |
| 43 | +## Arithmetic Manipulation Used |
| 44 | + |
| 45 | +The exploit requires careful calculation: `tokensSentToUserVault = amountOut - collateralDepositAmount` must equal the vanity contract address. We need a vanity address with 7+ leading zeros to make this arithmetic feasible compared to `FLAG_HASH`. |
| 46 | + |
| 47 | +## Why the Vanity Address Matters |
| 48 | + |
| 49 | +- We need a contract deployed at a **small numeric address** (7 leading zeros) to make the arithmetic work. |
| 50 | +- The calculation `tokensSentToUserVault = amountOut - collateralDepositAmount` must equal our contract address. |
| 51 | +- We need an address comparatively smaller than `FLAG_HASH` so that in the first callback, `amountOut = vanityAddress + smallCollateralAmount` is achievable through legitimate token swaps, while in the second call you can use `collateralDepositAmount > FLAG_HASH`. |
| 52 | +- This address then gets written to transient storage, allowing our contract to **pass the `msg.sender` check** on the second call. |
| 53 | + |
0 commit comments