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
sUSDv2metapool pairedsUSDwithsaddleUSD, the LP token of a base stable pool. That design only works if every swap path consistently converts rawsaddleUSDunits through the base pool’s virtual price. The canonical Saddle code had already patched an olderMetaSwapUtilsbug in that normalization logic, but the livesUSDv2metapool was still deployed against the old vulnerable library at0x88Cc4aA0dd6Cf126b00C012dDa9f6F4fd9388b17. In the vulnerableswap()path, outgoingsaddleUSDwas not scaled down correctly and incomingsaddleUSDwas not scaled up correctly. An attacker used that inconsistency to perform profitable round trips inside the same metapool: the first swap handed out too muchsaddleUSDand pushed the pool’s invariantDbelow reality, and the reverse swap then priced the pool off that corrupted state and emitted moresUSDthan 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
MetaSwapUtilslibrary at0x88Cc4aA0dd6Cf126b00C012dDa9f6F4fd9388b17still used the older vulnerablecalculateSwap/swaplogic. - 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.
- The victim metapool contract was
- Root cause:
- A metapool LP token such as
saddleUSDis 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 vulnerableswap()/_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
sUSDv2pool 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.
- A metapool LP token such as
- Exploit path:
- The attacker funded exploit transactions with large temporary capital and targeted the
sUSDv2metapool. - In representative transaction
0x2b023d65485c4bb68d781960c2196588d03b871dc9eb1c054f596b7ca6f7da56, the attacker swapped14,800,272 sUSDfor9,657,586 saddleUSDthrough the vulnerableswap()path. - Economically, that first leg should have produced less
saddleUSD, but the buggy path failed to scale the outgoing base-pool LP token correctly. - BlockSec’s simulation split that first leg into a normal part that increased
Dthrough fees and an erroneous part where the pool effectively lost about31,932 saddleUSDof accounted value, pushingDdown from the economically correct level. - The attacker then swapped the acquired
saddleUSDback intosUSDthrough the same vulnerableswap()path. - Because the pool was now pricing against a depressed invariant and also failed to scale incoming
saddleUSDcorrectly, the reverse leg emitted16,860,043 sUSD. - That one round trip produced about
2,059,771 sUSDof profit before fees and funding costs. - The attacker repeated the pattern across additional transactions until defenders and whitehats intervened.
- The attacker funded exploit transactions with large temporary capital and targeted the
- What the attacker needed to be true:
- The live metapool had to point to the vulnerable
MetaSwapUtilslibrary instead of the patched one. - The metapool had to remain liquid enough that an incorrect
saddleUSDquote could be turned into meaningfulsUSDoutflow. - The pool’s invariant updates had to trust the wrong normalized value, so the first swap could move
Daway from reality rather than merely misquote a single trade. - No pause or monitoring action could interrupt the exploit before several transactions completed.
- The live metapool had to point to the vulnerable
- 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 ispath-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 corruptedD, 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
- Victim metapool:
- 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 sourceis not the same thing asfixed onchain; deployment provenance is part of protocol security, not release-process housekeeping. - Finally, it broadens the lesson from Nerve. Once invariant
Dis 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 pathcash-out, Saddle shows that two trips through the same vulnerableswap()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
saddleUSDis a drifting base-pool receipt token, not a$1stable 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
MetaSwapUtilsbytecode after the fix existed elsewhere. - Invariant-as-oracle failure: once the first trade pushed
Dbelow 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
saddleUSDand wrote a depressedDback 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
sUSDv2metapool 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
-
A real base-pool LP-token movement occurred
- The attacker really did swap
sUSDinto the livesUSDv2metapool and receivesaddleUSD, 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.
- The attacker really did swap
-
The vulnerable
swap()path promoted that movement into canonical metapool value truth- The stale
MetaSwapUtilslibrary at0x88Cc4aA0dd6Cf126b00C012dDa9f6F4fd9388b17converted rawsaddleUSDmovement into reserve and invariant updates without symmetrically applying base-pool virtual-price normalization. - That was the decisive promotion failure: Saddle stopped saying merely
LP movedand started sayingthis mis-scaled amount is the authoritative economic value that updates pool state and invariant D.
- The stale
-
Later pricing and payout paths still spent against that published story
- Once the first leg pushed
Dbelow economic reality, the reverseswap()leg and any later quotes treated the corrupted state as money-good metapool truth. - That surviving authority is why the exploit paid real
sUSDout. The loss was not justone bad quote; it wasone bad quote became canonical pool state and later reserve-release authority.
- Once the first leg pushed
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 tradesimulations under unchanged market state. For a healthy pool, the result should be fee loss; for Saddle-like failures, the first leg will often leaveDor reserve accounting in a state that makes the reverse leg profitable. - Treat
linked library addressas 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
Dmove 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 canonicalraw LP units <-> normalized valuetransformation 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 -> stableunder unchanged market state should lose fees, not move invariantDinto a state that prints profit on the reverse leg. - Monitor invariant sanity directly. If a single large LP-token-facing swap can drive
Ddownward 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
saddleUSDunits become normalized value before quote calculation, reserve bookkeeping, and invariant updates? - Do
swap()andswapUnderlying()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 -> LPtrade moveDdownward 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
sUSDv2metapool linked the intended patchedMetaSwapUtilslibrary instead of the older helper at0x88Cc4aA0dd6Cf126b00C012dDa9f6F4fd9388b17? - 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()andswapUnderlying()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 sourceis 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
- Core recurring pattern: metapool-virtual-price-normalization-failure.
- Earlier contained warning: synapse-protocol-nusd-metapool-virtual-price-normalization-exploit-bridge-halt-near-miss-2021-11-06 — the same broader bug family had already drained value from Synapse LPs before validators blocked the bridge cash-out.
- Precursor incident: nerve-bridge-fusdt-ust-metapool-virtual-price-normalization-exploit-2021-11-15 — preserves the earlier fork where a vulnerable
swap()leg was cashed out through correctly normalizedswapUnderlying()pricing. - Adjacent pattern: amm-invariant-scaling-mismatch — both are AMM math-consistency failures, but here the broken unit conversion concerns a drifting base-pool LP token rather than fee-constant mismatch in a constant-product pair.
- Adjacent pattern: lp-token-oracle-mispricing — use that note when an external protocol over-trusts an LP valuation; here the metapool itself corrupted the LP token’s price treatment internally.
- Positive-design companion: amm-invariant-unit-consistency-and-fork-equivalence-discipline — the sharper healthy-side follow-up when the question becomes how a forked AMM family should keep unit conversions, helper parity, reference-model checks, and deploy-time bytecode verification fused before edited math reaches production.
- Deployment-provenance companion: release-provenance-failure — useful when the reviewer wants the broader
patched repository versus live bytecodefailure class rather than the narrower metapool normalization mechanics.
Control surface
- The leverage sat in Saddle’s linked
MetaSwapUtilslibrary, specifically the exact branches where rawsaddleUSDunits were supposed to be normalized by base virtual price before quote and reserve updates. - The exploit was profitable because the metapool’s invariant
Dbecame 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 realsUSD.
Sources
- https://docs.saddle.finance/solidity-docs/meta/metaswaputils
- https://blocksec.com/blog/how-to-exploit-the-same-vulnerability-of-meta-pool-in-two-different-ways-nerve-bridge-saddle-finance-what-you-see-is-not-what-you-get
- https://www.blockchainbreaches.com/en/breaches/saddle-finance-2022
- https://smartcontractshacking.com/hacks/saddle-finance-hack-2022
- https://etherscan.io/address/0x824dcd7b044d60df2e89b1bb888e66d8bcf41491#code
- https://phalcon.blocksec.com/explorer/tx/eth/0x2b023d65485c4bb68d781960c2196588d03b871dc9eb1c054f596b7ca6f7da56
Last reviewed
- 2026-08-04 UTC