Cetus AMM 在 Sui 网络上遭受了 2 亿美元的黑客攻击,根本原因是流动性计算函数中的一个缺陷,该缺陷未能正确检测大数值计算中的溢出。攻击者利用此漏洞以极低的成本创建了大量的流动性头寸,并随后排干了资金池。该漏洞之前在 Aptos 的代码中就被发现过,但在移植到 Sui 时被重新引入。
Dedaub
2025 年 5 月 22 日,Sui 网络上的 Cetus AMM 遭受了一次毁灭性的攻击,导致超过 2 亿美元的损失。这一事件是近期历史上最重大的 DeFi 攻击事件之一,起因是“溢出”保护中一个微妙但至关重要的缺陷。本分析剖析了该漏洞的技术细节,并 بررسی 当时何时引入、修复和重新引入此问题的。
攻击者利用了 Cetus AMM 中截断流动性计算函数中最有效位的漏洞。当用户打开 LP 仓位时,会调用此计算。当打开这样的仓位时,用户可以通过指定“流动性”参数(你希望获得的池子的比例)并提供相应数量的代币来打开一个大或小的仓位。通过将流动性参数操纵到一个极高的值,他们在中间计算中导致了一个溢出,由于一个有缺陷的截断检查,这个溢出没有被检测到。这使得他们可以用仅仅 1 个单位的代币输入添加大量的流动性仓位,随后耗尽了总共包含数亿美元价值代币的池子。
注意:此问题的技术术语不是“溢出”,而是 MSB(最高有效位)截断,但为了简单起见,我们称其为“溢出”。
攻击以精心策划的序列展开。以下是其中一个攻击交易的示例(简化):
根本原因在于 clmm_math.move
中的 get_delta_a
函数,它计算给定流动性数量所需的 A 代币数量:
public fun get_delta_a(
sqrt_price_0: u128,
sqrt_price_1: u128,
liquidity: u128,
round_up: bool
): u64 {
let sqrt_price_diff = sqrt_price_1 - sqrt_price_0;
let (numberator, overflowing) = math_u256::checked_shlw(
// Dedaub: 结果不适合 192 位
full_math_u128::full_mul(liquidity, sqrt_price_diff)
);
// Dedaub: checked_shlw “溢出” 结果,因为它 << 64
assert!(overflowing);
let denominator = full_math_u128::full_mul(sqrt_price_0, sqrt_price_1);
let quotient = math_u256::div_round(numberator, denominator, round_up);
(quotient as u64)
}
使用交易中的实际值:
liquidity
:10,365,647,984,364,446,732,462,244,378,333,008(大约 2^113)sqrt_price_0
:60,257,519,765,924,248,467,716,150 (tick 300000)sqrt_price_1
:60,863,087,478,126,617,965,993,239 (tick 300200)sqrt_price_diff
:605,567,712,202,369,498,277,089 (大约 2^79)关键计算:
numerator = checked_shlw(liquidity * sqrt_price_diff)
= checked_shlw(~2^113 * ~2^79)
= checked_shlw(2^192 + ε)
// checked_shlw 将一个 256 位寄存器左移 64 位
= ((2^192 + ε) * 2^64) mod 2^256
= ε
此乘法产生的结果超过 192 位。当此值在 checked_shlw
中左移 64 位时(即,“checked shift left by one 64-bit word”),它会溢出一个 256 位整数,但为此检查设计的溢出检查失败。
但是等等。不是应该有一个经过检查的操作来防止这个问题吗?
关键缺陷在于 checked_shlw
函数:
public fun checked_shlw(n: u256): (u256, bool) {
let mask = 0xffffffffffffffff << 192; // 这是不正确的!
if (n > mask) {
(0, true)
} else {
((n << 64), false) // 溢出的确切位置
}
}
掩码计算 0xffffffffffffffff << 192
没有产生预期的结果。开发人员可能想要检查 n >= (1 << 192)
,但实际的掩码并没有达到这个目的。因此,大多数大于 2^192 的值都会未经检测地通过,随后的左移 64 位会在 Move 中导致静默溢出(这不会触发移位操作的运行时错误)。
在 Move 中,围绕整数运算的安全性旨在防止溢出和下溢,这些溢出和下溢可能会导致意外行为或漏洞。具体来说:
对于具有选中算术的语言来说,当位移截断结果时,不触发错误是正常的。大多数智能合约审计师都明白这一点。
由于溢出,numerator
回绕成一个非常小的值。当除以分母时,它会产生一个接近于 0 的商。这意味着该函数返回只需要 1 个单位的 A 代币即可铸造大量的流动性仓位。
用数学术语来说:
值得注意的是,攻击中涉及的数值是经过精确计算的——攻击者利用合约中的一些现有函数来计算这些数值,特别是 get_liquidity_from_a
。
Ottersec 的审计 在早期版本的代码(2023 年初)中发现了一个非常相似的溢出漏洞,专门为 Aptos 设计:
“在
numberator
值上运行u256::shlw
之前,没有对其进行验证。因此,非零字节可能会被删除,从而导致错误的值计算。”
他们建议用 u256::checked_shlw
替换 u256::shlw
并添加溢出检测,这解决了该问题。请注意,此版本的代码具有 256 位无符号整数运算的自定义实现,因为 Aptos 当时不支持此原生实现。Move 2 / Aptos CLI ≈ v1.10 于 2024 年初推广到主网。
非常不幸的是,当团队几个月后将代码移植到 SUI 时(Sui 始终支持 256 位整数),在checked_shlw
中引入了一个错误。Ottersec 和 MoveBit 对此版本的 AMM 的审计没有发现此问题。Zellic 在 2025 年 4 月进行的后续审计 没有发现超出信息性发现的问题。执行数值计算的库代码可能超出了范围,而且,由于原生支持 256 位运算,因此可能会忽略这些问题。
在 DeFi 中,边缘情况不是边缘情况——它们是攻击向量。AMM 特别容易受到攻击,因为它们涉及跨越极端范围的复杂数学运算。Cetus 攻击表明,即使是“checked”操作也需要仔细验证。
Cetus 攻击有力地提醒我们,DeFi 中的安全性很难,但并非无法实现。一个有缺陷的溢出检查,加上闪电贷的可组合性和集中流动性机制,导致了超过 2 亿美元的盗窃。
对于在 Sui 和 Aptos 等基于 Move 的链上构建的开发人员来说,此事件强调了理解你的语言的整数语义、严格测试边缘情况以及与深入了解平台和 DeFi 领域的审计师合作的重要性。
如果你需要帮助保护你的 Aptos 或 Sui 网络项目,请通过 Dedaub 与我们联系——我们的团队专注于复杂 DeFi 协议中出现的数学复杂性和边缘情况。
- 原文链接: dedaub.com/blog/the-cetu...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!