背景
2025年5月22日、SUIでCetusへの攻撃を監視しました。
https://suiscan.xyz/mainnet/tx/DVMG3B2kocLEnVMDuQzTYRgjwuuFSfciawPvXXheB3x
この攻撃により、総額2億2300万ドルの損失が発生した。
攻撃とインシデントの分析
攻撃者はまず flash_swap を使用して haSUI を SUI に交換します。 flash_swap は swap と flashloan のバリエーションです。 token0 から token1 への flash_swap では、最初に token1 を取得し、次に repay_flash_swap を通じて token0 を支払うことができます。

上記の操作により、攻撃者は 5,765,124.79 SUI を取得し、同じトランザクションで 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 を通じて流動性ポジションを作成しました。このポジションの tickLower は 300000、 tickUpper は 300200 でした。


ティック範囲に対応する価格範囲は次のとおりです。

その後、攻撃者はこの価格帯で 10,365,647,984,364,446,732,462,244,378,333,008 の流動性を追加しました。

流動性を追加するためのコードを見てみましょう。

流動性を追加するために必要な amount_a と amount_b は get_amount_by_liquidity によって取得されることがわかります。次に、get_amount_by_liquidity の具体的な実装と対応するパラメータを見てみましょう。

arg2 は currentTick であり、currentTick = -138185 は arg0 lowerTick より小さいです。次のコードは

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 の具体的な実装は次のとおりです。

Cetus の根本的な問題は、checked_shlw 関数にあります。この関数の具体的なコードを見てみましょう。

この関数のロジックは非常に単純です。数値を 1 ワード (64 ビット) 左にシフトします。オーバーフローが発生した場合は 0 を返し、オーバーフローが発生しない場合は左シフト後の数値を返します。したがって、左シフト後にオーバーフローが発生したかどうかを検出するには、input > 2 ^ (256 - 64) - 1 かどうかを判断する必要があります。しかし、コードでは input > 0xffffffffffffffff << 192 かどうかを判断します。(0xffffffffffffffff << 192) > 2 ^ (256 - 64) であるため、コードによって入力数 2 ^ (256 - 64) < input <(0xffffffffffffffff << 192) が切り捨てられて返され、オーバーフロー検出が失敗します。この時点で、攻撃者によって構築されたデータは次のとおりです。

つまり、入力 = 10365647984364446732462244378333008 * 605567712202369498277089 =6277101735386680763835789423207666416102355444464034512896 となり、入力は 2^(256-64) より大きいですが、入力は 0xffffffffffffffff << 192 より小さくなります。したがって、左にシフトすると必ずオーバーフローが発生しますが、プログラムのオーバーフロー検出は失敗します。オーバーフローが発生した後、v1 = 流動性 * (upperSqrtPriceX64 - lowerSqrtPriceX64) - 2 ^ (256 - 64) = 491983144293873864340816 となります。
この値は lowerSqrtPriceX64 * upperSqrtPriceX64 よりもはるかに小さいため、get_delta_a の戻り値は 0 になります。したがって、攻撃者は 10365647984364446732462244378333008 の流動性を追加するために 1 token_a (p + 1) を支払うだけで済みます。

その後、攻撃者はremove_liquidityを通じて追加された流動性を削除し、repay_flash_swapを通じてflash_swapの未払いトークンを支払うことで攻撃を完了しました。攻撃者は5,765,124 SUIと10,024,321 haSUIの利益を得ました。最終的に、Cetus チームは 2 つの PR を通じて脆弱性を修正しました。
https://github.com/CetusProtocol/integer-mate/pull/6/files

最初の修正では脆弱性は完全には修正されませんでした。 mask = 1 << 192 = 2 ^ 256 なので、64 ビットを左シフトした後のオーバーフロー切り捨てを回避するには、n >= mask または n > mask - 1 の場合にのみ完全に修正できます。
https://github.com/CetusProtocol/integer-mate/pull/7/files

要約する
この脆弱性の原因は、関数が左シフトオーバーフローをチェックするときにマスクの間違った値を選択し、オーバーフローチェックが失敗し、対応する値が切り捨てられることです。最終的に、攻撃者はコードロジックに準拠していない小さなトークンを使用して、大量の流動性を追加することになりました。プロジェクト関係者は、経済モデルやコード操作ロジックを設計する際に多者間検証を実施し、契約がオンラインになる前に相互監査のために複数の監査会社を選択するようにすることをお勧めします。
