2023年11月23日,KyberSwap 遭遇了一系列攻击,总损失超过4800万美元。根本问题源于 KyberSwap 再投资过程中错误的舍入方向,这导致了不正确的 Tick 计算,并最终导致流动性重复计算。
我们发布了一份详尽的报告:“又一起精度损失的悲剧:KyberSwap 事件深度分析”,其中深入探讨了此次事件的细节。为了更深入地理解,我们鼓励您阅读完整的分析。下面,我们对此次事件进行一个简要的介绍,并将其列为 2023 年十大安全事件之一。
背景
KyberSwap 是一个去中心化的自动做市商(CLAMM)平台。为了满足集中流动性的市场需求,KyberSwap Elastic 在 Uniswap V3 的基础上推出,并进行了一些改进,包括再投资曲线,以实现流动性提供收益的自动复投。
1. Tick 和平方根价格
Uniswap V3 类 CLAMM 中的 Tick 用于离散地标记价格,以便 LP 可以在固定范围内提供流动性,而不是整个范围(因此称为“集中”)。
流动性可以放置在任意两个 Tick 之间(不一定是相邻的),即一对 Tick 索引(一个下限 Tick 和一个上限 Tick)。具体来说,每个 Tick 的价格(在整数索引 i 处)定义如下:
实际上,使用的是 平方根价格(表示为 sqrtP 或 sqrtPrice):
也可以根据当前的平方根价格计算当前的 Tick:
显然,对于给定的 Tick,只有一个平方根价格被计算出来,但多个平方根价格可能指向同一个 Tick。更详细的解释,请参考 Uniswap V3 和 KyberSwap 的文档。
2. 再投资曲线
基于 Uniswap V3 的 CLAMM 在 LP 费用的池子利用率和再投资所需的高昂 Gas 费用方面存在不足。因此,KyberSwap 采用了 再投资曲线 来解决这个问题。
再投资曲线的关键在于,每次互换所收集的费用作为“再投资流动性”累积到池子中,范围是无限的。再投资代币会被铸造给 LP,并相应地分配累积的再投资流动性。此外,再投资流动性也参与互换和价格计算过程。
对应上述计算的代码显示在以下代码片段的 computeSwapStep 函数中,对应于 pool。

需要注意的是,由于再投资流动性的存在,该函数中的 liquidity 是两个组成部分的和:baseL 表示基础流动性,reinvestL 表示累积的用于再投资的流动性。
3. KyberSwap 中的互换
前面讨论的 KyberSwap 池的 swap 函数的实现可以抽象为下图:

与 Tick 计算相关的关键逻辑位于互换的 while 循环内,如蓝色矩形所示。具体来说,主要逻辑涉及 computeSwapStep 函数和 _updateLiquidityAndCrossTick 函数。前者计算给定互换的输入和输出金额以及 nextSqrtP 等关键状态,后者处理 穿越 Tick 的情况。
传统上,当价格上涨时,我们称之为 Tick 向右/向上移动;否则,我们称 Tick 向左/向下移动。
为了更好地理解稍后将讨论的漏洞,探索 computeSwapStep 函数的相关代码逻辑至关重要,如下所示:

具体而言,calcReachAmount 函数用于计算目标价格 targetSqrtP 所需的输入代币(第 50-57 行)。如果 usedAmount 大于 specifiedAmount,则 Tick 未被穿越,并且 nextSqrtP 从 deltaL(即 delta 流动性,第 59-62 行)计算得出。deltaL 使用 estimateIncrementalLiquidity 函数确定,最终价格 nextSqrtP 使用 calcFinalPrice 函数计算(第 70-79 行)。如果需要较少的输入,nextSqrtP 会被设置为下一个 Tick 的价格,但此情况在攻击中未使用。
上述步骤清楚地表明,如果 Tick 未被穿越,computeSwapStep 返回的 nextSqrtP 不应大于下一个 Tick 的 sqrtP。然而,由于价格对流动性(基础流动性和 delta 流动性)和精度损失的依赖性,攻击者能够操纵 nextSqrtP 使其变大,而 Tick 未被穿越。
漏洞分析
根本原因在于 SwapMath 合约(由 computeSwapStep 函数调用)的 delta 流动性计算(即 estimateIncrementalLiquidity 函数)中错误的舍入方向导致了错误的 Tick 计算。这反过来又会不正确地影响后续的 Tick 计算。

有趣的是,在检查第 188 行的注释(由蓝色矩形突出显示)时,我们发现 deltaL 本应向上舍入,以便向下舍入 nextSqrtP。然而,由于在第 189 行使用了 mulDivFloor 函数,deltaL 被错误地向下舍入。因此,nextSqrtP 被不准确地向上舍入。
攻击分析
攻击者发起了一系列攻击交易,每笔交易都耗尽了多个池子。为简单起见,以下讨论基于 攻击交易 中的第一次攻击。
核心攻击逻辑由以下六个步骤组成:
-
通过 AAVE 的闪电贷借入 2000 WETH。
-
在受害者池子 0xfd7b 中,用 6.850 WETH 兑换 6.371 frxETH。此步骤用于将当前 Tick 和
currentSqrtP推入当前没有流动性的位置。
currentSqrtP似乎是攻击者随机选择的,并且互换恰好在该价格停止。- 此步骤后,基础流动性(
baseL)为零,但再投资流动性(reinvestL)非零。
- 向池子添加流动性,然后移除部分流动性。此步骤用于将范围和总流动性控制到所需的数量。
- Tick 范围是根据
currentSqrtP选择的。 - 攻击所需的流动性可以从 Tick 范围推导出来,尽管相应的计算逻辑需要进一步探讨。
- 在池子中用 387.170 WETH 兑换 0.06 frxETH。此步骤用于操纵当前 Tick,使
nextTick==currentTick。具体来说,步骤 4 中的互换巧妙地欺骗池子,使其认为 Tick 111,310 未被穿越。然而,实际上,currentSqrtP确实大于 Tick 111,310 的sqrtP。
- 在池子中用 0.06 frxETH 兑换 396.244 WETH。注意,互换方向与上一步相反。在此步骤中,流动性被双重计算,使互换有利可图,并最终耗尽池子。
- 偿还闪电贷,并收获 6.364 WETH 和 1.117 frxETH。
如需深入研究,请参阅我们的 全面分析,其中包含更多计算和图示。
总结
此次事件的根本问题源于 KyberSwap 再投资过程中不正确的舍入,导致了不准确的 Tick 计算,并最终导致流动性重复计算。此次事件凸显了 DeFi 协议中精度损失问题的复杂性和隐蔽性,对整个社区构成了严峻挑战。
这次 2023 年的攻击因其复杂性而脱颖而出,具有异常精细的计算,并成为许多精度相关安全事件的典型例子,这些事件严重考验了社区。此外,经过与当局的广泛协商,攻击者发表了一份挑衅性的公开声明,要求完全控制该协议。
阅读本系列的其余文章:
- 引言:2023 年十大“精彩”安全事件
- #1:利用 Flashbots Relay 中的漏洞 Harvesting MEV Bots
- #2:Euler Finance 事件:2023 年最大的黑客事件
- #4:Curve 事件:编译器错误导致无害源代码生成有缺陷的字节码
- #5:Platypus Finance:靠运气幸免于三次攻击
- #6:Hundred Finance 事件:催化了易受攻击的 Fork 协议中精度相关漏洞的浪潮
- #7:ParaSpace 事件:与时间赛跑,阻止行业最关键的攻击
- #8:SushiSwap 事件:笨拙的救援尝试导致一系列模仿攻击
- #9:MEV Bot 0xd61492:从掠食者到猎物,一次巧妙的利用
- #10:ThirdWeb 事件:受信任模块之间的不兼容性暴露了漏洞





