又一起精度损失的悲剧:KyberSwap 事件的深入分析

该文章深入分析了KyberSwap遭受的攻击事件,攻击总损失超过4800万美元。根本原因是KyberSwap在再投资过程中,不正确的舍入方向导致了错误的tick计算,最终导致流动性被重复计算。攻击者通过操纵tick和价格,利用price计算中的精度损失,在step4中欺骗pool,然后在step5中double count流动性,从而获利。

由 BlockSec 提供

BlockSec:对 KyberSwap 事件的深入分析

原文:

https://phalcon.xyz/blog/yet-another-tragedy-of-precision-loss-an-in-depth-analysis-of-the-kyber-swap-incident-1

2023 年 11 月 23 日,我们观察到一系列针对 KyberSwap 的攻击。这些攻击导致总损失超过 4800 万美元。我们的初步分析表明,该漏洞是由于 tick 操纵和双重流动性计算造成的。但是,由于空间限制,我们无法在该帖子中深入探讨广泛的细节。尽管其他安全研究人员随后进行了有见地的分析,但问题的根本原因(精度损失)仍未暴露。

有趣的是,几天后情节变得更加复杂。2023 年 11 月 30 日,经过与官方的多轮讨论后,攻击者发送了一条消息,对外人看来充满了挑衅,要求完全控制。抛开这一点不谈,攻击者还透露了一条关键信息:问题确实与精度损失有关,如下图所示。这一披露强化了我们调查的证据。因此,我们的目标是在本报告中提供全面的分析。

主要要点 (TL;DR)

  • 我们的调查显示,根本问题源于 KyberSwap 的再投资过程中不正确的舍入方向。这随后导致不正确的 tick 计算,并最终导致双重流动性计算。
  • 这一事件突显了 DeFi 协议中精度损失问题的复杂性和隐蔽性,给整个社区带来了巨大的挑战。
  • 这些攻击的频率清楚地提醒人们 迫切需要积极的威胁预防措施,这可以大大有助于减少未来的损失。

在接下来的章节中,我们将首先提供有关 KyberSwap 的一些重要背景信息。随后,我们将对漏洞和相关攻击进行深入分析。

0x1 背景

KyberSwap[1] 是一个去中心化的自动做市商 (CLAMM) 平台。为了满足集中流动性市场需求,KyberSwap Elastic[3] 基于 Uniswap V3[2] 推出,并进行了一些改进,包括再投资曲线,以实现流动性提供收益的自动复利。

0x1.1 Tick 和平方根价格

Uniswap V3 类 CLAMM 中的 Tick 用于以离散方式标记价格,以便 LP 可以在固定范围内提供流动性,而不是在整个范围内提供流动性(因此称为“集中”)[4]。

为了使 LP 能够指定具有自定义价格区间的流动性头寸,该协议需要一种方法来跟踪各个价格点上的聚合流动性。Uniswap V3 通过将可能价格的空间划分为离散的“ticks”来实现此目的,由此 LP 可以在任何两个 ticks 之间贡献流动性。

根据[5],流动性可以放置在任意两个 tick(不必相邻)之间的范围内,即一对 tick 指数(较低的 tick 和较高的 tick)。具体来说,每个 tick 的价格(在整数索引 i 处)定义如下:

实际上,使用平方根价格(表示为 sqrtPsqrtPrice):

也可以根据当前平方根价格计算当前 tick:

使用平方根价格以及流动性 L 是避免同时更改的一种实用方法。具体来说,在 tick 内交换时价格会发生变化;跨越 tick 或铸造或销毁流动性时流动性会发生变化。更详细的说明,请参阅 Uniswap V3 的白皮书[5]。

显然,虽然为给定的 tick 仅计算一个平方根价格,但多个平方根价格可能指向相同的 tick

0x1.2 再投资曲线

基于 Uniswap V3 的 CLAMM 受到 LP 费用的池利用率以及再投资所需的大量 gas 费用的影响。因此,KyberSwap 采用了再投资曲线[6] 来解决该问题:

再投资曲线的唯一目的是在集中流动性模型中本地再投资原本未使用的 LP 费用。这意味着集中流动性头寸的 LP 费用会自动复利,而无需 gas 费用或手动管理开销。此外,LP 仍然可以选择随时单独收取其自动复利的费用收益。

再投资曲线的关键在于,每次交换中收取的费用都会作为额外的流动性累积到池中,作为无限范围内的再投资流动性。再投资代币被铸造给 LP,并且累积的再投资流动性会相应地分配给 LP。此外,再投资流动性还参与交换和价格计算过程。

准确地说,不是常数乘积公式:

每次交换中,费用都会累积到 Δ L 中:

Δ L 的计算可以简化为(假设价格偏差低于阈值):

然后,可以通过修改后的常数乘积公式得出交换量和最终价格:

上面介绍的计算的相应代码显示在相应的以下代码片段的 computeSwapStep 函数中。

应该注意的是,由于再投资流动性,此函数中的 liquidity 是两个组成部分的sum:baseL 代表基础流动性,reinvestL 代表再投资的累积流动性。

0x1.3 KyberSwap 中的交换

Uniswap V3 中交换的控制流可以描述如下[5]:

因此,KyberSwap 池(前面讨论过)的 swap 函数的实现可以抽象为下图所示:

与 tick 计算相关的关键逻辑位于 swapping while 循环中,如蓝色矩形高亮显示。具体来说,主要逻辑包括 computeSwapStep 函数和 _updateLiquidityAndCrossTick 函数。前者计算关键状态,例如给定交换的输入和输出量以及 nextSqrtP,而后者处理跨 tick 发生的情况。

传统上,当价格上涨时,我们称之为向右/向上移动 tick;否则,我们说 tick 向左/向下移动。

为了更好地理解后面将讨论的漏洞,我们需要探索 computeSwapStep 函数的相关代码逻辑,如下图所示:

首先,从第 50 行到第 57 行,调用 calcReachAmount 函数来计算达到 targetSqrtP(下一个 tick 或用户指定的目标价格)所需的输入代币量。

接下来,在第 59 行和第 62 行之间,进行测试以确定是否应跨越该 tick。

具体来说,如果在精确输入交换中使用的量(usedAmount)大于用户指定的量(specifiedAmount)(攻击中使用的情况),则意味着不应跨越该 tick,并且需要从增量流动性(deltaL,即增量流动性)中导出 nextSqrtP

  • 随后,在第 70 行和第 79 行之间,使用 estimateIncrementalLiquidity 函数从输入量、当前流动性和价格导出 Δ LdeltaL)。最后,使用 calcFinalPrice 函数,基于 deltaL、输入量、当前价格和流动性来计算交换后的最终价格 nextSqrtP

相反,如果所需量小于用户指定的量(这意味着 nextSqrtP > 0),则使用当前和目标 sqrtP 计算 deltaL,并且 nextSqrtP 是下一个 tick 上的 sqrtP。省略详细信息,因为此分支未在攻击中使用。

上面概述的步骤清楚地表明,如果未跨越该 tick,则 computeSwapStep 返回的 nextSqrtP 不应大于下一个 tick 的 sqrtP。但是,由于价格对流动性(基础流动性和增量流动性)的依赖性以及精度损失,攻击者能够操纵 nextSqrtP 使其更大,而没有跨越该 tick。

0x2 漏洞分析

根本原因在于 SwapMath 合同的增量流动性计算(即 estimateIncrementalLiquidity 函数)(由 computeSwapStep 函数调用)中不正确的舍入方向导致的错误 tick 计算。这反过来又会不适当地影响后面的 tick 计算。

有趣的是,在检查第 188 行的注释(蓝色矩形高亮显示)时,我们发现 deltaL 旨在向上舍入,以便向下舍入 nextSqrtP。但是,由于在第 189 行使用了 mulDivFloor 函数,因此 deltaL 被错误地向下舍入。因此,nextSqrtP 被不准确地向上舍入。

0x3 攻击分析

攻击者发起了多次攻击交易,每次交易都耗尽了多个池。为了简单起见,以下讨论基于攻击交易中的第一次攻击。

核心攻击逻辑包括以下六个步骤:

1. 通过 AAVE 闪电贷借入 2,000 WETH。

2. 在受害者池 0xfd7b 中将 6.850 WETH 兑换为 6.371 frxETH。此步骤用于将当前 tick 和 currentSqrtP 推送到当前不存在流动性的位置。

  • currentSqrtP 似乎是由攻击者随机选择的,并且交换会在此价格处精确停止。
  • 在此步骤之后,基础流动性(baseL)为零,但再投资流动性(reinvestL)为非零。

3. 向池中添加流动性,然后删除部分流动性。此步骤用于将范围和总流动性控制到所需的量。

  • tick 范围是根据 currentSqrtP 选择的。
  • 攻击所需的流动性可以从 tick 范围得出,尽管相应的计算逻辑需要进一步探索。

4. 在池中将 387.170 WETH 兑换为 0.06 frxETH。此步骤用于操纵当前 tick,以使 nextTick == currentTick

  • 输入量是根据流动性和 currentSqrtP 选择的。

5. 在池中将 0.06 frxETH 兑换为 396.244 WETH。请注意,与上一步相比,交换方向相反。在此步骤中,流动性被双重计算,以使交换有利可图,并因此耗尽池。

6. 偿还闪电贷,并获得 6.364WETH 和 1.117 frxETH。

Obvisouly,最后两次交换(步骤 4 和步骤 5)是操纵 tick 计算并使交换有利可图以耗尽池的关键攻击步骤。我们将在以下小节中深入探讨详细信息。

重要的是要注意,步骤 3 对于操纵流动性至关重要。由于需要通过舍入操作进行精确的 tick 操纵,因此直接添加流动性无法实现目标。删除流动性是为了精确控制范围内攻击者所需的流动性。

0x3.1 第 4 步:操纵当前 tick 和 currentSqrtP

在前几个步骤(步骤 1 和 2)之后,攻击者已准备好用于操纵的 tick 范围和流动性。具体来说:

  • currentSqrtP 位于所需位置
  • 当前 tick = 110,909,下一个 tick = 111,310,围绕 currentSqrtP

此步骤将 WETH 兑换为 frxETH。在 computeSwapStep 函数中,我们具有以下执行跟踪:

如上图所示,到达目标(即下一个 tick)的量将通过调用 calcReachAmount 函数来计算:

  • usedAmount = calcReachAmount(liquidity, currentSqrtP, targetSqrtP)

请注意,可以在交换之前推导出此计算。通过仔细选择 specifiedAmountusedAmount = specifiedAmount + 1),攻击者控制了交换,使目标(即下一个 tick 111,310)未达到,从而导致 nextSqrtP = 0。

在这种情况下,由于未跨越该 tick,因此需要从增量流动性(累积为交换费用)中导出 nextSqrtP(即最终价格)。

首先,通过以下方式计算来自费用的增量流动性 deltaL

  • deltaL = estimateIncrementalLiquidity(absDelta, currentSqrtP)

然后是最终价格 nextSqrtP

  • nextSqrtP = calcFinalPrice(absDelta, liquidity, deltaL, currentSqrtP)

回顾上一节中讨论的舍入方向错误,此处 deltaL 被错误地向下舍入,从而导致 nextSqrtP 被向上舍入。具体来说,在这种情况下,基于相同的 absDelta(387,170,294,533,119,999,999),由于不同的舍入方向,计算结果有所不同:

因此,在步骤 4 中进行 tick 操纵之后,当前状态总结如下:

  • currentSqrtP 为 20,693,058,119,558,072,255,665,971,001,964,略大于 tick 111,310 处的 sqrtP(111,310 处的 sqrtP = 20,693,058,119,558,072,255,662,180,724,088)。
  • 当前 tick = 111,310,下一个 tick = 111,310

如上图所示,步骤 4 中的交换巧妙地欺骗了池,使其相信未跨越 tick 111,310。但是,实际上,currentSqrtP 确实大于 tick 111,310 的 sqrtP

0x3.2 第 5 步:流动性双重计算

基于步骤 4 中的操纵,步骤 5 中的攻击逻辑非常简单。在此阶段,攻击者精心策划了从 frxETH 到 WETH 的反向交换,这将使 tick 和 currentSqrtP 向左移动。具体来说,在循环中两次调用 computeSwapStep 函数,这最终以一种意想不到的方式触发了流动性双重计算[7],从而产生了额外的利润。

如上面的跟踪所示:

  • 在第一次调用 computeSwapStep 函数时,currentSqrtP 已移至 tick 111,310 的 sqrtP。这是一个很小的交换,实际上只使用 3 wei 的 frxETH 来真正到达 tick 111,310。随后,在 _updateLiquidityAndCrossTick 函数中,当前 tick 应跨越 tick 111,310(向左/向下移动),即使它在步骤 4 中未真正以正确/向上的方向穿过 tick 111,310。这导致 tick 111,310 处的流动性被计算两次
  • 在第二次调用 computeSwapStep 函数时,先前对流动性的双重计算可能会导致产生额外的利润。具体来说,通过利用这种流动性双重计算,最后一步中的交换价格会被扭曲,从而导致交换出大量的 WETH,从而产生利润。

0x4 攻击和利润摘要

截至撰写本文时,我们已经观察到对不同链(包括以太坊、Optimism、Polygon、Arbitrum、Avalanche 和 Base)的多次攻击,造成超过 4800 万美元 的损失。这些攻击是由不同的攻击者发起的,如下所示:

这些攻击交易的完整列表已收集在我们准备的文档中。请参阅该文档以获取更详细的信息。

0x5 结论

总之,这是一个源于不正确的舍入逻辑的微妙漏洞。该漏洞利用非常复杂。事实上,今年我们已经观察到一系列与精度损失问题相关的安全事件,这给社区带来了巨大的挑战。

这些持续的攻击再次表明了积极的威胁预防的重要性,这种策略可以有效地帮助减轻潜在的损失。

参考

[1] https://docs.kyberswap.com/

[2] https://blog.uniswap.org/uniswap-v3

[3] https://docs.kyberswap.com/liquidity-solutions/kyberswap-elastic

[4] https://docs.kyberswap.com/liquidity-solutions/kyberswap-elastic/concepts/tick-range-mechanism

[5] https://uniswap.org/whitepaper-v3.pdf

[6] https://docs.kyberswap.com/liquidity-solutions/kyberswap-elastic/concepts/reinvestment-curve

[7] https://100proof.org/kyberswap-post-mortem.html

  • 原文链接: blocksecteam.medium.com/...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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