我们监控到 SUI 上针对 Cetus 的攻击事件,攻击共造成 223M USD 的损失
<!--StartFragment-->
2025年5⽉22⽇,我们监控到 SUI 上针对 Cetus 的攻击事件:
https\://suiscan.xyz/mainnet/tx/DVMG3B2kocLEnVMDuQzTYRgjwuuFSfciawPvXXheB3x
攻击共造成 223M USD 的损失。
<!--EndFragment-->
<!--StartFragment-->
攻击者⾸先通过 flash_swap 来将 haSUI 兑换为 SUI , flash_swap 是 swap 和 flashloan 的变体,在 token0 对 token1 的 flash_swap 中,可以先获得 token1 ,再通过 repay_flash_swap 来⽀付 token0 。
<!--EndFragment-->
<!--StartFragment-->
通过上述操作,攻击者获得了 5,765,124.79 SUI ,需要在同⼀个 transaction 中⽀付 10,024,321.29 haSUI 。且该池⼦的 haSUI 的 sqrtPriceX64 由 18,956,530,795,606,879,104 , tick = 545 ,变为了 18,425,720,184,762,886 , tick =-138185 ,相当于价格由原先的 1.056 降低到了 0.0000009977 ,价格⼤幅降低⾄原来的 0.00009% 。
随后,攻击者通过 open_position 创建了⼀个 Liquidity Position ,其中该 Position 的 tickLower 为 300000 , tickUpper 为 300200 。
<!--EndFragment-->
<!--StartFragment-->
tick 区间对应的 price 的区间为:
<!--EndFragment-->
<!--StartFragment-->
随后,攻击者在该价格区间内添加流动性 10,365,647,984,364,446,732,462,244,378,333,008 。
<!--EndFragment-->
<!--StartFragment-->
我们看⼀下添加流动性的代码:
<!--EndFragment-->
<!--StartFragment-->
可以看出,添加流动性需要的 amount_a 和 amount_b 由 get_amount_by_liquidity 得出,接下来,我们看⼀下get_amount_by_liquidity 的具体实现和对应的参数。
<!--EndFragment-->
<!--StartFragment-->
由于 arg2 为 currentTick ,且 currentTick = -138185 ⼩于 arg0 lowerTick 。所以,接下来代码会走到
<!--EndFragment-->
<!--StartFragment-->
由于 arg0 为 lowerTick ,所以 cetus::tick_math::get_sqrt_price_at_tick(arg0) = 60257519765924248467716150 ,且 arg1 为 upperTick , cetus::tick_math::get_sqrt_price_at_tick(arg1) = 60,863,087,478,126,617,965,993,239 。
函数 get_delta_a 的具体实现如下:
<!--EndFragment-->
<!--StartFragment-->
Cetus 的核⼼问题出在 checked_shlw 函数上,我们看该函数的具体代码:
<!--EndFragment-->
<!--StartFragment-->
该函数的逻辑很简单,将⼀个数左移动⼀个 word ,就是64位。如果发⽣溢出,就返回0,如果没发⽣溢出就返回左移后的数。所以,检测左移后是否溢出需要判断 input > 2 ^ (256 - 64) - 1 ,然⽽,代码中却判断 input > 0xffffffffffffffff <<192 ,由于 (0xffffffffffffffff << 192) > 2 ^ (256 - 64) ,所以该代码会导致 input 在 2 ^ (256 - 64) < input <(0xffffffffffffffff << 192) 的数字被截断返回,且溢出检测失效。此时,攻击者构造的数据为:
<!--EndFragment-->
<!--StartFragment-->
所以, input = 10365647984364446732462244378333008 * 605567712202369498277089 =6277101735386680763835789423207666416102355444464034512896 , input ⼤于 2^(256-64) ,但是 input ⼩于 0xffffffffffffffff << 192 。所以,左移时必然发⽣溢出,但是程序的溢出检测失败。发⽣溢出后, v1 = liquidity * (upperSqrtPriceX64 - lowerSqrtPriceX64) - 2 ^ (256 - 64) =491983144293873864340816 。
由于,该值远⼩于 lowerSqrtPriceX64 * upperSqrtPriceX64 ,最终 get_delta_a 的返回值为0。因此,攻击者需要⽀付的token_a 为1(p + 1),即可添加 10365647984364446732462244378333008 的 liquidity 。
<!--EndFragment-->
<!--StartFragment-->
随后,攻击者通过 remove_liquidity 将添加的流动性移除,并通过 repay_flash_swap ⽀付 flash_swap 未⽀付的 token 后完成攻击,攻击者获利 5,765,124 SUI 和 10,024,321 haSUI 。最后,Cetus团队通过两个PR完成了对该漏洞的修复:
https\://github.com/CetusProtocol/integer-mate/pull/6/files
<!--EndFragment-->
<!--StartFragment-->
第⼀次修复没有完全修复该漏洞, mask = 1 << 192 = 2 ^ 256 ,所以只有 n >= mask 或 n > mask - 1 ,才能完全修复,避免左移64位后发⽣溢出截断:
https\://github.com/CetusProtocol/integer-mate/pull/7/files
<!--EndFragment-->
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!