解析1.28亿美元盗窃案:攻击者如何利用Balancer四舍五入漏洞

2025年11月,攻击者利用Balancer V2 ComposableStablePool合约中的算术精度损失漏洞,在30分钟内从六个区块链网络盗取1.2864亿美元。本文深入分析了攻击的三阶段模式、漏洞代码路径及自动化利用合约架构。

攻击者如何通过四舍五入漏洞从Balancer盗取1.28亿美元

作者: Dikla Barda, Roaman Zaikin & Oded Vanunu

2025年11月3日,Check Point Research的区块链监控系统检测到一个针对Balancer V2 ComposableStablePool合约的复杂漏洞利用。攻击者利用池不变量计算中的算术精度损失,在30分钟内从六个区块链网络盗取了1.2864亿美元。

该攻击利用了_upscaleArray函数中的一个四舍五入误差漏洞,结合精心构造的batchSwap操作,使攻击者能够人为压低BPT(Balancer池代币)价格,并通过重复的套利周期提取价值。漏洞利用主要发生在攻击者智能合约部署期间,构造函数执行了超过65次微型交换,这些操作将精度损失累积到了灾难性的程度。

引言

2025年11月3日凌晨,Check Point的区块链威胁分析系统标记了以太坊主网上涉及Balancer V2 Vault合约的异常活动。几分钟内,我们的自动检测系统识别出一个正在进行的关键漏洞利用,多个流动性池出现了大规模资金外流。

该攻击利用了Balancer的ComposableStablePool处理小额交换时的数学漏洞。当代币余额被推到特定的四舍五入边界(8-9 wei范围)时,Solidity的整数除法会导致显著的精度损失。攻击者通过执行批量交换序列来利用这一点,将这些微小的误差累积成灾难性的不变量操纵。

背景:Balancer V2架构

金库系统

Balancer V2使用一个集中的"Vault"合约(0xBA12222222228d8Ba445958a75a0704d566BF2C8),该合约持有所有池中的所有代币,将代币存储与池逻辑分离,以降低Gas成本并实现资本效率。这种共享流动性设计意味着池数学中的单个漏洞可能同时影响所有ComposableStablePool——这正是本次攻击中发生的情况。

内部余额机制

Balancer V2的内部余额系统允许用户一次性存入代币,并在多次操作中使用它们,而无需重复的ERC20转账:

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

此系统在攻击中变得至关重要。漏洞利用合约在部署期间在其内部余额中积累被盗资金,然后在后续交易中将它们提取到最终的接收地址。

漏洞:稳定池数学中的算术精度损失

根本原因

ComposableStablePool使用Curve的StableSwap不变量公式来维持相似资产之间的价格稳定。不变量D代表池的总价值,BPT价格计算为D除以totalSupply。然而,为不变量计算准备余额的缩放操作会引入四舍五入误差。

易受攻击的代码路径:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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;
}

// 简化表示 - 实际实现更复杂
function _calculateInvariant(uint256[] memory balances) private pure returns (uint256) {
    uint256[] memory scaledBalances = _upscaleArray(balances, scalingFactors);
    uint256 invariant = computeStableInvariant(scaledBalances, amplificationParameter);
    return invariant;
}

mulDown函数执行向下取整的整数除法。当余额很小(8-9 wei范围)时,这种四舍五入会产生显著的相对误差——每次操作高达10%的精度损失。

这种精度误差会传播到不变量D的计算中,导致计算值异常降低。由于BPT价格等于D除以总供应量,降低的D直接压低了BPT价格,为攻击者创造了套利机会。

单个交换产生的精度损失可以忽略不计,但在一个包含65次操作的batchSwap交易中,这些损失会急剧复合。缺乏不变量变化验证使得攻击者能够通过累积的精度误差系统地压低BPT价格,从每个池中提取数百万美元的价值。

攻击分析

三阶段模式

攻击者在单个batchSwap交易中执行了复杂的三阶段交换序列:

第1阶段:调整到四舍五入边界 将大量BPT交换为基础代币,将一个代币的余额推到关键的8-9 wei阈值,在此阈值下四舍五入误差最大化。

第2阶段:触发精度损失 执行涉及边界定位代币的小额交换。_upscaleArray函数在缩放期间向下取整,导致不变量D被低估,BPT价格被人为压低。

第3阶段:提取价值 以被压低的价格铸造或购买BPT,然后立即以全价赎回为基础资产。价格差异代表纯利润。

这个三阶段循环在同一batchSwap交易中重复了65次。所有阶段原子性地发生,防止了干预,并确保精度损失在共享的余额状态中累积,最终从每个目标池中提取数百万美元。

了解了漏洞机制后,让我们看看攻击者是如何自动化此漏洞利用的。

漏洞利用合约架构

攻击者部署了合约0x54B53503c0e2173Df29f8da735fBd45Ee8aBa30d,采用三地址操作结构:

  • 攻击者1:0x506D1f9EFe24f0d47853aDca907EB8d89AE03207(部署者)
  • 漏洞利用合约:0x54B53503c0e2173Df29f8da735fBd45Ee8aBa30d
  • 攻击者2:0xAa760D53541d8390074c61DEFeaba314675b8e3f(接收者)

基于构造函数的攻击

对交易0x6ed07db…的分析显示,盗窃发生在合约部署期间。构造函数自动执行了四舍五入误差利用,同时针对两个Balancer池。

构造函数向Balancer的协议费用收集器生成了65次代币转账——这些是在操纵期间收集的交换费用,而非被盗资金本身。转账金额显示了迭代精度利用的特征模式,从0.414 osETH递减到0.000000000000000003 osETH,因为四舍五入误差复合到了可忽略的值。

被盗价值出现在InternalBalanceChanged事件中,这些事件记录了金库内部会计系统中的余额更新。漏洞利用合约的内部余额增加了:

  • 池1(osETH/wETH-BPT):+4,623 WETH,+6,851 osETH
  • 池2(wstETH-WETH-BPT):+1,963 WETH,+4,259 wstETH
  • 合计:6,586 WETH(4,623 + 1,963)+ 6,851 osETH + 4,259 wstETH

这些内部余额的增加代表了实际被盗资金。InternalBalanceChanged事件显示,漏洞利用合约的金库内部账户被记入了被耗尽的资产。虽然基础代币物理上仍保留在金库合约中,但金库的会计系统现在将漏洞利用合约识别为这些余额的所有者,从而能够进行后续提款。

提款功能

构造函数累积了被盗资金后,函数0x8a4f75d6将它们转移给了攻击者2:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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++;
    }
}

此函数提取合约自身的内部余额。UserBalanceOp的发送者等于漏洞利用合约地址,因为该合约合法拥有在构造函数执行期间累积的资金。

交易0xd155207…确认此提款将6,586 WETH从漏洞利用合约的内部余额转移到了攻击者2地址。

两阶段攻击

第1阶段 - 盗窃(构造函数执行):

  • 交易:0x6ed07db1a9fe5c0794d44cd36081d6a6df103fab868cdd75d581e3bd23bc9742
  • 操作:部署漏洞利用合约
  • 方法:构造函数对两个池执行batchSwap操作
  • 结果:通过四舍五入漏洞盗取6300万美元,存储在合约的内部余额中
  • 证据:65次费用转账 + InternalBalanceChanged事件显示+6,586 WETH,+6,851 osETH,+4,259 wstETH

第2阶段 - 提取(函数调用):

  • 交易:0xd155207261712c35fa3d472ed1e51bfcd816e616dd4f517fa5959836f5b48569
  • 操作:调用函数0x8a4f75d6
  • 方法:将内部余额提取给攻击者2
  • 结果:资金转移到最终接收者
  • 证据:manageUserBalance,发送者 = 漏洞利用合约

结论

Balancer漏洞利用证明了DeFi协议中的数学漏洞如何通过自动化和精心调整的参数被武器化。攻击者的成功源于认识到,当通过原子交易中的数十次操作放大时,可忽略的四舍五入误差会变得可利用。

尽管进行了广泛的审计,但该漏洞仍然存在,因为传统测试侧重于单个操作的正确性,而不是对抗性批量操作的累积效应。行业必须朝着持续的安全验证、经济攻击建模和对抗性测试发展,考虑微小缺陷如何复合为灾难性的漏洞利用。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计