
草案 ERC:鏈級協議託管標準 (ProtocolEscrow)
我們提出一個共享託管合約的最小標準介面,可作為鏈級基礎設施或獨立部署,允許任何鏈上協議委託資產託管,而無需將其業務邏輯與資金管理耦合。
類別: EIPs → ERC
狀態: 徵求討論 (Request for Discussion)
作者: Louis Liu (@louisliu2048)
依賴項目: ERC-20
摘要
我們提出了一個共享託管合約(Shared Escrow Contract)的最小標準介面——可作為鏈級基礎設施或獨立合約部署。該介面允許任何鏈上協議委託資產託管,而無需將其業務邏輯與資金管理耦合。
協議向 ProtocolEscrow 註冊,用戶存入協議子帳戶,協議在完成自身的授權檢查後觸發釋放。託管層絕不檢查協議如何授權提款,它僅強制執行:調用是由已註冊的協議合約發起的,且滿足基本的安全不變量。
動機
每個獨立持有用戶資產的協議都面臨三個重複出現的問題:
1. 安全攻擊面。 每個協議的資金池都是獨立的攻擊目標。大多數大型 DeFi 漏洞針對的是託管邏輯而非業務邏輯。重入漏洞、轉帳手續費(fee-on-transfer)記帳錯誤、重放漏洞——這些都是已解決的問題,不應在每個協議中重新解決。
2. 合規負擔。 在許多司法管轄區,持有用戶資金會觸發託管人義務。如果協議團隊將託管委託給獨立治理的鏈級基礎設施,則可以更有力地主張其為軟體提供商,而非金融託管人。
3. 重複工程。 每個協議都獨立重新實現重入保護、餘額增量記帳、重放保護和暫停機制。這種共享基礎設施的重複建設對任何人均無益處。
一個值得明確說明的細微之處:ProtocolEscrow 並未移除協議對其資金的合法業務控制。協議仍然完全定義何時提款有效、可以釋放多少以及釋放給誰。改變的是違規成本:協議團隊無法再悄無聲息地行動。每一次合約變更都必須經過公開的時間鎖;每一次釋放都會在鏈上記錄。合法的協議運作方式與以前完全相同,而惡意行為者現在必須公開行動,否則就無法行動。
ERC-4626 標準化了收益金庫(Yield Vault)介面。本標準解決了互補的問題:無收益託管,其目標是嚴格的資產隔離和無許可的釋放觸發。
使用案例
ZK 隱私傳輸協議。 ZK 隱私協議的價值在於其密碼學,而非託管工程。透過 ProtocolEscrow,協議合約簡化為:驗證 ZK 證明 → 調用釋放。無需編寫託管代碼。協議團隊還能避開託管人分類:資金由鏈的獨立治理機構持有,而非協議團隊。
跨鏈橋。 跨鏈橋鎖定合約是價值最高的 DeFi 攻擊目標。透過 ProtocolEscrow,跨鏈橋註冊為協議;用戶存入其子帳戶;中繼者在確認目標鏈最終性後調用釋放。releaseId = keccak256(srcChainId, srcTxHash, asset, recipient, amount) 自然地保證了每次轉帳的唯一性。
支付與發票協議。 預付發票、里程碑付款、去中心化場外交易(OTC)——任何在可驗證鏈上條件達成前持有資金的條件支付流程。條件邏輯存在於註冊的協議合約中;ProtocolEscrow 處理資產結算。
時間鎖定託管。 歸屬計劃(Vesting schedules)、懸崖釋放(Cliff releases)、定時支付——所有這些都可以簡化為「檢查 block.timestamp,然後調用釋放」。無需自定義金庫。
共享鏈級基礎設施。 對於將 ProtocolEscrow 作為預部署系統合約的區塊鏈,每個新協議從第一天起就擁有經過審計的託管基礎設施。隨著協議從自管金庫遷移到共享託管層,鏈的總體攻擊面會隨之減少。
規範
本文中的關鍵詞「必須」(MUST)、「不得」(MUST NOT)、「應該」(SHOULD)、「建議」(RECOMMENDED) 和「可以」(MAY) 應按照 RFC 2119 的描述進行解釋。
定義
| 術語 | 定義 |
|---|---|
| ProtocolEscrow | 實現此標準的託管合約。 |
| protocolId | 唯一且雙向綁定到一個協議合約地址的 bytes32 標識符。 |
| asset | ERC-20 代幣地址,或 address(0) 代表原生代幣。 |
| sub-account | 由 ProtocolEscrow 維護的邏輯餘額槽位 (protocolId, asset)。 |
| releaseId | 由協議生成的用於重放保護的一次性標識符。在給定 protocolId 的所有資產中強制執行全局唯一性。 |
| authorization data | 協議在調用釋放前驗證的協議特定數據(ZK 證明、簽名、時間條件等)。ProtocolEscrow 絕不檢查此數據。 |
第一部分:核心介面(強制)
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;
interface IProtocolEscrow {
// 當記錄存款時觸發(amount = 實際入帳金額,扣除轉帳手續費後)
event Deposited(bytes32 indexed protocolId, address indexed asset, address indexed depositor, uint256 amount);
// 當執行結算釋放時觸發
event Released(bytes32 indexed protocolId, address indexed asset, address indexed recipient, uint256 amount, bytes32 releaseId);
/// @notice 將資產存入協議子帳戶。
/// 必須在以下情況回滾:protocolId 未註冊、存款已暫停(若實現了暫停)、
/// 資產不在白名單內(若實現了白名單)、protocolId=0、amount=0、
/// ERC-20 且 msg.value>0,或原生代幣且 msg.value!=amount。
/// 使用餘額增量法(balance-delta)入帳(對轉帳手續費代幣安全)。
/// 如果 hook 模式為 REQUIRED,則在入帳後調用 IEscrowDepositHook.onDeposit。
/// 必須具備重入保護。
function deposit(bytes32 protocolId, address asset, uint256 amount, bytes calldata data) external payable;
/// @notice 執行結算釋放。僅能由已註冊的協議合約調用。
/// 必須在以下情況回滾:調用者非註冊合約、to=address(0)、amount=0、
/// releaseId 已被使用(全局範圍)、餘額不足、釋放已暫停。
/// 遵循 CEI 模式:標記 releaseId 已使用 → 扣除餘額 → 轉帳。
/// ERC-20 轉帳必須使用 SafeERC20。必須具備 nonReentrant。
function release(bytes32 protocolId, address asset, address to, uint256 amount, bytes32 releaseId, bytes calldata data) external;
/// @notice 當前子帳戶餘額。
function escrowBalance(bytes32 protocolId, address asset) external view returns (uint256);
/// @notice 最大可存款金額。若暫停或未註冊則返回 0。
/// 對於已註冊協議且資產允許的情況,未暫停時返回 >0。
function maxDeposit(bytes32 protocolId, address asset) external view returns (uint256);
/// @notice 最大可釋放金額。若釋放暫停則返回 0。
function maxRelease(bytes32 protocolId, address asset) external view returns (uint256);
/// @notice release() 的忠實預演。若成功則返回 (true, 0)。
/// 鏡像 release() 的簽名以確保模擬完整性——檢查調用者和接收者。
function canRelease(
address caller, bytes32 protocolId, address asset, address to,
uint256 amount, bytes32 releaseId, uint256 dataLength
) external view returns (bool ok, bytes32 reasonCode);
}
第二部分:可選 — 存款鉤子介面
協議可以通過實現 IEscrowDepositHook 並將 hook 模式設為 REQUIRED 來選擇接收存款通知。
interface IEscrowDepositHook {
// 由 ProtocolEscrow 在入帳後調用。
// 必須在 msg.sender != ProtocolEscrow 時回滾。回滾將導致存款失敗。
function onDeposit(bytes32 protocolId, address depositor, address asset, uint256 amount, bytes calldata data) external;
}
| 模式 | 行為 |
|---|---|
| NONE | 無回調。直接入帳並觸發 Deposited 事件。 |
| REQUIRED | 入帳後調用 onDeposit (CEI)。鉤子回滾則存款回滾。 |
第三部分:可選 — 擴展查詢介面
interface IProtocolEscrowQueryable {
function protocolContractOf(bytes32 protocolId) external view returns (address);
function isProtocolRegistered(bytes32 protocolId) external view returns (bool);
function isDepositPaused() external view returns (bool);
function isProtocolDepositPaused(bytes32 protocolId) external view returns (bool);
function protocolReleasePauseUntil(bytes32 protocolId) external view returns (uint64);
// 全局範圍 — 無資產參數
function isReleaseIdUsed(bytes32 protocolId, bytes32 releaseId) external view returns (bool);
// 若資產被接受則返回 true;若無白名單,則應對所有資產返回 true
function isAssetAllowed(address asset) external view returns (bool);
// 若無限制則返回 type(uint256).max
function maxCallDataBytes() external view returns (uint256);
}
第四部分:標準錯誤代碼
計算方式為 keccak256(abi.encodePacked("CODE_NAME")) — 使用緊湊編碼(packed encoding),而非 abi.encode。
| 常數名稱 | 條件 |
|---|---|
| REASON_UNREGISTERED | protocolId 沒有對應的註冊合約。 |
| REASON_UNAUTHORIZED | 調用者不是 protocolId 的註冊合約。 |
| REASON_RELEASE_PAUSED | 協議釋放在活躍的暫停窗口內。 |
| REASON_RELEASE_ID_USED | releaseId 已被消耗。 |
| REASON_INSUFFICIENT_BALANCE | 子帳戶餘額 < 金額。 |
| REASON_INVALID_AMOUNT | 金額為 0。 |
| REASON_INVALID_RECIPIENT | 接收者為 address(0)。 |
| REASON_DATA_TOO_LONG (可選) | dataLength 超過實現的最大值。 |
第五部分:關鍵行為規則
註冊
protocolId↔protocolContract是的一對一、雙向且永久的綁定。不設unregisterProtocol。- 重新註冊已存在的
protocolId必須回滾。 - 支持註冊後更改地址的實現必須使用兩階段流程:提議 → 時間鎖 → 激活。不可變部署除外。
存款
- 使用餘額增量法(balanceAfter − balanceBefore)入帳。對於轉帳手續費代幣是強制性的。
- ERC-20 存款若
msg.value > 0必須回滾(防止誤送 ETH 無法找回)。
釋放
- 必須同時檢查正向(
protocolContracts[protocolId] == msg.sender)和反向(protocolIdsByContract[msg.sender] == protocolId)綁定。 - CEI 順序:標記 releaseId 已使用 → 扣除餘額 → 轉帳。
ProtocolEscrow不得檢查協議的授權機制。
Release ID 防重放
- 在
(protocolId, releaseId)級別強制執行唯一性——跨所有資產全局生效。 - 明確禁止以
(protocolId, asset, releaseId)為範圍:這會產生跨資產重放風險,即相同的授權載荷可用於掏空多個子帳戶。 - 建議構造方式:
keccak256(abi.encode(protocolId, chainId, asset, recipient, amount, nonce))。
資產白名單 (可選)
- 從白名單中移除資產僅阻止未來的存款。現有儲備必須保持可釋放狀態。
暫停語義 (可選)
- 釋放暫停必須有時間限制 (TTL) 並自動過期。禁止永久凍結。
- 若實現了
emergencyGuardian:冷卻時間必須嚴格大於maxPauseSeconds(相等則允許無縫鏈接 ≡ 永久凍結)。守護者不得為 EOA。守護者不得具備任何資產轉移能力。提前手動取消暫停必須僅限於治理操作。
運作流程
存款
用戶 → ProtocolEscrow.deposit(protocolId, USDC, 1000, data)
├─ 驗證:已註冊、未暫停、資產允許、msg.value==0
├─ safeTransferFrom(user, this, 1000)
├─ credited = balanceAfter - balanceBefore ← 對轉帳手續費安全
├─ reserves[protocolId][USDC] += credited
├─ [若為 REQUIRED] Protocol.onDeposit(protocolId, user, USDC, credited, data)
└─ 觸發 Deposited(protocolId, USDC, user, credited)
釋放
用戶 → Protocol.withdraw(authData, ...)
├─ 協議驗證授權(ZK 證明 / 簽名 / 時間鎖 / 等)
└─ 協議 → ProtocolEscrow.release(protocolId, USDC, user, 1000, releaseId, "")
├─ 檢查:caller == protocolContracts[protocolId]
├─ 檢查:protocolIdsByContract[caller] == protocolId
├─ 檢查:to!=0, amount>0, 未暫停, releaseId 未使用, 餘額>=amount
├─ usedReleaseIds[protocolId][releaseId] = true ← CEI 步驟 1
├─ reserves[protocolId][USDC] -= 1000 ← CEI 步驟 2
├─ safeTransfer(USDC, user, 1000) ← CEI 步驟 3
└─ 觸發 Released(...) + ReleaseIdConsumed(...)
設計原理
為何只有兩個功能? 介面表面被刻意最小化:接收資金和釋放資金。所有業務邏輯——何時接受、需要什麼驗證、如何追蹤用戶餘額——都屬於協議。這種邊界意味著 ProtocolEscrow 可以經過一次審計後由任意數量的協議共享。
為何是認證中立的? 與在託管層硬編碼 ZK 證明驗證的設計不同,release 信任調用的註冊合約已經完成了其自身的授權檢查。ZK 協議、多簽協議、時間鎖協議以及任何未來的方案都可以在不更改合約的情況下共享同一個託管層。
為何 releaseId 在資產間是全局的? 以 (protocolId, asset, releaseId) 為範圍會產生跨資產重放風險:同一個 ZK 無效化器(nullifier)或簽名載荷可能會在每個資產中提交一次,從而掏空多個子帳戶。全局範圍完全消除了這類攻擊。協議應將資產包含在其 releaseId 的原像中作為第二層防禦。
為何存款鉤子是可選的? 要求所有協議都執行 onDeposit 會增加外部調用(重入風險面),而無狀態協議並不需要。將鉤子設為由治理按協議配置(NONE / REQUIRED),可以讓簡單的託管協議跳過回調,同時讓 ZK 隱私協議接收實時的承諾更新。
為何 canRelease 與 release 完全鏡像? 預覽函數只有在能準確預測實際調用時才有用。包含 caller、to 和 dataLength 可以防止「預覽返回 true 但實際調用因 UNAUTHORIZED_CALLER、INVALID_RECIPIENT 或 DATA_TOO_LONG 而回滾」的假陽性情況。
為何沒有強制提款 (ForceWithdraw)? 鏈級的強制提款需要 ProtocolEscrow 理解每個協議的授權語義——誰被允許強制退出哪個用戶。由於授權邏輯被刻意保留在協議合約中,託管層無法實現通用的強制退出。需要抗審查性的協議必須確保其自身的提款是無許可的。
先前技術
| 關聯項目 | 關係 |
|---|---|
| ERC-4626 | 啟發了預覽模式(maxDeposit, maxRelease, canRelease)。ERC-4626 解決收益記帳;本標準解決無收益託管。 |
| Tornado Cash / Railgun | 這些協議各自維護自己的資金池。本標準將該層提取為共享基礎設施,讓協議純粹專注於其證明系統。 |
| vosa-20 | 一種基於 ZK 的 ERC-20 隱私傳輸協議,直接促成了本標準。vosa-20 需要一個共享託管層來運作,而無需自身持有用戶資金。 |
| Gnosis Safe Modules | 將資產託管與授權邏輯分離的相似哲學。Safe Modules 運作於帳戶級別;ProtocolEscrow 運作於協議級別,具有治理控制的註冊和隔離的子帳戶。 |
| ERC-1155 | (protocolId, asset) 子帳戶模型在概念上與 ERC-1155 的多代幣 ID 空間相似,但應用於託管餘額而非代幣所有權。 |
安全考量
重入。 deposit 和 release 都必須具備 nonReentrant。deposit 調用 onDeposit(不可信);release 調用 safeTransfer(可能是不可信代幣)。
releaseId 跨資產重放。 通過協議全局去重消除。已消耗的 releaseId 不能在同一協議下的任何資產中重放。按資產劃分範圍不符合本標準。
轉帳手續費代幣。 必須使用餘額增量入帳。若按調用者提供的金額入帳會產生虛假餘額,最終導致釋放回滾。
ERC-20 且 msg.value > 0。 必須回滾。隨 ERC-20 存款發送的 ETH 沒有找回路徑。
emergencyGuardian 暫停濫用。 冷卻時間必須嚴格大於 maxPauseSeconds。相等的值允許無縫的連續暫停,構成事實上的永久凍結。
治理密鑰洩漏。 洩漏的治理密鑰無法直接轉移用戶資產。但它可能會註冊一個惡意的協議合約來掏空其自身的子帳戶。具體的治理模型由部署定義;對於高風險操作,實現應使用帶有時間鎖的多簽。
協議合約變更窗口。 在「提議 → 激活」期間,舊合約仍具備授權。支持暫停的部署可以在此窗口期間凍結釋放。不支持暫停的部署應相應調整時間鎖長度。
集中風險。 ProtocolEscrow 中的漏洞會同時影響所有註冊協議。這是公認的權衡:一個高質量的審計合約取代了許多低質量的各協議金庫。
測試清單(摘要)
第一級 — 核心一致性(強制)
- ERC-20 和原生代幣的 deposit/release 正常路徑
- 餘額按增量入帳,而非按調用者提供的金額
- 未註冊調用者、錯誤的 protocolId、反向綁定不匹配均觸發回滾
- 重複的 releaseId 回滾;不同資產使用相同 releaseId 亦回滾(全局範圍)
- 金額 > 餘額回滾;protocolId=0 / to=0 / amount=0 均回滾
- ERC-20 且 msg.value>0 回滾;原生代幣且 msg.value!=amount 回滾
canRelease是忠實的:當且僅當 release 會成功時返回 (true,0);每個失敗都有正確的reasonCode- 重入:
release中的 ERC-20 轉帳回調不會破壞狀態;(若實現了 hook)onDeposit重入不會破壞狀態
第二級 — 擴展特性(若實現了該功能則為必測)
- Hook 模式:NONE 跳過回調;REQUIRED 在 hook 失敗時回滾;非 ProtocolEscrow 調用者回滾
- 暫停:全局/各協議存款和釋放暫停的隔離;TTL 自動過期
- 守護者冷卻:冷卻期內的第二次暫停回滾;守護者無法取消暫停
- 白名單:非允許資產存款回滾;已移除資產的釋放仍能成功
- 合約變更:時間鎖狀態機(ETA 前、過期後、並發提議)
- 治理:任何特權函數的未授權調用者均回滾
向後兼容性
此 ERC 引入了新的介面,不修改任何現有標準。現有的 ERC-20 和原生代幣機制保持不變。希望採用此標準的協議通過實現上述註冊和釋放觸發流程進行集成。
開放討論問題
- protocolId 派生標準。 ERC 是否應強制要求
keccak256(abi.encode(chainId, name, owner)),還是留給具體實現決定? - Released vs ReleaseIdConsumed。 我們指定了這兩個事件——一個用於財務審計追蹤,一個用於重放檢測。這種 Gas 重複是否值得,或者一個事件應承擔兩個角色?
- canRelease 參數開銷。
caller、to和dataLength雖然使預演更忠實,但增加了 calldata 成本。是否應將更輕量級的canReleaseSimple(protocolId, asset, amount, releaseId)納入標準? - 治理介面標準化。 規範將治理模型完全留給實現定義。ERC 是否應指定最小的提議/執行介面,還是保持開放?
- 預部署 vs. 獨立部署。 設計時考慮了鏈級預部署,但作為獨立合約同樣有效。ERC 是否應明確兼顧兩者?
- 多資產批量釋放。 可選的
batchRelease是否應成為標準的一部分,還是留給協議自行處理? - 協議災難恢復要求。 如果協議的提款功能永久失效,
ProtocolEscrow不提供鏈級回退方案。目前的無許可提款准入檢查僅驗證正常路徑。標準是否應要求註冊協議展示災難恢復路徑(例如:通過合約遷移保留用戶資金訪問權限的文檔化升級計劃)?三種立場:- 嚴格: 要求將文檔化的升級/恢復計劃作為註冊的治理前提。
- 軟性: 作為最佳實踐建議;將執行權留給各個部署。
- 中立: 超出範圍——協議級別的容錯是協議自身的責任。
我們期待來自隱私協議、跨鏈橋、支付系統或任何目前維護自身資金池的團隊的反饋。目標是確定這種抽象是否處於正確的層級、介面是否存在漏洞,以及安全屬性是否足以用於生產環境。
1 則貼文 - 1 位參與者
[閱讀完整主題](https://ethereum-magicians.org/t/draft-erc-chain-level-protocol-escrow-standard-protocolescrow/28154)