In-depth Analysis of the Balancer V2 Attack Event

CN
16 hours ago

Written by: BlockSec

On November 3, 2025, Balancer V2's Composable Stable Pools and several cross-chain fork projects were attacked, resulting in total losses exceeding $125 million. BlockSec issued a warning immediately [1] and subsequently released a preliminary analysis report [2]. This was a highly complex attack. Our investigation revealed that the root cause was price manipulation triggered by precision loss in the invariant calculations, which distorted the price computation of the BPT (Balancer Pool Token). This invariant manipulation allowed the attacker to profit from a single batch swap from specific stable pools.

Although some researchers provided valuable analyses, there were also some misleading interpretations, and the details regarding the root cause and the attack process have not yet been fully clarified. This report aims to provide a comprehensive and accurate technical analysis of the incident.

Key Points (TL;DR)

  • Root Cause: Inconsistent Rounding and Precision Loss

  • The upscaling operation uses one-way rounding (rounding down), while the downscaling operation uses two-way rounding (rounding up and down).

  • This inconsistency causes precision loss, which, when exploited by the attacker through a carefully designed swap path, violates the standard principle that "rounding should always favor the protocol."

  • Attack Execution Process

  • The attacker meticulously constructed parameters (including iteration counts and input values) to maximize the impact of precision loss.

  • The attacker employed a two-phase attack strategy to evade detection: the first phase executed the core attack in a single transaction without immediate profit, while the second phase extracted assets for profit through independent transactions.

  • Protocol Inability to Pause, Leading to Escalated Attack Impact

  • Due to certain limitations, the protocol could not pause operations [3]. This inability to halt operations exacerbated the attack's impact and facilitated multiple subsequent and imitation attacks.

In the following sections, we will first introduce key background information on Balancer V2, followed by an in-depth technical analysis of the identified issues and related attacks.

0x1 Background Knowledge

1.1 Balancer V2's Composable Stable Pool

The component affected by this attack is the Composable Stable Pool [4] of the Balancer V2 protocol. These pools are designed to maintain a near 1:1 price parity (or trade at known exchange rates) for assets, allowing for large swaps with minimal price impact, thereby significantly enhancing capital efficiency between similar or highly correlated assets. Each pool has its corresponding Balancer Pool Token (BPT), which represents the liquidity provider's share in the pool and is tied to the corresponding underlying assets within the pool.

  • The pool uses Stable Math (based on Curve's StableSwap model), where the invariant D represents the virtual total value of the pool.

  • The price of BPT can be approximately represented as

The above formula indicates that if the value of D is reduced in calculation (even if no actual loss of funds occurs), the price of BPT will also be cheaper.

1.2 batchSwap() and onSwap()

Balancer V2 provides the batchSwap() function, allowing users to perform multi-hop swaps within the Vault [5].

This function determines two types of swaps based on the input parameters:

  • GIVEN_IN ( "Given Input"): The caller specifies the exact amount of input tokens, and the pool calculates the corresponding amount of output tokens.

  • GIVEN_OUT ( "Given Output"): The caller specifies the desired amount of output tokens, and the pool calculates the required amount of input tokens.

Typically, a single batchSwap() operation consists of multiple swaps between tokens, executed through the onSwap() function. The following describes the execution path when the SwapRequest is specified as GIVEN_OUT type (note: ComposableStablePool inherits from BaseGeneralPool):

The following shows the calculation process of amount_in in a GIVEN_OUT type swap, which involves the invariant D.

1.3 Scaling and Rounding

To unify the calculation precision between different token balances, Balancer performs the following two steps:

  • Upscaling: Before performing calculations, the token balances and amounts are scaled up to a unified internal precision.

  • Downscaling: After the calculations are completed, the results are converted back to their original precision, applying directional rounding (for example, to prevent the pool from undercharging fees, input amounts are typically rounded up, while output amounts are usually rounded down).

Clearly, upscaling and downscaling are theoretically a pair of corresponding operations—corresponding to multiplication (mul) and division (div), respectively. However, in specific implementations, there are inconsistencies between these two operations. Specifically, downscaling has two directions or variants: divUp and divDown; while upscaling has only one direction, which is mulDown.

The reason for this inconsistency is not clear. According to comments in the _upscale() function, the developers believe that the impact of one-way rounding can be negligible.

// Upscale rounding wouldn't necessarily always go in the same direction: in a swap for example the balance of

// token in should be rounded up, and that of token out rounded down. This is the only place where we round in

// the same direction for all amounts, as the impact of this rounding is expected to be minimal (and there's no

// rounding error unless _scalingFactor() is overridden).

0x2 Vulnerability Analysis

The fundamental issue arises from the rounding-down operation executed during the upscaling process in the BaseGeneralPool._swapGivenOut() function. Specifically, _swapGivenOut() incorrectly rounded down the swapRequest.amount by calling the _upscale() function. Subsequently, this rounded value was used as amountOut when calculating amountIn in the _onSwapGivenOut() function. This behavior effectively violates the general security practice that rounding direction should always benefit the protocol.

As a result, for a given pool (e.g., wstETH/rETH/cbETH), the calculated amountIn underestimated the actual required input amount. This allowed users to exchange a lesser amount of one underlying asset (e.g., wstETH) for another asset (e.g., cbETH), leading to a decrease in the invariant D due to effective liquidity reduction. Consequently, the corresponding BPT (wstETH/rETH/cbETH) price was also underestimated (deflationary), as BPT price = D / totalSupply.

0x3 Attack Analysis

The attacker implemented a carefully designed two-phase attack, likely aimed at reducing the risk of detection:

  • Phase One: Execute the core attack in a single transaction without immediate profit.

  • Phase Two: Extract assets for profit through separate transactions.

The first phase can be further divided into two sub-phases: parameter calculation and batch swap. Below, we will illustrate these two sub-phases through an example of an attack transaction on Arbitrum.

3.1 Parameter Calculation

In this sub-phase, the attacker combined off-chain calculations with on-chain simulations to precisely adjust the parameters for each hop in the next sub-phase (batch swap phase) based on the current state of the Composable Stable Pool (including scaling factors, upscaling coefficients, BPT exchange rates, swap fees, etc.). Interestingly, the attacker also deployed a helper contract to assist with these calculations, possibly to reduce the risk of front-running attacks.

At the beginning, the attacker first collected basic information about the target pool, including the scaling factors for each token, upscaling parameters, BPT exchange rates, and fee percentages. They then calculated a key value called trickAmt, which is the target amount of token manipulation used to trigger precision loss.

Let the scaling factor of the target token be denoted as sF, calculated as follows:

To determine the parameters used in step 2 of the next sub-phase (batch swap), the attacker initiated a series of subsequent simulation calls to the auxiliary contract's 0x524c9e20 function, with the call data (calldata) as follows:

uint256[] balances; // Balances of pool tokens (excluding BPT)

uint256[] scalingFactors; // Scaling factors for each pool token

uint tokenIn; // Index of the input token for this hop's simulation

uint tokenOut; // Index of the output token for this hop's simulation

uint256 amountOut; // Desired output token amount

uint256 amp; // Amplification parameter of the pool

uint256 fee; // Pool swap fee percentage

The returned data is as follows:

uint256[] balances; // Pool token balances (excluding BPT) after the swap

Specifically, the initial balances and iteration counts were calculated off-chain and passed as parameters to the attacker's contract (reported as 100,000,000,000 and 25, respectively). Each iteration executed three swaps:

  • Swap 1: Push the amount of the target token to trickAmt + 1, assuming the swap direction is 0 → 1.

  • Swap 2: Continue to swap out the target token using trickAmt, which will trigger rounding down in the _upscale() call.

  • Swap 3: Execute a callback operation (1 → 0), where the amount to be swapped is determined by the current token balance in the pool, truncating the two most significant decimal places, i.e., rounding down to the nearest multiple of 10{d-2}, where d is the number of decimal places of that value. For example: 324,816 → 320,000.

  • Note: Due to the use of the Newton-Raphson method in StableMath calculations, this step may sometimes fail. To address this, the attacker set up a two-retry mechanism, using 9/10 of the original value as a fallback for each retry.

The attacker's auxiliary contract is derived from Balancer V2's StableMath library, as evidenced by the custom error messages resembling "BAL" that appear in the contract.

3.2 Batch Swap

The batch swap is implemented through the batchSwap() operation, which can be divided into three steps:

  • Step 1: The attacker swaps BPT (wstETH/rETH/cbETH) for the underlying assets, precisely adjusting the balance of one token (cbETH) to the critical value at the rounding boundary (amount = 9). This creates conditions for the next step's precision loss.

  • Step 2: The attacker then swaps a carefully constructed amount (= 8) between another underlying asset (wstETH) and cbETH. Due to rounding down during the scaling of token amounts, the calculated Δx slightly decreases (e.g., from 8.918 to 8), leading to an underestimation of Δy, which in turn reduces the invariant D (based on Curve's StableSwap model). Since BPT price = D / totalSupply, the BPT price is artificially suppressed.

  • Step 3: The attacker reverses the swap of the underlying assets back to BPT, profiting from the suppressed BPT price while restoring the pool's balance.

0x4 Summary of Attacks and Losses

We have summarized the various attacks and their corresponding losses in the table below, with total losses exceeding $125 million.

0x5 Conclusion

This incident involved a series of attack transactions targeting the Balancer V2 protocol and its forked projects, resulting in significant economic losses. Following the initial attack, subsequent and imitation attacks emerged across multiple blockchains. This event provides several important insights into the design and security of DeFi protocols:

  • Rounding Behavior and Precision Loss: Using one-way rounding (rounding down) in upscaling operations, while employing two-way rounding (rounding up and down) in downscaling operations. To prevent similar vulnerabilities, protocols should adopt higher precision arithmetic operations and implement strict validation mechanisms. The standard principle that "rounding direction should always favor the protocol" must be upheld.

  • Evolution of Attacks: The attacker executed a highly complex two-phase attack to evade detection. The first phase executed the core attack in a single transaction without immediate profit; the second phase extracted assets and realized profits through separate transactions. This incident once again highlights the ongoing cat-and-mouse game between security researchers and attackers.

  • Operational Security Awareness and Threat Response: This incident emphasizes the importance of timely alerts for initialization and operational states, as well as proactive threat detection and defense mechanisms to mitigate potential losses from ongoing or imitation attacks.

While maintaining business continuity and operational stability, industry participants can use BlockSec Phalcon as the last line of defense for asset protection. The professional team at BlockSec is always ready to provide comprehensive security assessment services for your project.

Note: After we released our report, Balancer also published its official preliminary analysis report [6], confirming our analytical conclusions.

Reference

[1] https://x.com/Phalcon_xyz/status/1985262010347696312

[2] https://x.com/Phalcon_xyz/status/1985302779263643915

[3] https://x.com/Balancer/status/1985390307245244573

[4] https://docs-v2.balancer.fi/concepts/pools/composable-stable.html

[5] https://docs-v2.balancer.fi/reference/swaps/batch-swaps.html

[6] https://x.com/balancer/status/1986104426667401241

免责声明:本文章仅代表作者个人观点,不代表本平台的立场和观点。本文章仅供信息分享,不构成对任何人的任何投资建议。用户与作者之间的任何争议,与本平台无关。如网页中刊载的文章或图片涉及侵权,请提供相关的权利证明和身份证明发送邮件到support@aicoin.com,本平台相关工作人员将会进行核查。

Share To
APP

X

Telegram

Facebook

Reddit

CopyLink