Balancer Boosted Pools Linear-Pool Rate Manipulation / Cached-BPT Mispricing Drain (2023-08-27)
- Name: Balancer Boosted Pools linear-pool rate manipulation / cached-BPT mispricing drain
- Date: 2023-08-27
- Protocol: Balancer V2 Boosted Pools (
LinearPool+ nestedComposableStablePool/bb-a-USDfamily) - Chain: Ethereum mainnet and Optimism
- Category: LP-token oracle mispricing / nested-pool rate-cache manipulation / boosted-pool constituent mispricing / precision-loss exploit
- Loss: about
$1.2Mtotal according to Balancer’s technical postmortem (~$980kon mainnet and~$215kon Optimism) - Summary: Balancer’s August 2023 boosted-pool exploit was not a simple replay of the read-only-reentrancy issue it had disclosed five days earlier. The live drain instead abused Balancer’s own linear-pool math. By making tiny
BPT -> mainswap legs round the main-token output down to zero, the attacker could movebb-a-USDCbalance-sheet state in a direction that made the linear pool’sgetRate()jump sharply upward. Balancer’s nested boosted pool (bb-a-USD) treated that constituent linear-pool rate as cached pricing input for swaps betweenbb-a-USDC,bb-a-DAI, andbb-a-USDT. In the representativebatchSwap()path, one boosted-pool step refreshed the cachedbb-a-USDCrate after the attacker had pumped it, and later hops in the same batch immediately consumed that new cache entry as scaling-factor truth across more than one downstream constituent market. Once that poisoned rate cache was live, the boosted pool overvaluedbb-a-USDC, and the remaining swap legs turned modest flash-loaned USDC into mispriced DAI, USDT, and residual USDC / aUSDC withdrawals. The important control-surface point is that the cache write had already widened fromone pool stores its own internal markintoone attacker-refreshed constituent mark can fan out through several higher-layer swap and redemption paths in the same route. - Trigger / entry point:
- On
2023-08-22, Balancer publicly warned that some pools were affected by a critical vulnerability and urged LPs to exit; the Emergency SubDAO mitigated many pools but some boosted pools remained at risk. - On
2023-08-27, attackers began draining remaining liquidity from vulnerable boosted pools on mainnet and Optimism. - The representative public trace most often analyzed is Ethereum transaction
0x2a027c8b915c3737942f512fc5d26fd15752d0332353b3059de771a35a606c2d. - The directly relevant architecture was Balancer’s boosted-pool stack: a
Composable Stable Poolsuch asbb-a-USDholding linear-pool BPTs likebb-a-USDC,bb-a-DAI, andbb-a-USDTas constituent assets.
- On
- Root cause:
- The decisive failure was not privileged access and not just generic
oracle manipulation. Balancer let a nested pool treat a constituent pool’s attacker-shapeable internal rate as trustworthy swap pricing input. - In the vulnerable
LinearPool.onSwap()path, tinyBPT -> mainswaps eventually passed through_downscaleDown, and forbb-a-USDCthe computed USDC output could round all the way to zero once the to-be-downscaled amount fell below the 6-decimal USDC precision boundary. - That let the attacker change the linear pool’s balances in a one-sided way: the pool effectively accepted extra BPT-side value without paying the corresponding main-token amount out.
- Because linear-pool
getRate()depends on the relationship between nominalized pool assets and effective BPT supply, those zero-output legs could ratchet the reportedbb-a-USDCrate upward very sharply. - The nested boosted pool cached that manipulated rate and used it as a scaling factor for subsequent swaps, so
bb-a-USDCstarted trading insidebb-a-USDas if it represented far more underlying value than it really did. - The decisive publication boundary was also about ordering, not only writability: Balancer’s own postmortem and BlockSec’s reconstruction preserve that one hop could return an older cached rate while refreshing the cache underneath, and the next hop in the same
batchSwap()then consumed the newly inflated cache as canonical pricing input. - In BlockSec’s representative trace, the
bb-a-USDC -> bb-a-DAIpath first traded at roughly1.00x, then after the cache refresh the next hop traded at about19.1x; a siblingbb-a-USDC -> bb-a-USDThop then cleared at about12.44x. - Balancer’s later discussion and BlockSec’s reconstruction also preserved a second enabling flaw, commonly described as a
resets rate on 0 supplyissue, which helped the attacker rebalance or reacquire scarce bb-a-token inventory needed to complete the loop.
- The decisive failure was not privileged access and not just generic
- Exploit path:
- The attacker flash-borrowed capital, with BlockSec’s representative transaction starting from a
300,000 USDCAave flash loan. - They first reoriented the
bb-a-USDClinear pool by swapping a small amount of USDC for aUSDC, leaving the later rate-manipulation path easier to execute. - They then ran a multi-step Balancer
batchSwap()sequence throughbb-a-USDCand the nestedbb-a-USDpool. - Key legs used tiny
bb-a-USDC -> USDCstyle conversions whose downscaled USDC output rounded to zero; BlockSec’s decomposition highlights steps3.2and3.6as the zero-output legs. Economically, these legs pushedbb-a-USDCaccounting in one direction without paying out the balancing main-token amount. - Those zero-output steps caused
bb-a-USDC’s reported linear-pool rate to spike, andbb-a-USDcached the manipulated rate as if it were a fair constituent valuation. In the traced batch, one hop still used the older cache entry while updating it underneath, and the very next hop consumed the fresh inflated cache. - With the boosted pool now overvaluing
bb-a-USDC, the attacker swapped intobb-a-DAIandbb-a-USDTat distorted exchange rates — about19.1xon the keybb-a-USDC -> bb-a-DAIhop and about12.44xon thebb-a-USDC -> bb-a-USDThop in BlockSec’s reconstruction — and then redeemed those linear-pool BPTs for real DAI and USDT in their respective underlying pools. - In BlockSec’s traced mainnet example, the final extracted balances after flash-loan repayment were roughly
114,324 DAI,253,461 USDT, and0.970495 aUSDC, all sourced from a much smaller initial capital commitment than honest pricing would have allowed. - Additional attacks repeated the same broad pattern across the remaining vulnerable boosted-pool liquidity until the affected pools were effectively emptied or exited.
- The attacker flash-borrowed capital, with BlockSec’s representative transaction starting from a
- What the attacker needed to be true:
- A boosted pool had to trust constituent linear-pool rates as cached pricing inputs.
- The relevant linear pool needed a precision-loss path where tiny swap outputs could round to zero while still moving the state that fed
getRate(). - Enough nested liquidity had to remain after Balancer’s August 22 warning for the mispricing to be worth executing.
- The attacker needed a batch-swappable route from the poisoned
bb-a-USDCmark into other constituent assets and then into redeemable underlying tokens. - External liquidity in the constituent bb-a-tokens was thin enough that the zero-supply / rate-reset helper flaw materially improved exploitability.
- Technical anatomy worth remembering:
- This incident is best preserved as a
nested pool trusted a manipulable constituent receipt-token ratefailure, not merely as an abstract rounding bug. - The dangerous pricing object was not an external Chainlink feed or a third-party LP oracle. It was Balancer’s own cached view of one pool’s BPT rate being reused as truth inside another pool.
- The ordering matters. In the representative exploit path, the cache was not just written sometime earlier and later trusted in a separate epoch; one step in the same
batchSwap()refreshedbb-a-USDC’s cached rate after the attacker had already distorted the linear pool, and later steps immediately spent against that fresh publication. - The consumer fan-out matters too. Once
bb-a-USDhonored the refreshed constituent cache, the bad publication no longer lived only inside one local quote path: it repriced multiple downstream constituent swap legs and then the healthier underlying redemptions those legs unlocked. - The rounding edge was tiny in code but huge in consequence: for the
bb-a-USDC -> USDCdirection, once the to-be-downscaled main-token output fell below the USDC precision boundary,_downscaleDownreturned0, sobb-a-USDCinventory could be added without any balancing USDC leaving the pool. - The exploit shows how precision loss becomes economically fatal once a receipt token is recursively reused as liquidity inside a higher-level AMM.
- It is also a useful contrast with Balancer’s earlier disclosed read-only-reentrancy issue from the same month: both involved unsafe rate updates in nested pools, but the successful August 27 drain path weaponized linear-pool math directly rather than a view-only callback window.
- Balancer’s official postmortem emphasizes that over 80% of vulnerable TVL had been mitigated or withdrawn after disclosure; the eventual exploit mainly hit the remaining liquidity that stayed in still-at-risk pools.
- This incident is best preserved as a
- Representative transactions and pools:
- Representative mainnet exploit transaction:
0x2a027c8b915c3737942f512fc5d26fd15752d0332353b3059de771a35a606c2d - Affected boosted pool family highlighted by Balancer: the Aave-boosted
bb-a-USDstack, including theBalancer Aave Boosted StablePool - Key constituent linear pools:
bb-a-USDC,bb-a-DAI,bb-a-USDT
- Representative mainnet exploit transaction:
- Response / mitigation:
- Before the live exploit, Balancer had already published a warning on
2023-08-22, enabled recovery-style exits where possible, paused or mitigated pools still within their pause windows, and urged LPs to withdraw from remaining at-risk pools. - After the exploit began on
2023-08-27, Balancer publicly disclosed that the residual at-risk pools were actively being drained and again urged any remaining LPs to exit immediately. - Balancer’s later technical postmortem said the long-term fix required architectural changes stronger than ad hoc pool-level mitigations, including changes in how pool state, scaling, and rate updates interact.
- The incident later led to Balancer governance restitution discussions for affected LPs.
- Before the live exploit, Balancer had already published a warning on
Three constituent-rate-authority promotions this incident collapsed
-
A local
bb-a-USDCbalance-sheet distortion existed at all- The attacker did not begin with a fake Balancer pool or forged external oracle. They began inside a real
LinearPoolwhose own swap math let tinyBPT -> mainlegs round the main-token output down to zero while still changing the internal balances that fedgetRate(). - That meant Balancer already had one authentic but dangerous accounting fact on its hands:
bb-a-USDC’s locally computed constituent rate could be pushed away from honest redemption value by public trading.
- The attacker did not begin with a fake Balancer pool or forged external oracle. They began inside a real
-
That attacker-shaped local rate was still promoted into canonical boosted-pool constituent value
- The higher-layer
bb-a-USDpool did not treat the refreshedbb-a-USDCmark as a suspicious, same-route local observation. Its cache path promoted that freshly manipulatedLinearPoolrate into the scaling-factor truth used to pricebb-a-USDCagainst sibling constituents. - Balancer’s own postmortem and BlockSec’s reconstruction make the publication boundary sharper: one hop in the same
batchSwap()refreshed the cachedbb-a-USDCrate after manipulation, and later hops in that very route consumed the new cache as canonical constituent pricing input.
- The higher-layer
-
That published constituent truth still deserved immediate reserve-spend and redemption authority
- Once
bb-a-USDhonored the inflatedbb-a-USDCcache, the bad mark no longer stayed trapped inside one local quote. It inherited authority to buy mispricedbb-a-DAIandbb-a-USDT, and those positions could then be redeemed for real DAI and USDT from healthier underlying pools. - The cash-out path is why this was a treasury event rather than a quirky math bug: Balancer let
one constituent pool's attacker-shaped internal markbecomelive multi-asset reserve and redemption authoritywithout any independent sanity boundary in between.
- Once
The durable lesson is that a constituent pool can compute some internal rate, that rate is still worthy of publication as higher-layer pricing truth, and that published truth still deserves immediate authority over healthier downstream reserves are three different promotions. The exploit worked because Balancer silently let one manipulated LinearPool rate pass all three.
Pattern tags
- lp-token-oracle-mispricing
- state-publication-authority-failure
- Internal receipt-token rate /
getRate()publication became higher-layer swap truth - Publication and activation collapsed inside one attacker-authored
batchSwap()route rather than across a slower sync boundary
Why the exploit worked
- Balancer’s lower-layer
bb-a-USDCmath did not merely produce a bad quote; it produced a spendable bad quote. Once tinyBPT -> USDClegs could round the USDC output to zero while still changing the state that fedgetRate(), the attacker could manufacture a constituent BPT rate that no honest redemption path justified. - The nested
bb-a-USDpool then treated that constituent rate as if it were a money-good scaling factor rather than a fresh, attacker-shaped local mark. This is the composability mistake worth preserving: a receipt token’s internal accounting rate became another pool’s live oracle. - Cache timing removed the last safety margin. In the representative
batchSwap()path, one hop refreshed the cachedbb-a-USDCrate after manipulation and later hops in the same route immediately consumed that refreshed cache. The system did not require the new mark to survive even one block, one external challenge window, or one independent sanity pass before stronger assets traded against it. - Cache fan-out made the publication materially more dangerous. The refreshed
bb-a-USDCmark did not stay trapped inside one constituent quote; it propagated into multiple downstream swap legs and then into the healthier underlying redemptions those legs unlocked. - The monetization path was balance-sheet asymmetric. A manipulated
bb-a-USDCrate only mattered because the boosted pool let the attacker rotate that fake constituent value intobb-a-DAIandbb-a-USDT, then redeem those positions for real DAI and USDT from healthier underlying pools. - The exploit therefore was not just
rounding bug in a pool. It wasprecision-loss let one internal receipt-token mark be published as canonical higher-layer value, and Balancer immediately spent against that publication.
Detection and audit pivots
- Trace every path where one pool’s internal rate, cache entry, or receipt-token valuation is consumed as another pool’s swap, mint, or redemption input. If a nested AMM treats
getRate()like an oracle, review it like an oracle. - Simulate atomic routes that do
distort constituent pool -> refresh cached rate -> consume refreshed cache -> redeem stronger assetsinside one transaction. If that bundle pays, the protocol has collapsed publication and reserve authority. - Review mixed-decimal downscaling edges aggressively. A tiny leg that outputs
0at the token boundary but still moves internal accounting can become a one-sided rate ratchet once the receipt token is reused elsewhere. - Ask whether the downstream consumer can tell who refreshed the cache and how recently. A nested pool that cannot distinguish
stale but attacker-refreshed this routefrommature and independently trustedis over-trusting its own publication surface. - Compare the quoted constituent BPT rate against executable redemption value. If the constituent token can only justify its new rate because the attacker has not yet unwound the exact path that created it, the higher-layer pool should not treat that rate as swap truth.
- Treat
warning issued, some liquidity remainsas a real exploitability condition, not only an ops footnote. Residual nested liquidity plus a known publication bug is still live reserve authority.
Failure-prevention checklist
- Distinguish
rate observed,rate cached, andrate spend-valid. A constituent BPT rate written during a public swap path should not automatically become immediate pricing authority for later hops in the same route. - Refuse swap legs whose token output rounds to zero while still mutating the accounting state that determines pool rate, share price, or cached scaling factors.
- Add consumer-side bounds on constituent-rate movement. Nested pools should cap how far one constituent BPT rate can move before swaps, joins, exits, or cache activations narrow or fail closed.
- Keep cache refresh and cache consumption from collapsing into one attacker-authored transaction path. Delays, staged activation, or route-local
use old cache onlyrules are healthier than immediate promotion. - Haircut or isolate recursively reused receipt tokens when their fair value depends on internal accounting that public traders can perturb cheaply.
- Verify redeemability before honoring a constituent mark. If the nested pool is about to trade against
bb-a-USDCas if it were worth many times more, require an independent check that real underlying redemption paths support that claim. - Test composable pools under mixed-decimal and low-liquidity boundary conditions, especially where
downscale,upscale, cached rates, and nested BPT reuse interact. - Add asset-local degraded modes that can freeze one constituent rate or one boosted-pool family without requiring the whole protocol to discover and unwind risk manually.
- Treat recovery-mode exits and warnings as containment tools, not architectural fixes. The permanent fix is to stop attacker-shaped lower-layer accounting from becoming immediate higher-layer pricing truth.
Audit prompts
- Can one public route both manipulate a constituent pool’s internal rate and later consume the refreshed cache before any independent check or delay intervenes?
- Which exact accounting variables still change when the user-facing token output rounds to zero, and do those variables feed
getRate(), cached scaling factors, or other higher-layer pricing inputs? - What prevents one constituent BPT rate from repricing an entire boosted pool by an absurd multiple inside one transaction?
- Does the nested pool know whether a cached rate was refreshed by the current trader earlier in the same batch, or does it blindly honor the newest value as canonical?
- If the constituent BPT were redeemed honestly for underlying assets immediately after publication, how much of the quoted new rate would survive?
- Are there any
0 supply, reset, or reacquisition helper paths that let attackers replenish scarce intermediate inventory needed to complete a poisoned-cache loop? - When one constituent rate becomes suspicious, can operators or automated guards isolate that constituent locally, or is the only safe action a broad protocol warning and LP evacuation?
- If this same architecture were cloned with different token decimals or a new nested route, what invariant test proves
rounding cannot create one-sided state motion that another pool later prices as real value?
Lessons that should already be standard
- Internal receipt-token rates are oracles once another component consumes them. The fact that Balancer authored the rate itself did not make it safer to spend against.
- Publication timing is part of solvency design. A cache that can be refreshed and spent inside one attacker-controlled route is not just a gas optimization; it is reserve authority.
- Precision bugs become treasury bugs when receipt tokens are recursively composable.
Tiny output rounded to zerosounds local until a higher-layer pool treats the resulting rate jump as canonical value. - Nested pools need their own distrust boundary. A downstream AMM should not inherit full confidence in a constituent pool’s freshly updated accounting state just because both live under the same protocol brand.
- Honest redemption value matters more than elegant internal math. If the new
bb-a-USDCrate could not survive immediate redemption into underlying assets, it never deserved to price DAI or USDT swaps. - Operational warnings reduce blast radius but do not resolve publication-authority flaws. Residual liquidity remains exposed until architecture stops same-route publication from becoming same-route spend.
- Mixed-decimal composability is a first-class exploit surface. Whenever scaling, rounding, cached rates, and nested receipt tokens interact, tiny math edges can become giant pricing lies.
Why this incident matters
- It is a clean case where a protocol reused its own lower-layer receipt token as pricing truth in a higher-layer pool and accidentally let tiny math asymmetries print fake constituent value.
- It expands the corpus beyond
external oracle was manipulatedtoward the more composability-specific lesson thatinternal receipt-token rates are also oracles once another pool consumes them. - The sharper publication-authority seam is that the attacker did not only create a bad constituent mark; Balancer’s own cached-rate machinery let that transient mark become reusable swap truth for the nested pool before conditions normalized.
- The fan-out seam is worth preserving separately: once the nested pool accepted the refreshed constituent cache, that one publication no longer informed only
bb-a-USDC’s local accounting story; it repriced multiple downstream constituent trades and the healthier underlying assets those trades could later redeem. - The even sharper timing seam from Balancer’s and BlockSec’s writeups is that publication and activation effectively collapsed into the same
batchSwap(): the attacker did not need to wait for a later keeper sync or a later block once the constituent cache had been refreshed. - It also preserves an awkward but important operational lesson: Balancer had correctly recognized broad danger and warned users, yet a different exploit path still monetized the same fragile architecture before all capital could leave.
Internal linkages
- Closest recurring pattern: lp-token-oracle-mispricing.
- Publication-authority companion: state-publication-authority-failure — the attack did not stop at shaping
bb-a-USDC’s constituent rate; the nested pool’s cache path made that attacker-timed rate canonical swap pricing truth. - Positive-design publication companion: sync-harvest-and-cache-write-authority-discipline — the sharper follow-up is how cached constituent rates, reserve refreshes, and similar helper writes should have been bounded before a nested pool could honor them immediately, including later hops in the same multi-step route.
- Positive-design reserve-authority companion: internal-mark-publication-quarantine-and-reserve-spend-separation — useful when the follow-up is how Balancer should have separated
constituent rate updatedfromnested pool may spend against that rate, especially inside onebatchSwap()route. - Adjacent pattern: oracle-manipulation.
- Useful contrast: read-only-reentrancy — Balancer’s August 22 disclosure centered on callback-window stale reads, but the successful August 27 drain instead weaponized constituent-pool rate math directly.
- Useful comparison: sentiment-balancer-bpt-read-only-reentrancy-2023-04-04 — another case where Balancer BPT-related state became unsafe pricing truth for a downstream consumer.
- Useful comparison: balancer-v2-composable-stable-pools-exact-out-rounding-bias-invariant-deflation-exploit-2025-11-03 — a later Balancer exploit where small rounding assumptions again became fatal once composable BPT mechanics let attackers iterate the bias.
Control surface
- The leverage sat in
LinearPoolswap rounding,getRate()-style constituent BPT valuation, cached rate propagation into nested boosted pools, andbatchSwap()routes that could turn a poisoned constituent rate into withdrawals of stronger underlying tokens. - The quiet design mistake was letting one Balancer pool’s internally manipulable receipt-token rate become another Balancer pool’s live swap-pricing truth.
- The consumer boundary mattered as much as the write boundary: once the refreshed
bb-a-USDCcache was accepted, it fanned out through multiple constituent swap paths and then into healthier underlying redemptions rather than staying a local preview or isolated quote. - Put differently: the rate cache was not harmless gas optimization or bookkeeping; it was the publication surface that decided when attacker-shaped lower-layer state became canonical higher-layer pricing.
Sources
- https://medium.com/balancer-protocol/rate-manipulation-in-balancer-boosted-pools-technical-postmortem-53db4b642492
- https://forum.balancer.fi/t/vulnerability-found-in-some-pools/5102
- https://forum.balancer.fi/t/reentrancy-vulnerability-scope-expanded/4345
- https://blocksec.com/blog/tiny-rounding-down-big-fund-losses-an-in-depth-analysis-of-the-recent-balancer-incident
- https://etherscan.io/tx/0x2a027c8b915c3737942f512fc5d26fd15752d0332353b3059de771a35a606c2d
- https://forum.balancer.fi/t/bip-544-linear-pool-hack-restituion/5541
Last reviewed
- 2026-08-04 UTC