Background
On May 22, 2025, we monitored an attack on Cetus on SUI:
https://suiscan.xyz/mainnet/tx/DVMG3B2kocLEnVMDuQzTYRgjwuuFSfciawPvXXheB3x
The attack caused a total loss of 223M USD
Attack and incident analysis
The attacker first uses flash_swap to exchange haSUI for SUI. flash_swap is a variant of swap and flashloan. In the flash_swap of token0 to token1, token1 can be obtained first, and then token0 can be paid through repay_flash_swap.

Through the above operations, the attacker obtained 5,765,124.79 SUI and needed to pay 10,024,321.29 haSUI in the same transaction. The sqrtPriceX64 of the haSUI in the pool changed from 18,956,530,795,606,879,104, tick = 545 to 18,425,720,184,762,886, tick = -138185, which is equivalent to a price reduction from the original 1.056 to 0.0000009977, a significant reduction to 0.00009% of the original price.
Then, the attacker created a Liquidity Position through open_position, where the tickLower of the Position was 300000 and the tickUpper was 300200.


The price range corresponding to the tick range is:

The attacker then added 10,365,647,984,364,446,732,462,244,378,333,008 liquidity within this price range.

Let's look at the code for adding liquidity:

It can be seen that amount_a and amount_b required to add liquidity are obtained by get_amount_by_liquidity. Next, let's take a look at the specific implementation and corresponding parameters of get_amount_by_liquidity.

Since arg2 is currentTick, and currentTick = -138185 is less than arg0 lowerTick. Therefore, the code will go to

Since arg0 is lowerTick, cetus::tick_math::get_sqrt_price_at_tick(arg0) = 60257519765924248467716150, and arg1 is upperTick, cetus::tick_math::get_sqrt_price_at_tick(arg1) = 60,863,087,478,126,617,965,993,239.
The specific implementation of the function get_delta_a is as follows:

The core problem of Cetus lies in the checked_shlw function. Let’s look at the specific code of this function:

The logic of this function is very simple. It shifts a number left by one word, which is 64 bits. If an overflow occurs, it returns 0. If no overflow occurs, it returns the number after left shift. Therefore, to detect whether there is an overflow after left shift, it is necessary to judge whether input > 2 ^ (256 - 64) - 1. However, the code judges that input > 0xffffffffffffffff <<192. Since (0xffffffffffffffff << 192) > 2 ^ (256 - 64), the code will cause the input number to be truncated and returned when 2 ^ (256 - 64) < input <(0xffffffffffffffff << 192), and the overflow detection fails. At this time, the data constructed by the attacker is:

Therefore, input = 10365647984364446732462244378333008 * 605567712202369498277089 =6277101735386680763835789423207666416102355444464034512896 , input is greater than 2^(256-64), but input is less than 0xffffffffffffffff << 192 . Therefore, overflow will inevitably occur when shifting left, but the overflow detection of the program fails. After the overflow occurs, v1 = liquidity * (upperSqrtPriceX64 - lowerSqrtPriceX64) - 2 ^ (256 - 64) = 491983144293873864340816.
Since this value is much smaller than lowerSqrtPriceX64 * upperSqrtPriceX64, the final return value of get_delta_a is 0. Therefore, the attacker needs to pay token_a of 1 (p + 1) to add 10365647984364446732462244378333008 of liquidity.

Subsequently, the attacker removed the added liquidity through remove_liquidity and completed the attack by paying the unpaid tokens of flash_swap through repay_flash_swap. The attacker made a profit of 5,765,124 SUI and 10,024,321 haSUI. Finally, the Cetus team fixed the vulnerability through two PRs:
https://github.com/CetusProtocol/integer-mate/pull/6/files

The first fix did not completely fix the vulnerability. mask = 1 << 192 = 2 ^ 256, so only n >= mask or n > mask - 1 can it be completely fixed to avoid overflow truncation after left shifting 64 bits:
https://github.com/CetusProtocol/integer-mate/pull/7/files

Summarize
The cause of this vulnerability is that the function selected the wrong value of mask when detecting left-shift overflow, which caused the overflow detection to fail and the corresponding value to be truncated. Ultimately, the attacker added a huge amount of liquidity with a very small token that did not conform to the code logic. It is recommended that the project party conduct multi-party verification when designing the economic model and code operation logic, and try to select multiple audit companies for cross-audit when auditing the contract before it goes online.
