更新于2023年9月15日:Balancer 已发布了官方事后复盘,其中详细描述了此次事件的整个过程,包括经验和教训。这篇事后复盘的文章叙述细致且精彩,引人入胜,绝对值得您花时间阅读。
从安全角度来看,此次事后复盘揭示存在两个漏洞。第一个是我们报告中讨论过的向下取整错误,第二个是报告中所述在攻击步骤 3.6 和 3.7 中发生的“供应量为零时重置费率”问题。Balancer 的报告认为第二个是更关键的问题,而第一个是辅助因素。然而,我们认为两个漏洞对于盈利性利用都同样重要:
-
第一个漏洞被用来推高代币费率,是盈利的根本原因。没有它,盈利将不可行。
-
第二个漏洞通过平衡 bb-a-tokens 的债务使漏洞利用成为可能。如果没有它,由于 bb-a-tokens 流动性差,攻击将失败(除非攻击者以某种方式获得它们)。
2023 年 8 月 22 日,Balancer 公开宣布存在影响多个增强型资金池的关键漏洞,并敦促用户立即从受影响的资金池中提取 LP。Balancer 已启动紧急缓解程序以保护大部分 TVL,但仍有部分资金面临风险。不幸的是,在 8 月 27 日,即五天后,我们注意到已发生数起攻击。此后,超过 212 万美元的资产被盗。
在撰写本报告时(距离公告发布已超过三周,我们认为此时可以安全发布),Balancer 尚未发布对该漏洞的深入分析。在本报告中,我们旨在提供一个全面的分析,主要基于一次攻击交易。
主要收获(TL;DR)
- 我们的调查表明,根本原因源于***
linear资金池中向下取整逻辑导致的币价操纵。这进而错误地影响了相应boosted资金池使用的缓存代币费率***。 - 此事件强调了及时通知从易受攻击来源分叉的项目的重要性,这对整个社区都构成了重大挑战。
- 持续不断的攻击凸显了主动威胁防御的必要性,这可以有效地帮助减轻潜在损失。
在接下来的章节中,我们将首先提供关于 Balancer 的一些基本背景信息。之后,我们将对该漏洞及相关攻击进行全面分析。最后,我们将简要总结我们迄今为止观察到的攻击及其相应的利润。
0x1 Balancer 背景
Balancer V2 [1] 是一个去中心化的自动做市商 (AMM) 协议,代表着可编程流动性的灵活构建块。与其他 AMM 中代币核算与资金池逻辑相结合的方式不同,Balancer 将代币核算和管理与资金池逻辑分开,从而可以通过减少大量代币转移来提高交易效率。
Balancer 支持多种资金池。每个资金池都关联着一个名为 BPT (即 Balancer Pool Token) 的 LP 代币。基本上,BPT 的价值是根据所有底层代币的总价值计算的。
Balancer 支持多跳交易,也称为 batch swaps,它利用注册在 Vault 中的所有资金池的最佳价格。具体来说,Vault 提供 batchSwap 函数以促进多跳交易。
Balancer 资金池中的 flash swap 消除了对执行交易所需任何输入代币的持有需求。相反,在识别出不平衡后,您可以指示 Vault 执行交易,然后获得奖励。
0x1.1 Balancer 中的各种资金池
下面,我们简要介绍一些与此漏洞相关的资金池概念。
-
Linear Pools:
Linear资金池 [2] 是 Balancer 资金池,它们以已知的汇率促进资产与其包装的、产生收益的对应资产之间的兑换。顾名思义,Linear资金池使用线性数学。一个linear资金池将持有三种代币,包括:- 两种资产,即具有相同底层代币价值的
main代币和wrapped代币; - 相应的
BPT(Balancer Pool Token)。请注意,BPT是 ERC-20 代币。
- 两种资产,即具有相同底层代币价值的
-
Nesting Linear Pools: Linear Pool 的 BPT 可以嵌套在另一个资金池中。这会在基础资产和外部资金池代币之间创建一个简单的
batchSwap路径,因为交易者可以从 BPT 交易到 Linear Pool 的底层代币之一。 -
Composable Stable Pools: Composable Stable Pools [3] 是为预期以接近平价或已知汇率持续交易的资产设计的。Composable Stable Pools 使用稳定数学,允许大额交易在遇到显著价格影响之前发生,从而大大提高了同类和相关类交易的资本效率。
当一个资金池允许与其自身 LP 代币进行交易时,它就是可组合的。将其 LP 代币放入其他资金池(或“嵌套”)可以轻松地从嵌套资金池代币实现到外部资金池代币的
batchSwap。
- Boosted Pools:
Boosted资金池 [4] 旨在提高大型资金池闲置流动性的资本效率。Boosted资金池实际上是其他资金池的一个子类。例如,一个boosted资金池可以构建在linear资金池之上。
Boosted Pools 的设计旨在通过允许用户为常用代币提供交易流动性,同时将闲置代币转发到外部协议,从而实现高资本效率。这使流动性提供者除了赚取交易费用外,还能获得 Aave 等协议的优势。
0x1.2 易受攻击的增强型资金池的具体示例:Balancer Boosted Aave USD
Balancer Boosted Aave USD(符号:bb-a-USD)是一个可组合稳定币资金池,可在三种稳定币(即 USDC、USDT 和 DAI)之间进行交易,同时将闲置流动性发送到 Aave。底层 linear 资金池是:
bb-a-USDC(包含 USDC 和已包装的 aUSDC)bb-a-USDT(包含 USDT 和已包装的 aUSDT)bb-a-DAI(包含 DAI 和已包装的 aDAI)
具体来说,bb-a-USD 是一个可组合稳定币资金池的集合,其中包含三个不同 linear 资金池的资金池代币,而每个 linear 资金池都有关联的稳定币:DAI、USDC 和 USDT。下图由官方文档 [5] 提供,展示了 bb-a-USD 的结构:

0x1.3 如何计算 BPT 的价格
一个自然出现的重要问题是,在兑换特定数量(即 amountIn)的 BPT 为另一代币的特定数量(即 amountOut)时,如何确定 BPT 的价格。
Balancer 在其采用的数学公式 [6, 7] 中提供了详细描述,具体取决于不同的资金池。为简洁起见,我们在此处进行抽象和总结最相关的概念。
以 linear 资金池为例,BPT 的价格是在 LinearPool 合约的 onSwap 函数中计算的。

计算可以总结如下:

$$amountOut=amountIn*tokenRate$$
这里的 tokenRate 是根据以下公式计算的:

$_INITIAL_BPT_SUPPLY$ 是一个常量值:$2^{112} - 1$。
在上述公式中,分子可以简化为 main 代币余额与 wrapped 代币余额之和,而分母是预定义值(即 _INITIAL_BPT_SUPPLY)与 BPT 余额之差。
值得注意的是,为了进行计算,所有涉及的代币余额都需要标准化,因为不同的代币可能有不同的精度。具体来说,给定代币的实际余额将乘以一个相应的放大因子,该因子由 _scalingFactors 函数确定。
(1) `Linear` 资金池的放大因子
BPT 和 main 代币都具有常规的、恒定的放大因子。

(2) 像 `bb-a-USD` 这样的 `Boosted` 资金池的放大因子
boosted 资金池的计算有点复杂。具体来说,返回的放大因子是原始放大因子(例如 1e18)与代币费率的乘积,该代币费率是从缓存的代币费率(如果存在)获得的。

缓存的代币费率从何而来?存在一个名为 _updateTokenRateCache 的私有函数。显然,该函数将首先通过调用该代币的 getRate 函数来检索费率,然后进行缓存。

再次以 bb-a-USDC 为例,相应的 getRate 函数的核心逻辑遵循我们之前讨论的公式。

请注意,有三种可能的路径可以触发 _updateTokenRateCache 函数:

此外,对于通过 onSwap 函数的路径,在进行更新时会进行过期检查:

0x2 漏洞分析
根本原因在于***linear 资金池的 onSwap 函数中的向下取整逻辑导致的币价操纵。这进而错误地影响了 boosted 资金池使用的缓存代币费率***。
具体来说,当调用 _downscaleDown 函数时,amountOut 会被向下取整。因此,如果 amountOut 和 scalingFactors[indexOut] 之间存在显著的数量差异,_downscaleDown 函数的返回值可能会为零。

例如,如果我们使用 bb-a-USDC(作为 BPT)在 bb-a-USDC 资金池中交易 USDC(作为 main 代币),当 amountOut 小于 1,000,000,000,000 时,返回值总是会被向下取整为零。这将增加 bb-a-USDC 的余额,因为它会被视为单向增加 bb-a-USDC 的流动性。
因此,如果 BPT 是用于交易的代币,则其费率将上升,这与计算费率的公式相符,因为分子保持不变而分母减小。此漏洞可能被利用,导致(巨大的)价格差异。
0x3 攻击分析
攻击交易包含以下攻击步骤:
- 通过 Aave 的 Flashloan 借入 300,000 USDC。
- 在 bb-a-USDC 资金池中,将 1.067753 USDC 兑换为 0.970495 aUSDC。
- 在
bb-a-USDC和bb-a-USD资金池中执行batchSwap,即以 42,203 USDC 获得 15,628bb-a-USDC、139,431bb-a-DAI和 248,868bb-a-USDT。详细步骤总结如下表(包含精度):

- 将 LP 代币兑换为相应的底层稳定币:
- 在
bb-a-DAI资金池中,139,431bb-a-DAI-> 141,127DAI - 在
bb-a-USDC资金池中,15,628bb-a-USDC-> 15,685USDC - 在
bb-a-USDT资金池中,248,868bb-a-USDT-> 253,461USDT
- 偿还闪电贷,最终利润为:
- 114,324 DAI
- 253,461 USDT
- 0.970495 aUSDC
值得注意的是,攻击者在步骤 2 中从 bb-a-USDC 资金池中耗尽了 aUSDC 和 USDC,这使得步骤 3 中的币价操纵更容易,即攻击者只需专注于 USDC 和 bb-a-USDC。
这里的步骤 3 起着关键作用。现在让我们深入研究此步骤的细节,以找出攻击者为何能够盈利。具体来说:
- 步骤 3.1 用于从
bb-a-USDC资金池中提取USDC并交换bb-a-USDC; - 步骤 3.3 和 3.4 用于将
bb-a-USDC兑换为bb-a-DAI,而步骤 3.5 用于将bb-a-USDC兑换为bb-a-USDT。 - 步骤 3.7 用于在
bb-a-USDC资金池中用USDC兑换bb-a-USDC。
这里的步骤 3.2 和 3.6 由于上述向下取整问题,没有交换任何目标代币(即
USDC),因此交易后目标代币的余额保持不变,这可以被视为向bb-a-USDC资金池增加了额外的bb-a-USDC流动性。
显然,异常交易主要发生在步骤 3.4、3.5 和 3.7。接下来,我们将逐一详细分析这些步骤。
(1) bb-a-USDC -> bb-a-DAI
在步骤 3.3 中,bb-a-USDC 和 bb-a-DAI 之间的汇率几乎为 1,而在步骤 3.4 中,汇率变为 19:
- 步骤 3.3:1,000,339,378,515,783,699 / 1,000,000,000,000,000,000 = 1.00
- 步骤 3.4:139,430,482,942,020,211,267,110 / 7,300,000,000,000,000,000,000 = 19.10
回顾我们之前讨论的代码逻辑,在步骤 3.3 中,在返回之前缓存的代币费率以计算放大因子(1,012,181,365,780,643,700)后,它会将费率更新为计算一个新值(40,240,000,000,000,000,000)。这个更新后的值随后在步骤 3.4 中用作新的放大因子。由于原始放大因子保持不变(即 1e18),这意味着新费率比旧费率高约 40 倍。

但是,这个显著的增长来自哪里?让我们回顾一下计算 tokenRate 的公式。由于在步骤 2 中耗尽了 aUSDC 的余额,tokenRate 的计算可以简化为:


这里的 nominalMainBalance 的实际值是由于步骤 3.2 中发生的向下取整。
(2) bb-a-USDC -> bb-a-USDT
步骤 3.5 使用相同的技巧来获得更多的 bb-a-USDT,bb-a-USDC 和 bb-a-USDT 之间的汇率超过 12:
- 248,868,905,733,352,246,491,156 / 20,000,000,000,000,000,000,000 = 12.44
(3) USDC -> bb-a-USDC
此外,bptBalance 在步骤 3.6 中增加,然后在步骤 3.7 中 bptSupply 变为零。通过这样做,有可能以接近 1:1 的汇率将 USDC 兑换为 bb-a-USDC。

0x4 攻击与利润总结
截至目前,我们已经观察到数十起攻击,造成超过212 万美元的损失。总而言之,这些攻击是由三个不同的账户执行的,如下所示:

Balancer 由于此漏洞总计损失了约 100 万美元。在 Balancer 首次遭受攻击不到 12 小时后,其分叉协议Beethoven X 也遭受了类似的攻击,导致估计损失约 110 万美元。Beethoven X 遭受的损失比 Balancer 更大!此次安全事件的总损失累计达到约 212 万美元。
完整的攻击交易列表已收集在我们准备的文档中。请参考该文档以获取更详细的信息。
关于攻击者的一些观察
在分析每个网络发起的交易后,我们发现Fantom 上的攻击交易链与以太坊和 Optimism 上的交易链存在显著差异。
具体来说,除了关键函数中的显著差异外,Fantom 上的攻击者利用了两种独特的技巧来避免被 MEV Bot 抢跑。此外,在 Fantom 上发动攻击的资金在攻击前 163 天就已经准备好。
从上述观察中,我们可以推断:
-
至少有两名不同的攻击者参与其中。
-
Fantom 上的攻击者是一名经验丰富的惯犯。
0x5 结论
总而言之,这是一个微妙的漏洞,其根源在于向下取整逻辑。然而,利用此漏洞并非易事。具体来说,攻击者通过利用 linear 资金池中的向下取整问题,成功地提高了缓存代币费率,从而操纵了相应 boosted 资金池中的代币价格。
此事件也强调了及时通知那些从易受攻击来源分叉的项目的 Yet. 尽管 Balancer 已经发出警报,但针对分叉协议的攻击仍在继续,这凸显了这些分叉项目需要及时了解其源项目安全更新的必要性。然而,确保这些分叉项目及时收到通知仍然是社区面临的一个持续挑战。
此外,持续不断的攻击系列也凸显了主动威胁防御的重要性,这可以有效地帮助减轻潜在损失。
参考
- [1] https://docs.balancer.fi/concepts/overview/basics.html
- [2] Linear Pools: https://docs.balancer.fi/concepts/pools/linear.html
- [3] Composable Stable Pools
- [4] Boosted Pools
- [5] https://docs.balancer.fi/concepts/pools/boosted.html#example
- [6] https://docs.balancer.fi/reference/math/linear-math.html
- [7] https://docs.balancer.fi/reference/math/stable-math.html



