Introduction to Balancer Protocol
Balancer v2 is a DeFi protocol and one of the largest automated market makers (AMMs) in the Ethereum ecosystem. It allows users to create and manage liquidity pools with multiple tokens and varying weights, enabling efficient trading and liquidity provision. Balancer v2 introduced several improvements over its predecessor, including enhanced gas efficiency, flexible pool configurations, and support for custom fee structures.
Balancer Core Concept
-
Governance Token
BAL: Gives holders voting rights in protocol decisions and incentive liquidity provision. - Vault Architecture: Uses Singleton design to improve efficiency and reduce gas of instantiating new proxy for each new vault.
- Multi-Token and Varying Weight Pools: Enables pools with up to 8 different tokens and customizable weights instead of a 50/50 ratio for flexible liquidity provision.
Balancer Exploit History
Balancer has been hit by three major security incidents over the past five years, revealing a familiar pattern of protocol-level edge cases being weaponized with large financial consequences, including:
- 2020 — Deflationary-token handling (≈ $500K): An attacker exploited how Balancer processed tokens with transfer-tax/deflationary mechanics by manipulating on-chain transfer amounts during swaps, causing incorrect accounting and enabling drains of roughly half a million dollars.
- 2023 — Boosted-pool accounting bug (≈ $900K): A flaw in the boosted-pools’ reward/accounting logic allowed adversaries to game boost calculations and extract approximately $900K despite earlier warnings.
- 2025 — Rounding-precision bug in ComposableStablePools (≈ $128M): The largest incident stemmed from precision loss/rounding behavior in batch swap calculations for composable stable pools, which an attacker chained into repeated profitable swaps to drain ~ $128M.
Decoding Contract Logic Flaws
-
Precision Flaw in Rounding Scaling
The issue was not a case of random rounding noise accumulating over repeated swaps.
The real vulnerability came from a directional mismatch in how amounts were
scaled inside Balancer’s
ComposableStablePool. Different code paths applied opposite rounding directions when converting
values between scaled and unscaled domains, creating a subtle but systematic bias. When
exercised
repeatedly through
batchSwap, this bias allowed the attacker to consistently understate the required input amount and gradually weaken the pool invariant.-
ScalingHelpers Contract
The vulnerability originated from contradicting scaling directions in
core helpers.
The
_upscalepath usedmulUpto round values upward,_downscaleDownrelied ondivDownto round downward, and_downscaleUpuseddivUpto round upward again. Because these were applied inconsistently across swap paths, amounts would drift in a predictable direction every time the attacker cycled between upscaling and downscaling. This directional drift — not accidental rounding noise — is what mathematically enabled the exploit.
-
_upscale&_upscaleArrayfunctions: These helper variants apply the scaling factor using a multiplication helper that performs a round upmulUpin certain swap paths (notably the EXACT_OUT path). When the scaling factor is non-integer, rounding up pushes token amounts away from the true fractional value, introducing a systematic bias when used repeatedly. Attackers used tiny, repeated micro-swaps to push balances to rounding boundaries and exploit that bias. -
_downscaleDown&_downscaleDownArrayfunctions: These functions rescale using division with a with round down helper usingdivDown. When paired with the upscale semantics above, the net effect across a swap sequence is asymmetric: one direction systematically loses precision relative to the other, allowing the invariant (D) and derived BPT price to be underestimated after repeated operations. -
_downscaleUp&_downscaleUpArrayfunctions: These variants use a round up usingdivUpin other code paths. The coexistence of bothdivUpanddivDownin different swap/settlement paths is what created the directional rounding bias — not a single mistaken operator. Properly symmetric scaling (same rounding direction for complementary operations) would have prevented the exploitable drift.
-
- BaseGeneralPool Contract This contract implements ScalingHelpers for normalizing token amounts during swaps providing general specialization settings hook for all derived contracts.
-
ComposableStablePool Contract
- Implements the specific logic for composable stable pools by inheriting BaseGeneralPool contract
-
Adds preminted
BPTby overriding BaseGeneralPoolonSwapfunction in_swapGivenOut. -
_swapGivenOutchecks if the swap involvesBPTthen it executes_swapWithBptto treat the swap as a single swap which computes theamountOutusing theswapFeePercentageto charge the protocol fees.
-
Swaps Contract.
The exploit hinged on how Balancer’s
batchSwapmechanism interacts with ComposableStablePools. In Balancer v2, swaps are not executed token-to-token in isolation; instead,Vault.batchSwapdefers all balance settlement until the end of the multi-step swap. This design temporarily allows a contract to “borrow” intermediate balances inside the same transaction. When combined with the contradicting scaling directions inScalingHelperseach micro-swap slightly understated the true token input required. Because the vault only settles deltas at the end, the attacker chained dozens of these under-priced swaps in one atomic call — compounding the imbalance and pushing the invariant into an artificially favorable state.
-
ScalingHelpers Contract
The vulnerability originated from contradicting scaling directions in
core helpers.
The
-
Improper Access Control
Another crucial vulnerability is the validation loophole that allows user to manage their
balances and handles transfers and withdrawals.
-
manageUserBalancefunction This function processes user balance operations depending on their specified kind, allowing a user or an approved relayer to_depositToInternalBalance,_withdrawFromInternalBalance,_transferInternalBalance, and_transferToExternalBalance. It relies on a validation step to determine whether the caller is authorized to perform the requested action. -
_validateUserBalanceOpfunction This function compares the caller (msg.sender) to a user-suppliedop.senderto decide whether the caller is acting as the user or as an approved relayer. The flaw was that the protocol trusted op.sender as provided by the caller without independently verifying it. The attacker simply setop.sender = msg.sender, preventing the relayer-approval branch from running entirely. As a result, the malicious contract was treated as the legitimate owner of the internal balance, enabling unauthorized_withdrawFromInternalBalancecalls to drain other users’ funds.
-
Hack Analysis
In preparation for the exploit, the attacker gathered pool balances, and scaling factors, protocol fees,
and BPT rate then repeatedly simulated swaps tailored to
the token decimals via a malicious deployed contract that included console.log statements which strongly suspected that parts
of the exploit were vibe-coded — written or generated quickly without the usual production cleanup.
These console.log calls
are common during development for debugging but are almost never left in deployed contracts because they
consume unnecessary gas. Their presence suggests the
attacker may have relied on an LLM-generated template or hastily assembled the exploit code with minimal
revision, copy-pasting directly from an AI output or
test script into mainnet deployment.
-
Attacker EOA labeled Balancer Exploiter 1
0x506D1f9EFe24f0d47853aDca907EB8d89AE03207deployed 2 malicious contracts:
-
Malicious Contract 1: 0x54b53503c0e2173df29f8da735fbd45ee8aba30d -
Malicious Contract 2: 0x679b362b9f38be63fbd4a499413141a997eb381e
-
-
Used
Malicious Contract 1to wrapETHonOsETH/WETH-BPTpool.
-
Wrapped
ETHtoWETHvia Balancer Vault Contract -
Deposited
WETHintoOsETH/WETH-BPTpool via ComposableStablePool Contract - Performed this transaction to get initial pool status of balance, amplified fees, scaling factors, and BPT rate.
-
Wrapped
-
Simulated repetitive swaps from
Malicious Contract 1that previously wrapped ETH utilizingMalicious Contract 2address via helper function with selector0x524c9e20passing various balances’ arrays, amplified, scaling factors, token indices, fee, and anticipatedamountOutto expose the precise rounding flaw that helped attacker perform real exploit. -
Executed the first exploit from
Malicious Contract 1bybatchSwapinternal underlying balance of wrapped ETH inOsETH/WETH-BPTpool to swapBPTforOsETHutilizing the crafted rounding flaw because of the asymmetry upscale and downscale conflicting rounding directions to input token amount smaller than required which consequently reduced the pool variants.
-
Last batch swap on Ethereum was
wstETH-WETH-BPTpool by lastbatchSwapof internal underlying balance of wrapped ETH.
-
Last exploit phase was clean withdrawal of all accumulated balances from
Malicious Contract 1usingmanageUserBalancefunction to withdraw internal balances to EOA labeledBalancer Exploiter 2.- Withdrew 6,587.440315017497938362 WETH.
- Withdrew 44.154666355785411629 Balancer osETH/wETH.
- Withdrew 6,851.122954235076557965 Staked ETH osETH.
- Withdrew 4,259.843451780587743322 Wrapped Liquid ETH wstETH.
-
Withdrew 20.413668455251157822 Balancer wstETH-WETH-BPT.
-
Improper access control, missing pause protection and centralized pool operations amplified
the exploit
-
Improper access flaw allowed the attacker to withdraw users’ funds from their accounts
using
_withdrawFromInternalBalancefunction bypassing the access check of whether the caller is either the contact caller or relayer approved by the owner_validateUserBalanceOp. The exploit extended beyond just manipulating contradicting rounding directions to withdrawing other users’ funds from their internal balances.function manageUserBalance( UserBalanceOp[] memory ops ) external { // rest of the function code... ( kind, asset, amount, sender, recipient, checkedCallerIsRelayer ) = _validateUserBalanceOp( ops[i], checkedCallerIsRelayer ); // rest of the function code... }function _validateUserBalanceOp( UserBalanceOp memory op, bool checkedCallerIsRelayer ) private view returns ( UserBalanceOpKind, IAsset, uint256, address, address payable, bool ) { address sender = op.sender; if (sender != msg.sender) { if (!checkedCallerIsRelayer) { _authenticateCaller(); checkedCallerIsRelayer = true; } _require( _hasApprovedRelayer(sender, msg.sender), Errors.USER_DOESNT_ALLOW_RELAYER ); } return ( op.kind, op.asset, op.amount, sender, op.recipient, checkedCallerIsRelayer ); } -
Missing pause protection on other chains like Arbitrum, Polygon, Base, Sonic, and
Opimism meant there was no emergency mechanism to halt operations during the exploit
window,
allowing the attacker to continue exploiting the vulnerability without interruption.
function manageUserBalance(UserBalanceOp[] memory ops) external { bool checkedNotPaused = false; // rest of the function code... if (!checkedNotPaused) { _ensureNotPaused(); checkedNotPaused = true; } // rest of the function code... } -
The Balancer protocol's centralized pool operations, which aggregate all pool tokens
within a single Vault contract, significantly
amplified the impact of the November 2025 exploit by allowing a single vulnerability in
the ComposableStablePools' arithmetic logic
to affect every pool simultaneously across multiple blockchain networks. This design,
intended to reduce gas costs and enable capital
efficiency, created a single point of failure where a subtle rounding error in the
_upscaleArrayfunction could be weaponized to systematically deflate pool invariants and drain over $128 million in less than half an hour.- The centralized Vault Contract holds all tokens across all pools, meaning a flaw in the core math logic of the ComposableStablePools could ripple across the entire protocol.
- The exploit leveraged the protocol’s Internal Balance system, which allowed the attacker to accumulate stolen funds within the exploit contract’s internal balance during the constructor phase before withdrawing them externally, delaying detection.
- Because the Vault is shared across all pools, the precision loss vulnerability—triggered when balances were driven to the 8–9 wei range—affected every ComposableStablePool, enabling the attacker to execute the same exploit sequence across Ethereum, Arbitrum, Base, Polygon, Optimism, and Sonic.
- The interconnectedness of the pools meant that a single vulnerability in the invariant calculation could be amplified through batched swap operations, allowing the attacker to repeatedly manipulate BPT pricing and extract value at artificially low rates.
-
Improper access flaw allowed the attacker to withdraw users’ funds from their accounts
using
Affected Assets
- WETH (Wrapped Ether): 6,587 WETH (~$23M)
- osETH (StakeWise’s Staked ETH): 6,851 osETH (~$24M)
- wstETH (Wrapped Staked ETH): 4,260 wstETH (~$15M)
- frxETH (Frax ETH): ~$10M
- rsETH and rETH (Staked ETH Variants): ~$8M
Root Cause Summary
- Contradicting scaling directions: created a deterministic rounding bias between upscaling and downscaling operations, allowing pool balances to drift in the attacker’s favor during repeated swaps.
- ComposableStablePool: inherited this asymmetric scaling logic, causing invariant (D) calculations and BPT pricing to be understated when certain swap paths were combined.
- Batch swap: allowed multiple biased calculations in a single transaction, enabling amplification of the rounding drift before the Vault updated balances.
- Internal Balance: allowed the attacker to accumulate manipulated gains without immediate external transfers, delaying detection and enabling controlled multi-pool exploitation.
-
Validation flaw: in
manageUserBalanceenabled the attacker to setop.sender = msg.senderbypassing relayer approval and authorizing unauthorized withdrawals. - Protocol-wide centralization: through the Vault meant a single arithmetic flaw affected every ComposableStablePool across Ethereum, Arbitrum, Polygon, Optimism, Base, and Sonic.
- Lack of pause protection: on several networks prevented Balancer from halting the exploit cascade once the vulnerability was discovered.
Security Takeaways
- 🧩 Symmetry in arithmetic is mandatory: scaling, rounding, and invariant math must preserve equal treatment in both directions; mismatched rounding paths create predictable, exploitable drift.
- 🧩 Invariant-critical math must be formally verified: stable-pool logic should undergo symbolic and adversarial testing to detect systematic bias under extreme values.
- 🧩 Batch operations require strict safeguards: deferred settlement amplifies precision flaws; protocols should cap looped operations or enforce consistent rounding rules across all batch paths.
- 🧩 Never trust user-provided identities: validation functions must not rely on caller-supplied fields like op.sender without independent verification.
- 🧩 Internal accounting must enforce ownership checks: internal balance systems need strict isolation and cannot assume the caller is the owner simply because values match.
- 🧩 Centralized vault models increase blast radius: consolidating all pool assets in a single contract magnifies the impact of any arithmetic bug; isolation boundaries or per-pool math guards reduce systemic failure.
- 🧩 Multi-chain deployments require synchronized pausing mechanisms: a vulnerability on one network becomes a cross-chain liability if contracts on other chains cannot be paused simultaneously.
Post-Hack Mitigation
Balancer immediately initiated its post-incident recovery procedures, publishing an on-chain message
offering a one-time 20% white-hat bounty to the attacker in exchange for the full return of stolen
assets to a designated recovery address. The message emphasized that active blockchain forensics were
underway and that the team was cooperating with law-enforcement agencies. The bounty window was limited
to 48 hours, signaling urgency and encouraging voluntary restitution.
Forensics teams also highlighted that the attacker funded the exploit transaction using 100 ETH withdrawn from Tornado Cash, with no corresponding recent deposits of that size. This pattern suggests that the attacker likely relied on proceeds from earlier hacks rather than newly sourced funds, indicating prior operational maturity and possibly a history of similar exploits.
StakeWise DAO activated its emergency multisig shortly after the attack and executed a coordinated set of on-chain actions to reclaim stolen assets. Through these efforts, the DAO successfully recovered 5,041 osETH (≈ $19M) and 13,495 osGNO (≈ $1.7M) directly from the exploiter’s address.
The full account of how ~5,041 osETH and 13,495 osGNO were recovered from the Balancer V2 exploiter. https://t.co/zlkjU4y87f
— StakeWise (@stakewise_io) November 4, 2025
On Ethereum mainnet, the reclaimed osETH accounts for roughly 73.5% of the 6,851 osETH originally drained from the Balancer pool. The remainder had already been swapped into ETH before recovery actions were initiated. All stolen osGNO, however, were retrieved in full. StakeWise stated that the returned assets will be redistributed to users affected by the exploit, allocated pro-rata according to their balances at the moment of the attack. The DAO also announced that a complete post-mortem and a detailed plan for next steps will be released once the recovery and accounting processes are finalized.
Incident Report Table
| Severity | Likelihood | Status | Impact | Vulnerability | Description |
|---|---|---|---|---|---|
| Critical | High | Exploited | High | Contradicting Scaling DirectionsmulUpdivDowndivUp |
Inconsistent rounding logic between upscale and downscale paths created a deterministic precision drift. Attacker looped batch swaps to understate required input amounts and collapse pool invariants across all ComposableStablePools. |
| Critical | High | Exploited | High | Asymmetric Batch Swap Settlement | Balancer’s deferred settlement in batchSwap allowed
multiple biased calculations in a single transaction. Precision errors compounded before
balances were committed, enabling systemic extraction of value across multiple pools. |
| Critical | High | Exploited | High | Improper Access ControlmanageUserBalance()_validateUserBalanceOp() |
Protocol trusted user-supplied op.sender without
verifying true ownership. Attacker set op.sender = msg.sender, bypassed relayer checks, and
executed unauthorized withdrawals via _withdrawFromInternalBalance. |
| High | High | Design Risk | High | Centralized Vault Architecture | A single Vault holding all assets meant one arithmetic flaw in ComposableStablePools propagated across Ethereum, Arbitrum, Base, Polygon, Optimism, and Sonic. Increased blast radius and amplified systemic damage. |
| High | Medium | Contributed | Medium | Missing Pause Protection Across Chains | No synchronized pause system on secondary networks allowed attacker to continue draining pools after the vulnerability was identified. Exploit continued uninterrupted across chains. |
| High | Low | Exploited | Medium | Internal Balance Abuse | Internal Balance allowed accumulation of manipulated amounts without immediate on-chain withdrawals. Enabled delayed detection and extended exploit execution window. |
Relevant Links
-
Attacker EOAs:
- Balancer Exploiter 1 0x506D1f9EFe24f0d47853aDca907EB8d89AE03207
- Balancer Exploiter 2 0xAa760D53541d8390074c61DEFeaba314675b8e3f
- Balancer Exploiter 3 0x872757006b6F2Fd65244C0a2A5fdd1f70A7780f4
- Balancer Exploiter 4 0x045371528A01071D6E5C934d42D641FD3cBE941c
- Balancer Exploiter 5 0xf19FD5c683a958ce9210948858B80d433F6BfaE2
- Balancer Exploiter 6 0x0e9c9473D0c504Da72763426719F6f03A15544D5
- Balancer Exploiter 7 0x424993DD317EC44db13ee94FE4d1Ea6e204E77d1
-
Attacker Exploit Contracts:
- Exploit Contract 1 0x54B53503c0e2173Df29f8da735fBd45Ee8aBa30d
- Exploit Contract 2 0x679B362B9f38BE63FbD4A499413141A997eb381e
-
Attacker transactions Hashes:
- Swap Transaction 0x6ed07db1a9fe5c0794d44cd36081d6a6df103fab868cdd75d581e3bd23bc9742
- Withdraw Transaction 0xd155207261712c35fa3d472ed1e51bfcd816e616dd4f517fa5959836f5b48569
-
Token and Protocol Addresses:
- Vault Contract 0xBA12222222228d8Ba445958a75a0704d566BF2C8
- ProtocolFeesCollector Contract 0xce88686553686DA562CE7Cea497CE749DA109f9F
osETH/wETH-BPTContract 0xDACf5Fa19b1f720111609043ac67A9818262850cwstETH/WETH-BPTContract 0x93d199263632a4EF4Bb438F1feB99e57b4b5f0BD- StakeWise Staked ETH
osETHContract 0xf1c9acdc66974dfb6decb12aa385b9cd01190e38 - Wrapped ETH
WETHContract 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 - Wrapped Staked ETH
WstETHContract 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
Reverse Engineering the Exploiter’s Contracts
The attacker deployed two cooperating contracts, and the bytecode of both reveals a complete, self-contained Balancer-math laboratory engineered specifically to test, measure, and weaponize the contradictory rounding rules inside ComposableStablePools. This was not a simple drain script — it was a purpose-built exploit environment that probes pool state through raw selectors, performs swap simulations, logs internal values with Hardhat-style console.log strings, and then executes the precision-manipulation exploit only after confirming that the invariant has drifted into a profitable range.
-
Exploit Contract 1 — The Exploit Engine
Contract 1 is the attacker’s main controller. It is responsible for gathering low-level pool information, modeling Balancer math, and
performing the final exploit. The proof is embedded directly in the bytecode through a collection of function selectors and debugging
strings that correspond exactly to the variables the attacker monitored.
-
Evidence of direct pool-state interrogation:
Contract 1 uses the following selectors to fetch the exact values it logs:
-
Current token balances
0x70a08231—>balanceOf(address)Used repeatedly inside loops that immediately precede the debug strings"mytoken i"and"mybal i". This confirms the contract pulled each token’s on-chain balance before simulating swaps. -
Internal Balancer Vault balances
0x0f5a6efa—>getInternalBalance(address user, address[] tokens)Called directly before blocks containing"Asset Deltasi"and"Done with amts1", proving the contract compared external balances with Vault internal balances to track intermediate deltas. -
BPT / pool-rate reads
0x679aefce—>getRate()These calls sit immediately before the debug strings"poolRate0"and"poolRate1", showing that the attacker was reading the BPT exchange rate and using it in invariant simulations. -
Batch swap result path
0x945bcec9—>batchSwap(...)This selector appears directly before the debug string"Asset Deltasi"showing that the contract logs the rawint256[]deltas returned by the Vault. -
Amplification and invariant reads
0x0e8e3e84—>amplification getterAppears in code regions that print"currentAmp"and"startBalancesi".0x82687a56—>invariant/swap-math viewAppears immediately before"End__Invariant".
-
Current token balances
-
Embedded debug strings (direct proof of internal math reconstruction):
Contract 1 contains literal ASCII strings hard-coded into the deployed runtime — behavior typical only of Hardhat/Foundry
console.log, never of a production exploit.-
Pool and rate tracing
"poolRate0""poolRate1"
-
Token and balance tracing
"mytoken i""mybal i""startBalancesi"
-
Amplification and invariant tracing
"currentAmp""End__Invariant"
-
Batch swap output tracing
"Doing Batch""Asset Deltasi""Done with amts1"
-
Exploit-search variables
"nonTrickIndex""trickAmt""trickRate"
-
Execution markers
"Start."
-
Pool and rate tracing
-
Custom swap-simulation and math routines:
The contract uses the above selectors to rebuild Balancer’s stable math inside the bytecode. It performs:
- full invariant recalculation
- upscale/downscale replications
- simulated EXACT_OUT swaps
- fuzzing loops around
"trickAmt"and"trickRate" - swap batching under
"Doing Batch" - invariant checks under
"End__Invariant"
-
Internal balance staging:
After simulating and confirming drift, Contract 1 uses
getInternalBalance,batchSwap, and ERC20transferFromcalls to stage manipulated deltas in Balancer’s internal accounting. This is where the debug string"Asset Deltasi"corresponds directly to returned values from thebatchSwapselector0x945bcec9. -
Vibe-coded console.log fingerprint:
The presence of all those raw debug strings
"Start.","Doing Batch","currentAmp","End__Invariant", and"trickAmt"in the final deployed bytecode proves:- the contract was vibe-coded
- likely assembled quickly with LLM/test harness assistance
- deployed without stripping development instrumentation
- used as a live invariant-testing harness on mainnet
-
Evidence of direct pool-state interrogation:
Contract 1 uses the following selectors to fetch the exact values it logs:
-
Contract 2 — The Helper Module
Contract 2 is much smaller and serves as a remote test target for Contract 1. Its purpose is not to manipulate Balancer directly, but
to offload selector tests and sanity-check behaviors during the invariant-search phase.
-
Selector-reaction testing
Contract 1 sends encoded calls to Contract 2 to:
- test argument formats
- verify return value shapes
- confirm correct indices before passing them into Balancer math
- validate “trick indexes”
"nonTrickIndex"and trial values"trickRate"and"trickAmt"
-
Mathematical cross-checking
Contract 2 returns structured values used by Contract 1’s invariant routines to compare:
- predicted deltas
- scaling factor boundaries
- manipulated indices
- intermediate values used in
"Done with amts1"blocks
The presence of these signature debug logs confirms the cross-contract feedback loop.
-
Selector-reaction testing
Contract 1 sends encoded calls to Contract 2 to:
Level Up Your Security Skills with Cyfrin Updraft
A hands-on smart contract security education platform designed to help developers, auditors, and protocol teams master adversarial thinking through challenges, tutorials, and real-world exploit case studies.
Start Learning