Saddle Finance sUSDv2 MetaPool Virtual-Price Scaling Exploit (2022-04-30)

  • Name: Saddle Finance sUSDv2 MetaPool virtual-price scaling exploit
  • Date: 2022-04-30
  • Protocol: Saddle Finance
  • Chain: Ethereum
  • Category: metapool virtual-price normalization failure / LP-token scaling mismatch / stale-library redeploy
  • Loss: commonly reported as roughly 11.9M net after whitehat rescue; BlockSec described about 4,900 ETH on the attack path and said 1,360 ETH was saved
  • Summary: Saddle’s Ethereum sUSDv2 metapool paired sUSD with saddleUSD, the LP token of a base stable pool. That design only works if every swap path consistently converts raw saddleUSD units through the base pool’s virtual price. The canonical Saddle code had already patched an older MetaSwapUtils bug in that normalization logic, but the live sUSDv2 metapool was still deployed against the old vulnerable library at 0x88Cc4aA0dd6Cf126b00C012dDa9f6F4fd9388b17. In the vulnerable swap() path, outgoing saddleUSD was not scaled down correctly and incoming saddleUSD was not scaled up correctly. An attacker used that inconsistency to perform profitable round trips inside the same metapool: the first swap handed out too much saddleUSD and pushed the pool’s invariant D below reality, and the reverse swap then priced the pool off that corrupted state and emitted more sUSD than the attacker had put in. Multiple exploit transactions drained the pool until BlockSec whitehat bots began front-running later attempts and diverting part of the extractable value to a recovery address.
  • Trigger / entry point:
    • The victim metapool contract was 0x824dcd7b044d60df2e89b1bb888e66d8bcf41491.
    • Its linked MetaSwapUtils library at 0x88Cc4aA0dd6Cf126b00C012dDa9f6F4fd9388b17 still used the older vulnerable calculateSwap / swap logic.
    • The bug was not a missing admin check or external oracle failure. It sat in the public swap math for a metapool that held a base-pool LP token whose value drifted with getVirtualPrice().
    • Anyone able to call the public swap path with enough capital could exploit it.
  • Root cause:
    • A metapool LP token such as saddleUSD is not a flat unit-price stablecoin. Its economic value must be normalized through the base pool’s virtual price.
    • Saddle’s safe path swapUnderlying() handled this normalization symmetrically, but the vulnerable swap() / _calculateSwap() implementation did not properly scale LP-token amounts up and down.
    • The bug had reportedly been fixed in Saddle’s source tree after the earlier Nerve incident, but the live sUSDv2 pool was still deployed against the old library because of what postmortem coverage described as a deployment error.
    • That redeploy mismatch mattered as much as the math bug itself: the source repository was safer than the bytecode actually protecting funds.
  • Exploit path:
    1. The attacker funded exploit transactions with large temporary capital and targeted the sUSDv2 metapool.
    2. In representative transaction 0x2b023d65485c4bb68d781960c2196588d03b871dc9eb1c054f596b7ca6f7da56, the attacker swapped 14,800,272 sUSD for 9,657,586 saddleUSD through the vulnerable swap() path.
    3. Economically, that first leg should have produced less saddleUSD, but the buggy path failed to scale the outgoing base-pool LP token correctly.
    4. BlockSec’s simulation split that first leg into a normal part that increased D through fees and an erroneous part where the pool effectively lost about 31,932 saddleUSD of accounted value, pushing D down from the economically correct level.
    5. The attacker then swapped the acquired saddleUSD back into sUSD through the same vulnerable swap() path.
    6. Because the pool was now pricing against a depressed invariant and also failed to scale incoming saddleUSD correctly, the reverse leg emitted 16,860,043 sUSD.
    7. That one round trip produced about 2,059,771 sUSD of profit before fees and funding costs.
    8. The attacker repeated the pattern across additional transactions until defenders and whitehats intervened.
  • What the attacker needed to be true:
    • The live metapool had to point to the vulnerable MetaSwapUtils library instead of the patched one.
    • The metapool had to remain liquid enough that an incorrect saddleUSD quote could be turned into meaningful sUSD outflow.
    • The pool’s invariant updates had to trust the wrong normalized value, so the first swap could move D away from reality rather than merely misquote a single trade.
    • No pause or monitoring action could interrupt the exploit before several transactions completed.
  • Technical anatomy worth remembering:
    • This was not a generic oracle manipulation. The metapool mispriced its own base-pool LP token internally.
    • The sharp lesson is not just swap math bug; it is path-dependent LP-token normalization. A metapool cannot safely mix plain stablecoins and base-pool LP tokens unless every path agrees on how raw LP units map to normalized value.
    • Saddle is also a deployment-provenance failure. A bug that had already been noticed and patched in the codebase still reached production because one live pool linked to the wrong helper library.
    • The exploit is especially instructive because the attacker did not need one vulnerable path and one correct path, as in the earlier Nerve case. Here, two uses of the same vulnerable swap() path were enough because the first mispriced trade corrupted D, and the second trade harvested the now-corrupted curve.
    • The hidden danger in LP-token metapools is that accounting errors are stateful. A bad first quote does not just overpay once; it can move the invariant that defines every later quote.
  • Representative transactions and addresses:
    • Victim metapool: 0x824dcd7b044d60df2e89b1bb888e66d8bcf41491
    • Vulnerable library: 0x88Cc4aA0dd6Cf126b00C012dDa9f6F4fd9388b17
    • Representative exploit transaction analyzed by BlockSec: 0x2b023d65485c4bb68d781960c2196588d03b871dc9eb1c054f596b7ca6f7da56
    • Phalcon analysis for that transaction: https://phalcon.blocksec.com/explorer/tx/eth/0x2b023d65485c4bb68d781960c2196588d03b871dc9eb1c054f596b7ca6f7da56
  • Response / mitigation:
    • Saddle paused affected metapool activity and migrated away from the vulnerable library after the exploit.
    • BlockSec reported that its whitehat infrastructure saved 1,360 ETH by replaying or front-running later exploit attempts to a recovery address.
    • The incident became a clean case study in why post-patch deploy verification matters: repository fixes, audits, and intended libraries do not protect funds if the live pool still links stale bytecode.

Why this incident matters

  • It is one of the clearest preserved examples of a metapool treating a base-pool LP token as if it were a plain stablecoin in one critical path.
  • It also shows that fixed in source is not the same thing as fixed onchain; deployment provenance is part of protocol security, not release-process housekeeping.
  • Finally, it broadens the lesson from Nerve. Once invariant D is allowed to move off reality because LP-token normalization is inconsistent, the exploit surface is wider than a simple one-path arbitrage story.

Pattern tags

  • Same-path invariant corruption: unlike Nerve’s vulnerable path -> correct path cash-out, Saddle shows that two trips through the same vulnerable swap() path were enough because the first leg already wrote false normalized value into the metapool’s state.
  • Raw-LP-versus-normalized-value confusion: the exploit only exists because saddleUSD is a drifting base-pool receipt token, not a $1 stable slot, yet one live path still treated raw LP units too literally.
  • Stale-library redeploy: the production risk survived not because the math family was unknown, but because one live metapool still linked the old MetaSwapUtils bytecode after the fix existed elsewhere.
  • Invariant-as-oracle failure: once the first trade pushed D below economically correct reality, every later quote inherited the lie as if the pool itself had become an attacker-authored price surface.

Why the exploit worked

  • Saddle let one public swap path disagree with the economic meaning of its own base-pool LP token. The pool was not just misquoting one trade; it was updating reserves and invariant state from an incorrectly normalized value transformation.
  • The first exploit leg did double duty: it overpaid the attacker in saddleUSD and wrote a depressed D back into pool state. That meant the second leg did not need a separate healthy path to cash out; the metapool had already poisoned its own pricing surface.
  • The bug survived because deployment provenance failed. Reviewers could truthfully say the issue had been patched in source, but the live sUSDv2 metapool still depended on the older helper library that encoded the vulnerable normalization logic.
  • Economically, the protocol let a drifting receipt token impersonate a flat stable asset at exactly the moment reserve accounting mattered. Once raw LP units and normalized value diverged inside one branch, the pool began spending against accounting fiction.
  • Operationally, the incident also shows why LP-token-bearing metapools deserve fork-family response. A normalization bug in one derived codebase is not a local curiosity; it is often a warning that sibling deployments sharing the same math or linked libraries should be treated as suspect immediately.

Three metapool-LP value promotions this incident collapsed

  1. A real base-pool LP-token movement occurred

    • The attacker really did swap sUSD into the live sUSDv2 metapool and receive saddleUSD, the LP token for Saddle’s base stable pool.
    • At this stage the only justified statement was raw saddleUSD moved; it was not yet justified to treat that raw receipt-token amount as if it already represented correctly normalized stable value.
  2. The vulnerable swap() path promoted that movement into canonical metapool value truth

    • The stale MetaSwapUtils library at 0x88Cc4aA0dd6Cf126b00C012dDa9f6F4fd9388b17 converted raw saddleUSD movement into reserve and invariant updates without symmetrically applying base-pool virtual-price normalization.
    • That was the decisive promotion failure: Saddle stopped saying merely LP moved and started saying this mis-scaled amount is the authoritative economic value that updates pool state and invariant D.
  3. Later pricing and payout paths still spent against that published story

    • Once the first leg pushed D below economic reality, the reverse swap() leg and any later quotes treated the corrupted state as money-good metapool truth.
    • That surviving authority is why the exploit paid real sUSD out. The loss was not just one bad quote; it was one bad quote became canonical pool state and later reserve-release authority.

Healthy review gets sharper once those promotions are separated. Saddle was not only a swap math bug or only a stale deploy; it was raw LP movement -> canonical normalized-value publication -> real reserve-spend authority, all collapsed inside one public path.

Detection and audit pivots

  • Diff every path that can move the base-pool LP token — swap(), swapUnderlying(), quote helpers, liquidity add/remove helpers, and invariant updates — and force the reviewer to point at the exact place virtual-price normalization is applied on input and output.
  • Run trade out -> immediate reverse trade simulations under unchanged market state. For a healthy pool, the result should be fee loss; for Saddle-like failures, the first leg will often leave D or reserve accounting in a state that makes the reverse leg profitable.
  • Treat linked library address as part of the threat model, not deployment trivia. If a bug was previously fixed in a shared helper, enumerate every live pool still pointing to older bytecode before assuming the issue is closed.
  • Review invariant movement directly after large trades involving LP-token legs. If one trade can make D move as though value vanished from the pool beyond normal fees and slippage, the metapool is already mis-accounting normalized reserves.
  • Ask whether offchain tooling and monitors would even notice path drift. If routers, simulations, or dashboards reproduce only one path’s semantics, a second inconsistent branch can remain invisible until money is live.
  • Keep a fast incident checklist for metapool forks: enumerate affected pools, library hashes, pause powers, and bridge / wrapper cash-out routes that could turn an AMM-side math bug into wider ecosystem loss.

Failure-prevention checklist

  • Centralize base-pool LP normalization so swap(), swapUnderlying(), quote helpers, liquidity helpers, and invariant updates all reuse one canonical raw LP units <-> normalized value transformation instead of hand-rolling scale-up and scale-down math in sibling branches.
  • Treat linked-library addresses and implementation hashes as solvency-critical deployment inputs. A release should fail closed unless the pool’s live bytecode and every linked helper match the reviewed patched artifact set.
  • Add round-trip and path-equivalence tests for LP-token-bearing routes: stable -> LP -> stable under unchanged market state should lose fees, not move invariant D into a state that prints profit on the reverse leg.
  • Monitor invariant sanity directly. If a single large LP-token-facing swap can drive D downward beyond fee effects or make the next reverse quote dramatically richer than the first leg’s economic input, the pool is already treating accounting fiction as pricing truth.
  • Maintain a live inventory of every pool using the same metapool helper family so a public bug in one fork or sibling deployment triggers immediate enumeration, pause review, and bytecode comparison across the whole cluster.
  • Make fork-family response part of incident readiness. Once a public writeup shows one metapool family mis-normalizing base-pool LP tokens, pause criteria and review scripts should assume sibling pools are suspect until proven safe.
  • Require offchain simulators, routers, and monitoring to reproduce the exact same normalization semantics as execution. If only one path is simulated or one helper is mocked optimistically, path drift can stay invisible until the pool is already monetizable.

Audit prompts

  • Where, in the live deployed bytecode rather than just the repository, do raw saddleUSD units become normalized value before quote calculation, reserve bookkeeping, and invariant updates?
  • Do swap() and swapUnderlying() prove the same LP-token truth symmetrically, or does one path still skip the scale-down / scale-up step the other path uses?
  • Can one stable -> LP trade move D downward in economic terms rather than merely upward through fees, and if so what exact branch allows that impossible state transition?
  • What deployment artifact proves the sUSDv2 metapool linked the intended patched MetaSwapUtils library instead of the older helper at 0x88Cc4aA0dd6Cf126b00C012dDa9f6F4fd9388b17?
  • If a patch exists in source, what automated check compares every live pool’s linked library address and bytecode hash against that patched artifact set before the team declares the issue fixed?
  • Do view helpers, routing simulations, and monitoring dashboards exercise both swap() and swapUnderlying() semantics, or could they mask a path-specific normalization failure until funds are already live?
  • If a large LP-token leg leaves the pool, what invariant or regression test proves the next reverse swap cannot profit from the resulting state without external price movement?
  • When a sibling fork such as Synapse or Nerve fails publicly, what exact operational process enumerates every local deployment sharing the same helper family and decides whether to pause them immediately?

Lessons that should already be standard

  • Patched in source is not a security property. The only version that matters is the bytecode and linked libraries actually defending the live pool.
  • A base-pool LP token is a drifting receipt token, not a stable slot. Forgetting that in one branch turns the metapool into a pricing engine against itself.
  • Same-path repetition can be enough once the first bad trade corrupts invariant state. You do not always need one broken route and one healthy route to cash out a normalization bug.
  • Linked library addresses are part of the custody boundary. Deployment provenance deserves the same paranoia as admin keys, oracle addresses, and upgrade pointers.
  • Fork-family alerts should be immediate, not leisurely. Once one public metapool bug shows LP-token normalization drift, sibling deployments should be treated as already inside the blast radius.
  • Offchain observability is part of prevention. If routers and monitors flatten two economically different paths into one imagined semantics, the team will discover the real difference only when an attacker does.

Internal linkages

Control surface

  • The leverage sat in Saddle’s linked MetaSwapUtils library, specifically the exact branches where raw saddleUSD units were supposed to be normalized by base virtual price before quote and reserve updates.
  • The exploit was profitable because the metapool’s invariant D became path-dependent fiction. Once the first swap under-accounted the value that had left the pool, the reverse swap could cash out the broken accounting as real sUSD.

Sources

Last reviewed

  • 2026-08-04 UTC