Balancer Exploit - Small Rounding Errors Lead to Big Losses
Overview
On November 3, 2025, the Balancer V2 protocol was exploited, resulting in a loss of approximately $128 million across multiple pools on various networks including Ethereum, Base, Avalanche, Gnosis, Berachain, Polygon, Sonic, Arbitrum, and Optimism.
The exploit targeted the Composable Stable Pool contract, which is a type of pool that allows users to trade between different assets with stable exchange rates, such as stablecoins (DAI, USDC, USDT, etc.) or correlated assets like wrapped staked ETH (wstETH), StakeWise staked ETH (osETH), and ETH (WETH).
Key Information
Exploiter Address: https://etherscan.io/address/0x506d1f9efe24f0d47853adca907eb8d89ae03207
Attack Transaction (example): https://etherscan.io/tx/0x6ed07db1a9fe5c0794d44cd36081d6a6df103fab868cdd75d581e3bd23bc9742
Fund Withdrawal Transaction: https://etherscan.io/tx/0xd155207261712c35fa3d472ed1e51bfcd816e616dd4f517fa5959836f5b48569
Understanding the Composable Stable Pool
The ComposableStablePool contract has a liquidity provider token (BPT) that represents the pool’s liquidity. This token is registered in the pool’s balances along with other underlying tokens so that users can add or remove liquidity by swapping BPT with underlying tokens just like normal tokens. These operations are called join and exit swaps.
In order to handle token transfers efficiently, instead of transferring tokens directly for each swap, the pool uses a delta calculation to track the changes in the pool’s balances. These deltas are then used to update the pool’s balances after each swap. At the end of the swap, the vault collects all deltas and performs the actual token transfers. Using this feature, the attacker can temporarily use the pool’s balances and perform a swap to make some profit and pay back to the pool without the need for explicitly doing token flashloans.
Now, let’s take a look at the attack transactions.
Exploit Analysis
Look at the transaction where the attacker withdrew a huge amount of tokens such as WETH, osETH, wstETH, etc. out of the pool. However, this transaction is not the actual attack transaction.
Take a deeper look, we can see that these tokens have been recorded as internal balances of the attacker in the vault contract. So, the real attack transaction must have been done prior to this transaction. The attack had been split into multiple transactions to avoid MEV bots front-running the attack.
And the previous transaction, that records the internal balances for the attacker in this pool is the following one:
https://etherscan.io/tx/0x6ed07db1a9fe5c0794d44cd36081d6a6df103fab868cdd75d581e3bd23bc9742
Look at this transaction, we can see a list of calls to the attacker’s contract 0x679B362B9f38BE63FbD4A499413141A997eb381e with the method 0x524c9e20. After that, the attacker performed a batch of swaps between tokens inside the pool (including the BPT token).
At the end of the batchSwap call, we can see that the internal balances of the attacker have been increased by a large amount for all tokens inside the pool.
So now, we need to figure out how the attacker made profit from these swaps. To understand the attack, let’s trace through the balances of these tokens inside the pool for every swap in the batchSwap call. Before the batchSwap call, the balance of tokens inside the pool were as follows:
As mentioned above, the pool saves the BPT share token of the pool as a normal token in its balances array. So, the attacker can swap the BPT token with other tokens inside the pool just like normal tokens.
At the beginning of the batchSwap call, we can see a series of swaps from the BPT token to the WETH and osETH tokens. This means the attacker was trying to drain the balance of both underlying tokens of the pool. This is the result at the end of these swaps:
So, we can clearly see that the attacker was trying to precisely adjust the balance of the osETH tokens, setting it to 18 and then attempting to swap out 17 wei of osETH. Why was the number 17 chosen? To understand this, we need to look at the implementation of the onSwap function.
Trace through the logic of the onSwap function, we come up with the _swapGivenOut function, this function is responsible for calculating the amount of tokens to be swapped in for a given amount of tokens to be swapped out. In this case, the attacker was trying to swap out 17 wei of osETH, so the function will calculate the amount of WETH to be swapped in.
In the _swapGivenOut function, the balances of tokens in the pool are upscaled using the scaling factors of the tokens. These numbers represent the exchange rates of the tokens in the external market.
Both swap amount and balances are upscaled using these scaling factors. After the upscaleArray function call, the balances of the tokens in the pool are updated as follows:
However, the swap amount is unchanged after the _upscale call. This might be the reason why the attacker chose the number of 17 for the swap amount. We can guess that there is some rounding error here, in the _upscale implementation.
Look at the implementation of the _upscale function, we can see that the amount is rounding down after multiplying by the scaling factor.
In this case, we have the following calculation:
17 * 1,058,132,408,689,971,699 / 10**18 = 17.98825094772952 // Rounded down to 17
// the loss is approximately 0.98825094772952 / 17 ~= 5.81%
Because the amount of token in is calculated based on the amount out provided by the attacker as follows:
swapRequest.amount = _upscale(swapRequest.amount, scalingFactors[indexOut]);
uint256 amountIn = _onSwapGivenOut(swapRequest, balances, indexIn, indexOut);
So, the pool will incorrectly calculate the required amount of token in that favors the attacker for each swap based on the following formula from the documentation of Balancer:
Solving this equation using the Newton-Raphson method, we can calculate the amount of input token needed to swap out a given amount of output token, amplification parameter (A), balances of the tokens in the pool and the invariant (D) of the pool.
Using a series of these swaps, the attacker can gradually drain all of the underlying tokens of the pool and make a profit.
Conclusion
This attack is an interesting example of how small rounding errors can lead to significant losses in DeFi protocols. The _upscale function, instead of rounding up to benefit the pool, rounds down, which favors the attacker. According to the comments in the code, this issue had been acknowledged, but the development team skipped fixing it because they underestimated the impact of the rounding error.
This incident shows that even a well-established protocol like Balancer, which has been around for a while and has undergone multiple audits, can still be exploited by small rounding errors. It highlights the importance of regular security audits and thorough testing of complex logic and formulas in DeFi protocols.













