Root Cause
Balancer V1's BPool contract uses a custom 18-decimal fixed-point math library (BNum). The functions joinswapPoolAmountOut and calcSingleInGivenPoolOut allow a caller to specify a desired BPT (Balancer Pool Token) output amount and receive back the required token input. When pool reserves are compressed to near-zero via a flash loan, the fixed-point computation of the required token input rounds down to zero — but the contract does not validate that the effective input is nonzero before minting BPT.
// Simplified BPool vulnerability (Balancer V1 BNum fixed-point)
function calcSingleInGivenPoolOut(
uint tokenBalanceIn, // Near-zero after flash loan drain
uint tokenWeightIn,
uint poolSupply,
uint totalWeight,
uint poolAmountOut, // Attacker specifies desired BPT amount
uint swapFee
) public pure returns (uint tokenAmountIn) {
uint normalizedWeight = bdiv(tokenWeightIn, totalWeight);
uint newPoolSupply = badd(poolSupply, poolAmountOut);
uint poolRatio = bdiv(newPoolSupply, poolSupply);
// With tokenBalanceIn ≈ 0, bpow(poolRatio, 1/weight) ≈ 1
// newTokenBalanceIn ≈ tokenBalanceIn ≈ 0
uint tokenRatio = bpow(poolRatio, bdiv(BONE, normalizedWeight));
uint newTokenBalanceIn = bmul(tokenRatio, tokenBalanceIn);
// bsub rounds to 0 — no minimum effective input check
tokenAmountIn = bsub(newTokenBalanceIn, tokenBalanceIn); // = 0 after rounding
tokenAmountIn = bdiv(tokenAmountIn, bsub(BONE, swapFee)); // still 0
// Contract does NOT revert if tokenAmountIn == 0; BPT minted anyway
}
Three missing safeguards converge to create the exploit:
- No minimum effective input check (
require(tokenAmountIn > 0)before the swap executes) - No minimum pool balance guard during swaps (the
MIN_BALANCEconstant is only enforced atbind/rebindtime, not duringjoinswapPoolAmountOut) - No relative-error validation (no check that the minted BPT is proportional to a reasonable token input)
Balancer Labs shut down in March 2026 following a related $116M exploit (November 2025) in V2 contracts. This V1 pool was entirely unmaintained legacy code. Forks of Balancer V1 that have not applied patches remain exposed.
Attack Steps
| Step | Action | Detail |
|---|---|---|
| 1 | Take flash loan | Borrow large WBTC amount via flash loan |
| 2 | Drain BPool reserves | Swap out almost all WBTC from the target BPool, compressing pool balance to ~1 satoshi |
| 3 | Call joinswapPoolAmountOut | Specify desired BPT output (4,408.8 BPT); contract computes tokenAmountIn via calcSingleInGivenPoolOut |
| 4 | Rounding underflow | With near-zero tokenBalanceIn, fixed-point math rounds required input to 0 satoshis |
| 5 | BPT minted gratis | Contract mints 4,408.8 BPT without receiving meaningful WBTC payment |
| 6 | Redeem BPT | Attacker redeems newly minted BPT for remaining pool assets at favorable rate |
| 7 | Repay flash loan | Return borrowed WBTC; net profit ~$234K |
Impact
- Loss: ~$234,000 from a legacy Balancer V1 WBTC BPool
- Chain: Ethereum (legacy V1 deployment, unmaintained since Balancer Labs' closure March 2026)
- Bug family: Same class as the November 2025 exploit that drained $116M from Balancer V2 and led to company shutdown
- Affected parties: Remaining V1 LPs who had not migrated; any forks of Balancer V1 code
- Balancer's response: Warning issued to legacy V1 LPs to exit; no patch possible on immutable V1 contracts
Lessons for Auditors
-
Arithmetic operations on near-zero values require explicit floor checks. Any function where the output of fixed-point math is used as a payment amount must assert
result > 0before proceeding. A minimum-output check is not a precision nicety — it is a security invariant. -
MIN_BALANCEscope must cover all code paths that touch pool reserves. In BPool, the minimum balance constant only applied tobind/rebind. Auditors should trace every code path that modifies pool token balances and verify that minimum-balance invariants hold at each point, not just initialization. -
Flash loans dramatically expand the attack surface for rounding bugs. Rounding errors that are inconsequential at normal pool depths become exploitable when an attacker can temporarily compress reserves to dust levels. Auditors should model the "near-zero reserve" scenario explicitly when reviewing pool math.
-
Sunset unmaintained contracts with explicit migration deadlines. Balancer V1 remained deployed and holding user funds for months after the company that built it ceased operations. Protocols that fork or deploy legacy code must have explicit sunset plans; any contract with a known critical bug family should be put into wind-down mode (deposits disabled, LPs incentivized to exit) immediately.
-
Forks inherit all vulnerabilities. Any project that forked Balancer V1 without independently auditing and patching the BPool math library carries this exact exposure. Audit scope must explicitly include the math library, not just protocol-level logic.