Balancer Boosted Pools中的汇率操纵——技术事后分析

Balancer 协议中的 Boosted Pools 存在漏洞,允许攻击者操纵汇率,从而耗尽池中的资金。攻击源于 Linear Pools 的两个漏洞:四舍五入错误和不正确的初始化处理,这些漏洞与 Vault 的其他功能相结合,导致了攻击的发生。该漏洞已于 2021 年存在,但最近才被发现和利用。Balancer 正在积极努力解决漏洞并保护用户资金。

Balancer Boosted Pools 中的费率操纵 —— 技术验尸报告

概述与时间线

6 月 23 日,Balancer Labs 通过 Immunefi 收到了来自 GothicShanon89238 的报告,指出某些类型的费率提供者可能会被操纵,从而对某些 Balancer 池产生影响。幸运的是,我们能够完全缓解此漏洞,主要是通过在 7 月份发布新版本的 Composable Stable Pool。这位白帽因此次披露获得了 13 万美元的赏金。

我们正准备发布这篇关于相对较小问题的 事后分析文章... 然而 8 月 11 日,同一位白帽提交了另一份报告,同样与费率操纵有关。难道我们在之前的补丁中遗漏了什么吗?

事实并非如此。这个问题完全不同,而且不幸的是,更加根本。这次的问题不是 Stable Pool 本身,而是它的组成部分:嵌套的 Linear Pool,它们将常规的 Stable Pool 转换为资本高效的 Boosted Pool

可组合性既可能是 DeFi 最大的优势,也可能是其最大的弱点。我们最简单的池类型——通过简单的加法计算其不变量——仍然存在一个非常微妙的错误,与其他平台功能相结合,可以用来操纵组成池的费率。

尽管发现又一条通往毁灭的道路令人沮丧,但在那个星期五的晚上,这个新漏洞似乎仍然不如第一个漏洞那么严重。潜在的灾难得以避免,我们准备下班回家,并在周一处理它。

然而,概念验证不断涌现,每一次模拟的漏洞利用都比上一次更有利可图。到周六凌晨,我们清楚地意识到,在两年多的时间里,我们收到了第一份有效的关键漏洞报告。周一黎明时分,领导层拿到了我们周末紧急分析的结果:基本上所有的 Boosted Pool 都存在漏洞,占当时 Balancer 的 10 亿美元 TVL 的 20% 以上。

幸运的是,代表超过 80% TVL 的矿池可以得到缓解,剩余的 20% 大部分由密切的合作伙伴或已知的 LP(例如,DAO)持有,并且可能会在公开披露后迅速撤回。不幸的是,这并不容易。所需的具体缓解措施非常复杂,并且需要非常精确的协调。

鉴于此,一个生态系统贡献者团队仔细考虑了执行我们自己的白帽救援,并为此准备了一些合约和脚本。然而,很快就发现这是完全不可行的。

这次攻击包括数十个步骤,并且需要针对每个矿池进行非常精确的定制。矿池和网络的数量之多使得这种努力无法维持。更糟糕的是,由于这些是“组成”矿池,清空它们会破坏容器的平衡,套利机器人可能会像我们保存价值一样快地消耗掉价值。更不用说白帽攻击会直接暴露攻击向量,而且我们没有时间警告合作伙伴。

由于我们确信几乎所有处于风险中的流动性都可以通过直接缓解或在披露后迅速撤回,因此我们选择了这条道路。GothicShanon89238 因负责任的披露以及协助评估缓解计划而获得了最高的 100 万美元赏金。

8 月 22 日,紧急子 DAO 和其他受信任的各方执行了所有可能的补救措施,披露 了该漏洞,并提供了一个用于简化提款的 UI,包括处理 staking 和嵌套池。这项工作 成功 的程度超出了所有人的预期。

绝大多数面临风险的资金在 48 小时内被提取,只有一小部分最初令人恐惧的 Vault 仍然面临风险。我们一半希望在周五收到另一份报告:但红色电话没有响。周五过去了。然后是周六。面临风险的 TVL 继续下降。我们开始认为我们又躲过了一劫。

确实如此:直到 8 月 27 日星期天早上,我们才被不同的子弹击中。针对 Balancer 的首次成功利用正在进行中。

我们的监控系统首先报告了对主网 USD Boosted Pools 的攻击,我们最初假设尽管披露的信息很少,但黑帽不知何故发现了所报告的漏洞。然而,当我们检查交易时,我们发现情况并非如此。这次攻击仍然涉及费率操纵,但其机制非常不同:更通用,并且在许多方面比我们研究了数周的攻击向量更糟糕。

我们再次考虑尝试白帽救援,但出于许多相同的原因拒绝了。Balancer 迅速 披露 了该漏洞,并敦促任何剩余的 LP 退出。

虽然很遗憾地报告了 Balancer 历史上首次用户资金损失,但我们感到欣慰的是,由于合作伙伴的合作以及社区的快速反应,在漏洞利用发生时,只有一小部分大部分闲置资金仍然存在风险。

Mainnet 的总损失约为 98 万美元,Optimism 上的损失为 21.5 万美元。(在补救措施后立即首次披露该问题时,仍有超过 4000 万美元面临风险。)

上面的图表突出了相对幅度,下面的图表说明了时间因素:社区在公告发布后如何快速地提取资金。

正如这些图表清楚地表明的那样,这只是 Balancer 的一个小挫折。Boosted Pools 既不是 TVL 的最大贡献者,也不是 DAO 的主要收入来源。请参阅本报告的最后一节,了解我们计划如何利用从这次经历中吸取的教训。

背景

V2 Vault

Vault 是 Balancer 协议的核心合约。它通过一个发布 Pool ID 的注册系统来记录所有池,持有所有池 token,并处理 token 记账和转移,包括包装和解包原生 ETH。

尽管池合约支持一些常见功能,例如设置兑换费,并且可能会公开一个专门用于其特定功能的接口(例如,LBP 和 Managed Pool 允许你更改权重),但用户通常通过 Vault 访问核心池功能。

核心思想是通过允许任何任意合约成为池来支持创新,只要它满足 Vault 的最小接口即可:

  • joinPool: 将 token 存入池中
  • exitPool: 从池中提取 token
  • swap: 在池中兑换 token
  • batchSwap: 原子性地执行多个兑换

BatchSwap 在这里特别重要,因为它可以执行任何任意顺序的兑换(使用相同或不同的池),将一个兑换的输出“链接”到另一个兑换的输入。

此外,批量兑换可以按以下两种“方向”进行:

  • GivenIn: 先处理第一个兑换,最后处理最后一个兑换;数量 = 第一个兑换的 tokenIn
  • GivenOut: 以相反的顺序处理兑换;数量 = 最后一个兑换的 tokenOut

还有另外两个值得注意的功能。BatchSwap“在最后结算”,这意味着你可以使用比你实际拥有的更多的 token 进行兑换,只要你在最后偿还它们即可。我们将此功能称为 闪电兑换;它本质上是一个内部闪电贷。使用此功能,即使没有任何 token(或甚至没有任何 token 转移,当与内部余额结合使用时),也可以执行套利交易。

最后,由于 Vault 是非可重入的,并且 join、exit 和 swap 是单独的调用,因此无法在批量兑换中将 join 或 exit 与 swap 组合在一起,或者在 join 期间执行 swap。稍后会详细介绍。

对资本效率的追求

像 Balancer 这样的自动化做市商 (AMM) 系统会激励流动性提供者将其 token 存入非托管池中,以促进在分布式交易所 (DEX) 上进行交易。价格是“内部”的(仅从余额和权重得出,而不是像预言机这样的外部价格馈送),因此每个池中的 token 余额越高,单个交易的价格影响就越低,从而鼓励更高的交易量和更大的交易。

然而,大量的“闲置”token——除了保持滑点和价格影响较低之外没有任何作用——也代表着 LP 的巨大机会成本,他们放弃了通过简单地将 token 存入借贷平台可能获得的任何收益。

我们推出了 Balancer V2,其中包含一个内置的解决方案。不是通过创建我们自己的借贷平台,而是通过引入“资产管理人”作为 V2 Vault 的基本功能,并与流行的贷款机构 合作

任何池中的任何 token 都可以拥有资产管理人。这是一种可以移入和移出 Vault 的 token 的合约(例如,为了将它们存入借贷协议),而 Vault 的行为就像它拥有全部余额一样。交易者可以获得高流动性的所有优势,而 LP 可以从“托管”token 中获得收益。

当然,只有“现金”余额(实际存在于 Vault 中的 token)可以被提取,因此资产管理人必须确保 Vault 中保留足够的“主要”token,以支持预期的交易量。在实践中,这种“再平衡”被证明比预期的更加困难。此外,资产管理人可以(并且预期)处理多个池的“投资”,但与协调这件事相关的问题似乎同样棘手。

到 2021 年 12 月,我们以不同的方式在池级别解决了这个问题,即使用 Linear Pool。

Linear / Boosted Pools

借贷协议通过接受“主要”或“基础”token(例如,通常是像 DAI、USDC 和 USDT 这样的稳定币)的存款来工作,以换取平台 token(在 Aave 的情况下为“aToken”),这些 token 代表基础 token 总收益的份额。由于这些平台 token 通常是“重新定价”的,因此与 Balancer Vault(以及几乎所有其他 AMM)原生不兼容,因此必须对其进行“包装”。因此,我们将每个相关的生息 token 称为“包装”token。

将一种 token 兑换成另一种 token 的过程称为“包装”(基础 token 到生息 token)或“解包”(生息 token 返回到基础 token)。它必须在借贷平台上完成,而且往往成本较高。

我们没有让一个资产管理人合约处理多个池(和池类型)的此操作,而是定义了一种新的池类型:Linear Pool。此池包含主要 token 和包装 token。包装 token 的比例越高,收益越高,池的资本效率越高。

类似于资产管理人必须平衡他们在 Vault 中的“现金”和“托管”头寸一样,Linear Pool 必须保持足够的主要 token 余额以支持交易(因为几乎所有交易都是用主要 token 完成的),同时留下足够的包装 token 余额以获得可接受的收益,同时考虑到包装和解包成本很高。

像所有其他池一样,Linear Pool 使用“比例因子”来计算 token 的小数位数。所有外部调用都接受并返回“原生”token 小数位数的数量(例如,“10”USDC 是 10_000_000 wei,因为 USDC 有 6 个小数位数)。

比例因子的目的是确保所有计算都以相同的精度进行,而与原生 token 的小数位数无关。Vault 存储原生小数位数的余额,并且来自用户的外部数量(例如,在 userData 中编码)同样以原生小数位数表示。在执行计算之前,池必须将值“放大”到完整的 18 位小数精度,因为 Math 函数专门对 18 位小数浮点数进行运算,然后在返回结果时将它们“缩小”回原生编码。

Linear Pool 也有一个费率提供者,用于表示包装数量相对于主要或基础 token 的数量。Linear Pool 数学以与 token 小数位数相同的方式来考虑这一点:作为原始 token 余额的乘数。费率越高,包装 token 的相对价值越大(因此,正如我们将在下面看到的,Linear Pool 的 BPT 作为宿主池的组成部分的价值越大)。

Linear Pool 的核心思想是建立一个“自由贸易区”,其中心表示促进主要 token 交易和最大化收益之间的理想平衡。在该区域内的兑换是免费的。

使池“失去平衡”的兑换将收取与不平衡量成比例的费用。然而,与所有其他池不同的是,兑换费不会累积到 LP。相反,池会保留它们,等待将池带回自由区的交易。届时,费用将奖励给重新平衡者。

通过这种机制,“零售”用户可以免费交易,并且(付费)重新平衡者可以承担包装和解包的成本。

这一切听起来都很优雅,但出现了两个问题。人们究竟在兑换什么,以及为什么有人会向基本上没有兑换费的 Linear Pool 提供流动性,因此不会为 LP 累积任何价值?

我们只讨论了 Linear Pool 中的两个 token:主要 token 和包装 token。可能用户可能想要直接兑换这些 token;这可能比在借贷平台上包装或解包更便宜。但这不是最常见的用例,人们不会直接将 LP 放入 Linear Pool 中;你不会在 Balancer 界面上找到它们。实际上,还有第三个 token:Linear Pool 本身的 BPT(Balancer Pool Token)。Linear Pool 是第一种“可组合”池,因此我们接下来转向该主题。

可组合池

从口语意义上讲,所有 Balancer 池都是“可组合的”。池包含 token,但本身也是 token:BPT 指的是 ERC-20 Balancer Pool Token,它是所有池的基础合约。没有任何东西可以阻止用户在其他池中使用 BPT,或者根据自己的喜好进行深度嵌套。

但在这里,我们指的是严格技术意义上的“可组合”:“递归”池,其中包含其自身的 BPT 作为组成 token。正如我们将看到的,这种可能违反直觉的创新与其他协议功能的结合非常强大。这是 Balancer Boosted Pools 的基础。

在确定术语“可组合”之前,我们过去常常称它们为“phantom”池(例如,StablePhantomPool),因为池创建者只传入组成 token(例如,在 bb-a-USD StablePhantom 的情况下为 DAI、USDC 和 USDT),但最终的池不知何故有一个额外的 token:它自己的 BPT。

让我们首先看看这是如何完成的,然后再描述它启用的行为。

Balancer 池在其构造函数中向 Vault 注册自己,并且此时已知池地址,因此池可以简单地将自己的地址插入到池创建者在注册时提供的 token 列表中。

下一步是池初始化——最初的 join,它用 token 余额来启动池并使其正常运行。回想一下,池也可以铸造和销毁它们自己的 BPT。因此,只需稍微整理一下(例如,你需要在该 join 中提供“phantom”token 以及一些用于数量和限制的魔术值),池就会为调用者铸造大量的 BPT,然后将几乎所有 BPT 以及其他组成 token 余额拉入 Vault 中。

为什么要经历所有这些扭曲?回想一下,兑换总是涉及一个池和两个 token:tokenIn(进入 Vault,增加池余额)和 tokenOut(离开 Vault,减少池余额)。Join 意味着发送组成 token 并接收 BPT,而 Exit 意味着发送 BPT 并接收组成 token。

因此,如果 BPT 本身是组成部分,则可以像任何其他 token 一样进行兑换。兑换 BPT token 等效于单 token join,兑换 BPT token 出去是单 token exit。在可组合池中,因此可以有效地规避 Vault 的重入限制,并在单个批量兑换中将 join 和 exit 与 swap 组合在一起。

当 Linear Pool 嵌套在其他池中时(通常是 Stable Pool,如原始的 StablePhantomPool),这种方法的真正威力就显现出来了。

bb-a-USD Composable Stable Pool 有三个组成 token(加上它自己):这些是三个 Linear Pool,每个 Linear Pool 包含主要稳定币 token 和包装稳定币 token,以及它们自己的 BPT。例如,bb-a-DAI 是一个包含 DAI 和 waDAI(包装的 aDAI)的 Linear Pool。

该池中的 LP 可以获得收益(除了任何 LM 激励之外),因为很大一部分基础 token 实际上已包装并持有在 Aave 上。此外,交易者能够在单个批量兑换中交易任何基础稳定币与任何其他稳定币(甚至是 aToken 版本),如上所述,其中涉及 join 和 exit。

例如,要将 USDT 兑换为 DAI,批量兑换将是:

  1. 在 USDT Linear 中,USDT 兑换为 bb-a-USDT(join USDT Linear)
  2. 在 bb-a-USD 中,bb-a-USDT 兑换为 bb-a-DAI(在 Linear BPT 之间兑换)
  3. 在 DAI Linear 中,bb-a-DAI 兑换为 DAI(exit DAI Linear)

此外,如果 bb-a-USD 稳定池与另一个 token 配对(例如,在 Weighted Pool 中),那么该另一个 token 也将立即拥有所有稳定币的流动性。

这曾经是一件美好的事情。

Vault 安全功能

暂停窗口

正如我们已经充分了解到的那样,智能合约风险是不可避免且始终存在的。为了以防万一,我们希望有一种方法可以简单地停止一切,即除了按比例提款之外的所有改变状态的操作。我们的解决方案是“暂停窗口”,由 TemporarilyPausable 合约实现。

在 Vault 部署后的某个时期(默认情况下为三个月),治理可以暂停 Vault,这实际上会关闭协议并让每个人都提取他们的资金。

同样,每个池都有一个暂停窗口,该窗口从关联工厂的部署开始。例如,使用标准的三个月暂停窗口,紧接在工厂之后部署的池将有一个三个月的窗口,而两个月后部署的池将有一个月的窗口。

这听起来很简单,但是存在权衡和竞争问题,所有这些问题都在最终设计中得到了解决。

在发生漏洞或安全事件时,治理必须能够非常迅速地采取行动。另一方面,暂停协议不应掉以轻心。

我们的解决方案是使暂停可撤销。在暂停期间,如果出现错误警报,也可以取消暂停 Vault 或池。因此,即使对威胁的有效性存在不确定性,治理也不必犹豫是否暂停。

安全性与自由之间由来已久的权衡呢?Balancer 作为一个不可协商的核心价值观是无需许可的。暂停能力难道与此背道而驰吗?如果政治力量或监管机构希望我们暂停他们不喜欢的某些池或完全关闭该平台怎么办?

该解决方案是一个在窗口期满后永久取消暂停所有内容的定时锁,从而确保它仍然是无需许可的。如果发生安全事件,LP 将有整个暂停期间的时间来提款。

还有一个额外的改进,以防我们在暂停窗口结束前发现漏洞,导致 LP 没有时间在取消暂停之前提款。这是添加“缓冲期”(通常为一个月)。如果在原始窗口到期时 Vault 或池已暂停,它将保持暂停状态,直到该额外缓冲期结束,然后永久取消暂停。这保证了有时间评估威胁并提款。

总而言之,所有池(以及 Vault 本身)都可以在初始暂停窗口期间由治理可逆地暂停。接下来是一个单独的缓冲期,在此期间可以不可撤销地取消暂停。所有期限到期后,所有内容都将取消暂停,并且它将永远保持无需许可。

此暂停窗口在当前的事件中起着重要的作用,我们将在最后一节关于经验教训中对此进行详细说明。

恢复模式

当 V2 启动时,Vault 本身和所有池都有暂停窗口,如上所述。例如,在第一个版本的 Weighted Pool 中,所有改变状态的代码路径(例如,swap 和 join)都受到 whenNotPaused 修饰符的保护,因此如果池被暂停,它们将恢复:除了按比例退出,因为这是即使在暂停期间也必须始终有效的关键功能。

然而,在 Composable Stable Pool 的早期迭代之一中,我们意识到未满足此“保证”。Stable Pool 确实在暂停时“保持开放”按比例退出的路径:但随后更新了费率缓存和不变量。

费率缓存代码包含外部调用,不变量计算是迭代的,并且两者都可能在按比例退出期间恢复。此外,代码路径比原始的 Weighted Pool 中的代码路径多得多,并且必须对每个代码路径进行评估,以确定它是否应该在暂停期间工作。代码开始变得复杂且难以理解,特别是自从暂停还禁用了协议费用以来。

解决方案是在 Base Pool 级别引入恢复模式。现在,治理可以为所有池启用一个新的、大大简化的代码路径,该路径仅燃烧 BPT 以获得池 token 的按比例份额:没有协议费用,没有 token 小数位数或费率的缩放,没有可能失败的外部调用。所有“现代”(V2+)池都具有它,包括合作伙伴 Linear Pool 派生的类型。

它已经不止一次地拯救了我们,包括在当前的事件中。

漏洞简述

我们最初的努力的一个主要重点是验证漏洞的范围,并确保它仅限于 Boosted Pool(即具有 Linear Pool 成分的可组合/幻影稳定池)。

我们认为情况确实如此,即使启用漏洞利用的疏忽涉及基本 token 缩放,并且包含在多个池类型之间共享的通用库(ScalingHelpers)中。

如缩放函数注释中所述,在将要用于 token 转移的输出值返回到 Vault 时,控制舍入方向至关重要。这可以保证任何 舍入错误将有利于池,并且不能在获得比进入的 token 更多的 token 的“循环”交易中被利用:池数学的一个重要不变量。

例如,在 Linear Pool 中返回计算的 swap 数量时,我们有:

// amountOut token 正在退出 Pool,因此我们向下舍入。
return _downscaleDown(amountOut, scalingFactors[indexOut]);
// amountIn token 正在进入 Pool,因此我们向上舍入。
return _downscaleUp(amountIn, scalingFactors[indexIn]);

然而,人们认为“放大”操作(计算数学函数的“输入”与解释输出)对舍入的敏感度要低得多,因此我们可以采用更节能的始终向下舍入的策略。

// 放大取整不一定总是朝同一方向:
// 例如,在 swap 中,输入 token 的余额应向上取整,
// 而输出 token 的余额应向下取整。这是我们唯一的地方
// 朝同一方向对所有数量进行取整,因为这造成的
// 取整的影响预计微乎其微。

事实确实如此:在 Linear Pool 中,一些特性共同创造了一条舍入错误很重要的路径。

  1. Linear Pool 在平衡时没有费用,因为它们在大多数时间都设计为平衡的,并且没有最低余额。这允许交换极小的 token 数量,直到包括单个 wei。
  2. Linear Pool 使用预先铸造的 BPT 进行初始化,从而创建了基本上无限的供应。
  3. 由于 Vault 的 batchSwap“在最后结算”,因此所有这些实际上无限的供应都可用于在闪电 swap 操作中“借用”。
  4. 虽然批量 swap 在最后结算,但批量中的各个 swap 会根据缩放的余额(包括费率)执行计算,这些计算不依赖于 Vault 的池余额(仅在最后原子地更改),而是依赖于中间池状态(特别是 BPT 供应),从而影响后续 swap 的数学运算。

这些功能共同实现以下最简单的漏洞利用,使用“GivenOut”批量 swap:

  1. 以 > 1 的费率“借用”BPT(闪电 swap),并将其换成主要 token 和包装 token,以将 token 余额减少到接近于零。
  2. 制作一个利用 GivenOut swap 中舍入错误的交易,使总余额等于虚拟供应,这会将费率重置为 1(因为费率 = 余额/供应)。
  3. 以新的较低费率偿还闪电 swap 的 BPT 以获取利润。

这已经够糟糕了,但如果像我们最初认为的那样,只能将费率降至 1,那么潜在 damage 将受到限制,因为大多数费率都非常接近 1(尤其是在流动性高的池(这将是最诱人的目标)中)。

不幸的是,在主要 token 和包装 token 之间进行交易时,也可以利用相同的舍入错误,从而可以在不影响供应的情况下减少总余额。回想一下,费率 = 余额/供应,因此反复这样做可以使费率显着低于 1,从而有效地耗尽池。

缓解措施

如果我们能够暂停 Linear Pool,缓解措施将是微不足道的。然而,即使对于最新的工厂部署,该窗口也已经过去。

只有 V5 Composable Stable Pools 可暂停,因此如 论坛帖子 中披露的那样,我们暂停了它们并启用了恢复模式,因此所有 swap(以及所有攻击)都被阻止,但用户仍然可以提取他们的资金。

虽然我们无法直接暂停 Linear Pool,但所有 Linear Pool 都包含包装 token,并且所有攻击都涉及与它们进行 swap。大多数 易受攻击的池 使用可升级的包装器,因此可以更改其代码以阻止 swap(从而阻止攻击),但允许直接解包。这样,用户可以使用恢复模式从池中提款,然后恢复底层稳定币。

在前端,除了横幅和警告外,UI 还提供了一个专用的、用户友好的 提款页面,以便轻松指导 LP 完成提款过程,即使这涉及 staking 或多个嵌套级别。

这些措施非常有效。在采取缓解措施之前,面临风险的总 TVL 为 2.42 亿美元。幸运的是,可以使用一种或两种“暂停”方法来保护大多数受影响的池(包括大多数大型池)。

在我们采取缓解措施后,只有大约 4000 万美元(不到 20%)仍然存在风险。一些非常旧的池不支持恢复模式,或者使用不兼容的包装器。我们将这些池标记为“有风险”,并将其他池标记为“已缓解”。

由于所有激励措施很快将被终止,并且池无法(或者在任何情况下都不会)被重新激活,因此我们敦促所有 LP 立即退出,对于那些在“有风险”池中持有资金的人来说,尤其紧急。然后,我们通过及时的 Discord 公告和关于提款进度的推文来让用户了解最新情况。

如上文的概述和时间线中所述,这些努力也非常成功;到 8 月 27 日早上,超过 95% 的 LP 成功提款,我们都松了一口气。

事实证明,这有点为时过早。

漏洞利用

因为在那个星期天清晨,我们安装的监控系统开始亮了起来。一个默默无闻的合作伙伴 Boosted Pool 首先倒下,也许是作为一次试验。然后,Mainnet 和 Optimism 上的主要 USD Boosted Pools 遭受了重创——并且在接下来的两天里反复遭到攻击——用于减少数量。

在缓解准备期间,我们并没有认真关注潜在的漏洞利用。毕竟,该错误已经存在了两年,并且安全性非常严格;即使在内部,也只有极少数人知道该漏洞。

即使在披露之后,我们仍然有信心,我们没有透露足够的信息,以至于黑帽可以在绝大多数 LP 可以退出之前弄清楚它。事实上,根据我们之前处理漏洞的经验,我们认为他们很可能永远不会找到该错误。

从技术上讲,我们在两个方面都是正确的。

随着池余额开始下降,我们认为也许我们在第二点上是错误的。毕竟,那里有一些非常狡猾的黑帽行动,其中一些拥有民族国家的力量,所以一切皆有可能。

令我们非常沮丧(和惊讶)的是,当我们检查漏洞利用交易时,他们没有使用白帽最初发现的相同的“GivenOut”方法。这些都是“GivenIn”。毕竟,他们没有弄清楚最初的攻击向量;他们找到了另一个,甚至更糟糕的一个。

第二种方法不是降低费率,而是使用精心设计的 swap 将余额和供应设置为精确的值,以便将费率提高到非常高的值(例如,30-50;通常它非常接近于 1)。这利用了 token 小数位数的精度和上面描述的舍入错误来实现真正极端的费率值,但严格来说,这两种技术都不是必需的。

在此阶段之后,攻击者拥有大量 Linear Pool 成分的 BPT。由于其费率现在如此之高,因此该 token 的 BPT 在父 Composable Stable Pool 中以溢价进行交易。在下一阶段,攻击者将这种人为抬高的 BPT 换成不成比例的其他 token 的 BPT。例如,在 USD 池中,如果 bb-a-USDC 的费率为 50,则可用于购买大量的 bb-a-DAI 和 bb-a-USDT。

就其本身而言,提高 token 的费率是可以的(这就是为什么我们在设计过程中不担心它的原因)。如果没有造成巨大损失,将无法支付它:只要费率保持在高位。

不幸的是,一种方法可以再次降低它:一直降回到 1。

因为所有他们的 BPT 都是预先铸造的,并且他们不允许传统的加入,所以 Linear Pool 使用一种不寻常的初始化方法:设置交易,其中主要 token 换入了一些预先铸造的 BPT。



大多数池类型都有一个 MINIMUM\_BPT 数量,该数量在初始化时会被销毁,从而保证即使所有 LP 退出,供应量也不会恢复为零。鉴于此保证,检查总供应量是否为零是确定池是否正在被初始化的简单方法(并且应该具有比率 1)。

在线性池中,**总**供应量实际上永远无法接近于零。不幸的是,这并不重要。重要的是**流通**或“虚拟”供应量(并且在线性数学中正在检查)。 通过上述技术,流通供应量**可以**恢复为零,从而欺骗池认为它正在被初始化,并将比率重置为 1。

提高然后降低比率的能力复制了原始漏洞的条件;然后可以通过以折扣价偿还 BPT 来获利。 事实上,情况往往更糟。从 50 到 1 就像从 1 到 0.02(远低于原始攻击实际可设置的比率)。

### 总结

事实证明,线性池中存在**两个**漏洞:自 2021 年成立以来就存在。 一个由白帽发现并报告,另一个由黑帽发现并利用。鉴于时间安排,如果没有最初的披露,黑帽可能永远不会找到第二个向量,即使披露的信息很少(主要只是池列表)。

它们本身在很大程度上无关紧要,但它们与嵌套增强池上下文中其他平台功能的交互“武器化”了这些错误并启用了利用。

![](https://img.learnblockchain.cn/2026/01/02/0EctqmHKz9zJAxpqI.png)

如果我们打算重新启动线性池(在被利用之前,仍在考虑中),我们确定了四项干预措施,这些措施单独或组合起来可以阻止攻击。

1. 在向上扩展以及向下扩展期间使用舍入方向。 这会将额外的 wei 添加到数学输入(和输出)中,从而消除舍入误差。
2. 与 #1 类似,只需在金额中添加另一个 wei,并在金额外减去一个 wei,同样可以消除任何舍入误差。 我们甚至可以在此处使用 10 或 100 wei 的“缓冲”; 这种小“税”对于任何实际交易都可以忽略不计,但会阻止通过低余额进行操纵。
3. 强制执行最低余额(包括 BPT)。 恢复任何导致极低余额的交换(例如,低于 1e6)也将大大有助于防止通过小额交易进行费率操纵。 它本身可能不足够,但肯定会使任何舍入误差的影响小得多。
4. 这会向每个操作添加一个存储读取(这可能是我们一开始没有这样做的部分原因),但是使用显式的“已初始化”标志而不是检查零供应量将绝对防止利用中使用的费率重置步骤。

### 经验教训

**复杂性分类**

任何具有像 Balancer 这样多样化和产生价值的功能集的系统都将具有一定程度的不可约的复杂性。 关键问题是:1)多少复杂性算作太多? 2)如何最好地组织和控制这种复杂性?

考虑到任何具有组件和交互的系统,你可以拥有 1)具有复杂交互的简单组件; 或 2)具有高度约束交互的更复杂组件。 增强池具有第一种复杂性。

就其本身而言,线性池非常简单。 稳定池在概念上也同样简单,并且它们的数学虽然有点棘手,但已得到充分理解。 可以充分理解每个组件,甚至可以理解它与第二个组件的交互。

但是,当我们使稳定池可组合、将线性池放入其中,然后添加任意收益代币和费率提供商(所有这些都与 Vault 交互)时,我们创建了一种[三体问题](https://en.wikipedia.org/wiki/Three-body_problem) ,就像它在物理学中的对应物一样,没有封闭形式的解决方案(并且一些数值解最终变成了漏洞利用)。

我们设计的 Vault 具有很大的灵活性,并且通常对我们很有用。 但是,不可能预测将来需要哪种灵活性,并且在少数情况下,我们猜错了:例如,协议费用和可组合性的程度。 因此,我们不得不创建新的组件并以以前未预料到的方式组合它们,这在某些情况下导致了漏洞。

更好的方法可能是在组件内封装复杂性(理想情况下,可以根据需要进行迁移)并高度约束它们之间的交互。 这可能会产生一个庞大的、偶尔混乱的二体问题集群:但至少这些问题可以解决。 这比发送一个母球撞向一排台球并希望一切顺利要好得多。 正如我们所发现的,抓球有点太容易了!

**估算暂停窗口**

在为之倾注了将近一年的心血(更不用说三个全面的审计)之后,对我们的代码抱有高度的信心是可以理解的,但是对于 V2 的发布(以及所有后续版本),我们对玫瑰色的眼镜完全走了埃尔顿·约翰的路线。

事后看来,三个月的暂停窗口似乎天真得无可救药。我们在发布**两年**后发现了 [Vault 中的第一个漏洞](https://forum.balancer.fi/t/reentrancy-vulnerability-scope-expanded/4345)。 同样,线性池于 21 年 12 月推出:关键漏洞也被忽略了几乎相同的时间。

我们也在没有恢复模式的情况下启动了(尽管说实话,当我们只有 V1 加权池时,我们并不真正需要它)。 由于没有预料到后续池类型的复杂性,我们意识到(在解决一个不相关的问题时),我们最初的“按比例退出”逃生舱是不够的。

如果线性池的暂停窗口为两年或三年,而不是三个月,那么缓解措施将是微不足道的:只需暂停所有线性池即可。 就这样,即使是 L2 暂停窗口也已全部过期。 我们只能暂停 V5 可组合稳定池,具有讽刺意味的是,这是由于导致从旧池迁移的先前漏洞所致。

收益代币包装器是可升级的,这纯粹是出于盲目的运气。 如果它们是不可变的,我们将只能缓解少数池(V5 稳定池)。 如果我们没有恢复模式,那么缓解措施也将是不可能的(即,干预措施也会阻止提款)。

真正“无限”的暂停窗口会损害我们的免许可状态(这是限制它的最初原因),但是选择一个大致对应于主要协议版本预期寿命的暂停窗口(例如,3-5 年)似乎是合理的。

**让安全专业人员参与**

两年多没有出现漏洞,虽然总体上非常积极地表明协议非常健康,但这让我们变得有些自满。 我们没有为这里所需的“工业规模”白帽行动做好准备(我们之前成功的努力小了一个数量级,更加安全和独立)。 我们也没有“侦查”或黑客通讯方面的经验。

我们希望我们永远不必再使用它们,但该事件的一个积极结果是与安全咨询资源建立了更紧密的联系,并且对它们的功能和工具更加熟悉。

### 下一步

安全一直是 Balancer 的首要任务。 所有版本都经过严格的内部审查和严格的第三方审查(例如,V2 Vault 具有顶级公司的三个[审计](https://github.com/balancer/balancer-v2-monorepo/tree/master/audits))。

发布后,我们会进行广泛的威胁监控,并已征募 [Immunefi 的](https://immunefi.com/) 白帽大军,以确保,尽管这对我们来说可能很痛苦且代价高昂,但我们有最好的机会在黑帽利用它们之前发现并纠正任何残留的漏洞。

在过去的几个月中,这尤其有效,这些挑战逐渐升级。 从一个相对默默无闻的可组合稳定池漏洞开始,一个关键的线性池问题大大提高了风险,最终导致了我们的有史以来第一次漏洞利用:由以前未知的**第二个**线性池漏洞启用。

Balancer 仍然致力于创新的流动性质押和其他收益产品(以及一般的资本效率),但显然,我们最初的方法过于复杂且不确定,无法全面构建。 因此,我们将停止使用线性池:但已经在全力以赴地研究如何以更好、效率更高(可能甚至达到 100%)的方式以及以更简单、更安全的方式来完成它们的新想法。

在过去的几年中,我们吸取了许多惨痛的教训,并将继续应用它们来增强我们的协议并支持 DeFi 领域的创新。

>- 原文链接: [medium.com/balancer-prot...](https://medium.com/balancer-protocol/rate-manipulation-in-balancer-boosted-pools-technical-postmortem-53db4b642492)
>- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
juani_72357
juani_72357
江湖只有他的大名,没有他的介绍。