Ethereum
Price manipulation

Boost Hook's $46.8K Loss and How Olympix Would Have Prevented It

Loss
Loss
Exploit date
May 12, 2026

Date: May 12, 2026

Chain: Ethereum

Loss: about $46.75K (20.93 WETH net to the attacker)

Class: leveraged entry price read from a Uniswap V4 pool's slot0 spot price

Nine leveraged longs opened in a single Ethereum transaction at 18:32:11 UTC on May 12, 2026, each one posting 2 ETH of collateral and borrowing 8 ETH from the protocol that hosted it. The protocol was Boost, a perpetual trading venue built inside a Uniswap V4 hook, and every one of those nine positions bought PERP at a price the same transaction had pushed up moments earlier. When the attacker sold back into the pool and repaid a 120 WETH flash loan, it kept 20.93 WETH, and the hook was left holding 38.33 ETH of bad debt from five forced liquidations, according to DARKNAVY's trace. BlockSec put the loss at about $46.75K.

BoostHook already judged whether a position should be liquidated by a 300-second time-weighted average price, a reference a single transaction can barely move, and it never consulted that average when a position was opened.

A Perp Exchange Living Inside a Pool

Uniswap V4 lets a pool attach a hook, a contract the pool calls before and after swaps and liquidity changes. Boost used that to turn a single ETH/PERP pool into a leveraged trading venue. According to BlockSec's weekly roundup, Boost was the pool's only liquidity provider, and the hook, BoostHook, lent ETH out of its own liquidity so traders could go long PERP with up to 5x leverage.

Opening a long with BoostHook works like a margin trade. A trader sends ETH as collateral and picks a leverage. The hook borrows the rest of the position's ETH from its own liquidity bands, swaps the whole amount into PERP in the pool, and records the position with its collateral, its debt and the PERP it holds. Each position also stores the pool price at the moment it opened, which the hook later uses to track how the trade is doing.

The Price the Hook Wrote Down

The verified BoostHook source on Blockscout shows where that opening price comes from. Inside openLong(), after the collateral and borrow are sized, the hook reads the pool's current state directly.

        uint256 collateral = msg.value;
        uint256 borrowEth  = collateral * (leverage - 1);
        uint256 borrowFee  = (borrowEth * BORROW_FEE_BPS) / 10_000;
        uint256 effectiveCol = collateral - borrowFee;

        (uint160 sqrtP,,,) = poolManager.getSlot0(_poolId());

getSlot0 returns the pool's instantaneous price, the square root of the PERP/ETH ratio at this exact point in the transaction. A few lines later, that value is saved into the position as its entry price.

            holdingTOKEN:      swapTokensOut,
            openSqrtPriceX96:  sqrtP,
            leverage:          uint8(leverage),

The only output check in openLong() is a slippage floor that the caller sets for themselves, so a trader who is also moving the price can set it wherever they like. Meanwhile, a constant at the top of the same contract shows the hook had a manipulation-resistant reference on hand for a different job.

    uint32  public constant TWAP_SECONDS              = 300;    // liquidation health uses this TWAP window

Liquidation health followed a five-minute average that one transaction can barely move, while entry followed slot0, which one large swap can move as far as the swapper wants. BlockSec's root cause is exactly that split: the hook used slot0 as the entry price for leveraged positions and had no TWAP or other check on it.

Borrow, Pump, Open Nine, Dump

The attack ran from a contract the attacker deployed, and DARKNAVY's trace walks through it in order. The contract borrowed 120 WETH from Morpho Blue and unwrapped it to ETH. It spent 100 ETH buying PERP through Sat1SwapRouter, which pushed the ETH/PERP price sharply up and left the attacker holding PERP bought at the lower, pre-pump price.

With the price inflated, the contract called openLong() nine times, each with 2 ETH of collateral at 5x leverage. Every call recorded 1.92 ETH of effective collateral and 8 ETH of debt, with a 0.08 ETH borrow fee sent to the BoostStaking contract. Most of the buying in each of those positions was the hook's own borrowed ETH, so the hook itself was paying the inflated price. The PERP each position received shrank from about 3,495.94 on the first open to about 1,612.25 on the ninth as the pool's depth at those prices was used up.

The contract then sold its PERP back through the router. Selling collapsed the price, and in the hook's afterSwap() callback the liquidation logic woke up. MAX_LIQS_PER_BLOCK capped it at five liquidations in that block, and those five realized between about 7.05 and 7.47 ETH of bad debt each, 38.33 ETH in total. Four positions survived with 8 ETH of debt apiece. The attacker's payout had already settled by then, so the contract rewrapped its ETH, repaid Morpho, and the attacker's wallet came away 20.93 WETH ahead.

Two Clocks in One Contract

The bug lives in a mismatch between two reads of the same pool. The hook asked the slow, averaged question when it was deciding whether to close someone out, and the fast, manipulable question when it was deciding how much a new position was worth. Since every new position was funded mostly by the hook's own liquidity, the fast price decided how much of the protocol's ETH went into PERP at a bad rate, and the attacker controlled that price for the length of one transaction.

Pricing from a pool's live slot0 keeps showing up in post-mortems for the same reason. It's the most convenient number a hook or vault has, it's always current, and it costs nothing to read. Earlier this year BeatSwap's vesting contracts priced BTX from a PancakeSwap V3 pool's live slot0 and checked slippage against it, which let 12,000 USDT buy $77.5K of BTX. Any value a contract pays out or lends against needs a price the caller can't set inside the same transaction, whether that's a TWAP, an external feed, or a deviation check against one.

How Olympix Would Have Caught the Entry Price

Olympix reads a hook like BoostHook as states, transitions and entry points. The states that matter here are each position's collateral, debt and entry price, the hook's total debt, and the ETH sitting in its liquidity bands. openLong(), close() and the swap callbacks are the transitions that change them. Olympix's static analysis would have surfaced the signal in openLong(): a spot price read from getSlot0() flowing straight into a stored entry price and into a borrow sized against it, with no TWAP and no deviation check in between, inside a contract that already uses a TWAP for the closely related liquidation decision.

That signal tells the engine where to aim. The invariant BoostHook depends on is that a position opened at fair value shouldn't leave the protocol worse off than its collateral can cover, and that no sequence of trades in one transaction should let a trader take more ETH out of the hook than they put in. BugPoCer would have run Counterexample-Guided Invariant Refutation from the hook and its pool outward, inferred that invariant from how positions, debt and liquidations interact, and searched for a sequence that breaks it with the pool's price left free to move.

The counterexample it would have returned is the May 12 transaction in development form. Borrow ETH, buy PERP to push the slot0 price up, open several leveraged longs at the inflated price, sell PERP back, and finish with more ETH than you started with while the hook carries bad debt. BugPoCer would have returned that sequence with the minimal conditions that trigger it, including how the per-block liquidation cap leaves positions standing, and a generated Forge test that replays it. Run at every merge and before release, it would have flagged the line that wrote sqrtP into openSqrtPriceX96 before the hook ever held real ETH.

One Pool, One Price Worth Trusting

Boost built a careful liquidation path and then let the opening path trust the one number in the pool an attacker could set at will. A leveraged product inherits every weakness of the price it opens at, and a hook that lends its own liquidity pays for those weaknesses directly. To see how Olympix traces every price read in your hooks and vaults and returns a runnable counterexample when one can be moved in a single transaction, book a technical walkthrough.

On-Chain References

Protocol: Boost (BoostHook, Uniswap V4 ETH/PERP pool)

Chain: Ethereum

Date: May 12, 2026, 18:32:11 UTC

Loss: about $46.75K per BlockSec (20.93 WETH net to the attacker)

Attacker EOA: 0xB0A0...5104

Attack contract: 0xB64b...5A6D

BoostHook: 0x3DB1...EacC

PERP token: 0x6c6b...11f8

BoostStaking: 0x4ae2...ca07

Uniswap V4 PoolManager: 0x0000...8a90

Flash loan source (Morpho Blue): 0xBBBB...FFCb

Exploit tx: 0xb45c...3811

Block: 25080848

See what Olympix finds on your own code

Send us a repo that's already been audited, and we'll run Olympix at the same commit and show you what comes back.