MetaPool Virtual-Price Normalization Failure / LP-Token Scaling Mismatch

  • Name: MetaPool virtual-price normalization failure / LP-token scaling mismatch
  • Category: exploit pattern / stable-swap accounting failure / metapool LP-token pricing bug
  • Summary: This exploit family hits Curve-style metapools that pair one plain asset with the LP token of a base pool. The LP token is not a $1 stablecoin; its economic value drifts with the base pool’s getVirtualPrice(). Safe metapool math therefore has to normalize that LP token symmetrically whenever it enters or leaves pricing calculations. If one swap path scales the LP token correctly but another path forgets to scale it up on input or down on output, the pool starts treating a drifting LP share like a flat stable asset. The attacker can then harvest too much base-pool LP on one leg, distort the metapool’s invariant D, and cash the inconsistency out on a second leg. Sometimes the profit comes from routing through one vulnerable path and one correct path, as in Nerve. Sometimes the same family first appears as a contained near miss, as in Synapse, where the AMM bug succeeded but the attacker failed to monetize through the paused bridge exit rail. Sometimes two vulnerable swaps are enough, as in Saddle, because the first under-accounted LP outflow pushes D to the wrong level and the reverse swap then prices the pool off that corrupted state.
  • What it is:
    • A metapool holds one ordinary pooled asset plus the LP token of another base pool.
    • The base-pool LP token has its own time-varying virtual price, so metapool math must translate between raw LP-token units and normalized pool value.
    • At least one quote or execution path mishandles that translation: it fails to scale LP-token input up by base virtual price, fails to scale LP-token output back down, or otherwise applies inconsistent normalization across related functions.
    • Because the vulnerable path updates reserves and invariant state from the wrong economic quantity, the pool’s D and subsequent price curve no longer reflect the value that actually left or entered the pool.
    • The attacker chains swaps so the metapool honors two different truths about the same LP token and pays the spread in real assets.
  • Typical preconditions:
    • The target metapool uses a base-pool LP token as one side of the pool.
    • The protocol exposes multiple paths involving that LP token, such as swap() versus swapUnderlying(), or multiple branches inside one swap path.
    • One or more of those paths normalize LP-token value inconsistently.
    • The pool is liquid enough that the mispricing can be converted into meaningful stablecoin or base-pool outflows.
    • No downstream guardrail verifies that a swap-then-reverse-swap cycle only loses fees under identical market state.
  • Common vulnerable surfaces:
    • calculateSwap() or _calculateSwap() branches that treat the base-pool LP token like a unit-price stablecoin
    • swap() paths that forget to divide outgoing LP-token amounts by base virtual price before transfer and bookkeeping
    • reverse swap paths that forget to multiply incoming LP-token value by base virtual price before quote calculation
    • codebases with both swap() and swapUnderlying() implementations whose normalization logic drifts over time
    • redeployments or library-linked pools where the source repository is patched but one live pool still points at an older library
  • Exploit sequence:
    1. The attacker identifies a metapool whose LP-token normalization is inconsistent across swap paths or swap directions.
    2. The attacker performs a first swap that makes the vulnerable path hand out too much base-pool LP token or under-account for the value that just left the pool.
    3. That bad accounting pushes the metapool’s invariant D and reserve state away from the economically correct curve.
    4. The attacker immediately performs a second swap or routes through a second path that prices against the corrupted state.
    5. The metapool pays out more stablecoin or underlying value than the attacker economically put in.
    6. The attacker repeats until the pool is exhausted or defenders pause the contracts.
  • Variants:
    • Vulnerable-vs-correct path arbitrage — one path mishandles LP-token normalization while another path prices the same LP token correctly, so the attacker harvests excess LP on the first leg and sells it on the second.
    • Two-vulnerable-swaps invariant-shift exploit — even without a correct second path, the first vulnerable swap moves D far enough off reality that the reverse vulnerable swap becomes profitable.
    • Output-side normalization failure — outgoing LP-token amount is not scaled down before transfer, so the pool sends too many LP shares.
    • Input-side normalization failure — incoming LP-token amount is not scaled up before quote calculation, so the reverse leg prices too favorably from a corrupted state.
    • Stale-library redeploy — the canonical source tree is patched, but one live pool still links to the old vulnerable helper library.

Three metapool-LP value promotions reviewers should not collapse

  1. A base-pool LP-token state change exists at all

    • The metapool now holds or releases some raw amount of base-pool LP token, and the base pool’s getVirtualPrice() implies some normalized economic value for that amount.
    • This is only the weak fact that an LP-token quantity moved. It does not yet mean the metapool updated reserves, invariant state, or quotes using the right normalized value.
  2. The protocol publishes one canonical metapool value story from that movement

    • A particular swap(), swapUnderlying(), quote helper, or invariant-update path converts the raw LP-token amount into reserve deltas and a new D.
    • This is the dangerous promotion metapool bugs keep getting wrong: the system stopped saying merely LP moved and started saying this is the authoritative economic value that now defines pool state.
  3. Downstream paths still spend against that published story

    • Reverse swaps, sibling swap paths, routers, redemptions, and bridge or exit rails then honor the corrupted reserve / invariant story as if it were already money-good solvency truth.
    • That is a stronger claim than one branch mis-scaled an LP token. The real loss arrives because later paths are still willing to release stablecoins, underlying, or bridged value against the bad canonical state.

Healthy review gets materially better once teams stop flattening those promotions into one vague sentence like the metapool mispriced the LP token.

  • Detection heuristics:
    • Diff every LP-token-bearing path against every other one; swap(), swapUnderlying(), quote helpers, and deposit/withdraw helpers should agree on where base virtual price is applied.
    • Treat any path asymmetry around base-pool LP tokens as high risk. scale up on input and scale down on output should be explicit and test-covered, not implicit.
    • Run differential tests where a metapool LP-token swap is reversed immediately under unchanged market state. The expected result should be a fee loss, not a gain.
    • Compare deployed library addresses and bytecode against the patched repository version; a fix in source is irrelevant if a live pool still points to stale code.
    • Review invariant movement directly. If a single swap can push D downward because the pool under-accounted outgoing LP value, the pool is already lying to itself about solvency.
  • Defenses:
    • Centralize LP-token normalization in one helper used by all quote and execution paths.
    • Add differential tests between swap() and swapUnderlying() for the same economic route.
    • Add round-trip property tests ensuring a swap followed by the reverse swap cannot profit absent fee rebates or external price movement.
    • Verify deployed library addresses and bytecode as part of release and post-deploy monitoring, not just source control review.
    • Treat base-pool LP tokens as their own asset class with explicit unit-conversion discipline, not as just another stablecoin slot.

What sound design looks like

  • A healthy metapool design keeps three questions separate: what raw LP-token movement happened, what canonical normalized value story did the pool publish from it, and which later paths are allowed to spend against that published story. Those should not collapse into one unreviewed swap updated state moment.
  • A metapool that holds a base-pool LP token should have one explicit rule for translating between raw LP units and normalized economic value, and every quote, swap, add/remove-liquidity, fee, and invariant-update path should call that same rule.
  • The healthy mental model is LP token is a drifting receipt token, not LP token is just another stable slot. The virtual-price conversion is part of the asset definition, not a convenience adjustment for one function.
  • Quote helpers and execution paths should agree exactly. A reviewer should be able to diff swap(), swapUnderlying(), _calculateSwap(), and any view helpers and see the same normalization semantics rather than path-specific arithmetic folklore.
  • Deployment provenance is part of solvency. If source control says the normalization bug is fixed but one live pool still links an old library, the protocol is still vulnerable in the only place that matters.
  • The clean reviewer question is: where do we prove that every path moving base-pool LP uses the same value transformation before reserves and invariant state are updated?

Failure-prevention checklist

  • Centralize base-pool LP normalization in one helper or library routine and make every path call it instead of hand-rolling scale-up or scale-down math.
  • Differential-test swap(), swapUnderlying(), add/remove-liquidity helpers, and quote views against the same economic route so path drift becomes visible before deployment.
  • Add round-trip properties that require a same-block swap followed by the exact reverse route to lose only fees under unchanged market state, never produce positive PnL.
  • Assert invariant sanity directly: after any trade, D should move in a way consistent with the economically normalized value that actually entered or left the pool.
  • Treat base-pool LP tokens as a special asset class in code review, with explicit comments around where getVirtualPrice() is applied on input and output.
  • Gate deployments on bytecode and linked-library verification, not just repository diffs, especially for pools inheriting previously patched math helpers.
  • Maintain an inventory of all live pools and the exact library addresses or implementation hashes they depend on so stale-library survivors cannot hide in production.
  • Include emergency pause criteria for any pool whose LP-token normalization semantics are later questioned; repeated loopable arbitrage means response speed matters.

Audit prompts

  • Which exact functions convert raw base-pool LP units into normalized value, and do all quote and execution paths reuse that exact logic?
  • Does outgoing LP-token math divide by base virtual price exactly where incoming LP-token math multiplies by it, or has one side silently drifted?
  • If a trader executes swap() and then reverses through swapUnderlying() under unchanged state, can the pool ever return more value than it received apart from fees or rebates?
  • Can one bad first trade push invariant D away from economically correct state, so later trades become profitable even without an external correctly priced path?
  • Are any deployed pools still linked to older helper libraries or implementations even though the repository, audit report, or docs describe the bug as fixed?
  • Where is library-address / implementation-hash verification captured in deployment tooling, release review, and post-deploy monitoring?
  • If the metapool pairs a plain stablecoin with a base-pool LP token, where is the assertion that those two assets are never treated as unit-price peers without normalization?
  • Do view functions, simulations, and offchain routers reproduce the same normalization semantics as execution, or could offchain tooling mask a path-dependent bug until funds are live?
  • What exact invariant or differential test would fail if one branch forgot to apply getVirtualPrice() tomorrow?
  • If a public exploit in a sibling fork lands, how quickly can the team enumerate every live pool sharing that math and halt or disable them?

Lessons that should already be standard

  • A base-pool LP token is not a stablecoin slot. It is a receipt token with a drifting claim on another pool, and forgetting that for one branch is enough to create a drain.
  • Path symmetry is a solvency invariant, not just a code-style nicety. If two routes price the same LP token differently, the pool is an arbitrage machine against itself.
  • Fixed in source is not a defense. Live bytecode, linked libraries, and deployment manifests are the real security boundary.
  • Invariant corruption compounds. A bad first quote does not merely overpay once; it can move D so that later trades keep paying from the same accounting lie.
  • Differential tests are more valuable than local unit tests for this family. The bug often hides in disagreement between two individually plausible functions.
  • Forked metapool code needs herd-level response. Once one Saddle-derived or Curve-style implementation fails, sibling deployments should be treated as presumptively at risk until proven otherwise.
  • Virtual-price handling belongs in explicit API and code comments. If reviewers must mentally reconstruct where scaling happens, one path will eventually forget.

High-signal incidents

  • bridge-halt near miss (2021-11-06) — the contained early warning: the Saddle-derived metapool bug already let the attacker drain about $8.2M from Avalanche LPs, but validators halted the bridge exit before the attacker could mint the value out on another chain.
  • UST MetaPool virtual-price normalization exploit (2021-11-15) — the precursor on a Saddle-derived codebase where the AMM-side exploit completed cleanly: vulnerable swap() handed out too much base-pool LP, then swapUnderlying() converted that excess LP back through a correctly normalized path for profit.
  • Saddle Finance sUSDv2 MetaPool virtual-price scaling exploit (2022-04-30) — the canonical redeploy failure: a live Ethereum metapool still pointed at the old vulnerable MetaSwapUtils library, and two uses of the vulnerable swap() path were enough because the first swap pushed D below reality before the reverse leg cashed out.

Where this pattern stops

  • Use lp-token-oracle-mispricing when the decisive mistake is an external lending market, vault, or reward system over-trusting an LP-token mark derived from reserve state. This note is for cases where the metapool itself misprices the base-pool LP token inside its own invariant math.
  • Use amm-invariant-scaling-mismatch when the sharper story is broken AMM invariant math or fee-constant scaling without a special role for a drifting base-pool LP receipt token.
  • Use unit-normalization-failure when the important mismatch is token decimals or unit conversions across assets rather than omission of base-pool virtual-price normalization for one LP constituent.
  • Use release provenance failure only when deployment drift is the main analytical weight and the pool math itself is not the dominant lesson. Saddle belongs here first because the stale deployment re-exposed a very specific LP-normalization bug.
  • In short: this pattern begins when a metapool holds a base-pool LP receipt token and different paths stop agreeing on how that receipt token maps to economic value. It stops once the sharper story is external LP valuation, generic invariant math breakage, plain decimal drift, or release-process compromise without distinctive metapool semantics.

Internal linkages

Control surface

  • The leverage sits in the exact places where raw base-pool LP-token units become normalized pool value and back again.
  • If one path applies getVirtualPrice() and another path silently skips it, the metapool is no longer enforcing one coherent invariant. It is enforcing path-dependent accounting fiction.

Sources

Last reviewed

  • 2026-07-31 UTC