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:Rollup 的證明路徑與 L1 結算路徑之間存在驗證缺口,導致兩者處理不同的交易集合,最終達到不一致的狀態。
  • Raydium:缺少驗證檢查,使攻擊者得以操縱 LP 代幣贖回計算,耗盡四個流動池的全部儲備。

Web3 最佳安全審計機構

在上線前驗證設計、程式碼與業務邏輯

本週重點:Aztec

在此事件中,ZK 證明驗證器與 L1 結算邏輯處理了不同的交易集合,原因是某個參數未受到限制而無邊界約束。這種證明與結算之間的一致性缺口,適用於任何將這兩條路徑作為獨立程式碼運行的 Rollup 設計。

2026 年 6 月 15 日,Ethereum 上以隱私為核心的 Rollup——Aztec Connect,遭到攻擊,損失約 $2.15M [1]。根本原因是已驗證的 Rollup 交易集合與 L1 結算處理邊界之間存在不一致,導致 ZK 證明路徑與結算邏輯處理了不同的交易清單。攻擊者利用此缺口在 Rollup 狀態中記入無實際支撐的存款餘額,再透過正常的結算流程將其提領。

背景

Aztec Connect 是 Ethereum 上一個以隱私為核心的 Rollup,可在 L2 上進行私密交易。由於使用者資金源自 L1,必須先存入 Rollup 處理器合約,才能在 L2 Merkle 樹中以票據(note)形式表示。

存款流程分為兩個階段:

第一階段: 使用者呼叫 depositPendingFunds(),透過 increasePendingDepositBalance() 增加 userPendingDeposits[assetId][owner] 的值,並將代幣轉入 RollupProcessor。這會在 L1 上建立一筆待處理存款。

function depositPendingFunds(uint256 _assetId, uint256 _amount, address _owner, bytes32 _proofHash) external {
    increasePendingDepositBalance(_assetId, _owner, _amount);
    // ... 將代幣轉入合約
}

第二階段: 使用者提交存款證明,之後被納入 Rollup 並加入 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() 內部,兩個函式依序被呼叫。首先,verifyProofAndUpdateState() 驗證所有已解碼交易的 ZK 證明並更新 Rollup 狀態。然後,processDepositsAndWithdrawals() 處理 L1 結算,僅迭代前 _numTxs 個槽位,並對每筆存款呼叫 decreasePendingDepositBalance()(若使用者未在第一階段實際存款,此呼叫將回滾,從而將 Rollup 信用與真實的 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);
}

這種兩階段設計要求 L1 結算邏輯所處理的交易集合,與 ZK 證明所驗證的交易集合完全一致。若兩條路徑在處理哪些交易上出現不一致,存款便可在 Rollup 狀態中被記入,卻不消耗其在 L1 上的待處理餘額。

漏洞分析

在 Rollup 處理器合約(0x7d65...2728)中,numTxs 並未有效地與 ZK 證明所強制執行的交易集合綁定。因此,證明路徑與結算路徑可能處理不同的交易清單。

在鏈下的 rollup_circuit 中,num_txs 作為見證(witness)被載入,且僅受範圍約束。電路利用它來決定哪些槽位被視為真實交易,但並不驗證 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 的槽位仍會被遞迴驗證,但其公開輸入會被清零,因此不會對 Rollup 狀態產生貢獻:

在 Solidity 端,decodeProof() 從 calldata 元資料中讀取 numTxs,而該元資料並未被複製到由 verifyProofAndUpdateState() 驗證的重建 proofData 中。因此,結算迴圈的邊界同樣不在 ZK 證明的涵蓋範圍之內:

由於兩側均未對此值加以約束,攻擊者可以將 numTxs 設為低於實際已解碼交易數量的值。結算迴圈便會跳過那些已被證明在 Rollup 狀態中記入信用的交易。一筆無效交易可以佔據第一個已解碼槽位(位於結算掃描範圍內),而一筆真實存款則可位於後續槽位(由電路證明,但在結算掃描範圍之外)。證明會在 Rollup 狀態中記入該存款,但結算邏輯會完全跳過它,包括 decreasePendingDepositBalance() 的呼叫。這使得 L1 上的待處理存款餘額未被消耗,而 Rollup 狀態已反映該存款。

攻擊分析

以下分析基於交易 0x074ec9...9aeeb1

攻擊者分兩個階段利用了證明路徑與結算路徑之間的缺口。

第一階段:建立無實際支撐的餘額

  • 步驟 1:攻擊者提交多批 Rollup,每批包含兩筆已解碼交易:槽位 1 為一筆無效(垃圾)交易,槽位 2 為一筆真實存款,並將 numTxs 設為 1。L1 結算邏輯僅處理槽位 1 的垃圾交易,完全跳過槽位 2 的真實存款。

  • 步驟 2:然而,ZK 證明驗證並記入了所有已解碼交易,包括槽位 2 的存款。由於結算邏輯從未觸及此存款,decreasePendingDepositBalance() 未被呼叫,L1 上的待處理存款餘額依然未被消耗。攻擊者對七種不同資產重複此模式,在 Rollup 狀態中積累了無實際支撐的餘額。

第二階段:提取資金

  • 步驟 3:一旦七種無實際支撐的餘額建立完成,攻擊者便對每種資產發起標準提款。由於這些餘額存在於 Rollup 狀態中,結算邏輯認為這些提款合法,因此 L1 合約釋放了對應資金——合計約 $2.15M。

結論

此漏洞並非密碼學層面的弱點,而是 Rollup 架構中兩條關鍵程式碼路徑之間的狀態一致性缺陷。根本原因在於:numTxs 在兩側均未與已證明的交易集合綁定。電路僅對其進行範圍約束,而 Solidity 解碼器則從未經驗證的 calldata 元資料中讀取它。缺乏此綁定,證明路徑與結算路徑便可處理不同的交易清單。攻擊者將 numTxs 設為低於實際交易數量,使結算邏輯跳過了證明已在 Rollup 狀態中記入信用的存款。由此產生的無實際支撐餘額,隨後透過正常的結算流程被提領。

Aztec Connect Rollup 已宣布停止服務,交易處理與提款預計於 2024 年 3 月 31 日前結束 [2]。然而,Rollup 處理器合約仍於 2024 年 4 月 10 日透過一個 Pull Request 進行了升級 [3],而存在漏洞的邏輯正存在於該停止服務後的升級版本中。

修復方式需將 numTxs 與 ZK 證明所驗證的完整交易集合綁定,確保兩條路徑始終處理相同的集合。任何將證明驗證與 L1 結算分離的 Rollup 設計,都必須強制要求兩條路徑在一個可驗證且有邊界的相同交易集合上運行。哪怕只有一個參數存在差異,也可能將原本健全的證明系統轉變為建立無實際支撐餘額的攻擊向量。

參考資料

立即使用 Phalcon Explorer

深入解析交易,做出明智決策

免費立即試用

本週更多事件

Raydium

2026 年 6 月 10 日,Solana 上 Raydium 舊版 AMM v3 程式的四個流動池遭到攻擊,損失約 $1.34M [1]。提款處理器未驗證呼叫者提供的帳戶是否與流動池中儲存的對應帳戶相符,因此攻擊者替換了一個受其控制的帳戶來操縱支付計算。同一手法在數秒內耗盡了四個流動池的所有儲備。

背景

Raydium 的 AMM 是 Solana 上的恆定乘積做市商。每個流動池持有兩個代幣金庫,並鑄造代表儲備比例份額的 LP 代幣。當流動性提供者提款時,處理器按比例計算支付金額,並轉出兩個金庫的對應份額:

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

在 Solana 上,每種代幣類型由一個 Mint 帳戶定義,其中儲存總供應量、小數位數和鑄造權限。每位持有者的餘額儲存在與該 Mint 綁定的獨立 Token 帳戶中——一個 Mint 可以對應不同持有者的多個 Token 帳戶。這與 EVM 不同,EVM 的單一 ERC-20 合約在內部同時管理代幣定義和所有餘額。

在上述提款公式中,lp_supply 從流動池的 LP Mint 帳戶讀取——該帳戶追蹤 LP 代幣的總供應量。計算的正確性取決於此值來自真實的 LP Mint。然而,在 Solana 上,呼叫者以位置順序將每個帳戶傳入每條指令,因此處理器必須驗證每個呼叫者提供的帳戶是否與流動池狀態中儲存的標準帳戶相符。

漏洞分析

被攻擊的程式(27haf8...8vQv)並未開源,且其可執行資料(ProgramData)在攻擊後已被關閉,使得直接的位元組碼檢查成為不可能。以下分析基於從該程式最後一次升級緩衝區重建的位元組碼,並與鏈上交易行為交叉比對。

在提款處理器中,呼叫者傳入的 LP Mint 帳戶並未與流動池記錄中的 amm.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:攻擊者建立了一個假的 LP Mint 帳戶,decimals = 0,總 supply = 0
  • 步驟 2:攻擊者初始化了一個與假 LP Mint 綁定的 Token 帳戶,然後(以 Mint 權限)向其鑄造恰好 1 個代幣,將 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 秒內對另外三個流動池重複了相同的模式。四個流動池的提領金額合計為:

流動池 提領金額(約)
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

結論

根本原因是缺少一個帳戶驗證檢查:提款處理器使用呼叫者提供的 Mint 帳戶的 supply 作為 LP 供應量除數,卻未將其與流動池記錄中的 amm.lp_mint 綁定。在 Solana 上,每個呼叫者提供的帳戶都必須與流動池狀態中儲存的標準對應帳戶綁定。正確的實作應拒絕任何 key 與流動池儲存記錄不符的 LP Mint,並從流動池內部的 LP 計數器而非外部提供的 Mint 的 supply 計算贖回金額。被攻擊的合約是一個較舊的部署版本(最後一次升級於 2023 年 1 月),並於攻擊發生當天被關閉。根據 Raydium 團隊表示,全額賠償將由 Raydium 的財庫負責 [1]

參考資料

立即使用 Phalcon Security

偵測每一項威脅,發出關鍵警報,並阻擋攻擊。

免費立即試用

關於 BlockSec

BlockSec 是一家提供全方位區塊鏈安全與加密合規服務的機構。我們打造產品與服務,協助客戶進行程式碼審計(涵蓋智能合約、區塊鏈及錢包)、即時攔截攻擊、分析事件、追蹤非法資金,並履行 AML/CFT 義務,覆蓋協議與平台的完整生命週期。

BlockSec 已在頂尖學術會議上發表多篇區塊鏈安全論文,回報了多起 DeFi 應用的零日攻擊,阻止了多起駭客攻擊事件,成功挽救逾 2,000 萬美元,並保護了價值數十億美元的加密貨幣。

Best Security Auditor for Web3

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

BlockSec Audit