# Balancer V1 BPool: joinswap rounding drains pool reserves

Balancer V1 BPool joinswapPoolAmountOut rounds WBTC input down to 1 satoshi while minting full BPT, letting attacker accumulate ~99.9% of supply and drain ~$110K.

- Payout: Real-world hack
- Severity: high
- Program: BalancerV1BPool
- Credited: DeFiHackLabs
- Disclosed: 2026-08-31
- Write-up quality: 8/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2026-08/BalancerV1BPool_exp.sol
- Library page: https://bugbountylibrary.com/r/dhl%3A20260831-balancerv1bpool
- Tags: domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:high tech:evm tech:solidity technique:chained technique:source-review vuln:accounting-error vuln:business-logic vuln:precision-loss

**Target:** Balancer V1 BPool (DPI/USDC/WETH/WBTC)
**Root cause:** joinswapPoolAmountOut reverse-computes token input via calcSingleInGivenPoolOut in 18-dp fixed point but only enforces tokenAmountIn != 0 and <= maxAmountIn; no minimum-effective-input or relative-error check, so with WBTC reserve compressed to dust the required input rounds down to 1 satoshi while full BPT is minted.
**Steps:**
1. Compress WBTC reserve to ~2 satoshi via repeated `swapExactAmountIn(WETH, balWETH/2, WBTC, 0, MAXP)`
2. Loop: `joinswapPoolAmountOut(WBTC, totalSupply/8, maxIn=1)` mints large BPT for 1 satoshi, then `exitswapPoolAmountIn(WBTC, supply/1000, minOut=1)` pulls the satoshi back so next join rounds to dust again
3. Repeat until attacker holds ~99.9% of BPT supply
4. Call `exitPool(balanceOf(attacker), [0,0,0,0])` to redeem proportional share of DPI/USDC/WETH/WBTC
**How found:** Code-reading of verified BPool source: noticed joinswapPoolAmountOut lacks minimum-effective-input check and MIN_BALANCE only enforced in bind/rebind, not join/swap math; hypothesized that compressing a low-decimal reserve (WBTC 8-dp) against 18-dp weight math would round required input to 1 satoshi.
**Exploitation:** Attacker flash-loaned capital (Spark/Aave + Morpho + Uniswap V3, repaid in-tx), compressed WBTC reserve to dust with public swaps, then alternated joinswapPoolAmountOut (maxIn=1) and exitswapPoolAmountIn to accumulate nearly the entire BPT supply for ~1 satoshi per join, finally exitPool to redeem ~99.9% of all four reserves (~$110,839 in this pool, ~$234K across 5 pools).
**Escalation:** Same rounding flaw applied across 5 Balancer V1 BPools for ~$234K aggregate; nested flash loans scaled working capital.
**Insight:** In AMMs mixing low-decimal tokens (WBTC 8-dp) with 18-dp fixed-point weight math, check whether single-asset join/exit functions enforce a minimum effective input or relative-error bound — if only `!= 0` and `<= maxAmountIn` are checked, compressing the low-decimal reserve to dust makes the reverse-computed input round to 1 unit while full LP shares mint.
