20251103 - Balancer 攻击事件:还是 batchSwap,还是价格操纵+精度丢失

攻击背景介绍

2025.11.03,Balancer V2 的 可组合稳定池(Composable Stable Pools)遭到了黑客攻击,攻击者首先通过 batchSwap 操作用 BPT 兑换出大量 WETH 和 osETH(大幅度移除 Pool 中的流动性),然后通过精度丢失问题减少 Pool 中 WETH 和 osETH 的余额(抬高其兑换 BPT 的比例),最后将高价的 WETH 和 osETH 兑换回 BPT 以平衡 batchSwap 的资产,获利高达 1.2 亿美元(包括其他 fork 项目)。

官方公告:

攻击交易:

相关合约:

  1. Vault:https://etherscan.io/address/0xba12222222228d8ba445958a75a0704d566bf2c8
  2. osETH/wETH-BPT(ComposableStablePool):https://etherscan.io/address/0xdacf5fa19b1f720111609043ac67a9818262850c

项目背景介绍

遭受攻击的其中一个 ComposableStablePool 是 osETH/wETH-BPT。

osETH/wETH-BPT 的相关信息如下:

  • PoolId: 0xdacf5fa19b1f720111609043ac67a9818262850c000000000000000000000635
  • token: [WETH, osETH/wETH-BPT, osETH]
  • balance:
    • WETH: 4922356564867078856521
    • osETH/wETH-BPT: 2596148429267421974637745197985291
    • osETH: 6851581236039298760900
  • ScalingFactors:
    • 1000000000000000000
    • 1000000000000000000
    • 1058109553424427048

osETH/wETH-BPT 同时也是这个 Pool 的 LP Token,因为采用了 Composable 设计,它本身也被当作池内资产,从而可以被其他池子、vault、协议当作"普通 ERC20"来使用。

在这个具体的 osETH/wETH 池子里,osETH/wETH-BPT 的实际作用

  1. 代表你在这个 osETH ↔ WETH 稳定池中的流动性份额
    你加流动性得到的 LP 就是这个 BPT。
  2. 支持"swap 即 join/exit"(代码里最核心的逻辑)
    • 用 WETH swap 成 BPT → 等价于单边加入流动性(joinSwap)
    • 用 BPT swap 成 WETH → 等价于单边退出流动性(exitSwap)

Trace 分析

黑客在 TX1 中攻击了两个 Pool,攻击手法相同,所以这里选择第一个 osETH/wETH-BPT 进行分析。

Phase1

攻击者通过 batchSwap 用 osETH/wETH-BPT 多次换出 WETH 和 osETH 代币,大幅降低 Pool 中的流动性。

在 batchSwap 中,用户可以在先不提供 Token 的情况下执行批量操作,在批量操作结束后统一结算所需要的代币。

每笔兑换采用了 GIVEN_OUT 的形式,的数量如下表所示。每次兑换的 amountOut 都约为 balance 的 99%,直到这两种代币在 Pool 中的余额为 67000 。

为什么要进行多次 swap 操作

黑客为什么要进行多次 swap 操作,而不是一次性把所需要的代币数量都 swap 出来?

编写了一个测试脚本尝试了一下,其中 amount = 4922356564867078789521,是黑客所有 swap 操作的 amountOut 总和。测试函数在执行过程中会发生回滚,错误代码为"BAL#001",是属于 sub 函数的下溢出报错。

23717397 发生的攻击事件要在 23717396 进行 fork 测试

为什么采用 GIVEN_OUT 模式会出现 balance under flow

因为在 _calcBptInGivenExactTokensOut() 函数中需要计算 swap fee,由于是 GIVEN_OUT 模式,所以 swap fee 需要从 amountIn 中收取。StableMath 采取的方式是把 fee 的份额加在 amountOut 的数量上,然后再转换成 amountIn,这样收取的 amountIn 就包含了 fee 的比例了。

BaseGeneralPool.onSwap → ComposableStablePool._swapGivenOut → BaseGeneralPool._swapGivenOut → _swapWithBpt → _doExitSwap → _exitSwapExactTokenOutForBptIn → StableMath._calcBptInGivenExactTokensOut

问题就出在如果用户一下子把池子中绝大部分的代币(比如 99.99% )都兑换出来的话,计算得到的 amountOutWithFee 就会大于池子的余额 balances[i],导致在计算 newBalances[i] 发生 under flow 问题。

所以攻击者只能够每次兑换大约 99% 的代币余额,就是为了留有空间给 fee 的计算。最终只能通过多次兑换来不断减少 balance 的值,直到 6700。

Phase2

此时 Pool 中的流动性已经被大量移除,黑客在这个基础上利用精度丢失问题操控了 WETH 和 osETH 兑换 osETH/wETH-BPT 的比例。

在 Phase2 中反复进行以下操作:

  1. Swap WETH to osETH,使得 osETH 的余额为 18
  2. Swap WETH to osETH,amountOut 为 17 osETH,剩余 1 osETH 在 Pool 中
  3. Swap osETH to WETH,将部分 osETH 兑换成 WETH

其中一组 onSwap 操作

对应的 Balance 变化

其中较为关键的是 1,2 步的操作,其目的就是利用精度丢失抬高 osETH 的价格,进一步减少 invariant 的值。

invariant = WETH.balance + osETH.balance

为什么兑换出的值需要是 17 呢?

进入到 _swapGivenOut() 函数中时,会通过 _upscale 对 osETH 的值进行缩放(因为他是 rebasing token),计算其对应的 ETH 数量。

由于 osETH 的 ScalingFactor 为 1058109553424427048,当 amountOut 为 17 时能够计算得到最大的精度丢失值。

17 * 1058109553424427048 / 1e18 = 17.98786240821526 = 17 (lost of 0.98)

也就是兑换 amountOut 为价值 17.98 的 osETH,只需要按照 17 的数量来提供 WETH。每次进行这个操作都会多获取到池子中的 0.98 个 osETH,使得 invariant 的值不断缩小。

经过多次兑换后 Invariants 从 6700 + 6700 = 13400,缩小到了 889 + 1472 = 2361

Phase3

在经过 Phase2 对 Pool 代币余额的操纵后,osETH/wETH-BPT 代币的数量不变, WETH 和 osETH 的数量大幅减少。这导致了同样的 WETH 和 osETH 数量能够兑换出更多的 osETH/wETH-BPT 代币。

黑客在 Phase3 中用高价的 WETH 和 osETH 逐步兑换出 osETH/wETH-BPT,恢复在 Phase1 中被移除流动性。同时保留了剩余的 WETH 和 osETH 代币,完成获利。

  • Phase1 前的 osETH/wETH-BPT balanceBefore:2596148429267421974637745197985291
  • Phase3 后的 osETH/wETH-BPT balanceAfter:2596148429267377819971389412573662

(balanceAfter - balanceBefore) / 1e18 = 44.15466635578541

略微多于原来的余额。

获利情况

最终获利情况:

  • 4623.601508853283067843 WETH
  • 44.154666355785411629 osETH/wETH-BPT
  • 6851.122954235076557965 osETH

黑客用同样的手法清空了 wstETH-WETH-BPT 池,获得了 4259 wstETH,20 wstETH-WETH-BPT 和 1963 WETH 。

后记

2025 年的 Balancer 攻击事件分析也是做出来了。本来在做完 23 年的分析后也有点疲惫,再者已经有很多安全厂商已经给出了分析报告,所以有点想放弃写这篇分析了。但是后面收到了一些鼓励希望我能跟上分析,再加上看别人分析的报告还是有些地方解释得不清楚心里痒痒的,还是挤出点精力和事件完成了这篇分析。现在已经是深夜,可能这个时间点发出来也错过了刷到我这篇文章的时间窗口了,但是哥们就是要现在发,发完就去睡觉!

相关推荐
阿菜ACai3 天前
20230827 - Balancer 攻击事件:价格操纵 + 精度丢失的经典组合拳
漏洞分析
阿菜ACai2 个月前
20250918 - NGP Token 攻击事件:价格维持机制为攻击者做了嫁衣
漏洞分析
阿菜ACai2 个月前
20250917 - WETToken 攻击事件:价格操控产生的套利空间
漏洞分析
阿菜ACai2 个月前
Morpheus 审计报告分享3:StETH 的精度丢失转账机制
漏洞分析
阿菜ACai2 个月前
Morpheus 审计报告分享2:ChianLink 数据源有着不同的“心跳”
漏洞分析
阿菜ACai2 个月前
Morpheus 审计报告分享:AAVE 项目 Pool 合约地址更新导致的组合性风险
漏洞分析
墨染 殇雪2 个月前
文件上传漏洞基础及挖掘流程
网络安全·漏洞分析·漏洞挖掘·安全机制
阿菜ACai4 个月前
20250730 - AnyswapV4Router 授权漏洞: 绕过了不存在的 permit 函数
漏洞分析
阿菜ACai4 个月前
20250709 - GMX V1 攻击事件: 重入漏洞导致的总体仓位价值操纵
漏洞分析·defi