Cetus Protocol CLMM checked_shlw truncation / liquidity-overmint pool drain (2025-05-22)

  • Name: Cetus Protocol CLMM checked_shlw truncation / liquidity-overmint pool drain
  • Date: 2025-05-22
  • Protocol: Cetus Protocol
  • Chain: Sui
  • Category: concentrated-liquidity math truncation / liquidity overmint / pool drain
  • Loss: Cetus publicly disclosed about $223M stolen; third-party postmortems often described more than $260M drained before freezes, with about $162M later frozen on Sui and about $61.5M bridged to Ethereum
  • Summary: Cetus’s Sui CLMM relied on integer_mate::math_u256::checked_shlw() inside clmm_math::get_delta_a() to detect whether a 256-bit intermediate could be shifted left by 64 bits safely. The guard used 0xffffffffffffffff << 192 instead of the intended 1 << 192, so huge u256 values that should have been rejected slipped through and then wrapped during << 64. In narrow-range positions near the edge of the active price, that wrap collapsed the required token-A deposit from an enormous amount to roughly 1 unit while still minting liquidity as if the attacker had funded a pool-scale position. The attacker repeated the sequence across multiple pools: flash-swap into one side, open a narrow position, add near-full-pool liquidity for almost no cost, remove it correctly, and keep the drained counterasset.
  • Trigger / entry point:
    • Cetus CLMM’s liquidity-add path priced token-A requirements through clmm_math::get_delta_a().
    • get_delta_a() multiplied liquidity * (sqrt_price_1 - sqrt_price_0), then called math_u256::checked_shlw() to scale the result by 2^64 before division by sqrt_price_0 * sqrt_price_1.
    • The overflow guard in checked_shlw() used the wrong bound, so values above the safe 192-bit threshold often returned (n << 64, false) instead of aborting.
    • Move does not trap on left-shift truncation, so the wrapped 256-bit value flowed onward into a tiny numerator and therefore a tiny token-A quote.
  • Root cause:
    • Cetus trusted a homegrown multiprecision helper in the shared integer-mate library to prove that a left shift could not truncate meaningful bits.
    • The helper’s constant was wrong: 0xffffffffffffffff << 192 instead of the intended 1 << 192 threshold.
    • The CLMM then treated the wrapped result as if it were a valid high-precision intermediate and minted liquidity against the bogus quote.
    • In effect, the protocol separated the what liquidity should this position receive? calculation from the how much token A must be paid? calculation, and the arithmetic fault corrupted only the payment side.
  • Exploit path:
    1. The attacker targeted pools where an extremely narrow tick range near the active boundary made liquidity-for-token-A math especially sensitive to the << 64 scaling step.
    2. They used flash swaps to borrow one pool asset, often the token-A side used by the broken quote path, and positioned themselves with enough inventory to open and later unwind a very large CLMM position.
    3. They opened a new position with a narrow range such as [300000, 300200] and called the liquidity-add path with a liquidity target on the order of the pool’s existing liquidity.
    4. Inside clmm_math::get_delta_a(), the product liquidity * sqrt_price_diff exceeded the safe width, but checked_shlw() failed to report overflow and returned the truncated u256 residue after << 64.
    5. Because the numerator had wrapped to a tiny value, the division path concluded that only about 1 unit of token A was required, even though the minted position corresponded to pool-scale liquidity.
    6. The attacker then removed liquidity using correctly valued downstream math, received large quantities of the counterasset, repaid the flash swap, and kept the remainder as profit.
    7. The sequence was replayed across many Sui ecosystem pools until Cetus paused contracts and validators / infrastructure participants froze a large share of remaining funds on Sui.
  • What the attacker needed to be true:
    • The targeted pool needed to expose the broken get_delta_a() branch, which depended on tick range, current sqrt price, and a very large requested liquidity amount.
    • The attacker needed enough temporary capital or flash-swap access to set up the narrow-range position and repay the borrowed leg after draining the pool.
    • The wrapped intermediate had to stay small enough that the required token-A quote rounded to a trivial amount rather than reverting or demanding meaningful payment.
    • Liquidity removal and flash-swap repayment logic had to continue honoring the counterfeit position as if it had been backed by real liquidity.
  • Technical anatomy worth remembering:
    • This was not a stale oracle or governance failure. The exploit primitive was a wrong-width arithmetic proof in a shared math helper.
    • The bug lived in a library boundary, not just in one pool contract. integer_mate::math_u256::checked_shlw() was supposed to make wide-integer scaling safe for all callers.
    • The most important asymmetry was payment side broken, redemption side sound enough to cash out. The add-liquidity path undercharged; the remove-liquidity path still released real assets.
    • Move’s checked arithmetic story did not save the protocol because left shifts are not equivalent to checked multiplication. Truncating bits during << remained a separate hazard.
    • Cetus had already faced a related issue historically: an earlier Aptos audit had warned about truncation risk around shlw, but the Sui-port guard reintroduced the problem through a faulty custom check.
  • Representative transactions and addresses:
    • Representative attack tx block highlighted by Dedaub: DVMG3B2kocLEnVMDuQzTYRgjwuuFSfciawPvXXheB3x
    • Representative attack tx block highlighted by Verichains: ETCaBBiffASZ3oXBBcoM6VYd3NcTb5T1Sqo4xLECKZws
    • Vulnerable math library package: 0x714a63a0dba6da4f017b42d5d0fb78867f18bcde904868e51d951a5a6f5b7f57
    • Emergency integer-mate Sui release: sui-v1.1.1, published from commit fc84a51c29fabfbfbdb8ed76506b61a07493d492
    • Patched published-at package id in Move.toml: 0xdd45037f3f99cb2e43b94599777c50db5f8c44b6657ed1d2f9b227115d29edef
  • Response / mitigation:
    • Cetus acknowledged the hack on 2025-05-22, paused contracts, and published an emergency fix to integer-mate the same day.
    • The sui-v1.1.1 release notes for integer-mate explicitly framed the emergency patch as Fix checked_shlw in math_u256 module, which is a useful primary-source confirmation that the protocol’s first response was library-level rather than pool-specific.
    • Sui ecosystem participants froze a large portion of the remaining funds on Sui, while the attacker still bridged about $61.5M to Ethereum according to public analysis.
    • The Sui Foundation then facilitated the recovery-vote process for a narrowly scoped protocol upgrade that would let a designated address recover funds frozen in two attacker accounts.
    • In the public incident-response writeup, the Sui Foundation described the proposition as a one-off protocol upgrade limited to two pre-specified recovery transactions, and later noted that validators representing 90.9% of stake had voted yes.
    • After the vote, the frozen assets were moved into a 4-of-6 trust multisig split across Cetus, the Sui Foundation, and OtterSec for user recovery.
    • The episode also triggered wider review pressure on other Sui projects using the same library pattern or similar hand-rolled wide-integer math.

Pattern tags

  • issuance-redemption-solvency-asymmetry
  • The economically decisive break was add-liquidity under arithmetic fiction, remove-liquidity under reserve reality: Cetus minted a pool-scale LP claim after the token-A payment quote had already stopped meaningfully certifying cost, then later honored withdrawal against ordinary pool assets.
  • checked_shlw() functioned as a library-level proof object for this wide intermediate still fits safely inside the protocol's quote math; once that helper lied, higher-level CLMM mint logic kept treating counterfeit payment sufficiency as real.
  • The exploit also belongs to the narrower shared helper became the trust boundary subfamily: one reusable arithmetic guard in integer-mate quietly defined whether many callers were allowed to believe that left-shift scaling had preserved economic meaning.

Why the exploit worked

  • The exploit worked because Cetus let one shared arithmetic helper stand in for economic payment truth. get_delta_a() was supposed to tell the add-liquidity path how much token A a pool-scale liquidity request really cost. Once checked_shlw() stopped detecting dangerous << 64 truncation, that quote path no longer certified a real deposit requirement even though the surrounding CLMM logic still treated it as canonical.
  • The sharp asymmetry was not liquidity math got a little inaccurate; it was issuance side broke while redemption side kept meaning something. The add-liquidity path undercharged by collapsing an enormous token-A requirement to roughly 1, but the remove-liquidity path and swap-facing reserve accounting still paid out against actual pool assets. That is why the drain looked like a mint exploit wearing AMM clothing.
  • The bug’s location inside integer-mate mattered as much as the bad constant itself. A local math mistake in one pool can sometimes be quarantined conceptually as pool-specific logic. A helper named checked_shlw() invites every downstream caller to rely on it as a machine-checkable overflow proof, which means failure at that boundary automatically scales into a protocol-wide trust problem.
  • The exploit also depended on shape, not just magnitude. Narrow ranges near the active boundary made the wrapped numerator economically decisive: the counterfeit quote stayed tiny enough to pass as a valid payment amount while the minted liquidity was still large enough to cash out meaningfully on removal.
  • The Sui-side recovery story reinforces the same point from the opposite direction: the public mitigation artifacts centered on the shared helper and the frozen funds, not on any claim that the original positions had been economically valid. Once the arithmetic proof failed, the ecosystem treated the resulting LP claims as counterfeit state that had to be contained and unwound socially.

Detection and audit pivots

  • Treat every checked_*, safe_*, or width-guard helper used in issuance, liquidity-add, or collateral-admission math as solvency code. If higher-level state transitions proceed whenever that helper returns ok, the helper is already part of the reserve boundary.
  • Differential-test add liquidity -> remove liquidity immediately across edge ticks, narrow ranges, huge requested liquidity, and maximum-width intermediates. The reviewer question is not only does math revert on overflow? but can the protocol mint a claim whose burn path still honors more value than the mint path really collected?
  • Review left-shift, scaling, and fixed-point helpers separately from multiplication / division helpers. Cetus is a reminder that a language with checked arithmetic can still hide truncation risk in bit-shift semantics.
  • Force invariant tests to compare quoted payment, actual reserve delta, and minted liquidity as one object. If any one of those can drift while the others still look valid locally, the protocol may already be issuing pool claims under fiction.
  • Treat shared math packages as ecosystem-level blast-radius surfaces. Once a protocol imports a common wide-integer helper, disclosure and patch response should inventory every funded caller rather than only the first drained product.

Failure-prevention checklist

  • Treat checked_*, shift-guard, and fixed-point helpers as reserve-boundary code. Keep their threshold derivations explicit, review them like custody logic, and do not let higher-level minting paths assume helper returned a value means economic truth was proven.
  • Differential-test every solvency-critical math helper against a wider-precision oracle at bound - 1, bound, and bound + 1, then repeat those tests through real caller paths like add liquidity -> remove liquidity rather than only at the helper layer.
  • Bind quoted payment, actual token transfer, and minted liquidity together as one invariant. A CLMM should never mint a large position unless the reserve delta that actually entered the pool stays coherent with that issued liquidity.
  • Fail closed on shift and cast ambiguity. If a scale-up step cannot prove exact fit, the protocol should reject the state transition instead of carrying forward a wrapped value that still looks numerically plausible.
  • Inventory shared-library blast radius ahead of time. When a common math package changes, operators should know which pools, routers, and downstream products inherit that proof object.
  • Add route-local loss bounds around unusually large or boundary-shaped liquidity adds so one arithmetic helper failure cannot immediately turn into protocol-wide pool drains.
  • Re-audit portability assumptions whenever math code moves across runtimes or chains. Checked arithmetic in the host language does not automatically preserve the same safety story for shifts, truncation, and custom width guards.

Audit prompts

  • What exact inequality proves that liquidity * (sqrt_price_1 - sqrt_price_0) << 64 still fits the protocol’s intended width, and where is that derivation written down for reviewers?
  • Can any liquidity-add path mint a position from a quoted amount that is not cross-checked against the actual reserve delta that entered the pool?
  • If a user adds liquidity and removes it immediately at the same price range, can the protocol ever return materially more value than the mint path required them to contribute?
  • Which operations in this math path depend on shift or truncation semantics rather than ordinary checked multiplication and division, and are those operations guarded independently?
  • If checked_shlw() or a similar helper is wrong, which other markets, vaults, or routers inherit the same counterfeit-quote risk?
  • Which tick-boundary or narrow-range states make a width bug economically dangerous instead of merely producing a bad quote that still reverts?
  • What pool-local caps, pauses, or anomaly detectors would fire if a pool-scale position suddenly required only dust-sized payment?
  • Did testing compare production math against a wider reference implementation, or did the same local assumptions get copied into both code and tests?

Lessons that should already be standard

  • A checked-looking math helper is treasury code once issuance or liquidity logic starts trusting it as proof that payment math stayed real.
  • CLMM exploits do not need a broken price oracle if the protocol can mint pool claims under arithmetic fiction and later redeem them against honest reserves.
  • Language-level checked arithmetic is not a complete safety story. Shifts, casts, and truncation rules still need explicit review and explicit proofs.
  • Shared math libraries deserve fleet-wide blast-radius thinking. One bad literal in a common helper can become many pools’ solvency bug at once.
  • Boundary-state and narrow-range tests are not cosmetic for concentrated liquidity. They are often where finite-width math stops behaving like the algebra auditors think they are reviewing.
  • Social recovery and validator freezes may limit loss, but they should not soften the preserved lesson: the original failure was still an issuance-side solvency break at the math-helper boundary.

Why this incident matters

  • Cetus is a crisp exemplar of how checked-looking arithmetic helpers can be more dangerous than obviously unchecked code when everyone downstream treats them as proven-safe guards.
  • It preserves a different AMM failure mode than the usual invariant or oracle cases: the pool was not mispriced first; the attacker minted a claim on pool liquidity by collapsing the payment-side quote.
  • It is also a good reminder that concentrated-liquidity protocols inherit unusual attack surface from numeric edge cases near tick boundaries, especially when they use custom multiprecision math.
  • The most reusable design lesson is that a shared math helper like checked_shlw() is not just library plumbing once liquidity-add logic treats it as the proof that a pool-scale quote still fits safely inside the protocol’s arithmetic model.

Internal linkages

Control surface