# Balancer boosted pool precision-loss rate manipulation drains $2M

Precision loss in Balancer bb-a-USDC rate calculation let attacker inflate share price via batchSwap and drain ~$2M from boosted pools.

- Payout: Real-world hack
- Severity: critical
- Program: Balancer
- Analysis by: DeFiHackLabs
- Disclosed: 2023-08-27
- Write-up quality: 8/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2023-08/Balancer_exp.sol
- Library page: https://bugbountylibrary.com/r/dhl%3A20230827-balancer
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:critical tech:evm tech:solidity technique:chained vuln:business-logic vuln:flash-loan vuln:oracle-manipulation vuln:precision-loss

**Target:** Balancer Vault boosted pools (bb-a-USDC/DAI/USDT)
**Root cause:** Rate computation for bb-a tokens uses virtualSupply with integer division; swapping out tiny amounts rounds to zero USDC output while still burning bb-a-USDC, inflating the share rate. Business logic in batchSwap caches manipulated rates across steps.
**Steps:**
1. Flash loan 300k USDC from Aave.
2. Swap USDC->aUSDC GIVEN_OUT to drain all aUSDC from target pool (bb-a-USDC/USDC/aUSDC).
3. batchSwap step0: bb-a-USDC->USDC for `virtualSupply - 775_114_420_171 - 20_000_000_000` to burn almost all virtualSupply.
4. batchSwap step1: bb-a-USDC->USDC for `775_114_420_171` outputs zero USDC due to precision loss, inflating bb-a-USDC price.
5. batchSwap step2: bb-a-USDC->bb-a-DAI for `1e18` to write inflated rate into cache.
6. batchSwap step3/4: exchange bb-a-USDC for bb-a-DAI (`7300e18`) and bb-a-USDT (`14000e18`) at inflated price.
7. batchSwap step5: bb-a-USDC->USDC `20_000_000_000` to reset virtualSupply to 0 and price to 1.
8. batchSwap step6: USDC->bb-a-USDC `150_000e6` to repay batch borrow.
9. Swap bb-a-DAI/bb-a-USDC/bb-a-USDT back to DAI/USDC/USDT, repay Aave flash loan.
**Exploitation:** Attacker flash-borrowed 300k USDC, drained aUSDC from the pool, then used a 7-step batchSwap to burn nearly all bb-a-USDC virtualSupply, trigger a zero-output precision-loss swap that inflated the bb-a-USDC rate, cached the inflated rate, and swapped bb-a-USDC into bb-a-DAI/bb-a-USDT at the manipulated price before resetting the pool and repaying the loan.
**Escalation:** Chained precision-loss rounding with batchSwap rate caching to amplify a tiny rounding error into a full pool drain across three boosted pools.
**Insight:** In Balancer-style boosted pools, integer division in rate = balance/virtualSupply can round to zero on small swaps; batchSwap caches rates between steps, so a zero-output swap can poison the cache for subsequent profitable swaps. Look for GIVEN_IN swaps where output rounds to 0 but input tokens are still burned.

## Proof of concept (copied exactly from the original report)

**Drain aUSDC from target pool via GIVEN_OUT swap**

```
balancer.swap(
                IBalancerVault.SingleSwap(
                    targetPool,
                    IBalancerVault.SwapKind.GIVEN_OUT,
                    address(USDC),
                    address(aUSDC),
                    poolBalance[2],
                    bytes("")
                ),
                IBalancerVault.FundManagement(address(this), false, payable(address(this)), false),
                amounts[0],
                block.timestamp
            );
```

**Flash loan trigger with 300k USDC**

```
amounts[0] = 300_000 * 1e6;
        uint256[] memory interestRateModes = new uint256[](2);
        interestRateModes[0] = 0;
        interestRateModes[1] = 0;
        aave.flashLoan(address(this), assets, amounts, interestRateModes, address(this), bytes(""), 0);
```

