Back to Blog

〜598万ドルの損失:Aztec、Raydiumなど|BlockSec週次レポート

Code Auditing
June 17, 2026
13 min read
Key Insights

先週(2026/06/08 - 2026/06/15)の期間中、EthereumおよびSolanaにわたり4件の注目すべきインシデントが検出され、総損失額は約$5.98Mに上りました。以下の表に代表的なイベントを示します。

日付 インシデント 種別 推定損失
2026/06/08 Flooring Protocol 整数オーバーフロー ~$900K
2026/06/09 Top Token ガバナンス攻撃 ~$1.59M
2026/06/10 Raydium(Solana上) 入力検証の欠如 ~$1.34M
2026/06/15 Aztec 入力検証の欠如 ~$2.15M
  • Aztec: ロールアップの証明パスとL1決済パスの間にある検証のギャップにより、両者が異なるトランザクションセットを処理し、不整合な状態に至った。
  • Raydium: 検証チェックの欠如により、攻撃者がLPトークンの償還計算を操作し、4つのプールから全準備金を流出させた。

Web3向け最高のセキュリティ監査機関

ローンチ前に設計・コード・ビジネスロジックを検証

今週のハイライト:Aztec

このインシデントでは、ZK証明の検証器とL1決済ロジックが、1つのパラメータが無制限のまま放置されていたため、異なるトランザクションセットを処理しました。この証明と決済の間の整合性ギャップは、これら2つのパスが別々のコードとして実行されるすべてのロールアップ設計に当てはまります。

2026年6月15日、Ethereum上のプライバシー重視のロールアップであるAztec Connectが、約$2.15Mの被害を受ける攻撃を受けました [1]。根本原因は、検証済みロールアップトランザクションセットとL1決済処理境界の不一致にあり、ZK証明パスと決済ロジックが異なるトランザクションリストを処理することを可能にしました。攻撃者はこのギャップを悪用してロールアップ状態に裏付けのない入金残高を作り出し、通常の決済フローを通じてそれを引き出しました。

背景

Aztec ConnectはEthereum上のプライバシー重視のロールアップで、L2上でのプライベートトランザクションを可能にします。ユーザーの資金はL1上に存在するため、L2マークルツリー内のノートとして表現される前に、まずロールアッププロセッサコントラクトに入金する必要があります。

入金プロセスは2段階で機能します。

ステージ1: ユーザーがdepositPendingFunds()を呼び出すと、increasePendingDepositBalance()を通じてuserPendingDeposits[assetId][owner]が増加し、トークンがRollupProcessorに転送されます。これによりL1上に保留中の入金が作成されます。

function depositPendingFunds(uint256 _assetId, uint256 _amount, address _owner, bytes32 _proofHash) external {
    increasePendingDepositBalance(_assetId, _owner, _amount);
    // ... コントラクトにトークンを転送
}

ステージ2: ユーザーが入金証明を提出し、それが後にロールアップに組み込まれてL2状態に追加されます。processRollup()が実行されると、decodeProof()はエンコードされたcalldataからnumTxsを読み取り、デコードされた証明データと共に返します。両方がprocessRollupProof()に渡されます。

function processRollup(bytes calldata, bytes calldata _signatures) external {
    (bytes memory proofData, uint256 numTxs, uint256 publicInputsHash) = decodeProof();
    processRollupProof(proofData, _signatures, numTxs, publicInputsHash, rollupBeneficiary);
}

processRollupProof()内では、2つの関数が順番に呼び出されます。まずverifyProofAndUpdateState()がデコードされたすべてのトランザクションに対してZK証明を検証し、ロールアップ状態を更新します。次にprocessDepositsAndWithdrawals()がL1決済を処理し、最初の_numTxsスロットのみを反復して、各入金に対してdecreasePendingDepositBalance()を呼び出します(この呼び出しは、ユーザーがステージ1で実際に資金を入金していなかった場合にリバートし、ロールアップのクレジットを実際のL1転送に結び付けます)。

function processRollupProof(bytes memory _proofData, bytes memory _signatures,
    uint256 _numTxs, uint256 _publicInputsHash, address _rollupBeneficiary) internal {
    verifyProofAndUpdateState(_proofData, _publicInputsHash);       // 証明パス:すべてのデコードされたトランザクション
    processDepositsAndWithdrawals(_proofData, _numTxs, _signatures); // 決済パス:最初の_numTxsのみ
}
// processDepositsAndWithdrawals内:
end := add(proofDataPtr, mul(_numTxs, TX_PUBLIC_INPUT_LENGTH))
while (proofDataPtr < end) {
    // ... 各入金に対して:
    decreasePendingDepositBalance(assetId, publicOwner, publicValue);
}

この2段階設計では、L1決済ロジックがZK証明で検証されたものと全く同じトランザクションセットを処理する必要があります。2つのパスが処理するトランザクションで不一致が生じると、L1上の保留中残高を消費せずに入金がロールアップ状態にクレジットされる可能性があります。

脆弱性分析

ロールアッププロセッサコントラクト(0x7d65...2728)において、numTxsはZK証明によって強制されるトランザクションセットに効果的に束縛されていませんでした。そのため、証明パスと決済パスが異なるトランザクションリストを処理する可能性がありました。

オフチェーンのrollup_circuitでは、num_txsはウィットネスとしてロードされ、範囲制約のみが適用されます。サーキットはこれを使用してどのスロットが実際のトランザクションとして扱われるかをゲートしますが、num_txsがパディングでない証明の実際のカウントと等しいことを検証しません。

const auto num_txs = uint32_ct(witness_ct(&composer, rollup.num_txs));
field_ct(num_txs).create_range_constraint(MAX_TXS_BIT_LENGTH);
// ...
auto is_real = num_txs > uint32_ct(&composer, i);  // スロットごとに実トランザクションのロジックをゲート

プロバーは許可された範囲内でnum_txsを任意の値に設定できます。num_txsを超えるスロットは依然として再帰的に検証されますが、そのパブリック入力はゼロ化されるため、ロールアップ状態に寄与しません。

Solidity側では、decodeProof()verifyProofAndUpdateState()によって検証された再構築済みproofDataにコピーされないcalldataメタデータからnumTxsを読み取ります。そのため、決済ループの境界もZK証明によってカバーされていません。

どちら側もこの値を制約していないため、攻撃者はデコードされたトランザクションの実際の数よりも低い値にnumTxsを設定できました。すると決済ループは、証明がロールアップ状態にクレジットしたトランザクションをスキップします。実行不可能なトランザクションが最初のデコードされたスロット(決済スキャン範囲内)を占有し、実際の入金が後のスロット(サーキットによって証明されているが決済スキャン範囲外)に位置することができます。証明はロールアップ状態に入金をクレジットしますが、決済ロジックはdecreasePendingDepositBalance()の呼び出しを含め、それを完全にスキップします。これにより、ロールアップ状態がすでに入金を反映しているにもかかわらず、L1上の保留中入金残高が未消費のまま残りました。

攻撃分析

以下の分析はトランザクション0x074ec9...9aeeb1に基づいています。

攻撃者は証明パスと決済パスの間のギャップを2段階で悪用しました。

フェーズ1:裏付けのない残高の作成

  • ステップ1:攻撃者は複数のロールアップバッチを提出しました。各バッチには2つのデコードされたトランザクションが含まれていました。スロット1に実行不可能な(ジャンク)トランザクション、スロット2に実際の入金があり、numTxsは1に設定されていました。L1決済ロジックはスロット1のジャンクトランザクションのみを処理し、スロット2の実際の入金を完全にスキップしました。

  • ステップ2:しかしZK証明は、スロット2の入金を含むすべてのデコードされたトランザクションを検証してクレジットしました。決済ロジックがこの入金に到達しなかったため、decreasePendingDepositBalance()は呼び出されず、L1の保留中入金残高は未消費のまま残りました。攻撃者はこのパターンを7種類の異なる資産に対して繰り返し、ロールアップ状態に裏付けのない残高を積み上げました。

フェーズ2:資金の引き出し

  • ステップ3:7つの裏付けのない残高が確立されると、攻撃者は各資産の標準的な引き出しを開始しました。これらの引き出しは残高がロールアップ状態に存在していたため決済ロジックには正当に見え、L1コントラクトは対応する資金(合計約$2.15M)をリリースしました。

結論

この脆弱性は暗号学的な弱点ではなく、ロールアップアーキテクチャにおける2つの重要なコードパス間の状態整合性バグでした。根本原因:numTxsはどちら側においても証明済みトランザクションセットに束縛されていませんでした。サーキットは範囲制約のみを適用し、SolidityデコーダはZK証明でカバーされていないcalldataメタデータから読み取っていました。この束縛なしに、証明パスと決済パスは異なるトランザクションリストを処理できました。攻撃者は実際のトランザクション数よりも低い値にnumTxsを設定し、決済ロジックが証明によってすでにロールアップ状態にクレジットされた入金をスキップするようにしました。結果として生じた裏付けのない残高は、通常の決済フローを通じて引き出されました。

Aztec Connectロールアップはサンセットを発表し、トランザクション処理と引き出しは2024年3月31日をもって終了する予定でした [2]。しかし、ロールアッププロセッサコントラクトはプルリクエスト[3]を通じて2024年4月10日にもアップグレードされており、脆弱なロジックはそのサンセット後のアップグレードに存在しています。

修正には、ZK証明によって検証されたトランザクションの全セットにnumTxsを束縛し、両パスが常に同じセットを処理するようにすることが必要です。証明検証をL1決済から分離するすべてのロールアップ設計では、両パスが同一の検証可能な境界を持つトランザクションセットに対して動作することを強制する必要があります。たった1つのパラメータの不一致でも、それ以外は健全な証明システムが裏付けのない残高作成のベクターになりえます。

参考資料

Phalcon Explorerを始めよう

トランザクションを深く分析して賢明に行動する

今すぐ無料で試す

今週のその他のインシデント

Raydium

2026年6月10日、SolanaのRaydiumのレガシーAMM v3プログラム上の4つのプールが、約$1.34Mの被害を受ける攻撃を受けました [1]。引き出しハンドラは、呼び出し元が提供したアカウントがプールの保存された対応アカウントと一致するかを検証しておらず、攻撃者は制御下のアカウントに置き換えて支払い計算を操作しました。同じ手法で、数秒以内に4つのプールの全準備金が流出しました。

背景

RaydiumのAMMはSolana上の定積マーケットメーカーです。各プールは2つのトークンボールトを保有し、準備金の比例的な持分を表すLPトークンをミントします。流動性プロバイダーが引き出す際、ハンドラは比例的に支払いを計算し、両ボールトの対応するシェアを転送します。

coin_out = total_coin * withdraw_amount / lp_supply
pc_out   = total_pc   * withdraw_amount / lp_supply

Solanaでは、各トークン種別は総供給量、小数点以下桁数、ミント権限を保存するMintアカウントによって定義されます。各保有者の残高は、そのMintに束縛された別のTokenアカウントに保存されます(1つのMintは異なる保有者にわたって多くのTokenアカウントを持てます)。これはEVMとは異なり、単一のERC-20コントラクトがトークン定義とすべての残高を内部で管理します。

上記の引き出し計算式において、lp_supplyはプールのLP Mintアカウント(LP総供給量を追跡するもの)から読み取られます。計算の正確性はこの値が本物のLP Mintであることに依存しています。しかし、Solanaでは呼び出し元が各インストラクションにすべてのアカウントを位置的に渡すため、ハンドラは各呼び出し元提供のアカウントがプール状態に保存された正規のアカウントと一致することを検証する必要があります。

脆弱性分析

攻撃されたプログラム(27haf8...8vQv)はオープンソースではなく、その実行可能データ(ProgramData)は攻撃後にクローズされたため、直接のバイトコード検査は不可能でした。以下の分析は、プログラムの最後のアップグレードバッファから再構築されたバイトコードとオンチェーントランザクションの動作を相互参照することに基づいています。

引き出しハンドラにおいて、呼び出し元が渡したLP MintアカウントはプールのレコードされたAMMamm.lp_mintに束縛されていませんでした。オンチェーンバイトコードから再構築された以下のリバースエンジニアリング擬似コードはアカウントレイアウトを示しています。ハンドラはプール状態、PDA権限、両ボールト、ユーザーアカウントの束縛を確認しましたが、スロット5のLP Mintについては確認しませんでした。

let amm_info         = next_account_info(it)?;  // accounts[1] — プール状態(amm.lp_mintを保持)
// ...
let amm_lp_mint_info = next_account_info(it)?;  // accounts[5] — 呼び出し元提供のmint

let amm = AmmInfo::load(amm_info)?;
// authority、vaults、open_ordersの束縛はここで確認...
// >>> 欠如:accounts[5].key == amm.lp_mintの確認 <<<

let lp_mint = Mint::unpack(&amm_lp_mint_info.data.borrow())?;
let lp_mint_supply = lp_mint.supply;  // 未検証のmintから読み取り

let coin_amount = total_coin * withdraw_amount / lp_mint_supply;
let pc_amount   = total_pc   * withdraw_amount / lp_mint_supply;

LP Mintアカウントが束縛されていなかったため、攻撃者は完全に制御できるMintアカウントを代替することができました。総supplyを1に設定して1トークンをバーンすることで、1 / 1 = 100%の各準備金の支払い比率が得られました。

脆弱なコードは2023年1月3日のプログラムの最後のアップグレード以来、攻撃まで約1,254日間ライブで変更されていませんでした。

攻撃分析

以下の分析はトランザクション1csN6v...3s7sに基づいています。

  • ステップ1:攻撃者はdecimals = 0、総supply = 0のフェイクLP Mintアカウントを作成しました。
  • ステップ2:攻撃者はフェイクLP Mintに束縛されたTokenアカウントを初期化し、そこに正確に1トークンをミントし(Mint権限として)、Mintの総supplyを1に固定しました。
  • ステップ3:攻撃者は引き出し関数を呼び出し、期待されるアカウントスロットにフェイクLP Mintを渡し、ステップ2のTokenアカウント(1枚のフェイクLPトークンを保有)をLPソースとして渡しました。withdraw_amount = 1lp_supply = 1の場合、ハンドラはtotal_coin * 1 / 1total_pc * 1 / 1を計算し、これは両準備金の100%に等しくなりました(RAY/USDCプールでは893,700 USDCと66,837 RAY)。
  • ステップ4:ハンドラは攻撃者の1トークンをバーンし、両プールボールトから全準備金を転送し、RAY/USDCプールを完全に流出させました。

攻撃者は約15秒以内にさらに3つのプールに同じパターンを繰り返しました。4つのプール全体で流出した金額は以下の通りです。

プール 流出額(概算)
RAY/USDC ~66,837 RAY + ~893,700 USDC
RAY/wSOL ~74,720 RAY + ~5,603 wSOL
RAY/SRM ~8,622 RAY + ~10,692 SRM
RAY/Sollet ETH ~5,038 RAY + ~16 Sollet ETH

結論

根本原因は単一の欠落したアカウント検証チェックです。引き出しハンドラは、プールのレコードされたamm.lp_mintに束縛することなく、呼び出し元が提供したMintアカウントのsupplyをLP供給除数として使用しました。Solanaでは、呼び出し元が提供するすべてのアカウントはプール状態に保存された正規の対応アカウントに束縛される必要があります。正しい実装では、キーがプールの保存されたレコードと一致しないLP Mintを拒否し、外部から提供されたMintのsupplyではなくプール内部のLPカウンターから償還を計算すべきです。攻撃されたコントラクトは古いデプロイメント(2023年1月最終アップグレード)であり、攻撃と同日にクローズされました。Raydiumチームによると、完全な補償はRaydiumのトレジャリーによって処理される予定です [1]

参考資料

Phalcon Securityを始めよう

あらゆる脅威を検出し、重要なアラートを発し、攻撃をブロックします。

今すぐ無料で試す

BlockSecについて

BlockSecはフルスタックのブロックチェーンセキュリティおよびクリプトコンプライアンスプロバイダーです。コード監査(スマートコントラクト、ブロックチェーン、ウォレットを含む)の実施、リアルタイムの攻撃遮断、インシデント分析、不正資金の追跡、プロトコルおよびプラットフォームの全ライフサイクルにわたるAML/CFT義務の遵守を支援する製品とサービスを構築しています。

BlockSecは権威ある学会でブロックチェーンセキュリティに関する複数の論文を発表し、DeFiアプリケーションのゼロデイ攻撃を数件報告し、2,000万ドル以上を救済するために複数のハックをブロックし、数十億のcryptocurrencyをセキュリティ確保してきました。

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit