
一個缺失的零Payy 跨鏈橋如何接受偽造的 ZK 證明損失 192 萬美元2026 年 9 月 24 日Payy 在攻擊者從其以太坊跨鏈橋合約中提走 1,918,793.198148 USDC 之后暫停了整個網(wǎng)絡(luò)。最容易講的故事是私鑰被盜。但公開證據(jù)指向別處。Payy 已經(jīng)排除了私鑰泄露、社會工程攻擊以及鏈下基礎(chǔ)設(shè)施被利用的可能。攻擊者提交了一筆背后沒有任何資金支撐的提現(xiàn)并附上一份偽造的零知識證明。Payy 的驗證器接受了這份證明。隨后又發(fā)生了兩筆金額較小的偽造提現(xiàn)。根因是 Barretenberg 小群 IPA 檢查中的一個可靠性漏洞。所謂可靠性漏洞就是讓驗證器接受一個虛假陳述的證明。這個檢查沒有強制某個累加和從零開始。用軟件的話說驗證器約束不足。這才是真正的問題所在。不是“ZK 壞了”而是一條約束缺失、部署的驗證器版本過舊、升級路徑又太慢。攻擊路徑一步一步看Payy 的余額是隱私 note。一筆交易最多花費兩個輸入 note創(chuàng)建最多兩個輸出 note鏈上只公開它們的承諾也就是代表隱藏 note 的指紋。burn 是 Payy 對“提現(xiàn)到以太坊”的術(shù)語。它還會公開金額、以太坊收款地址和 burn hash而電路會把 burn hash 設(shè)為第一個輸入承諾。第一筆偽造 burn 公開了四個零承諾、一個零 burn hash、金額 1,828,589,378,132 個基礎(chǔ)單位以及攻擊者地址。USDC 有六位小數(shù)所以這是 1,828,589.378132 USDC。四個零承諾就是填充 note價值為零。交易前后 note 樹的根完全一樣。如果直接拿這筆交易去跑 Payy 的 burn 電路它會失敗。價值守恒斷言會拒絕一筆由零價值 note 支撐的正數(shù)提現(xiàn)。note 類型斷言也會失敗因為填充 note 的類型是 0而消息里寫的是 USDC note 類型。Payy 不會對每筆提現(xiàn)重跑電路它依賴的是“這筆交易遵守了電路規(guī)則”的密碼學(xué)證明。攻擊者的偽造證明通過了 Payy 的驗證器。Payy 的事后報告把這一點歸因于 Barretenberg 的可靠性漏洞。Payy 節(jié)點內(nèi)部的驗證器首先接受了這份證明。Payy 的第一層聚合電路會用編譯進(jìn)電路的同一個 Barretenberg 驗證器再驗證每筆交易證明而上層只驗證下一層。出于隱私考慮沒有任何一層能看到 note 的價值所以沒有任何一層能獨立重做余額檢查。Payy 自己的 prover 提交了這批交易Payy 的驗證者簽了名rollup 合約隨后支付了 burn。rollup 合約沒有“burn hash 必須非零”這條規(guī)則。這條規(guī)則本可以攔下這三條消息但它治不了驗證器的病。時間線與損失第一筆偽造 burn 在 UTC 03:25:41 被記錄在 Payy 自己的鏈上高度 33,195,439。UTC 04:21:23Payy 的 prover 在以太坊區(qū)塊 26,044,909 中結(jié)算了這批交易??珂湗虬?1,828,589.378132 USDC 轉(zhuǎn)給 0xAa4985dBDaBfACa344237D40F7E06C4a0BB57E70。八分鐘內(nèi)資金進(jìn)入第二個錢包并賣出換成約 683 ETH。UTC 04:21:47一個 burn 墊付方支付了攻擊者的第二筆 burn金額 1 USDC。墊付方會提前支付提現(xiàn)等提現(xiàn)結(jié)算后再收回款項??珂湗虼蠹s五小時后償還了它。這兩筆轉(zhuǎn)賬里只有一筆是損失。UTC 09:30:35第二筆結(jié)算在區(qū)塊 26,046,439 中向同一地址支付了 90,202.820016 USDC并償還了墊付方。總損失1,828,589.378132 1 90,202.820016 1,918,793.198148 USDC。UTC 14:04Payy 宣布遭到攻擊并暫停所有網(wǎng)絡(luò)交易。Payy 追蹤到第一筆付款中的 682.3 ETH 在 9 月 26 日通過 19 筆存款進(jìn)入 Tornado Cash。截至 10 月 11 日最后那筆 90,202.82 USDC 仍在收款錢包里。截至 10 月 11 日沒有任何公開信息表明 Payy 已恢復(fù)運行。其公開倉庫默認(rèn)分支自 6 月 24 日以來沒有新提交。代碼問題一個方程兩個未知數(shù)公開的重構(gòu)資料描述了 Payy 在提交 2f95947f 中 burn 分支的四條斷言。關(guān)鍵邏輯簡化后大致如下。這不是生產(chǎn)環(huán)境的精確代碼但能看出檢查的形狀。// Payy burn 分支的簡化模型非生產(chǎn)代碼。fnburn_circuit(inputs:[Note;2],outputs:[Note;2],msg:BurnMsg){letinput_valueinputs[0].valueinputs[1].value;letoutput_valueoutputs[0].valueoutputs[1].value;assert_eq!(input_value,output_valuemsg.value);// 價值守恒assert_eq!(msg.burn_hash,inputs[0].commitment);// burn 綁定第一個輸入assert!(msg.value0);// 提現(xiàn)金額為正assert_eq!(inputs[0].kind,msg.kind);// note 類型匹配消息}用四個填充 note 時input_value和output_value都是零。正數(shù)的msg.value會通不過價值守恒。類型斷言同樣會失敗。在這個故事里電路本身不是弱點驗證器才是。Barretenberg 的小群 IPA 檢查會對一個累加和做記賬。prover 聲稱一組數(shù)字的總和驗證器要在看不到這些數(shù)字的情況下確認(rèn)總和。規(guī)則很簡單累加和必須從零開始。每個值必須等于前一個值加上列表中的下一個數(shù)。最后一個值必須等于聲稱的總和。舊檢查把前兩條規(guī)則揉進(jìn)了一個方程。一個方程無法釘死兩個未知數(shù)。惡意 prover 可以讓累加和從錯誤的值開始并讓第一步錯出恰好抵消的量。之后每一步都是誠實的之后每個值都偏離同一個常數(shù)總和也一樣。這個常數(shù)由 prover 選總和也就由 prover 選。簡化模型能看出這個漏洞。// 簡化模型不是 Barretenberg 的實際代碼。// 舊檢查把兩個義務(wù)揉進(jìn)了一個等式。fnold_check(a:[Fr],t:[Fr],s:Fr,g:Fr)-bool{a[0]g*(a[1]-a[0]-t[0])Fr::ZERO(1..t.len()).all(|i|a[i1]a[i]t[i])a[t.len()]s}// 正確的檢查起始條件單獨約束。fnfixed_check(a:[Fr],t:[Fr],s:Fr)-bool{a[0]Fr::ZEROa[1]a[0]t[0](1..t.len()).all(|i|a[i1]a[i]t[i])a[t.len()]s}舊驗證器在這個步驟里檢查 prover 承諾值的四條斷言。修復(fù)后的代碼檢查五條多出來的就是a[0] 0。效果不是表面功夫。一個 Lean 4 模型用 Lean 4.34.1 和 Mathlib 驗證了兩條性質(zhì)。文件里沒有sorry除 Lean 三條標(biāo)準(zhǔn)公理外不依賴其他公理。定理和證明一共 57 行。第一條定理表明當(dāng)位置數(shù)至少為二且g ! 0時舊檢查對任意數(shù)字列表和任意聲稱的總和都會接受錯誤的總和。第二條表明當(dāng)g ! 1時修復(fù)后的檢查會強制被接受的總和等于真實總和。這并不能證明整個 Barretenberg 是可靠的。它只證明了一個檢查在兩條假設(shè)下的性質(zhì)承諾把 prover 綁定到一個累加和上且抽查都得到誠實回答在隨機點上通過一個組合方程意味著除可忽略概率外三條等式在每個位置都成立。這兩條假設(shè)把模型和部署的驗證器連起來但它們不覆蓋 C 代碼、Solidity 驗證器、Payy 的電路或 Payy 的合約。不過它們說明按模型來看舊檢查確實會接受錯誤總和而修復(fù)移除了那一個自由度。Payy 的復(fù)現(xiàn)包包含那份 16,256 字節(jié)的證明和公開輸入。根據(jù)事后報告這份證明在 Payy 當(dāng)時運行的構(gòu)建下能通過驗證。把公開的 burn hash 從 0 改成 1同一份證明就會失敗。這確認(rèn)了驗證器會拒絕被修改的輸入但并不能說明攻擊者最初是怎么構(gòu)造出那份證明的。修復(fù)為什么沒進(jìn)生產(chǎn)Payy 把服務(wù)鎖定在標(biāo)記為3.0.0-manual.20251030的 Barretenberg 構(gòu)建上配套 Noir1.0.0-beta.14。Cargo.toml里有一行、六個 Dockerfile 都指向這個構(gòu)建。Aztec 在 2026 年 5 月 14 日提交了修復(fù)。提交號58bf73a標(biāo)題fix!: small subgroup ipa bug (#44)。它改了 34 個文件包括原生驗證器、遞歸驗證器和 Solidity 驗證器模板。Aztec 在 2026 年 7 月 13 日的 v5.0.0 中發(fā)布了修復(fù)。6 月的兩個 v5 候選版本不含此修復(fù)6 月 27 日發(fā)布的 v4.4.0 也不含。Payy 被攻擊發(fā)生在 2026 年 9 月 24 日距離含修復(fù)的公開版本發(fā)布已經(jīng) 73 天。Payy 在 2026 年 5 月 15 日部署了一個新的鏈上驗證器用來修復(fù)自己某個電路里的另一個嚴(yán)重漏洞。合約創(chuàng)建時間比 Aztec 提交記錄的時間晚 20 小時 49 分鐘。事件報告說這個合約仍由 2025 年 10 月的構(gòu)建生成。一次全新的部署用的還是舊驗證器。Payy 最后一次公開倉庫同步在 6 月 24 日仍指向 2025 年 10 月的構(gòu)建。5 月 15 日部署的驗證器到 9 月 24 日仍在使用。對 Payy 這樣的 rollup 來說升級 Barretenberg 不是改一行依賴那么簡單。修復(fù)落在一個新的大版本里。驗證器被編譯進(jìn) Payy 的每一層聚合電路電路、驗證密鑰和鏈上驗證器合約必須一起重新生成、一起重新部署。Aztec 3 月 10 日的安全公告說修復(fù)會打包進(jìn) v5 發(fā)布對于需要新驗證密鑰的嚴(yán)重漏洞會先通知其網(wǎng)絡(luò)門戶portal的運營者再在論壇發(fā)公告。Payy 不在那個網(wǎng)絡(luò)上它把 Noir 和 Barretenberg 當(dāng)作庫用在自己的 rollup 上。一個庫如果有廠商網(wǎng)絡(luò)之外的用戶就需要一條能觸達(dá)這些用戶的渠道。到 7 月 13 日修復(fù)已經(jīng)公開提交還給協(xié)議文檔加了一節(jié)明確寫出這種偽造方式。但 Aztec 倉庫里沒有任何安全公告點名這個漏洞。10 月 8 日Aztec 針對另一個 Barretenberg 驗證器漏洞發(fā)布了兩份安全公告并建議“直接使用 Barretenberg 的用戶”打補丁。直接用戶需要的是事前通知不是事后通知。該改什么升級到包含58bf73a或之后提交的 Barretenberg 版本。重新生成電路、驗證密鑰和鏈上驗證器合約把它們作為一次協(xié)調(diào)好的發(fā)布一起重新部署。效果補上缺失的約束恢復(fù)“被接受的總和必須等于真實總和”這條性質(zhì)。不要把合約層的burnHash ! 0檢查當(dāng)成修復(fù)。它能攔下這三條消息但驗證器仍然會接受偽造證明。把它當(dāng)縱深防御加上去而不是根因治理。給單筆提現(xiàn)加上限對大額提現(xiàn)加延遲。效果一份偽造證明無法在一次結(jié)算里掏空整個跨鏈橋團(tuán)隊也有時間暫停和調(diào)查。Aztec 3 月 10 日的公告給其網(wǎng)絡(luò)門戶建設(shè)者的也是這個建議。給依賴項建立驗證器變更監(jiān)控。對你編譯進(jìn)生產(chǎn)的每一套證明棧都要讀上游的安全提交和發(fā)布說明。鎖版本可以但別鎖完就不管。效果可靠性修復(fù)會變成一張可執(zhí)行的工程工單而不是出事之后才看到的博客文章。保持一條能在幾天內(nèi)完成的電路、密鑰和驗證器合約重建路徑。如果某個嚴(yán)重驗證器修復(fù)需要新大版本和協(xié)調(diào)重部署那就在需要之前先演練一遍。效果上游發(fā)布到生產(chǎn)部署之間的窗口會縮短。對關(guān)鍵驗證器檢查使用形式化模型。57 行的 Lean 模型本身發(fā)現(xiàn)不了這個漏洞但它把檢查必須滿足的性質(zhì)寫清楚了。對舊檢查來說這條性質(zhì)是假的對修復(fù)后的檢查來說在給定假設(shè)下它成立。效果代碼評審有了一個精確的問題要回答而不是“證明系統(tǒng)大概沒問題”的模糊感覺。在隱私允許的地方加獨立不變量。非零 burn hash、每批最大 burn 額度、驗證器版本白名單這些都不是可靠性的替代品而是爆炸半徑控制。效果它們限制單次驗證器失效能造成的后果。總結(jié)Payy 這件事的核心不是私鑰被盜。它是一個驗證器因為少了一條約束而接受了虛假陳述也是一個生產(chǎn)系統(tǒng)在修復(fù)公開 73 天后仍在運行 2025 年 10 月的構(gòu)建。缺的那一行很小流程上的失敗很大。如果你的系統(tǒng)接受證明你跑的不是電路你信的是驗證器。把驗證器當(dāng)生產(chǎn)安全代碼對待盯住上游每一次變更保持重部署路徑隨時可用。還要記住在證明系統(tǒng)里一個缺失的零可以值 192 萬美元。