Root Cause
USM is a minimalist ETH-backed stablecoin with no governance. fund() accepts ETH and mints FUM (the governance/collateral token); defund() burns FUM and returns ETH. Both sides price FUM against an internal oracle derived from the ETH/FUM pool ratio, updated at call time.
Two interacting properties create the exploit surface:
- A flash loan calling
fund()with a large ETH amount temporarily raises the ETH-pool numerator, manufacturing an inflated FUM-per-ETH rate for that transaction block. - Burning FUM via many small
defund()calls extracts more ETH in aggregate than one large call because each call recalculates price mid-curve and each integer-division rounding step is slightly biased toward the caller.
// Simplified internal price in fund() / defund():
uint fumPrice = ethInPool.mul(WAD).div(fumSupply);
// defund() returns per call:
uint ethOut = fumBurned.mul(fumPrice).div(WAD);
// Splitting exploits rounding:
// 64 x defund(small) > 1 x defund(all) due to floor division bias
Attack Steps
| Step | Action | Detail |
|---|---|---|
| 1 | Flash-borrow large ETH | Enough to meaningfully shift the bonding-curve price numerator |
| 2 | Call fund() with borrowed ETH | Mints FUM at artificially high rate; protocol ETH pool spikes |
| 3 | Split FUM into 64 defund() calls | Each call re-prices at the elevated rate; floor rounding returns fractionally more ETH per small burn |
| 4 | Repay flash loan | Net profit: |
Impact
- Chain: Ethereum mainnet
- Protocol: USM (minimalist ETH-backed stablecoin, no governance)
- Loss:
70.83 ETH ($136,000 at exploit-time prices) - Status: No public post-mortem from USM team as of August 16, 2026
- Weekly context: Part of an Aug 9-15 streak that pushed total weekly confirmed losses past $37M across multiple exploits
Lessons for Auditors
- Bonding-curve prices are vulnerable to single-block manipulation. Any price function derived from live pool balances can be distorted by a flash loan within the same transaction. Require TWAP pricing or multi-block observations for any function that moves value.
- Test f(a) + f(b) > f(a+b) for all arithmetic paths. If a user can profit by splitting one large operation into many small ones, they will. Rounding should always favour the protocol (round down on outputs, round up on inputs).
- No governance does not equal no risk. USM's no-governance design eliminated one attack surface but also removed any emergency-pause capability. Protocols without governance need especially hardened pricing and withdrawal rate limits.
- Pair integration tests with call-splitting scenarios. A fuzz test that calls defund() N times with 1/N of the total FUM vs. once with the total FUM would have surfaced this rounding delta.