## 1. What happened

On September 1, 2025 (Bunni's post-mortem dates it September 2, a timezone difference), an attacker exploited Bunni v2, a Uniswap v4-hook-based AMM, on Ethereum and Unichain, stealing ~$8.4M. BlockSec first flagged ~$2.3M from the USDC/USDT pool on Ethereum; CertiK then identified the same pattern on Unichain's ETH/weETH pool. [https://rekt.news/bunni-rekt/]

Breakdown: ~$2.4M from Ethereum (USDC/USDT), ~$6M from Unichain (ETH/weETH). The attacker flashloaned a swap to push the spot price extreme, shrinking USDC active balance to 28 wei. Then 44 tiny withdrawals exploited rounding to erode it to 4 wei, collapsing computed liquidity 84.4%. A large swap reversed this artificial hole (+16.8% bounce), which the attacker sandwiched. Net: ~1.33M USDC + ~1M USDT from Ethereum alone. [https://blog.bunni.xyz/posts/exploit-post-mortem/] Unichain funds were bridged to Ethereum in 100-ETH chunks via Across Protocol, consolidated into two wallets funded through Tornado Cash. [https://rekt.news/bunni-rekt/]

## 2. Root cause

A rounding-direction error in `BunniHubLogic::withdraw()`. When LPs withdrew, the idle balance was reduced with `balance.mulDiv(shares, currentTotalSupply)`, which rounds down. Developers assumed this rounds up the idle balance and thus rounds down the active balance -- "safe" since lower liquidity increases swap price impact. This holds for one operation but breaks across sequences: the attacker drained active USDC to 28 wei, then 44 withdrawals exploited rounding to erode it to 4 wei (85.7% drop, disproportionate to shares burned), collapsing total liquidity 84.4%. A subsequent swap reversed this (+16.8%), creating a sandwichable gap. The fix: `mulDiv` to `mulDivUp`. [https://blog.bunni.xyz/posts/exploit-post-mortem/]

This was a pure smart-contract arithmetic bug, not key compromise or oracle manipulation. Trail of Bits, Pashov, and Cyfrin all audited the code. Cyfrin's June 2025 audit warned "it is statistically likely that there are more complex bugs still present" and recommended further fuzz testing before scaling. Bunni's TVL rose from $2.4M to $23.9M immediately after anyway. [https://rekt.news/bunni-rekt/]

## 3. Pattern

**Rounding / precision error in multi-step state updates.** An arithmetic rounding choice deemed "safe" in isolation becomes exploitable when accumulated across many small operations, distorting state (here, liquidity) enough that a later operation reverses it, creating a sandwichable gap.

```
// Vulnerable: BunniHubLogic::withdraw()
uint256 newBalance = balance - balance.mulDiv(shares, currentTotalSupply);
//                           rounds DOWN -- safe once, lethal repeated 44x

// Fix:
uint256 newBalance = balance - balance.mulDivUp(shares, currentTotalSupply);
```

The attacker chained 44 withdrawals on a cratered balance (28 wei to 4 wei), producing a liquidity hole that a swap bounce-back reversed. [https://blog.bunni.xyz/posts/exploit-post-mortem/]

## 4. The check that would have caught it

**Invariant fuzz test.** Invariant: for any sequence of minimal-share withdrawals on a pool whose active balance for one token is below threshold T (e.g. 1000 wei), the drop in computed total liquidity must not exceed the proportional share reduction by more than epsilon E (e.g. 1%).

A Foundry/Medusa campaign that pushes one token's active balance to dust via a large swap, then runs randomized minimal-withdrawal sequences, checking total liquidity does not disproportionately collapse, would have caught this. Cyfrin's post-exploit fork test confirmed reproducibility. [https://gist.github.com/giovannidisiena/716324d50b6649be3a0e91395890917e]

**Review rule:** Every `mulDiv` in state-update arithmetic must carry a comment justifying why the rounding direction is safe across *sequences*, not just singly. Any such use on a value drivable to dust by a prior operation is presumptively dangerous.

## 5. Sources

- Bunni post-mortem: https://blog.bunni.xyz/posts/exploit-post-mortem/
- Rekt.news: https://rekt.news/bunni-rekt/
- Cyfrin breakdown: https://gist.github.com/giovannidisiena/716324d50b6649be3a0e91395890917e
- BlockSec alert: https://x.com/phalcon_xyz/status/1962743751568433416
- CertiK alert: https://x.com/CertiKAlert/status/1962782447931703548
- Hacken alert: https://x.com/hackenclub/status/1962768341367390643
- Bunni pause notice: https://x.com/bunni_xyz/status/1962773674634756450
- Cyfrin audit: https://github.com/Cyfrin/cyfrin-audit-reports/blob/main/reports/2025-06-10-cyfrin-bunni-v2.1.pdf
- Trail of Bits audit: https://github.com/trailofbits/publications/blob/master/reviews/2025-01-bacon-labs-bunniv2-securityreview.pdf
