This is a lower bound: only assets with reliable historical USD prices are counted, so the actual loss may be higher.
0x013be97768b702fe8eccef1a40544d5ecb3c1961ad5f87fee4d16fdc08c781060x0e511aa1a137aad267dfe3a6bfca0b856c1a3682EthereumOn Ethereum mainnet block 10355807, transaction 0x013be97768b702fe8eccef1a40544d5ecb3c1961ad5f87fee4d16fdc08c78106 drained the Balancer STA/WETH/WBTC/LINK/SNX pool at 0x0e511aa1a137aad267dfe3a6bfca0b856c1a3682. The attacker EOA 0xbf675c80540111a310b06e1482f9127ef4e7469a called helper contract 0x81d73c55458f024cdc82bbf27468a2deaa631407, sourced public flash-loan liquidity from dYdX SoloMargin, manipulated Balancer's STA accounting, repaid the loan, and forwarded the extracted assets to the EOA.
The root cause was Balancer V1 trusting nominal tokenAmountIn values for a fee-on-transfer token. STA burns 1% on every transfer, so the pool received less STA than Balancer recorded internally. Because gulp(address token) was public, the attacker could repeatedly force Balancer's stored STA balance down to the true on-chain balance and then trade against that understated balance, causing severe mispricing and over-minting of value to the attacker side.
Balancer V1 prices swaps and pool-share joins from its internal _records[token].balance state rather than reading balanceOf on every operation. That design is safe only if token transfers are conservation-preserving.
STA is not conservation-preserving. Its transfer and transferFrom logic burn 1% of the nominal amount, credit the receiver with only the post-burn remainder, and reduce total supply:
uint256 tokensToBurn = cut(value);
uint256 tokensToTransfer = value.sub(tokensToBurn);
_balances[to] = _balances[to].add(tokensToTransfer);
_totalSupply = _totalSupply.sub(tokensToBurn);
Balancer also exposes a public reconciliation primitive:
function gulp(address token) external {
require(_records[token].bound, "ERR_NOT_BOUND");
_records[token].balance = IERC20(token).balanceOf(address(this));
}
Those two behaviors are jointly dangerous when the pool binds STA and still assumes its internal accounting equals actual token balances.
The vulnerability is an accounting-integrity failure caused by combining a fee-on-transfer token with pool math that assumes exact inbound delivery. In BPool::swapExactAmountIn, Balancer computes output from the stored balances and then increments inRecord.balance by the full tokenAmountIn before pulling tokens. In BPool::joinswapExternAmountIn, it similarly mints BPT using the nominal STA input and only later performs the transfer. Because STA burns on transfer, the pool receives less than the amount Balancer credited internally. The public gulp function then lets any caller rewrite the stored STA balance to the lower real balance. Once the attacker drives the recorded STA balance low, Balancer's pricing functions treat STA as artificially scarce and allow disproportionately favorable extractions of WETH, WBTC, LINK, and SNX. This is a protocol-level exploit path, not a privileged-access issue.
The relevant Balancer write points are explicit in victim code:
// BPool::swapExactAmountIn
tokenAmountOut = calcOutGivenIn(..., tokenAmountIn, _swapFee);
inRecord.balance = badd(inRecord.balance, tokenAmountIn);
_pullUnderlying(tokenIn, msg.sender, tokenAmountIn);
// BPool::joinswapExternAmountIn
poolAmountOut = calcPoolOutGivenSingleIn(..., tokenAmountIn, _swapFee);
inRecord.balance = badd(inRecord.balance, tokenAmountIn);
_pullUnderlying(tokenIn, msg.sender, tokenAmountIn);
The invariant that should hold is:
For every bound token t, _records[t].balance must track IERC20(t).balanceOf(pool),
and pricing/join math must use the amount actually received by the pool.
STA breaks that invariant because transferFrom burns part of the nominal amount before crediting the recipient. The attacker used this mismatch in an end-to-end sequence that is visible in the seed trace. First, the helper contract borrowed WETH from dYdX SoloMargin. Next, it accumulated large STA inventory and then used repeated gulp(STA) and tiny swapExactAmountIn(STA, 1, tokenOut, ...) calls to keep forcing Balancer's stored STA balance down and trade against the corrupted state. The trace also shows a later series of joinswapExternAmountIn(STA, ...) calls, which exploit the same nominal-input assumption on the pool-share mint path.
The seed trace records the manipulated loop directly, including repeated BPool::gulp(Statera) calls and BPool::swapExactAmountIn(Statera, 1, ...) calls, followed by joinswapExternAmountIn(Statera, 2), 3, 4, 6, 9, 14, 21, 31, 47, and 70. At the end of the same trace, the helper transfers 1135684364 WBTC units, 22593091001342188353163 LINK units, and 60915305864698149602959 SNX units to the attacker EOA, then withdraws 565532624083703214355 WETH into ETH and forwards that ETH to the same EOA. The balance-diff artifact independently confirms the positive ETH delta on the attacker EOA.
The adversary flow is a single permissionless transaction:
0xbf675c80540111a310b06e1482f9127ef4e7469a calls helper contract 0x81d73c55458f024cdc82bbf27468a2deaa631407.104331302777663079074403 WETH from dYdX SoloMargin via a flash loan.gulp(STA) to collapse Balancer's stored STA balance to the lower real balance, then immediately performs more STA-for-victim-asset swaps at mispriced rates.Every step uses public contracts and public entry points. The flash loan scales the exploit, but no privileged role, private key, or protocol-owned helper is required.
The victim Balancer pool lost multiple non-STA assets in the attack transaction. The evidence-backed extracted amounts are:
WETH/ETH: "565532624083703214355" with decimal = 18WBTC: "1135684364" with decimal = 8LINK: "22593091001342188353163" with decimal = 18SNX: "60915305864698149602959" with decimal = 18The loss is not a paper valuation claim. The trace shows these assets or their ETH unwind reaching the attacker EOA after flash-loan repayment, and the balance-diff artifact confirms the native ETH gain.
0x013be97768b702fe8eccef1a40544d5ecb3c1961ad5f87fee4d16fdc08c781060x0e511aa1a137aad267dfe3a6bfca0b856c1a36820xa7de087329bfcda5639247f96140f9dabe3deed10xbf675c80540111a310b06e1482f9127ef4e7469a0x81d73c55458f024cdc82bbf27468a2deaa631407BPool.sol and BMath.solContract.sol for STA