How an Attacker Drained $128M from Balancer Through Rounding Error Exploitation
嗯,用户让我用中文总结一下这篇文章,控制在一百个字以内,而且不需要特定的开头。首先,我需要通读整篇文章,抓住主要内容和关键点。 文章讲的是2025年11月3日,Check Point Research监测到针对Balancer V2的攻击。攻击者利用了池不变式计算中的算术精度损失漏洞,在不到30分钟内从六个区块链网络中盗取了1.2864亿美元。攻击手法涉及批处理交换操作和构造函数中的微小交易,导致BPT价格被压低,从而进行套利。 接下来,我需要将这些信息浓缩到100字以内。重点包括:时间、攻击目标、漏洞类型、受影响的网络数量、被盗金额、攻击手法和结果。 然后,组织语言,确保简洁明了。例如:“2025年11月3日,Check Point Research监测到针对Balancer V2的攻击。攻击者利用池不变式计算中的算术精度损失漏洞,在不到30分钟内从六个区块链网络中盗取了1.2864亿美元。通过批处理交换操作和构造函数中的微小交易,攻击者成功压低BPT价格并进行套利。” 检查字数是否符合要求,并确保没有使用“文章内容总结”之类的开头。最后,确认信息准确无误。 </think> 2025年11月3日, Check Point Research监测到针对Balancer V2的攻击。攻击者利用池不变式计算中的算术精度损失漏洞,在不到30分钟内从六个区块链网络中盗取了$1.2864亿。通过批处理交换操作和构造函数中的微小交易,攻击者成功压低BPT价格并进行套利。 2025-11-5 12:5:4 Author: research.checkpoint.com(查看原文) 阅读量:38 收藏

By: Dikla Barda, Roaman Zaikin & Oded Vanunu 

On November 3, 2025, Check Point Research’s blockchain monitoring systems detected a sophisticated exploit targeting Balancer V2’s ComposableStablePool contracts. The attacker exploited arithmetic precision loss in pool invariant calculations to drain $128.64 million across six blockchain networks in under 30 minutes.

The attack leveraged a rounding error vulnerability in the _upscaleArray function that, when combined with carefully crafted batchSwap operations, allowed the attacker to artificially suppress BPT (Balancer Pool Token) prices and extract value through repeated arbitrage cycles. The exploitation occurred primarily during attacker smart contract deployment, with the constructor executing 65+ micro-swaps that compounded precision loss to devastating effect.


Introduction

In the early morning hours of November 3, 2025, Check Point’s Blockchain Threat Analysis system flagged unusual activity on the Ethereum mainnet involving Balancer’s V2 Vault contract. Within minutes, our automated detection identified a critical exploit in progress, with massive fund outflows from multiple liquidity pools.

Balancer V2

The attack exploited a mathematical vulnerability in how Balancer’s ComposableStablePools handle small-value swaps. When token balances are pushed to specific rounding boundaries (8-9 wei range), Solidity’s integer division causes significant precision loss. The attacker weaponized this by executing batched swap sequences that accumulated these tiny errors into catastrophic invariant manipulation.


Background: Balancer V2 Architecture

The Vault System

Balancer V2 uses a centralized “Vault” contract (0xBA12222222228d8Ba445958a75a0704d566BF2C8) that holds all tokens across all pools, separating token storage from pool logic to reduce gas costs and enable capital efficiency. This shared liquidity design meant a single vulnerability in pool math could affect all ComposableStablePools simultaneously—exactly what happened in this attack.

Internal Balance Mechanism

Balancer V2’s Internal Balance system allows users to deposit tokens once and use them across multiple operations without repeated ERC20 transfers:

mapping(address => mapping(IERC20 => uint256)) private _internalTokenBalance;

This system became critical to the attack. The exploit contract accumulated stolen funds in its internal balance during deployment, then withdrew them to the final recipient address in subsequent transactions.


The Vulnerability: Arithmetic Precision Loss in Stable Pool Math

The Root Cause

ComposableStablePools use Curve’s StableSwap invariant formula to maintain price stability between similar assets. The invariant D represents total pool value, and BPT price is calculated as D divided by totalSupply. However, the scaling operations that prepare balances for invariant calculations introduce rounding errors.

Vulnerable Code Path:

function _upscaleArray(uint256[] memory amounts, uint256[] memory scalingFactors) 
    private pure returns (uint256[] memory) {
    
    for (uint256 i = 0; i < amounts.length; i++) {
        amounts[i] = FixedPoint.mulDown(amounts[i], scalingFactors[i]);
    }
    return amounts;
}
// Simplified representation - actual implementation is more complex
function _calculateInvariant(uint256[] memory balances) private pure returns (uint256) {
    uint256[] memory scaledBalances = _upscaleArray(balances, scalingFactors);
    uint256 invariant = computeStableInvariant(scaledBalances, amplificationParameter);
    return invariant;
}

The mulDown function performs integer division that rounds down. When balances are small (8-9 wei range), this rounding creates significant relative errors—up to 10% precision loss per operation.

This precision error propagates to the invariant D calculation, causing abnormal reduction in the calculated value. Since BPT price equals D divided by total supply, the reduced D directly lowers BPT price, creating arbitrage opportunities for the attacker.

Individual swaps produce negligible precision loss, but within a single batchSwap transaction containing 65 operations, these losses compound dramatically. The lack of invariant change validation allowed the attacker to systematically suppress BPT price through accumulated precision errors, extracting millions in value per pool

Attack Analysis

The Three Phase Pattern

The attacker executed a sophisticated three-stage swap sequence within single `batchSwap` transactions:

Stage 1: Adjustment to Rounding Boundary

Swap large amounts of BPT for underlying tokens to push one token’s balance to the critical 8-9 wei threshold where rounding errors are maximized.

Stage 2: Trigger Precision Loss

Execute small swaps involving the boundary-positioned token. The _upscaleArray function rounds down during scaling, causing the invariant D to be underestimated and BPT price to drop artificially.

Stage 3: Extract Value

Mint or purchase BPT at the suppressed price, then immediately redeem for underlying assets at full value. The price discrepancy represents pure profit.

This three-phase cycle repeated 65 times within the same batchSwap transaction. All stages occur atomically, preventing intervention and ensuring precision losses accumulate across the shared balance state, ultimately extracting millions from each targeted pool.

Having understood the vulnerability mechanism, let’s examine how the attacker automated this exploitation.

Exploit Contract Architecture

The attacker deployed contract `0x54B53503c0e2173Df29f8da735fBd45Ee8aBa30d` with a three-address operational structure:

– Exploiter 1: 0x506D1f9EFe24f0d47853aDca907EB8d89AE03207 (deployer)

– Exploit Contract: 0x54B53503c0e2173Df29f8da735fBd45Ee8aBa30d

– Exploiter 2: 0xAa760D53541d8390074c61DEFeaba314675b8e3f (recipient)

Constructor-Based Attack

Analysis of transaction 0x6ed07db… revealed the theft occurred during contract deployment. The constructor automatically executed the rounding error exploitation, targeting two Balancer pools simultaneously.

 The constructor generated 65 token transfers to Balancer’s Protocol Fees Collector—these are swap fees collected during the manipulation, not the stolen funds themselves. The transfer amounts display the characteristic pattern of iterative precision exploitation, decreasing from 0.414 osETH down to 0.000000000000000003 osETH as the rounding errors compound to negligible values.

The stolen value appears in InternalBalanceChanged events, which record balance updates within the Vault’s internal accounting system. The exploit contract’s internal balance increased by:

Pool 1 (osETH/wETH-BPT): +4,623 WETH, +6,851 osETH
Pool 2 (wstETH-WETH-BPT): +1,963 WETH, +4,259 wstETH
Combined total: 6,586 WETH (4,623 + 1,963) + 6,851 osETH + 4,259 wstETH

These internal balance increases represent the actual stolen funds. The InternalBalanceChanged events show that the exploit contract’s Vault-internal account was credited with the drained assets. While the underlying tokens physically remained in the Vault contract, the Vault’s accounting system now recognized the exploit contract as the owner of these balances, enabling later withdrawal.

Withdrawal Function

After the constructor accumulated stolen funds, function 0x8a4f75d6 transferred them to Exploiter 2:

function 0x8a4f75d6(address[] calldata targetPools) public {
    require(msg.sender == _callTx);
    
    poolIndex = 0;
    while (poolIndex < targetPools.length) {
        poolId = targetPools[poolIndex].getPoolId();
        (tokens[],) = vault.getPoolTokens(poolId);
        internalBals[] = vault.getInternalBalance(address(this), tokens);
        
        tokenIndex = 0;
        while (tokenIndex < tokens.length) {
            operations[tokenIndex] = UserBalanceOp({
                kind: 1,
                asset: tokens[tokenIndex],
                amount: internalBals[tokenIndex],
                sender: address(this),
                recipient: 0xAa760D53541d8390074c61DEFeaba314675b8e3f
            });
            tokenIndex++;
        }
        
        vault.manageUserBalance(operations);
        poolIndex++;
    }
}

This function withdraws the contract’s own internal balance. The UserBalanceOp has sender equal to the exploit contract address because the contract legitimately owns the funds accumulated during constructor execution.

Transaction `0xd155207…` confirms this withdrawal transferred 6,586 WETH from the exploit contract’s internal balance to Exploiter 2 address.

The TwoStage Attack

Stage 1 – Theft (Constructor Execution):

TX: 0x6ed07db1a9fe5c0794d44cd36081d6a6df103fab868cdd75d581e3bd23bc9742

Action: Deploy exploit contract

Method: Constructor executes batchSwap operations against two pools

Result: $63M drained via rounding error, stored in contract’s internal balance

Evidence: 65 fee transfers + InternalBalanceChanged events showing +6,586 WETH, +6,851 osETH, +4,259 wstETH

Stage 2 – Extraction (Function Call):

TX: 0xd155207261712c35fa3d472ed1e51bfcd816e616dd4f517fa5959836f5b48569

Action: Call function 0x8a4f75d6

Method: Withdraw internal balance to Exploiter 2

Result: Funds transferred to final recipient

Evidence: manageUserBalance with sender = exploit contract

Conclusion

The Balancer exploit demonstrates how mathematical vulnerabilities in DeFi protocols can be weaponized through automation and careful parameter tuning. The attacker’s success stemmed from recognizing that negligible rounding errors become exploitable when amplified through dozens of operations in atomic transactions.

Despite extensive audits, the vulnerability persisted because traditional testing focuses on individual operation correctness, not cumulative effects of adversarial batch operations. The industry must evolve toward continuous security validation, economic attack modeling, and adversarial testing that considers how tiny flaws compound into catastrophic exploits.


文章来源: https://research.checkpoint.com/2025/how-an-attacker-drained-128m-from-balancer-through-rounding-error-exploitation/
如有侵权请联系:admin#unsafe.sh