Reproduced Exploit
USM / FUM (Minimalist USD) — Flash-loan fund/defund extracts ETH buffer via inflated mid-price
1. Minimalist USM is an ETH-backed stablecoin; FUM is the buffer/equity token. Users fund() ETH to mint FUM and defund() FUM to redeem ETH from the shared pool.
Loss
~70.83 ETH (~$160K) drained from the USM ETH pool; PoC profit 70.831554410317728328 WETH
Chain
Ethereum
Category
logic
Date
Aug 2026
Source
Crypto Training
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.
Source & credit. Crypto Training original detection and analysis (live Twitter/X security-alert intake — not from DeFiHackLabs). Standalone Foundry PoC, offline
anvil_state.json, and full write-up: 2026-08-USM_FUM_exp in theevm-hack-registrymirror.
Vulnerability classes: vuln/logic/price-calculation · vuln/arithmetic/rounding · vuln/governance/flash-loan-attack · vuln/oracle/price-manipulation
Reproduction: the PoC compiles & runs in an isolated Foundry project at this project folder. Full verbose offline trace: output.txt. Verified vulnerable source: contracts_USM.sol (USM) and contracts_FUM.sol (FUM buffer token).
Key info#
| Loss | ~70.83 ETH (~$160K) drained from the USM ETH pool; PoC profit 70.831554410317728328 WETH |
| Vulnerable contract | USM (Minimalist USD) — 0x2a7FFf44C19f39468064ab5e5c304De01D591675 |
| Buffer token | FUM — 0x86729873e3b88DE2Ab85CA292D6d6D69D548eDF3 |
| Flash-loan source | Morpho Blue — 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb |
| Attacker EOA | 0xb92b2E47680c89DA8f951B8963ef469f461a50Fc |
| Attack contract | 0x5a5e29ba89663a3558273354E990426F3cAc7de7 |
| Profit receiver | 0xE3C6346b6f282029312d2caf4677ef39BeaBBF99 (received exactly 70.830977… ETH on-chain) |
| Attack tx | 0xfae5e751b8ce01457cbb6b529839f24a0cff50faaabcbd0fd02ca0cf559b050e |
| Chain / block / date | Ethereum / 25,716,150 (fork 25,716,149) / 2026-08-09 |
| Compiler | Solidity v0.8.9+commit.e5eed63a |
| Bug class | Flash-loaned fund() inflates stored ETH/USD mid + bid-ask; defund() sells FUM against that inflated buffer price and returns more ETH than was deposited |
TL;DR#
-
Minimalist USM is an ETH-backed stablecoin; FUM is the buffer/equity token. Users
fund()ETH to mint FUM anddefund()FUM to redeem ETH from the shared pool. -
On every
fund(), USM multiplies bothbidAskAdjustmentand the stored midethUsdPriceby a growth factor derived from pool expansion (contracts_USM.sol:251-252). That raises the implied ETH buffer (ethPool − USM/price) and therefore the FUM sell price used bydefund(). -
An attacker flash-borrowed ~11,579.98 WETH from Morpho Blue, unwrapped it, and called
fund()once — minting ~62.18M FUM. -
They then called
defund()in 64 equal FUM chunks. Each chunk re-priced FUM against the still-inflated mid / buffer and over-paid ETH relative to the original deposit (compounded by the arithmetic-average approximation inethFromDefund, contracts_USM.sol:759-761). -
After repaying Morpho, residual ETH (~70.83) was profit. On-chain it went to
0xE3C6…BF99; the PoC wraps it to WETH and sends it to the attacker EOA. -
Post-attack state: USM/FUM supplies unchanged,
ethPooldown by 70.830977… ETH, stored mid price exploded from ~$1,921.81 to ~$6,579,253,bidAskAdjustmentfrom 1.0 to ~3,423.46.
Background#
USM (Minimalist USD) by Alberto Cuesta Cañada / Jacob Eliosoff / Alex Roan is a fully on-chain, ETH-collateralized stablecoin. Design goals:
- USM — dollar-stable token, minted/burned against the ETH pool at an oracle-influenced ETH/USD price with sliding bid/ask.
- FUM — residual claim on the ETH buffer (pool ETH above the ETH-value of outstanding USM). Funding grows the buffer; defunding shrinks it.
- Sliding fees — large fund/mint operations move a
bidAskAdjustment(and the stored mid) so sequential trades pay super-linear fees. - Oracle — a composite oracle refreshes mid when available; between refreshes the mid can be nudged by trades.
The system had been live with a modest pool (~132.59 ETH, ~200.25k USM, ~329.46k FUM at the fork block). That thin buffer relative to a multi-thousand-ETH flash loan is what made the mid-slide extractable.
The vulnerable code#
1. fund() slides the mid price up with the pool#
// sources/USM_2a7FFf/contracts_USM.sol — _fundFum
(fumOut, adjGrowthFactor) = fumFromFund(ls, fumSupply, msg.value, debtRatio_, isDuringPrefund());
// ...
ls.bidAskAdjustment = ls.bidAskAdjustment.wadMulUp(adjGrowthFactor);
ls.ethUsdPrice = ls.ethUsdPrice.wadMulUp(adjGrowthFactor); // ← mid inflated
_storeState(ls);
fum.mint(to, fumOut);
adjGrowthFactor is roughly poolChange ** (netFumDelta / 2) — for a fund that multiplies the pool by ~88×, this factor is huge. The mid is treated as if the fund itself proved ETH more valuable.
2. FUM price is buffer / supply — buffer grows when mid grows#
// fumPrice: buffer = ethPool - usmSupply/ethUsdPrice
int buffer = ethBuffer(ethUsdPrice, ethInPool, usmEffectiveSupply, roundUp);
price = (buffer <= 0 ? 0 : uint(buffer).wadDiv(fumSupply, roundUp));
Higher ethUsdPrice → lower USM liability in ETH → higher buffer → higher FUM sell price.
3. defund() uses arithmetic-average sell pricing#
// ethFromDefund
uint avgFumSellPrice = fumSellPrice0 + fumSellPrice2;
unchecked { avgFumSellPrice /= 2; }
ethOut = fumIn.wadMulDown(avgFumSellPrice);
Combined with the inflated mid, chunked defunds return more ETH than the fund deposited. After 64 chunks the FUM mint/burn nets to zero while the pool is short ~70.83 ETH.
Root cause#
The protocol couples trade-size sliding fees to the mid ETH/USD used for buffer accounting. A flash-loaned fund() is allowed to:
- Temporarily dominate the pool.
- Push
ethUsdPriceandbidAskAdjustmentorders of magnitude above the oracle. - Leave that inflated mid in storage for subsequent same-tx
defund()s.
Defund then values FUM as if the buffer were much larger (because USM is “cheaper” in ETH at the inflated mid), paying out real ETH. There is no same-block circuit breaker, no max fund relative to pool, and no requirement that mid stay within a band of the oracle after a trade.
Secondary contributors:
- Arithmetic average in
ethFromDefund(chosen to avoid geometric-average collapse to zero) is not path-symmetric withfumFromFund’s geometric average. - Chunking re-evaluates price 64 times, harvesting residual asymmetry each step.
- Morpho 0-fee flash loans remove capital cost for the ~11.5k ETH war chest.
Preconditions#
- USM
ethPoolholds a meaningful buffer of real ETH (here ~132.59 ETH). - Morpho Blue (or any WETH flash lender) can supply multi-thousand ETH in one tx.
- Attacker can call
fund/defundpermissionlessly (post-prefund; prefund ended 2021-11-01). - No guardian pause / max-trade limit / mid-oracle band check on the path.
Attack walkthrough#
Numbers from the offline PoC run in output.txt (and matching mainnet balances at blocks 25,716,149 → 25,716,150).
| Step | Action | Result |
|---|---|---|
| 0 | Fork @ 25,716,149 | ethPool 132.588942983107019337 ETH; mid 1921.813593; bidAskAdj 1.0 |
| 1 | Morpho flashLoan(WETH, 11579.978…) | Attack contract holds flash WETH |
| 2 | WETH.withdraw | Unwrap full flash amount to ETH |
| 3 | USM.fund{value: ethIn}(this, 0) | Mint ~62.18M FUM; mid + adj explode |
| 4 | Loop 64× USM.defund(chunk) | Burn all FUM; pull ETH each time |
| 5 | WETH.deposit + Morpho pull | Repay 11579.978… WETH |
| 6 | Wrap residual ETH → WETH → owner | 70.831554410317728328 WETH profit |
Post-state (PoC logs):
ethPool after: 61.757965594337344181 ETHethPool drained: 70.830977388769675156 ETHlatestPrice after: 6,579,253.854629bidAskAdj after: 3,423.460989- Attacker WETH: 70.831554410317728328
Live tx: same drain (70.830977… ETH) to 0xE3C6…BF99; FUM supply and USM supply unchanged end-to-end.
Diagrams#
Remediation#
- Do not slide the oracle mid with trade size. Keep
ethUsdPriceequal to (or tightly banded to) the external oracle; apply sliding only to a separate fee/spread variable that does not enterethBuffer. - Cap single-tx fund/defund as a fraction of
ethPool(e.g. ≤ 5–10%) so flash size cannot dominate. - Same-block / same-tx invariant checks: after any sequence of fund/defund, require
ethPooland buffer to satisfy conservation relative to FUM mint/burn at oracle mid (or reject if mid drifted beyond X% from oracle). - Circuit breaker when
bidAskAdjustmentor mid diverges from oracle beyond a threshold — pause fund/defund. - Revisit
ethFromDefundaveraging so fund→defund of the full FUM position cannot extract value even under extreme size (path-independent or strictly fee-positive). - Operational: withdraw residual buffer / wind down if the deployment is abandoned.
How to reproduce#
# Offline (registry runtime — no RPC needed once anvil_state.json is present)
cd /path/to/evm-hack-registry
_shared/run_poc.sh 2026-08-USM_FUM_exp -vvvvv
# Expect: [PASS] testExploit — Attacker WETH profit ~70.83
PoC entrypoints:
- Test harness: test/USM_FUM_exp.sol —
ContractTest.testExploit - Attack contract:
USM_FUM_Exploit.attack→ Morpho callbackonMorphoFlashLoan - Fork block: 25,716,149 (mainnet), offline port 8545
Reference: TenArmor alert · Attack tx
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 2026-08-USM_FUM_exp (evm-hack-registry mirror).
- Attack transaction: view on explorer.
Alerts & third-party analyses
- Original alert / thread: post on X.
- Web3Sec X hacked database: search.
- Rekt leaderboard: search.
- Solodit incident search: search.
These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.