# SpiralHookV2: spot-price oracle manipulation bypasses tx.origin same-block guard

SpiralHookV2 valued SPIRAL collateral at live Uniswap V4 spot with no TWAP; a same-block pump plus six tx.origin-rotated borrows drained ~10.7 ETH.

- Payout: Real-world hack
- Severity: high
- Program: SpiralHookV2
- Credited: DeFiHackLabs
- Disclosed: 2026-09-12
- Write-up quality: 8/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2026-09/SpiralHookV2_exp.sol
- Library page: https://bugbountylibrary.com/r/dhl%3A20260912-spiralhookv2
- Tags: domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:high tech:evm tech:solidity technique:chained vuln:business-logic vuln:oracle-manipulation

**Target:** Uniswap V4 hook lending market (borrow/collateral valuation)
**Root cause:** borrow() values deposited SPIRAL at the live V4 spot price from getSlot0 with no TWAP or deviation cap, and the noSameBlockSwap guard keys on tx.origin so only the swapping EOA is blocked.
**Steps:**
1. Fork mainnet at block 25974145 (parent of attack block 25974146).
2. Deploy attack contract; call `pump{value: 54 ether}()` which does `pm.unlock` then `pm.swap(key, SwapParams(true, -54e18, 4295128740), "")` buying 141,059.24 SPIRAL and inflating spot to ~0.0012041 ETH/SPIRAL.
3. Settle ETH debt via `pm.sync(address(0)); pm.settle{value: 54 ether}(); pm.take(SPIRAL, this, owed)`.
4. From six distinct EOAs call `executeBorrow(collateral)` on the same contract, each with a fresh tx.origin so `lastSwapBlockOf[tx.origin]==0` passes the guard.
5. Each borrow deposits SPIRAL collateral valued at pumped spot and withdraws ETH (12.30, 22.40, 10.93, 12.12, 3.66, 3.30 ETH).
6. Net 64.7072 ETH out minus 54 ETH in = ~10.7072 ETH profit.
**How found:** Recognized that the same-block guard keyed on tx.origin rather than pool state, so any non-swapping EOA passes; combined with spot-price collateral valuation, a single pump enables many borrows from distinct origins.
**Exploitation:** Attacker flash-funded 54 ETH to buy SPIRAL in one V4 swap, pumping the pool spot within the block. Because borrow() reads getSlot0 directly, the deposited SPIRAL is valued at the inflated price. The noSameBlockSwap guard only records lastSwapBlockOf[tx.origin], so routing six borrows through one attack contract but from six different EOAs each clears the check. Total ETH withdrawn 64.7072, net ~10.7 ETH.
**Escalation:** Rotating tx.origin across six EOAs to reuse the same pumped spot for multiple borrows, multiplying extraction beyond a single borrow.
**Insight:** When a protocol guards against same-block oracle manipulation, check what the guard keys on: tx.origin/msg.sender-keyed guards are bypassed by routing through a shared contract from many EOAs, so the pump only burns one origin while the pool stays inflated for all others.
