Aztec Connect 疑遭 ZK Rollup 漏洞入侵 單筆交易被盜約 219 萬美元

【事件背景】 2026 年 6 月 14 日,已於 2024 年 3 月停用(deprecated)的 Aztec Connect RollupProcessor 合約遭到利用,受影響合約為 0xff1f2b4adb9df6fc8eafecdcbf96a2b351680455。攻擊者在一筆原子化交易內,從 L1 資產池抽走約 219 萬美元資產。成因在於 numRealTxs 與 decoded_slots 之間存在可被人為製造的邊界落差,合約雖已停用但因仍存有用戶殘餘資產且不可變(immutable),導致持續暴露在風險之中。本文根據合約原始碼與鏈上 calldata 還原攻擊細節。 【漏洞核心】 根因是 RollupProcessorV3 在 L1 結算迴圈實際遍歷的結算週期範圍,與 ZK public input hash 承諾(commit)到的範圍不一致,形成結構性缺口。攻擊者利用該缺口,令 32 個 public input slots 中的 31 個(即 slots 2–32)可在不經 L1 合約層結算驗證下,仍被 ZK 證明納入 L2 狀態根(state root)。 【關鍵機制:calldata 由攻擊者主導】 - Decoder.sol 中,numRealTxs 從 calldata offset 4516 讀取,鏈上沒有任何約束,等同完全由攻擊者控制。 - decoded_slots 會按 numTxsPerRollup 向上取整,以符合 SHA256 precompile 的資料佈局;取整過程產生 numRealTxs 與 decoded_slots 的 "gap" 區域,攻擊者可自由填入內容。 - RollupProcessorV3.sol 的結算循環只覆蓋 numRealTxs 對應的 slots,導致 gap slots 不會進入 L1 層驗證流程。 【安全假設如何被打穿】 一般設計假設是:每個 public input slot 不是在 L1 合約層完成驗證(例如存款會扣減 pendingDepositBalance),就是被 ZK 電路約束為 publicValue == 0。這次情境中出現三重失效: - SHA256 precompile 的承諾涵蓋全部 32 個 slots(實測輸入 8192 bytes = 32 × 256 bytes),gap slots 內容亦被 ZK proof 承諾。 - L1 結算迴圈只處理第 1 個 slot,gap slots [2..32] 不受任何 L1 層驗證。 - ZK 電路對 gap slot 的 publicValue(理應為 0)的約束被繞過或根本未被有效執行。 換言之,當電路約束缺失時,L1 合約層亦無法獨立發現 gap slots 的異常,三道防線相互依賴但無一能單獨兜底。 【雙路徑分歧模型】 同一份 calldata 會被兩條路徑消耗,且上界不同:ZK 端視為 32 個 slots,L1 端只視為 1 個 slot。對 "究竟哪些 slots 需要算數" 的認知落差,最終造成憑空鑄造(minting out of thin air)。 【攻擊流程:一筆交易內 14 次 processRollup()】 攻擊交易為 0x074ec931…aee1,包含 14 次 processRollup() 呼叫,結構呈兩階段:先 7 次鑄造,再 7 次提現,全程在同一筆原子化交易中完成。 第一階段:鑄造(Rollup #13277–13283,共 7 次) 1) 攻擊者 EOA 0x0f18d8b44a740272f0be4d08338d2b165b7edd17 呼叫主控入口合約 0x06f585f74e0da633ae813a0f23fb9900b61d0fcd,觸發 selector 0x6f3ce701。 2) 主合約依序呼叫三個 relay 合約,各自內置多組惡意 rollup calldata。每組 calldata 的關鍵參數: - numRealTxs = 1 - rollupSize = 1024 - numInnerRollups = 32 - Slot 1(L1 可見):proofId = 0(noop),publicValue = 0 - Slots 2–32(31 個 gap slots,L1 不可見):proofId = 1(deposit),publicValue = N,publicOwner = 攻擊者的 L2 地址 並附上相應 ZK proof(電路未將 gap slot 的 publicValue 約束為 0)。 3) Relay 合約 A 連續 5 次呼叫 RollupProcessor.processRollup()(Rollup #13277–13281): - Verifier 驗證 ZK proof 通過,SHA256 承諾涵蓋 32 個 slots - L1 結算僅跑到 1 × TX_PUBLIC_INPUT_LENGTH,只處理 noop - gap slots [2..32] 的假存款被 ZK 承諾進新的 Merkle root,攻擊者 L2 餘額增加 5 × 31N 4) Relay 合約 B 以同樣方式處理 Rollup #13282–13283(2 次),再增加 2 × 31N。 至此,攻擊者 L2 帳戶累積合共 7 × 31N 的"無對應 L1 資金支撐"存款,而 L1 vault 資產並未增加。 第二階段:提現(Rollup #13284–13290,共 7 次) 攻擊者將第一階段膨脹的 L2 餘額,分 7 個提現 rollup 兌換為 L1 資產: - Rollup #13284(DAI):withdraw() → RollupProcessor 直接轉出 270,513.054 DAI 至 0x0f18…edd17 - Rollup #13285(wstETH):轉出 167.890 wstETH → 攻擊者 - Rollup #13286(yvDAI):轉出 4,873.857 yvDAI → 攻擊者 - Rollup #13287(yvWETH,relay 合約 C 接手):轉出 16.570 yvWETH → 攻擊者 - Rollup #13288(LUSD):轉出 9,273.734 LUSD → 攻擊者 - Rollup #13289(yvLUSD):轉出 359.047 yvLUSD → 攻擊者 - Rollup #13290(ETH,最後一筆):RollupProcessor 透過內部 CALL 轉出 908.987 ETH 至攻擊者 該筆原子化交易成功執行(gasUsed = 4,513,539),且不存在合約層級的部分回滾可能。攻擊者淨得約 219 萬美元,資金全部來自 RollupProcessor 的合法用戶資產池。 【資金流向(截至 2026 年 6 月 15 日)】 鏈上追蹤顯示: - 資產由 RollupProcessor 經中介攻擊合約 0x06f585…d0fcD,在同一筆交易內直接流向攻擊者 EOA 0x0F18D8b44a740272f0be4d08338d2b165b7EdD17。 - 中介合約未留存餘額。 - 被盜資金仍 100% 完整停留於攻擊者 EOA,暫未觀察到洗錢行為。 【結論與建議】 本案教訓在於:ZK Rollup 合約的 L1 結算迴圈上界必須與 ZK public inputs 的承諾範圍嚴格一致。一旦 L1 的 numRealTxs 與 SHA256 承諾的 decoded_slots 之間出現缺口,任何"依賴 ZK 電路替 gap slots 做約束"的安全假設都可能被繞過。L1 必須獨立驗證 ZK proof 所承諾的每一個 public input slot,不能把驗證責任外判給電路層。 SlowMist 安全團隊建議項目方在部署 Rollup 系統前進行全面第三方安全審計,重點檢視 L1/L2 狀態邊界的邏輯一致性、calldata 解碼的可信邊界,以及鏈上對 ZK public inputs 的二次驗證機制。對已停用但仍持有歷史資產的合約,應有序完成資產遷移或銷毀,以消除持續暴露風險。 本文由 SlowMist 威脅情報團隊撰寫,並結合 MistEye 威脅情報系統、MistTrack 追蹤平台及 SlowMist Agent AI 驅動分析。