Loss: BlockSec reported that the attacker exhausted liquidity in Nerve’s fUSDT and UST metapools and realized about 900 BNB of profit across roughly 200+ attack transactions
Summary: Nerve’s BNB Chain metapools paired one pool stablecoin such as fUSDT with the LP token of Nerve 3Pool (BUSD, USDT, USDC). That design only works if every path that touches the base-pool LP token consistently converts raw 3LP units through the base pool’s virtual price. Nerve inherited Saddle’s vulnerable MetaSwapUtils logic where _calculateSwap() inside swap() mishandled LP-token normalization, while swapUnderlying() / _calculateSwapUnderlying() still applied the base-pool virtual-price adjustments. The attacker used that inconsistency as a two-path arbitrage: first swap fUSDT through the broken swap() path to receive too much Nerve 3LP, then unwrap that 3LP into base-pool stablecoins and sell them back through the correctly normalized swapUnderlying() path for more fUSDT than they started with. Repeating that loop across the fUSDT and UST metapools drained their liquid side.
Trigger / entry point:
The vulnerable public metapool contract highlighted by BlockSec was MetaSwap at 0xd0fBF0A224563D5fFc8A57e4fdA6Ae080EbCf3D3.
Its linked SwapUtils / MetaSwapUtils library was 0x02338Ee742ddCDe44488640F4edf1Aa947E670E7.
The exploitable surface was ordinary metapool swap math, not an admin key, oracle update, or bridge verifier compromise.
Anyone able to fund the public swap path with enough working capital could exercise it.
Root cause:
A base-pool LP token such as Nerve 3LP is not a flat $1 stablecoin. Its economic value drifts with the base pool’s getVirtualPrice().
Nerve’s live MetaSwapUtils fork inherited the same vulnerable swap() / _calculateSwap() logic later discussed in Saddle and Synapse writeups: when a metapool leg involved the base-pool LP token, the path did not preserve the same virtual-price scaling discipline that swapUnderlying() / _calculateSwapUnderlying() used.
In the concrete Nerve path the attacker abused, swap() treated outgoing 3LP as cheaper than it really was, so swapping fUSDT or UST into 3LP overpaid raw LP shares.
The attacker could then redeem or route those excess LP shares through the correctly normalized path and cash the mismatch out as real stablecoins.
Exploit path:
In BlockSec’s representative transaction 0xea95925eb0438e04d0d81dc270a99ca9fa18b94ca8c6e34272fc9e09266fcf1d, the attacker flash-borrowed 50,000 BUSD from Fortube.
They swapped that 50,000 BUSD on Ellipsis for about 50,351 fUSDT.
They then called Nerve’s vulnerable swap() path to exchange 50,351 fUSDT for about 36,959 Nerve 3LP.
Because swap() failed to scale the outgoing LP token by the base pool’s virtual price, that 36,959 3LP output was economically too large.
The attacker redeemed those LP shares through removeLiquidityOneCoin() on Nerve 3Pool, pulling out about 37,071 BUSD.
They fed the resulting base-pool stablecoin back through the correctly normalized swapUnderlying() path, which returned about 51,494 fUSDT.
After unwinding funding, that loop left a profit of about 1,143 fUSDT on the sample trade.
The attacker repeated the same pattern across roughly 200 or more transactions against the fUSDT and UST metapools until their liquid stablecoin side was exhausted.
What the attacker needed to be true:
The live metapool had to expose both a vulnerable LP-token-facing swap() path and a correctly normalized swapUnderlying() path.
The base pool needed enough liquidity that excess 3LP could be redeemed into a meaningful amount of BUSD, USDT, or USDC.
External liquidity such as Ellipsis had to exist so the attacker could cheaply cycle between the metapool stablecoin and the base-pool stablecoin needed for the next loop.
No fast pause or kill switch could stop the repeated transaction loop before the liquid side of the metapools was drained.
Technical anatomy worth remembering:
This was not a bridge-message authenticity failure even though the victim protocol was called Nerve Bridge. The exploit primitive lived entirely inside AMM math for Saddle-style metapools.
The clean lesson is one LP token, two truths: swap() / _calculateSwap() valued Nerve 3LP one way, while swapUnderlying() / _calculateSwapUnderlying() and the base pool valued it another way.
The profit did not require exotic market movement. It required only that the attacker could buy underpriced 3LP from the broken path and then redeem or resell those LP shares on the path that still honored virtual-price normalization.
Nerve is the precursor case for the later Saddle incident. Here the mismatch required one vulnerable leg plus one correct leg; later Saddle showed that once the first vulnerable leg also corrupts invariant D badly enough, even two uses of the vulnerable path can suffice.
BlockSec blog describing the sample flow: https://blocksec.com/blog/the-analysis-of-nerve-bridge-security-incident
Response / mitigation:
BlockSec wrote that Nerve did not pause immediately after the closely related Synapse metapool incident on 2021-11-06, leaving the same Saddle-derived flaw live until attackers used it on Nerve on 2021-11-15.
The public lesson was that metapool deployments inheriting the same library bug needed coordinated review and emergency pause, not protocol-by-protocol optimism about whether the exact same MetaSwapUtils edge would be rediscovered.
The incident later became the baseline comparison for Saddle’s 2022 re-exploitation of the same bug family.
Two-path self-arbitrage between vulnerable swap() / _calculateSwap() and correctly normalized swapUnderlying() / base-pool redemption paths
Why the exploit worked
Nerve’s metapool kept two incompatible truths for the same 3LP asset. The swap() / _calculateSwap() path treated the base-pool LP token too cheaply when paying it out, while swapUnderlying() / _calculateSwapUnderlying() and the base pool itself still honored getVirtualPrice()-style normalization. The attacker only needed to shuttle value between those two truths.
The dangerous asymmetry was specifically on the LP-token leg. In the public writeups, swapUnderlying() scaled incoming LP-token value up and outgoing LP-token value down, while swap() skipped the matching output-side normalization for the metapool’s base-pool LP token. That made fUSDT -> 3LP and UST -> 3LP the overpaying entry legs.
The metapool sat inside a larger, liquid public route. Fortube flash liquidity funded the loop, Ellipsis converted base-pool stablecoin into the pool-specific stablecoin, Nerve’s removeLiquidityOneCoin() turned the excess 3LP back into BUSD, and the correctly normalized path paid the attacker the spread. The exploit was therefore not just bad math in isolation; it was bad math with enough surrounding liquidity to cash out repeatedly.
Repeatability mattered as much as the first profitable trade. Because the protocol stayed live after the earlier Synapse warning shot, the attacker could run the same five-step loop roughly 200+ times until the pool-stablecoin side of the fUSDT and UST metapools was exhausted.
The broader preserved lesson is fork hygiene, not merely one arithmetic bug. Once a public Saddle-derived metapool implementation was known to disagree with itself about LP-token normalization, every live fork sharing that library had to be treated as already on borrowed time.
Detection and audit pivots
Diff every path that can move the base-pool LP token and write down exactly where virtual-price normalization happens. In Nerve’s case the right question was not does swap math look plausible? but why do _calculateSwap() and _calculateSwapUnderlying() disagree about when raw 3LP units become normalized value?
Run a round-trip property test on the economically identical loop: pool stablecoin → base-pool LP via swap(), then LP → underlying stablecoin via removeLiquidityOneCoin(), then underlying stablecoin → pool stablecoin via swapUnderlying(). Under unchanged market state, that route should lose fees, not print risk-free profit.
Inventory deployed library addresses, not just repository fixes. BlockSec’s writeups preserved the exact MetaSwap and linked utility contracts because a source-level patch elsewhere would not have saved a live pool still pointing at the vulnerable helper.
Treat public incidents in sibling forks as immediate production alerts. Synapse’s near miss was already enough evidence that every Saddle-derived metapool with the same normalization asymmetry needed emergency review or pause.
Monitor for telltale LP-token path divergence: repeated trades that drain the pool-specific stablecoin side while leaving a trail of oversized base-pool LP outflows and one-coin base-pool withdrawals compared with what normalized quotes should have allowed.
Failure-prevention checklist
Centralize base-pool LP normalization so swap(), swapUnderlying(), remove-liquidity helpers, quote views, and invariant updates all consume the same raw 3LP units <-> normalized value transformation instead of hand-rolled branch logic.
Add round-trip tests for the concrete Nerve route BlockSec published: pool stablecoin →3LP via swap(), 3LP→ underlying stablecoin via removeLiquidityOneCoin(), then underlying stablecoin → pool stablecoin via swapUnderlying(). Under unchanged market state, that loop should lose fees instead of printing surplus pool-stablecoin.
Monitor metapool reserve changes and LP-token outflows directly. If swap() can emit 3LP in quantities that nearby remove-liquidity paths immediately redeem for more underlying value than the input should buy, pause before repeated loops empty the pool-stablecoin side.
Treat public incidents in sibling forks as automatic deployment-inventory triggers. Once Synapse’s metapool bug was public, every live Saddle-derived MetaSwap using the same helper lineage should have been enumerated, diffed, and paused or cleared explicitly.
Keep linked-library addresses and implementation hashes in the release checklist. A repository fix elsewhere does not help a live pool still calling the old vulnerable helper.
Rehearse bridge-branded protocol but AMM-local bug response. Teams operating both swap and bridge surfaces should be ready to freeze the local AMM first, then decide whether downstream bridge rails need separate containment.
Prefer flatter pool topologies when LP-token normalization complexity becomes the main moving part. A metapool that cannot prove one canonical base-LP truth across every path should not stay live just because the abstraction is convenient.
Audit prompts
Where exactly does this deployment convert raw 3LP units into normalized value, and do swap(), swapUnderlying(), removeLiquidityOneCoin(), and quote helpers all reuse that same rule?
Can a trader execute pool stablecoin -> 3LP -> underlying stablecoin -> pool stablecoin under unchanged market state and ever come back with more pool-stablecoin than they started with apart from rebates or incentives?
If swap() pays out 3LP, what explicit test proves that the amount is scaled down by base virtual price before transfer and bookkeeping, rather than only in sibling paths?
Which live pools and library addresses would still be exposed if this exact helper bug were rediscovered today, and can operators enumerate them immediately from deployment inventory rather than from memory?
When a sibling fork such as Synapse fails publicly, what exact process decides whether to pause local metapools immediately or keep them live?
Does the incident-response plan distinguish between bridge protocol name appears in headlines and bridge verifier is the root cause, or could branding confusion delay the right mitigation for an AMM-local failure?
What monitoring would have fired first here: abnormal 3LP outflows, repeated loop-shaped trades, collapsing pool-stablecoin liquidity, or only post hoc loss accounting?
Lessons that should already be standard
A base-pool LP token is not just another stablecoin slot. If one path forgets to normalize it while a sibling path still does, the metapool becomes an arbitrage venue against itself.
Shared-code near misses are not comforting. Synapse should have been treated as a production incident for every sibling Saddle-derived metapool, including Nerve, not as somebody else’s bad day.
Bridge in the protocol name can hide the real lesson. This incident was an AMM-path-consistency failure first; confusing it with bridge-message security would miss the control surface that actually broke.
Round-trip economic tests matter more than branch-local plausibility. Both swap() and swapUnderlying() can look individually reasonable while still disagreeing enough to print money when composed.
Deployment inventory is part of incident response. Once a helper-library flaw is public, teams need the exact live contract and library map immediately, not a later source review.
Repeated small loops are still solvency events. A bug that requires 200+ transactions is not minor if nothing stops the pool from paying the same accounting lie over and over.
Why this incident matters
It is the clean precursor exemplar for metapool virtual-price normalization failure: one path overpays raw base-pool LP tokens, another path prices them correctly, and the spread becomes extractable.
It preserves the concrete five-step attack loop that later writeups often compress away: flash-loan stablecoin → buy pool stablecoin → broken swap() into excess LP → redeem LP to base asset → correct swapUnderlying() back into the pool stablecoin.
It also shows how exploit knowledge propagates across forks. Once one Saddle-derived metapool implementation failed publicly, unpatched sibling deployments became obvious follow-on targets.
Positive-design companion: metapool-lp-normalization-and-path-equivalence-discipline — use this when the sharper follow-up is how a metapool should keep one canonical raw LP units <-> normalized value rule across swap(), swapUnderlying(), liquidity helpers, invariant updates, and live linked libraries before path drift becomes self-arbitrage.
Adjacent pattern: amm-invariant-scaling-mismatch — both families are AMM math-consistency failures, but Nerve is specifically about inconsistent treatment of a drifting base-pool LP token rather than a constant-product fee-check mismatch.
Control surface
The leverage sat in the exact unit-conversion boundary where raw Nerve 3LP amounts were supposed to be translated through base-pool virtual price before quote and payout logic.
Once swap() stopped honoring the same LP-token truth as swapUnderlying() and the base pool, the metapool became a standing arbitrage machine against its own reserves.