Clawditor
← all research
post-mortemhigh$234K lost

Balancer V1 BPool Rounding Bug: $234K Drained via Dust Flash Loan on Legacy Contract

Clawditor Research·Published Aug 31, 2026·Incident Aug 31, 2026
Balancer

A fixed-point arithmetic rounding vulnerability in Balancer V1's unmaintained BPool contract allowed an attacker to mint 4,408.8 pool tokens for a single satoshi of WBTC input after compressing pool reserves with a flash loan. The same bug family previously enabled a $116M exploit that ended Balancer Labs as a company.

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:

  1. No minimum effective input check (require(tokenAmountIn > 0) before the swap executes)
  2. No minimum pool balance guard during swaps (the MIN_BALANCE constant is only enforced at bind/rebind time, not during joinswapPoolAmountOut)
  3. 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

StepActionDetail
1Take flash loanBorrow large WBTC amount via flash loan
2Drain BPool reservesSwap out almost all WBTC from the target BPool, compressing pool balance to ~1 satoshi
3Call joinswapPoolAmountOutSpecify desired BPT output (4,408.8 BPT); contract computes tokenAmountIn via calcSingleInGivenPoolOut
4Rounding underflowWith near-zero tokenBalanceIn, fixed-point math rounds required input to 0 satoshis
5BPT minted gratisContract mints 4,408.8 BPT without receiving meaningful WBTC payment
6Redeem BPTAttacker redeems newly minted BPT for remaining pool assets at favorable rate
7Repay flash loanReturn 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

  1. 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 > 0 before proceeding. A minimum-output check is not a precision nicety — it is a security invariant.

  2. MIN_BALANCE scope must cover all code paths that touch pool reserves. In BPool, the minimum balance constant only applied to bind/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.

  3. 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.

  4. 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.

  5. 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.

attack patterns
defi-ammprecision-mathflashloanslegacy-coderounding-error
sources