Balancer V2 Composable Stable Pools Exact-Out Rounding Bias / Invariant-Deflation Exploit (2025-11-03)

  • Name: Balancer V2 Composable Stable Pools exact-out rounding bias / invariant-deflation exploit
  • Date: 2025-11-03
  • Protocol: Balancer V2 ComposableStablePool / BaseGeneralPool / Vault batchSwap() path
  • Chain: Ethereum, Arbitrum, Base, Optimism, Polygon (with the same inherited bug also exploited on several Balancer V2 forks)
  • Category: directional rounding bias / roundtrip-swap non-invariance / composable stable-pool exploit / invariant deflation / legacy V2 exploit
  • Loss: about $121.1M gross theft across Balancer-deployed pools per Balancer’s post-mortem, with roughly $45.7M later protected or recovered through pauses, whitehats, and ecosystem interventions; broader fork losses pushed public tallies above $125M
  • Summary: Balancer V2’s legacy Composable Stable Pools let attackers turn a one-wei-looking rounding choice into a cross-chain drain because Vault.batchSwap() could route an exact-out swap through ComposableStablePool.onSwap() into BaseGeneralPool._swapGivenOut(), where amountOut was first upscaled by _upscale() using mulDown even though that economic path should have rounded against the trader. StableMath then computed amountIn from an already understated output quantity, and the later _downscaleUp() on amountIn could not recover the value already lost inside the invariant math. On its own, that directional bias was usually tiny. But Composable Stable Pools combined three features that made it monetizable: StableMath invariant calculations, rate providers that introduced non-trivial scaling imprecision, and composability that treated the pool’s own BPT as a swappable token inside batchSwap(). The attacker first used BPT-in-pool mechanics to push targeted pools into low-liquidity edge states, then ran carefully parameterized exact-out swap loops that repeatedly understated the required amountIn, walking invariant D downward on paper and making BPT artificially cheap. Once BPT value had been deflated far enough, the attacker reversed back through the pool, settled the temporary BPT deficit cheaply, and withdrew the accumulated real assets from Vault internal balances.
  • Trigger / entry point:
    • On 2025-11-03 07:46 UTC, coordinated exploit transactions began hitting Balancer V2 Composable Stable Pools across Ethereum, Arbitrum, Base, Optimism, and Polygon.
    • The directly affected Balancer pools were legacy CSPv5 / V2 Composable Stable Pools that combined BPT composability with rate providers; Balancer’s later post-mortem says most V2 pool types were unaffected, and CSPv6 pools were paused in time.
    • The decisive execution path lived in BaseGeneralPool._swapGivenOut() and the helper _upscale() function used before StableMath’s invariant-sensitive amount calculations.
    • The attacker used Balancer Vault batchSwap() semantics plus internal-balance settlement to run multi-hop, exact-out loops without needing to pre-hold all intermediate BPT used during the exploit.
  • Root cause:
    • The core bug was not reentrancy or privileged access. It was a directional rounding mistake in a solvency-critical math path that became exploitable only under specific pool structure and liquidity conditions.
    • In the exact-out path, Balancer upscaled swapRequest.amount with _upscale() using mulDown, even though the effective rounding direction for that context should have favored the protocol.
    • That rounded-down output amount then fed _onSwapGivenOut() and StableMath, producing an amountIn that was too low; because the invariant step had already been biased downward, the later _downscaleUp(amountIn, scalingFactors[indexIn]) only rounded up the already-understated answer rather than restoring the missing precision.
    • Composable Stable Pools made the bug exploitable because their own BPT was a tradeable pool token, so the attacker could use BPT-mediated exitSwap-style flows to drive one underlying balance toward edge-case low-liquidity values where the rounding loss became large enough to compound.
    • Rate providers and scaling factors added the imprecision needed to turn what looked like negligible precision loss into repeated invariant drift.
    • The deeper invariant failure was that Balancer V2 did not enforce a strong enough roundtrip swap should not create value or per-BPT share value should not fall under valid user operations property on these legacy pool paths.
  • Exploit path:
    1. The attacker targeted vulnerable V2 Composable Stable Pools that combined BPT composability, StableMath, and rate-provider scaling.
    2. Using batchSwap(), they first performed a BPT-mediated exitSwap that created a temporary BPT deficit inside the Vault and pushed one underlying balance toward a rounding boundary; in BlockSec’s representative Arbitrum reconstruction, the setup leg drove the target cbETH balance to 9 units in pool-native precision.
    3. They then executed a long, simulation-tuned sequence of exact-out swaps between underlying assets; in the same reconstruction, one key leg requested 8 units out after the setup state had been prepared, so _upscale(... mulDown) compressed the economically relevant amount before StableMath priced the path.
    4. Each loop made the pool undercharge on amountIn, which gradually deflated invariant D and therefore the implied value of BPT.
    5. Once BPT had become artificially cheap on paper, the attacker reversed the path, reacquired or settled the temporary BPT deficit at the depressed price, and kept the surplus real assets as profit.
    6. The exploited value often remained inside the Balancer Vault first as internal balance, then got extracted in separate manageUserBalance(WITHDRAW_INTERNAL) transactions.
    7. Copycats and fork exploiters reused the same pattern across other Balancer V2-derived deployments once the technique became legible onchain.
  • What the attacker needed to be true:
    • The target pool had to be a V2 Composable Stable Pool, not just any Balancer pool.
    • The pool needed rate providers or other scaling paths that made directional rounding error non-zero and magnifiable.
    • The attacker had to be able to drive balances into low-liquidity edge cases where tiny rounding bias became economically meaningful.
    • batchSwap() and BPT composability had to allow temporary deficit-style flows that made BPT-mediated drain/setup loops feasible in one transaction.
    • The pool’s pause window or operator controls had to be unavailable, expired, or slower than the exploit wave.
  • Technical anatomy worth remembering:
    • Vault.batchSwap() handed the request into ComposableStablePool.onSwap(), which inherited BaseGeneralPool._swapGivenOut(); the decisive bug sat exactly where swapRequest.amount was upscaled before _onSwapGivenOut() and StableMath derived the required amountIn.
    • BaseGeneralPool._swapGivenOut() upscaled swapRequest.amount before invoking _onSwapGivenOut(); that upscaling used mulDown through _upscale() even in the exact-out context.
    • Balancer’s later writeup, Certora’s analysis, and Trail of Bits’ retrospective all converge on the same practical lesson: aggregate solvency-style proofs were not enough because they missed local roundtrip and share-value invariants.
    • BlockSec reconstructed a two-stage attacker workflow: first a zero-immediate-profit manipulation transaction that ran the core precision exploit, then later withdrawals that converted Vault internal balance into externally visible profit.
    • One representative Arbitrum exploit transaction analyzed by BlockSec is 0x7da32ebc615d0f29a24cacf9d18254bea3a2c730084c690ee40238b1d8b55773.
    • Public analyses describe the attacker using a helper contract to simulate pool state and tune swap parameters, including loop counts and exact-out amounts, before firing the main batch swap; BlockSec’s reconstruction says the attacker passed an initial balance parameter of 100000000000, used 25 loop iterations in the sampled Arbitrum exploit, and called helper selector 0x524c9e20 on 0x679B362B9f38BE63FbD4A499413141A997eb381e to iterate post-swap balance estimates.
    • Balancer’s own post-mortem emphasized three necessary ingredients: the rounding error itself, rate-provider-induced imprecision, and a low-liquidity state created through composable BPT mechanics.
    • Another operational detail worth preserving is that many most-dangerous CSPv5 pools were already outside their pause windows, while still-pausable CSPv6 implementations were halted within minutes; the exploit wave was therefore partly shaped by lifecycle controls expiring before the latent math bug surfaced.
    • The exploit is a clean preserved example of tiny arithmetic bias + edge-state preparation + roundtrip amplification rather than a one-shot invariant check bypass.
  • Representative transactions and addresses:
    • Representative Arbitrum exploit transaction: 0x7da32ebc615d0f29a24cacf9d18254bea3a2c730084c690ee40238b1d8b55773
    • Widely cited attacker address across several chains: 0x506D1f9EFe24f0d47853aDca907EB8d89AE03207
    • Ethereum coordinator contract highlighted in public analysis: 0x54B53503c0e2173Df29f8da735fBd45Ee8aBa30d
    • Helper / math contract published onchain and later reused by copycats: 0x679B362B9f38BE63FbD4A499413141A997eb381e
  • Response / mitigation:
    • Balancer, Certora, Trail of Bits, Hypernative, SEAL 911, whitehats, and ecosystem partners formed a rapid incident-response war room once the exploit wave began.
    • CSPv6 implementations that still had active pause windows were paused quickly, protecting about $19.3M according to Balancer.
    • Recovery mode was activated on affected pools so LPs could exit remaining funds through simplified proportional exits.
    • Whitehats and partners recovered or protected tens of millions across several chains; Balancer’s post-mortem credits about $45.7M protected or recovered in aggregate.
    • Fork ecosystems deployed harsher emergency actions, including chain-level intervention and censorship on some networks, which preserved how severe and copyable the math bug was once public.
    • Balancer accelerated sunsetting of V2 stable-pool infrastructure and emphasized V3’s different architecture: Vault-level 18-decimal normalization, explicit rounding direction, and stronger formal/invariant testing around roundtrip safety.

Pattern tags

Why the exploit worked

  • Balancer let a locally tiny arithmetic choice cross three promotions that should have stayed separate: some exact-out amount can be rounded and fed into StableMath, that rounded object still counts as canonical invariant / BPT-share truth for this pool, and that accepted truth still deserves live batchSwap/internal-balance reserve authority right now.
  • The rounding loss was not immediately fatal in ordinary states. It became monetizable because Composable Stable Pools also let the attacker use pool BPT as a tradeable leg inside Vault routing, so the attacker could first manufacture the thin-balance state where the exact-out path’s downward bias mattered.
  • Once the pool had published the biased invariant state as if it were normal solvency truth, the attacker could keep looping that error. Each undercharged exact-out trade pushed invariant D and implied BPT value lower, which made the later unwind and settlement of the temporary BPT deficit cheaper than honest pool accounting should ever allow.
  • Internal-balance settlement then turned what looked like a local swap-math mistake into a cash-out path: the exploit did not need an oracle rewrite or admin privilege, only the ability to keep treating each newly biased state as money-good settlement truth for the next leg.
  • The deeper design miss was not just wrong rounding direction; it was failing to prove that exact-out loop there and back, BPT-mediated exit-style setup, and per-share value under low-liquidity edge states all preserved one coherent solvency story across Vault routing plus pool math.

Detection and audit pivots

  • Force whole-path near-roundtrip tests through the exact same interfaces attackers get: batchSwap() with pool BPT as an intermediate token, low-balance setup legs, exact-out loops, and later manageUserBalance(WITHDRAW_INTERNAL) cash-out. A pool that only looks safe in isolated swap-unit tests is not actually reviewed.
  • Monitor for invariant-share drift rather than only reserve theft. If valid user actions can ratchet invariant D downward while the pool’s real reserves are not already being spent equivalently, BPT price and per-share value should alarm before attackers can unwind into external profit.
  • Treat thin-balance states as a separate security regime. When one underlying leg is pushed near dust, exact-out quoting, BPT composability, or internal-balance settlement should face tighter limits or fail closed rather than continuing under the same precision assumptions as deep-liquidity states.
  • Review every scaling helper in economic context, not helper-local context. _upscale() using mulDown may look harmless as a reusable utility, but the only question that matters is whether that rounding direction is still correct after StableMath, later rescaling, and BPT-share publication all compose.
  • Keep a live inventory of which legacy pools still hold value after pause windows or recovery controls have expired. Balancer’s exploit wave was materially shaped by the fact that some pools had already outlived their fastest containment mechanism.
  • Audit pool-family feature conjunctions, not just individual modules. The dangerous surface was rounding bias + rate-provider scaling + swappable BPT + batch routing + internal balances + low-liquidity setup, and systems that only reason about those ingredients separately will miss the monetizable path.

Why this incident matters

  • It preserves one of the clearest modern examples of a mature, heavily audited DeFi protocol losing nine figures to arithmetic directionality rather than access control or classic reentrancy.
  • It shows that rounding should favor the protocol is not a slogan; it is a local invariant that must be proven across real user flows, especially multi-step roundtrips.
  • It also shows how composability changes the threat model: a tiny math bias that looks ignorable in isolated swaps can become fatal once internal settlement, swappable LP tokens, and low-liquidity edge states let attackers iterate it.
  • The incident is especially useful as a contrast case to Uranium or Kyber. Those broke AMM safety through scaling mismatch or tick/liquidity desynchronization; Balancer broke because a supposedly minimal rounding choice destroyed roundtrip non-profitability in a composable stable-pool design.

Failure-prevention checklist

  • Mark every exact-in and exact-out helper with its economic rounding direction explicitly. Shared scaling helpers like _upscale() and _downscaleUp() should not be reused across paths unless the protocol has proved that the direction still protects the pool after invariant math, not just before it.
  • Test whole-path roundtrips under adversarial state preparation, not just isolated swap correctness. push pool thin -> exact-out loop -> unwind -> settle BPT deficit should be a first-class invariant test for any pool whose own receipt token is swappable.
  • Treat pool BPT composability as a security boundary, not a UX feature. If the pool’s own receipt token can be used inside batchSwap() or equivalent routing, then joins, exits, swaps, and internal-balance settlement need one shared solvency proof.
  • Put explicit low-liquidity guards around exact-out math. If one underlying balance is pushed near dust, narrow or halt the paths that rely on precision-sensitive invariant calculations instead of continuing to quote normal executable liquidity.
  • Track LP-share monotonicity directly. A valid user path should not be able to walk invariant D down and make BPT cheaper on paper without a corresponding real asset loss already being accounted for.
  • Re-review legacy pools when lifecycle controls expire. Balancer’s pause-window split between CSPv5 and CSPv6 preserved the operational lesson that old math assumptions become more dangerous once the easiest containment lever is gone.

Audit prompts

  • For every exact-out path, where does the protocol round before invariant math, and is that rounding direction still correct after later rescaling and settlement?
  • Can the pool’s own LP token, internal balance system, or batch-routing semantics be used to create a temporary deficit or low-liquidity setup state before the exploitative loop starts?
  • What tests prove that swap there and back, exit via BPT -> exact-out loop -> re-enter, or similar near-roundtrips cannot create value once balances are pushed near dust?
  • Which pool features must all be present for the bug to monetize — rate providers, BPT composability, exact-out routing, low-liquidity setup, internal-balance settlement — and does the protocol ever reason about them together rather than as separate modules?
  • If a share token becomes cheaper because invariant accounting drifted, what monitoring or invariant would surface that before users can redeem the drift into real assets?
  • When pause windows, kill switches, or recovery-mode paths expire, what latent arithmetic bugs remain live with no fast containment tool left?

Lessons that should already be standard

  • Tiny rounding error is not a small-risk category once the protocol lets attackers choose the state where the error is measured.
  • Exact-out math deserves its own adversarial review; many protocols reason carefully about what they send out and too weakly about what they must demand back.
  • Swappable LP receipts turn local pool math into a multi-step state machine. The exploit surface is no longer just one swap, it is receipt token as setup primitive + swap loop + unwind.
  • Formal solvency claims are incomplete if they do not also prove roundtrip non-profitability and per-share monotonicity under edge-state routing.
  • Lifecycle controls matter as part of security posture. A legacy pool that still holds value after its pause window expires is effectively advertising that any dormant arithmetic bug must now be solved by incident response instead of prevention.

Internal linkages

Control surface

  • The decisive leverage sat in _swapGivenOut(), _upscale(), StableMath’s dependence on scaled amounts, Vault batchSwap() settlement semantics, BPT-as-pool-token composability, and the ability to push pools into low-liquidity edge states before iterating exact-out loops.
  • Once exact-out swaps could ratchet invariant D downward faster than the protocol realized, BPT stopped being a passive LP receipt and became the attacker’s pricing wedge against the pool’s own assets.

Sources

Last reviewed

  • 2026-07-29 UTC