The Black Magic of Price Manipulation: An Analysis of the Balancer V2 Invariant Calculation Vulnerability

CN
6 hours ago

Manipulating BPT Prices for Profit

Author: BlockSecTeam

Translation: Baihua Blockchain

On November 3, 2025, the Composable Stable Pools of Balancer V2 and several forked projects across multiple chains suffered a coordinated attack, resulting in total losses exceeding $125 million. BlockSec issued an alert immediately and subsequently released a preliminary analysis.

This was a highly complex attack. Our investigation revealed that the root cause was a loss of precision in the invariant calculation, leading to price manipulation that distorted the price calculation of BPT (Balancer Pool Token). This invariant manipulation allowed the attacker to profit from a single batch swap from a specific stable pool. Although some researchers provided insightful analyses, certain interpretations were misleading, and the fundamental causes and attack process have not been fully clarified. This blog aims to provide a comprehensive and accurate technical analysis of the event.

Key Points (TL;DR)

  • Root Cause: Inconsistent Rounding and Precision Loss

    • Upscaling operations used one-way rounding (downward rounding), while downscaling operations used two-way rounding (upward and downward rounding).

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

  • Attack Execution

    • The attacker meticulously designed parameters, including iteration counts and input values, to maximize the impact of precision loss.

    • The attacker used a two-phase method to evade detection: first executing the core attack in a single transaction without immediate profit, then realizing profits in another transaction by extracting assets.

  • Operational Impact and Amplification

    • Due to certain limitations, the protocol could not be paused. This inability to stop operations exacerbated the impact of the attack and led to numerous follow-up or imitation attacks.

In the following sections, we will first provide key background information about Balancer V2, then delve into the identified issues and related attacks.

0x1 Background

1. Composable Stable Pools of Balancer V2

The component affected in this attack was the Composable Stable Pools of the Balancer V2 protocol. These pools are designed to maintain a close 1:1 peg (or trade at known exchange rates) for expected assets and allow for large swaps with minimal price impact, significantly enhancing capital efficiency between similar or related assets. Each pool has its own Balancer Pool Token (BPT), representing the liquidity provider's share in the pool, along with the corresponding underlying assets.

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

The BPT price can be approximated as:

$$\text{BPT price} \approx \frac{D}{\text{totalSupply}}$$

From the above formula, it is evident that if D can be reduced on paper (even without any actual loss of funds), the BPT price will appear cheaper.

2. batchSwap() and onSwap()

Balancer V2 provides the batchSwap() function, which supports multi-hop swaps within the Vault. Depending on the parameters passed to this function, there are two types of swaps:

  • GIVEN_IN: The caller specifies the exact amount of input tokens, and the pool calculates the corresponding output amount.

  • GIVEN_OUT: The caller specifies the desired output amount, and the pool calculates the required input amount.

Typically, batchSwap() consists of multiple token swaps executed through the onSwap() function. The following outlines the execution path when a SwapRequest is assigned as a GIVEN_OUT swap type (note that ComposableStablePool inherits from BaseGeneralPool):

Vault.batchSwap()  BaseGeneralPool._swapGivenOut()    BaseGeneralPool._onSwapGivenOut()      StableMath._calcInGivenOut()

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

// inGivenOut token x for y - polynomial equation to solve// ax = amount in to calculate                                     // bx = balance token in                                                                 // x = bx + ax (finalBalanceIn)                                                                // D = invariant// A = amplification coefficient// n = number of tokens// S = sum of final balances but x                                                             // P = product of final balances but x                                                                            D                     D^(n+1)    x^2 + ( S - ----------  - D) * x -  ------------- = 0                        (A * n^n)               A * n^2n * P

3. Scaling and Rounding

To normalize calculations between different token balances, Balancer performs the following two operations:

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

  • Downscaling: The results are converted back to their native precision and directional rounding is applied (for example, input amounts are typically rounded up to ensure the pool is not undercharged, while output amounts are often rounded down).

Clearly, upscaling and downscaling are theoretically paired operations—multiplication and division, respectively. However, there is inconsistency in the implementation of these two operations. Specifically, the downscaling operation has two variants or directions: divUp and divDown. In contrast, the upscaling operation has only one direction, which is mulDown.

The reason for this inconsistency is unclear. According to comments in the _upscale() function, the developers believed that the impact of one-way rounding was negligible.

// Upscaling rounding does not necessarily always go in the same direction: for example, in a swap,

// the balance of the input token should be rounded up, while the balance of the output token should be rounded down. This is the only place where we round all amounts in the same direction,

// because the impact of this rounding is expected to be minimal

// (and unless _scalingFactor() is overridden, there is no rounding error).

0x2 Vulnerability Analysis

The fundamental issue stems from the downward rounding performed during the upscaling operation in the BaseGeneralPool._swapGivenOut() function. Specifically, _swapGivenOut() incorrectly rounds down swapRequest.amount through the _upscale() function. This rounded value is then used as amountOut, which is utilized to calculate amountIn through _onSwapGivenOut(). This behavior contradicts the standard practice that "rounding should be applied in a way that favors the protocol."

As a result, for a given pool (wstETH/rETH/cbETH), the calculated amountIn underestimated the actual required input. This allowed users to exchange a lesser amount of one underlying asset (e.g., wstETH) for another asset (e.g., cbETH), leading to a reduction in the invariant D due to effective liquidity reduction. Consequently, the corresponding BPT (wstETH/rETH/cbETH) price became artificially deflated, as $\text{BPT price} = \frac{D}{\text{totalSupply}}$.

0x3 Attack Analysis

The attacker executed a two-phase attack, possibly to minimize the risk of detection:

  • In the first phase, the core exploit was executed in a single transaction without immediate profit.

  • In the second phase, the attacker realized profits in another transaction by extracting assets.

The first phase can be further divided into two stages: parameter calculation and batch swap. Below, we use an example of an attack transaction (TX) on Arbitrum (https://app.blocksec.com/explorer/tx/arbitrum/0x7da32ebc615d0f29a24cacf9d18254bea3a2c730084c690ee40238b1d8b55773) to illustrate these stages.

Parameter Calculation Stage

In this stage, the attacker combines off-chain calculations with on-chain simulations to precisely adjust the parameters for each hop in the next stage (batch swap) based on the current state of the Composable Stable Pool (including scaling factors, amplification coefficients, BPT exchange rates, swap fees, and other parameters). Interestingly, the attacker also deployed a helper contract to assist with these calculations, possibly to reduce the risk of being front-run.

First, the attacker collects basic information about the target pool, including the scaling factor for each token, amplification parameters, BPT exchange rates, and swap fee percentages. They then calculate a key value trickAmt, which is the manipulated target token amount used to trigger precision loss.

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

$$\text{trickAmt} = \frac{10^{18} - 1}{\text{sF}}$$

To determine the parameters used in step 2 of the next stage (batch swap), the attacker makes a subsequent simulation call to the helper contract's 0x524c9e20 function with the following calldata:

uint256[] balances; // Balances of pool tokens (excluding BPT)
uint256[] scalingFactors; // Scaling factors for each pool token
uint tokenIn; // Input token index for this hop simulation
uint tokenOut; // Output token index for this hop simulation
uint256 amountOut; // Desired output token amount
uint256 amp; // Pool amplification parameter
uint256 fee; // Pool swap fee percentage

The return data is:

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

Specifically, the initial balances and the number of iterations are calculated off-chain and passed as parameters to the attacker's contract (reported as 100,000,000,000 and 25, respectively). Each iteration executes three swaps:

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

  2. Swap 2: Continue to swap out the target token with trickAmt, which triggers the downward rounding in the _upscale() call.

  3. Swap 3: Perform a reverse swap operation (1 → 0), where the amount to swap is derived by truncating the two most significant decimal digits of the current token balance in the pool, i.e., rounding down to the nearest multiple of $10^{d-2}$, where $d$ is the number of decimal digits. For example, 324,816 -> 320,000.

Note that due to the use of the Newton-Raphson method in StableMath calculations, this step may occasionally fail. To mitigate this, the attacker implemented two retry attempts, each time using 9/10 of the original value as a fallback.

The attacker's helper contract is derived from the StableMath library of Balancer V2, which can be evidenced by its inclusion of "BAL"-style custom error messages.

Batch Swap Stage

The batchSwap() operation can then be broken down into three steps:

  1. Step 1: The attacker swaps BPT (wstETH/rETH/cbETH) for underlying assets to precisely adjust the balance of one token (cbETH) to the edge of the rounding boundary (amount = 9). This creates conditions for precision loss in the next step.

  2. Step 2: The attacker then swaps a carefully designed amount (= 8) between another underlying asset (wstETH) and cbETH. Due to the downward rounding when scaling the token amounts, the calculated Δx becomes slightly smaller (from 8.918 to 8), leading to an underestimated Δy, which reduces the invariant (D, from the Curve StableSwap model). Since $\text{BPT price} = \frac{D}{\text{totalSupply}}$, the BPT price is artificially deflated.

  3. Step 3: The attacker reverses the swap of the underlying assets back to BPT, restoring balance while profiting from the deflated BPT price.

0x4: Attacks and Losses

We summarize these attacks and their corresponding losses in the table below, with total losses exceeding $125 million.

[Table showing placeholders for attack and loss data]

0x5 Conclusion

This event involved a series of attack transactions targeting the Balancer V2 protocol and its forked projects, resulting in significant financial losses. Following the initial attack, a large number of follow-up and imitation transactions were observed across multiple chains. This incident highlights several key lessons regarding the design and security of DeFi protocols:

  • Rounding Behavior and Precision Loss: The one-way rounding (downward rounding) used in upscaling operations differs from the two-way rounding (upward and downward rounding) used in downscaling operations. To prevent similar vulnerabilities, protocols should adopt higher precision arithmetic and implement robust validation checks. The standard principle that "rounding should always favor the protocol" must be adhered to.

  • Evolution of Exploit Techniques: The attacker executed a complex two-phase exploit designed to evade detection. In the first phase, the attacker executed the core exploit in a single transaction without immediate profit. In the second phase, the attacker realized profits in another transaction by extracting assets. This again highlights the ongoing "arms race" between security researchers and attackers.

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

While maintaining operations and business continuity, industry participants can leverage BlockSec Phalcon as a final line of defense to protect their assets. The BlockSec expert team is ready to conduct comprehensive security assessments for your projects.

Article link: https://www.hellobtc.com/kp/du/11/6106.html

Source: https://x.com/BlockSecTeam/status/1986057732810518640

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

Share To
APP

X

Telegram

Facebook

Reddit

CopyLink