Aztec Connect、ZKロールアップの不整合を突かれ約219万ドル流出

【概要】 2026年6月14日、既に廃止されていたAztec ConnectのRollupProcessorコントラクト(0xff1f2b4adb9df6fc8eafecdcbf96a2b351680455)が悪用され、L1プールから約219万ドル相当の資産が単一のアトミック取引で引き出された。Aztec Connectは2024年3月に廃止済みだったが、当該コントラクトはイミュータブルで、なおユーザーの残存資産を保持していたため攻撃対象として露出し続けていた。本稿はコントラクトのソースコードとオンチェーンのcalldataを突合し、攻撃の技術的経緯を再構成する。 【攻撃の要点】 ■ 脆弱性の根因 RollupProcessorV3がL1で走査・精算する「L1精算サイクル」の範囲と、ZKの公開入力ハッシュがコミットする範囲の間に構造的なズレがあった。攻撃者はこのズレを利用し、32スロット中31スロット分の公開入力をZK証明でL2の状態ルートへ取り込ませつつ、L1コントラクト層での精算検証を受けさせない状態を作った。 ■ Decoder.sol:numRealTxsが攻撃者に完全に握られる numRealTxsはcalldataのオフセット4516から読み出され、オンチェーン側の制約がない。decoded_slotsはSHA256プリコンパイルのデータ配置要件に合わせ、numTxsPerRollupの倍数へ切り上げられる。この切り上げ処理がnumRealTxsとdecoded_slotsの間に"ギャップ領域"を生み、攻撃者はその領域を任意の内容で埋められる。 ■ RollupProcessorV3.sol:精算サイクルがnumRealTxs分しか処理しない L1の精算ループはnumRealTxsスロット分しかカバーしないため、ギャップ領域のスロットはL1の検証から外れる。 ■ 想定されていた防御モデルの破綻 通常は「各公開入力スロットは、(1) L1で検証される(例:depositでpendingDepositBalanceを減算)か、(2) ZK回路でpublicValue==0等に拘束される」ことが前提となる。ところが今回の条件では、 - SHA256プリコンパイルは32スロット全体(入力8192バイト=32×256バイト)を対象にコミットし、ギャップスロットの内容もZK証明でコミットされる。 - L1精算は先頭スロットのみを処理し、ギャップスロット[2..32]はL1レベルの検証対象外となる。 - 本来0であるべきギャップスロットのpublicValue拘束が攻撃者により回避、または回路側で十分に強制されていなかった。 結果として、防御線は相互依存の関係にある一方、どれも単独ではギャップスロットを守れず、ZK回路側の拘束が欠けた瞬間にL1も検知できなくなった。 ■ デュアルパスの乖離 同一のcalldataが「上限の異なる2つの経路」で消費される。ZK側は32スロットを"対象"と認識する一方、L1側は1スロットしか"精算対象"として扱わない。この解釈差が、無からのミントに直結した。 【攻撃フロー】 攻撃トランザクション0x074ec931…aee1にはprocessRollup()が14回含まれ、"前半7回でミント"→"後半7回で引き出し"の2段構成で、すべて単一のアトミック取引内で実行された。 ■ フェーズ1:ミント(L2上での架空入金で残高を水増し)— ロールアップ#13277〜#13283(計7回) 1) 攻撃者EOA 0x0f18d8b44a740272f0be4d08338d2b165b7edd17が、統制用マスターコントラクト0x06f585f74e0da633ae813a0f23fb9900b61d0fcdを呼び出し、セレクタ0x6f3ce701を起動。 2) マスターは3つのリレーコントラクトを順に呼び出し、各リレーには悪性ロールアップ用のcalldataが複数ハードコードされていた。主なパラメータは次の通り: - numRealTxs = 1 - rollupSize = 1024 - numInnerRollups = 32 スロット1(L1可視):proofId = 0(noop)、publicValue = 0 スロット2〜32(ギャップ31スロット、L1不可視):proofId = 1(deposit)、publicValue = N、publicOwner = 攻撃者のL2アドレス 加えて、対応するZK証明が添付(回路がギャップスロットのpublicValueを0に拘束していない)。 3) リレーコントラクトAがRollupProcessor.processRollup()を連続実行(ロールアップ#13277〜#13281、5回): - VerifierがZK証明の通過を確認(SHA256コミットは32スロット全体をカバー) - L1精算サイクルは1×TX_PUBLIC_INPUT_LENGTH=1スロットで終了し、noopのみを処理 - ギャップスロット[2..32]の架空depositがZKで新Merkle rootにコミットされ、攻撃者のL2残高が5×31N増加 4) リレーコントラクトBが同様にロールアップ#13282〜#13283を処理(2回)し、攻撃者のL2残高に追加で2×31Nを付与。 この時点で、攻撃者のL2口座には合計7×31Nの根拠のない入金が積み上がる一方、L1側ボールトは変化しない。 ■ フェーズ2:引き出し(膨張したL2残高をL1資産へ変換)— ロールアップ#13284〜#13290(計7回) 攻撃者はミント局面で得たL2残高を、7回のwithdrawalロールアップでL1資産へ交換した: - ロールアップ#13284(DAI):withdraw()によりRollupProcessorが270,513.054 DAIを0x0f18…edd17へ直接送金 - ロールアップ#13285(wstETH):167.890 wstETHを攻撃者へ送金 - ロールアップ#13286(yvDAI):4,873.857 yvDAIを攻撃者へ送金 - ロールアップ#13287(yvWETH、リレーCが担当):16.570 yvWETHを攻撃者へ送金 - ロールアップ#13288(LUSD):9,273.734 LUSDを攻撃者へ送金 - ロールアップ#13289(yvLUSD):359.047 yvLUSDを攻撃者へ送金 - ロールアップ#13290(ETH、最終):RollupProcessorが内部CALLで908.987 ETHを攻撃者へ送金 単一のアトミック取引(gasUsed = 4,513,539)として成功し、コントラクトレベルの部分的ロールバックは不可能だった。被害額は約219万ドルで、原資はRollupProcessorが管理する正規ユーザー資産プールから流出した。 【資金追跡】 オンチェーンのフォレンジック追跡(2026年6月15日、発生から約1日後時点)によると、 - すべての資産は単一トランザクションでRollupProcessorから中継の攻撃用コントラクト0x06f585…d0fcDを経由し、攻撃者EOA 0x0F18D8b44a740272f0be4d08338d2b165b7EdD17へ送付された。 - 中継コントラクトには残高が残っていない。 - 流出資金は攻撃者EOA内に100%残存し、現時点でマネーロンダリングの動きは確認されていない。 【まとめと提言】 今回の教訓は、ZKロールアップの精算ループの上限を、ZK公開入力へのコミット範囲と厳密に一致させる必要がある点にある。L1コントラクト層のnumRealTxsと、SHA256コミットにおけるdecoded_slotsの間にギャップが生じると、ギャップスロットの拘束をZK回路に依存する前提は攻撃者に突破され得る。L1は、ZK証明がコミットした公開入力スロットを漏れなく独立に検証すべきで、回路層へ検証責務を委譲してはならない。 SlowMist Security Teamは、ロールアップシステムのデプロイ前に、外部による包括的なセキュリティ監査の実施を推奨する。焦点はL1/L2の状態境界における論理整合、calldataデコードの信頼境界、ZK公開入力のオンチェーンでの二次検証に置くべきだ。廃止済みでもレガシー資産を保持するコントラクトについては、計画的な資産移行または破棄を行い、継続的な露出リスクを排除することが望ましい。 本稿はSlowMistのThreat Intelligence Teamが、MistEye Threat Intelligence System、MistTrack Tracking Platform、SlowMist AgentのAI駆動分析を活用して作成した。質問やフィードバックは随時受け付けている。