昨天的結論停在一個缺口上:ERC-7579 把帳戶本體做薄,攻擊面就被外包到模組上,而「模組數量是風險指標而不是能力指標」只描述了問題,沒給出解法。今天補這個缺口——使用者按下「安裝模組」的那一秒,誰替他判斷這顆模組能不能裝。ERC-7484 的答案不是「我們幫你審過了」,而是把「誰替你背書」變成鏈上一份可查詢、可撤銷、可設門檻的狀態,由帳戶自己在安裝或執行前去查。

📖 學

註冊表把信任搬到鏈上,而不是把安全證明出來

模組註冊表(Module Registry)是一份合約裡的清單,記錄「哪個見證人(attester)對哪顆模組做過什麼樣的見證(attestation)」。ERC-7484 定義的是這份清單的查詢介面,以及帳戶端那層轉接器(Adapter)該怎麼呼叫它。規格本身刻意不定義「什麼叫安全」——它只定義「安全這件事被誰主張過、有沒有被撤回、過期了沒有」。

這個分工決定了後面所有討論的邊界。註冊表不做靜態分析、不判斷模組的商業邏輯,它做的是把原本只存在於 PDF 與推文裡的社會事實,壓縮成合約能在一次 staticcall 裡讀完的狀態。價值在於可查詢可撤回,不在於「掛保證」。

規格另外附一份無擁有者的單一註冊表(Singleton Registry)參考實作,但不強制帳戶使用,只要求介面一致——集中查詢的好處與單點風險留到最後一節談。

介面拆解:誰在問、問誰、要幾票

interface IERC7484Registry {
    // 用「帳戶自己存好的」見證人清單來查
    function check(address module) external view;
    function checkForAccount(address smartAccount, address module) external view;
    function check(address module, uint256 moduleType) external view;
    function checkForAccount(address smartAccount, address module, uint256 moduleType) external view;
 
    // 設定 msg.sender 的信任見證人清單與門檻
    function trustAttesters(uint8 threshold, address[] calldata attesters) external;
 
    // 由呼叫方臨時指定見證人清單與門檻
    function check(address module, address[] calldata attesters, uint256 threshold) external view;
    function check(address module, uint256 moduleType, address[] calldata attesters, uint256 threshold) external view;
}

三個細節值得記住。第一,check 系列沒有回傳值,不通過就 revert。回傳 bool 會讓呼叫端有機會忽略結果,revert 讓「查了但沒理」比較難寫出來;規格對轉接器的要求也是同一個方向——註冊表 revert 必須被當成安全風險處理。

第二,兩者的差別是「誰的信任清單」。checkmsg.sender 存的清單,適合帳戶自己呼叫;checkForAccount 把帳戶地址當參數傳進去,適合由模組或第三方代查——一顆註冊表掛鉤在安裝流程裡替帳戶把關時,msg.sender 是掛鉤而不是帳戶,必須用後者才問得到正確清單。搞錯這一個,把關等於用了別人的信任設定。

第三,threshold 讓信任變成 N of M:門檻不夠、任一見證人撤回、清單未去重排序,都會 revert。帶 moduleType 的版本還會檢查見證人主張的型別是否相符。

attestation 與傳統審計報告的差別

面向傳統審計報告鏈上 attestation
載體PDF、部落格文章註冊表合約裡的一筆紀錄
撤回發勘誤,舊檔案照樣流通呼叫撤銷,下一個區塊起 check 就 revert
有效期通常沒有明確定義可設 expirationTime,過期即無效
綁定對象某個 commit 或某份原始碼已部署的合約地址
可組合性不行可要求 N of M、可鏈接前人的見證

最實質的差別是撤回會傳播。傳統審計報告發現問題後只能補公告,已經照著報告裝上模組的使用者收不到任何訊號;鏈上見證被撤銷後,任何在執行時查註冊表的帳戶會立刻開始 revert——這是規格建議「執行時也查」的真正理由。

見證綁的是部署後的地址而不是原始碼,消掉了「審的版本和部署的版本不一樣」這種落差,但同一顆邏輯在不同鏈上是不同地址,見證不會自動跨鏈生效。見證資料本身刻意保持開放,可以是布林旗標,也可以是指向鏈下報告的指標——Rationale 的例子是「模組會寫進哪些 storage slot」(帳戶用 delegatecall 時尤其要緊):鏈上驗證幾乎不可行,鏈下卻不難,見證人就是把結論搬上鏈的人。

呼叫時機:安裝時查,不等於執行時安全

規格的底線要求是,模組第一次被呼叫之前或之中至少要問過註冊表一次;建議做法是安裝時查一次、執行時再查一次。兩者防的不是同一件事:安裝時查,防的是使用者被引導去裝一顆從沒被看過的模組;執行時查,防的是「裝的時候乾淨、後來才出問題」——見證被撤銷、過期、見證人退出,只有執行時查得到。代價是每筆交易多一次外部呼叫的 gas,以及新的活性風險:註冊表出問題時帳戶連正常操作都做不了。挑錢包時值得問清楚。

模組化帳戶特有的攻擊面

註冊表存在的理由,是模組的權限本來就不對等。同樣一句「安裝一顆模組」,不同型別的後果差距極大:

模組型別拿到的能力被攻破時最壞情況
驗證器(validator)決定一筆 UserOperation 的簽章算不算通過換掉授權來源,攻擊者自簽自過,效果等同私鑰外流
執行器(executor)代帳戶發出 call直接搬走資產,過程中使用者不需要簽任何東西
掛鉤(hook)在每次執行前後攔截preCheck / postCheck 一直 revert 把帳戶鎖死;或反過來放行本該被擋的行為
後備處理器(fallback handler)接管未知的 function selector讓帳戶對外表現成另一個東西,例如偽造 ERC-1271 簽章回覆

Trail of Bits 在 2026 年 3 月整理 ERC-4337 帳戶常見錯誤時點出的一項正好落在這裡:帳戶若把驗證器和執行器當成同一種型別處理,一顆驗證器就可能取得代帳戶執行任意交易的能力,「驗證與執行分離」直接失效。這解釋了為什麼要把 moduleType 做進 check:註冊表不需理解型別語意,但能幫帳戶確認「見證人主張的型別」與「帳戶正要裝成的型別」一致。

卸載殘留與組合性風險

模組的狀態通常存在模組自己的儲存空間裡,以帳戶地址為鍵。帳戶呼叫 onUninstall 時模組應該清乾淨,但那是模組作者的責任,帳戶無法強制。實務上有三種殘留:沒清狀態,日後重裝時舊設定復活;卸載流程裡 revert,讓卸載做不到;以及模組對外部合約做過的授權(approve)還留著。卸載不等於撤權

組合性風險(composability risk)更難處理。每顆模組單獨審過,不代表組合起來安全:做時間鎖的掛鉤可能被一顆繞過 execute 路徑的執行器抄後路;社交復原執行器加上多因子驗證器,可能開出誰都沒設計過的復原路徑;多顆掛鉤串接時,執行順序本身就是安全屬性。註冊表的見證是「對單顆模組的主張」,沒有機制能替你把 M 顆模組的笛卡兒積審完——這是生態目前最沒解決的一塊。

這套機制的限制

註冊表是信任瓶頸。 集中查詢讓撤銷同步傳播,也讓它成為生態單點,最嚴重的情境是冒用受信任見證人身分。使用者的信任對象並沒有變少,只是從「模組作者」轉移到「見證人 + 註冊表 + 帳戶轉接器實作」的交集。

見證人的經濟誘因不對稱。 見證要花錢(審查成本、鏈上寫入、聲譽風險),收益卻擴散給所有使用者。目前主流做法是生態基礎設施方自己當見證人,啟動期合理,但等於賣模組的人同時替模組背書。要讓市場多元,需要質押、保險或付費見證把成本內化,這部分還在實驗階段。也因此,一顆模組上的見證數量現在更接近「有多少人願意花力氣看它」,而不是「它有多安全」。

「查得到」不等於「掛保證」。 check 沒 revert,只代表門檻數量的見證人在某個時間點做過某種主張且尚未撤回。它不承諾模組沒漏洞,不承諾見證人有能力發現那個漏洞,也不承諾見證人不會被入侵。當成「一層可撤回的過濾器」是對的,當成「安全認證」就會過度信任。

回到資產安全:這個架構下最有效的保護仍然是分層——高額資產放在模組最少、權限最窄的帳戶裡,實驗性模組放在額度受限的帳戶裡,而不是靠一份見證清單撐住全部身家。工具幫你篩掉明顯的壞東西,不會替你承擔後果。

🧠 記

  • ERC-7484 標準化的是註冊表查詢介面帳戶端轉接器行為,不定義「什麼叫安全」。
  • check 系列不回傳值、失敗直接 revert;checkmsg.sender 的信任清單,checkForAccount 用參數指定的帳戶清單——由掛鉤或第三方代查時必須用後者。
  • trustAttesters(threshold, attesters) 讓信任變成 N of M;清單要去重排序,任一見證被撤回或過期就會讓查詢失敗。
  • 鏈上見證相對審計報告的優勢是撤回會傳播綁定部署地址,這也是規格建議「執行時也查」的理由。
  • 模組型別決定風險等級:驗證器等同簽章授權、執行器等同直接動資產、掛鉤能鎖死帳戶、後備處理器能偽裝帳戶對外行為,要連 moduleType 一起查。
  • 三個沒解決的問題:註冊表是單點信任、見證人誘因不對稱、組合性風險沒人能逐一審完。卸載模組也不等於撤回它留下的授權。

✍️ 實踐

做一張「模組權限盤點表」,不上鏈、不花錢,20 分鐘。對象可以是你在用的智慧帳戶,也可以是任何公開的 ERC-7579 帳戶地址。

  1. 開一份表格,欄位:模組地址、型別、我以為它能做什麼、原始碼裡它實際能做什麼、誰見證過它、怎麼卸掉。
  2. 在區塊瀏覽器上查該帳戶的 ModuleInstalled 事件,把地址與型別填進前兩欄;若帳戶有 getInstalledModules 之類的 view 函式,直接讀。
  3. 挑一顆你最不熟的模組,打開已驗證原始碼,只讀三處:onInstall(安裝時要了什麼)、onUninstall(卸載時清了什麼、會不會 revert)、所有會發出對外呼叫的函式(它能代你動什麼)。填第四欄。
  4. 到註冊表查這顆模組有哪些見證、見證人是誰、有沒有到期時間。查不到就在第五欄寫「無」——「無」本身就是結論,不要跳過。
  5. 最後在表格最下面寫一行:如果這顆模組的作者今天變成惡意的,我最多損失多少? 用金額或資產名稱寫,不要寫「應該還好」。

自我檢查兩題:

  • 我的帳戶是安裝時查註冊表、每次執行時查,還是根本沒查?我憑什麼確認——文件寫的,還是合約程式碼?
  • 第 4 步裡「查不到見證」的模組,我當初憑什麼決定裝上去?那個理由今天還成立嗎?

🔗 延伸學習

💬 問 AI

我在評估一顆 ERC-7579 模組要不要裝到智慧帳戶上。請用 ERC-7484 的觀念建立判斷流程,並回答:
 
1. 它屬於哪個型別(validator / executor / hook / fallback),該型別在最壞情況下
   能對我的帳戶做什麼?請具體到「能不能不經我簽章就轉走資產」。
2. 我該用 check 還是 checkForAccount?若查詢由一顆 registry hook 代發,
   msg.sender 會變成誰,會不會查到錯的信任清單?
3. 「有 2 位見證人做過見證」排除了哪些風險、完全沒排除哪些風險?
   請把「沒排除」的部分列得比「排除」的部分詳細。
4. 這顆模組和我已裝的其他模組之間,有哪些組合性風險是單獨審查看不出來的?
   請針對執行順序、權限疊加、繞過路徑各給一例。
5. 卸載它之後,onUninstall 可能留下哪些殘留狀態或外部授權?我要另外做什麼才算真的撤權?
 
最後請告訴我:在什麼條件下,你會建議我「即使註冊表查得過也不要裝」。