ERC-8194:支付門控交易中繼協議
ERC-8194 引入了支付門控交易中繼(PGTR),這是一種允許 AI 代理和物聯網設備等主體透過支付收據而非私鑰簽名來授權鏈上操作的協議。
摘要
支付門檻交易中繼(Payment-Gated Transaction Relay, PGTR)定義了一種代表主體(包括人類、AI 代理、物聯網設備或合約)轉發鏈上交易的協議。在該協議中,授權證明是鏈上支付收據,而非加密簽名。PGTR 轉發器(Forwarder) 接收來自付款人的 X402 樣式支付,根據預期金額和目標驗證支付,並調用目標合約,同時將付款人地址作為經過驗證的發送者提供。目標合約透過 pgtrSender() 從轉發器讀取經過驗證的付款人地址,而非從 msg.sender 讀取。
PGTR 是**主體無關(actor-agnostic)**的:付款人可以是使用錢包 App 的人類、擁有資金地址的自主 AI 代理、物聯網設備或任何持有代幣的實體。該協議對所有主體一視同仁——授權基礎是「我付過錢了」,而不是「我簽過名了」。正是這一特性使得 TMP (EIP-TMP) 能夠在單一介面下支持人類↔代理、代理↔代理以及人類↔人類的任務交互。
PGTR 是一種新的原語。它不是 ERC-2771 的配置文件。ERC-2771 解決的是 Gas 抽象問題——簽名者仍需持有密鑰,且在中繼執行前必須產生離線簽名。PGTR 解決的是**密鑰抽象(key abstraction)**問題——任何控制代幣並能訪問 HTTP 端點的主體,都可以在無需管理私鑰的情況下授權鏈上操作。
動機
任何能夠持有代幣的主體(無論是人還是機器)都應該能夠授權鏈上操作,而無需承擔私鑰管理的開銷。現有的元交易標準(ERC-2771, ERC-4337)預設主體能夠產生有效的加密簽名。這一假設對於廣泛的主體類別並不適用:
- 無密鑰 AI 代理 — 運行在沙盒環境中的軟體代理,在這些環境中存儲密鑰是不切實際或不可取的。
- 偏好僅支付體驗的人類用戶 — 透過支付流程(例如:信用卡 → 穩定幣橋 → X402)進行交互的用戶,不應被要求為鏈上授權管理單獨的簽名密鑰。
- 微支付門檻 API — 接收 X402 支付並需要將其轉化為鏈上操作,且無需中間密鑰託管的 HTTP 服務。
- 物聯網與機器帳戶 — 授權邏輯為「我付過錢了」而非「我簽過名了」的設備和自動化系統。
PGTR 將所有這些案例統一在單一原語下:任何持有代幣的主體,無論是人還是機器,都可以在平等的基礎上參與受 PGTR 保護的協議。PGTR 建立了一個最小介面,目標合約透過實現該介面來信任 PGTR 轉發器,而轉發器實現該介面以便被 ERC-165 檢測。
規範
本文件中的關鍵詞「必須 (MUST)」、「不得 (MUST NOT)」、「要求 (REQUIRED)」、「應當 (SHALL)」、「應當不 (SHALL NOT)」、「應該 (SHOULD)」、「不應該 (SHOULD NOT)」、「推薦 (RECOMMENDED)」、「可以 (MAY)」和「可選 (OPTIONAL)」應按照 RFC 2119 中的描述進行解釋。
概覽
PGTR 流程分為三個步驟:
- 支付 — 付款人透過原子的 ERC-3009
transferWithAuthorization或等效機制將代幣轉移給轉發器(或轉發器指定的託管處)。 - 驗證 — 轉發器驗證:(a) 支付金額達到請求操作的門檻;(b) 支付收據之前未被使用過;(c) 收據未過期。
- 中繼 — 轉發器調用目標合約。目標合約透過轉發器上的
pgtrSender()讀取經過驗證的付款人地址。
ITMPForwarder 介面
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
/// @title ITMPForwarder — PGTR 轉發器介面
/// @notice 由 PGTR 轉發器合約實現。目標合約在讀取 pgtrSender() 之前
/// 必須檢查 isTrustedForwarder(msg.sender)。
interface ITMPForwarder is IERC165 {
/// @notice 返回 true。允許 ERC-165 檢測 PGTR 轉發器。
function isPGTRForwarder() external view returns (bool);
/// @notice 授權當前轉發調用的付款人地址。
/// 類似於 EIP-2771 附加在 calldata 後的 msg.sender。
/// 如果在活動的轉發調用之外調用,必須 revert。
function pgtrSender() external view returns (address payer);
/// @notice 如果 addr 被此合約信任為 PGTR 轉發器,則返回 true。
/// 鏡像了 EIP-2771 接收端的模式,用於對稱檢測。
function isTrustedForwarder(address addr) external view returns (bool);
/// @notice 每次成功轉發支付門檻調用時觸發。
/// @param payer 經過驗證的付款人地址。
/// @param target 接收調用的目標合約。
/// @param selector 轉發調用的 4 字節函數選擇器。
/// @param paymentAmount 支付的代幣數量(以代幣的最小單位計)。
event PaymentGatedCall(
address indexed payer,
address indexed target,
bytes4 indexed selector,
uint256 paymentAmount
);
}
目標合約要求
接受來自 PGTR 轉發器調用的合約必須:
- 維護一組受信任的轉發器地址:
mapping(address => bool) public trustedForwarders; - 暴露
isTrustedForwarder(address)並返回trustedForwarders[addr]。 - 當需要確定實際調用者時,檢查
msg.sender是否在trustedForwarders中。如果受信任,則透過ITMPForwarder(msg.sender).pgtrSender()讀取經過驗證的付款人,而非直接使用msg.sender:
function _effectiveSender() internal view returns (address) {
if (trustedForwarders[msg.sender]) {
return ITMPForwarder(msg.sender).pgtrSender();
}
return msg.sender;
}
- 當透過轉發器調用時,不得將
msg.sender作為僅限請求者操作的授權檢查。轉發器在轉發前必須將pgtrSender()設置為正確的請求者。
轉發器要求
PGTR 轉發器必須:
- 實現
ITMPForwarder並在supportsInterface(type(ITMPForwarder).interfaceId)中返回 true。 - 在單個交易中原子地執行支付轉移和目標調用。如果其中任何一個失敗,整個交易必須 revert。
- 在轉發前,驗證支付收據未被使用:
mapping(bytes32 => bool) public consumedReceipts;
bytes32 receiptHash = keccak256(abi.encode(
payer, amount, nonce, expiry, target, selector
));
require(!consumedReceipts[receiptHash], "Receipt already consumed");
consumedReceipts[receiptHash] = true;
- 在轉發前驗證收據未過期:
require(block.timestamp <= expiry, "Receipt expired"); - 在轉發調用期間將
pgtrSender()設置為經過驗證的付款人地址,並在調用返回或 revert 後重置。 - 在成功轉發後觸發
PaymentGatedCall事件。
PGTR 轉發器應該:
- 在生產環境中使用時,應為多簽或治理控制的地址。單一 EOA 轉發器必須披露為中心化風險。
- 為每個「目標+選擇器」組合實現可配置的最低支付金額。
- 在繼續操作前,驗證付款人的代幣授權(allowance)或簽名授權涵蓋了所需金額。
重放保護
支付收據必須是單次使用的。規範的收據雜湊(hash)為:
keccak256(abi.encode(payer, amount, nonce, expiry, target, selector))
其中:
payer— 支付主體的地址amount— 以代幣最小單位計的支付金額nonce— 每個付款人的唯一值以防止重放(可以是隨機 bytes32 或單調遞增計數器)expiry— 超過此 Unix 時間戳後收據必須被拒絕target— 目標合約地址selector— 預期調用的 4 字節函數選擇器
轉發器必須存儲已使用的收據雜湊並拒絕任何重複項。
ERC-165 介面檢測
ITMPForwarder interfaceId = 0xTBD
計算方式為以下函數選擇器的異或(XOR):
isPGTRForwarder()— 0xTBDpgtrSender()— 0xTBDisTrustedForwarder(address)— 0xTBD
確切的計算值請參見附錄 A。
與 ERC-2771 的關係
PGTR 和 ERC-2771 是解決不同問題的不同原語:
| 維度 | ERC-2771 | PGTR |
|---|---|---|
| 主體模型 | 僅限持有密鑰的主體 | 主體無關(人、代理、IoT、合約) |
| 授權證明 | 離線 ECDSA 簽名 | 鏈上支付收據 |
| 主體要求 | 必須持有私鑰 | 必須持有代幣 |
| 發送者檢測 | calldata 中附加 20 字節後綴 | 調用轉發器的 pgtrSender() |
| Gas 支付 | 中繼者支付 Gas | 轉發器(伺服器)支付 Gas |
| 重放保護 | 轉發器中的簽名 nonce | 轉發器中的收據雜湊 |
| 使用場景 | 為密鑰持有者提供 Gas 抽象 | 為任何代幣持有主體提供密鑰抽象 |
目標合約可以同時支持 ERC-2771 和 PGTR,方法是先檢查 msg.sender 是否為 ERC-2771 受信任轉發器,然後再檢查是否為 PGTR 受信任轉發器。
PGTR 並不聲稱是 ERC-2771 的「配置文件」。它使用了不同的發送者檢測機制(顯式函數調用 vs. 附加 calldata 字節)和根本不同的授權模型(支付驗證 vs. 簽名驗證)。僅實現 ITMPForwarder 的實現不得聲稱符合 ERC-2771。
原理
為什麼使用 pgtrSender() 而不是附加 calldata?
ERC-2771 將原始發送者附加為 calldata 的最後 20 個字節。這種方法要求目標合約在每個需要驗證發送者的函數中解析 calldata。PGTR 在轉發器上使用顯式的 pgtrSender() view 調用,這樣做:
- 實現更簡單 — 無需操作 calldata。
- 更具可審計性 — 身份驗證路徑是顯式且可搜索的。
- 具備組合性 — 任何函數都可以在
msg.sender上調用pgtrSender(),不受 calldata 大小限制。
權衡之處在於每次調用增加了一次額外的外部 STATICCALL(按當前定價約 700 Gas),相對於轉發操作本身的成本,這是可以忽略不計的。
為什麼使用支付收據而不是簽名授權?
簽名授權要求主體:
- 擁有私鑰訪問權限。
- 在調用中繼前計算並傳輸有效的簽名。
這一要求與在受限環境中運行的輕量級無密鑰代理不相容。支付收據僅要求主體持有代幣並能訪問支付端點——任何擁有資金錢包地址的自主代理都能滿足這些要求。
為什麼使用 mapping(address => bool) 而不是單一地址?
單一的受信任轉發器地址是單點故障。如果轉發器的 EOA 被攻破或變得不可用,目標合約將陷入癱瘓。映射(mapping)允許:
- 在不重新部署合約的情況下更換密鑰。
- 為了可用性設置多個並行轉發器。
- 在無停機的情況下進行階段性過渡(添加新的,移除舊的)。
轉發器運營者的安全考慮
- 收據過期窗口 — 短暫的過期窗口(約 60-300 秒)可以限制收據在傳輸過程中被攔截後的利用窗口。
- 每個選擇器的最低金額 — 為每個(目標,選擇器)對設置最低支付門檻,使重放收據在經濟上變得無利可圖。
- 搶跑(Frontrunning) — 由於收據雜湊包含付款人地址,攻擊者若要搶跑中繼調用,也必須控制代幣轉移。在單個交易中原子執行可以完全緩解搶跑問題。
- 轉發器被攻破 — 被攻破的轉發器可以代表之前已支付的付款人轉發任意調用。運營者應該使用多簽或鏈上治理密鑰作為轉發器的所有者。目標合約應該為所有轉發調用觸發事件,以便監控檢測異常。
向後兼容性
此 ERC 引入了一個新介面,未對現有標準進行修改。同時實現此標準和 ERC-2771 的目標合約仍與 ERC-2771 中繼者兼容,前提是它們按優先順序檢查轉發器類型。
安全考慮
- 重入(Reentrancy) — 轉發器必須在轉發調用周圍使用重入保護。目標合約必須為所有狀態修改操作使用重入保護。
- 收據重放 — 轉發器必須在轉發調用的交易中原子地消耗收據。收據不得跨鏈使用(如果擔心跨鏈收據的可移植性,請在雜湊中包含
chainId)。 - 轉發器信任邊界 — 目標合約在沒有治理控制的情況下不得添加轉發器。不受限制的
addForwarder在功能上等同於selfdestruct。 - pgtrSender() 原子性 — 轉發器必須在轉發調用返回後重置
pgtrSender()。如果轉發器允許讀取pgtrSender()的重入調用,之前的付款人地址可能會被錯誤地歸因於重入調用。
參考實現
請參見參考實現倉庫中的 src/interfaces/ITMPForwarder.sol:GitHub - daydreamsai/taskmarket-contracts。
TaskMarket.sol 中提供了一個完整的 PGTR 兼容目標合約參考實現,它實現了 trustedForwarders 映射模式,並暴露了用於 ERC-165 檢測的 supportsInterface(type(ITMP).interfaceId)。
版權
透過 CC0 放棄版權及相關權利。
附錄 A:介面 ID 計算
ITMPForwarder 介面 ID 的計算方式是介面中聲明的所有函數的 4 字節 Keccak256 選擇器的異或(XOR):
| 函數 | 選擇器 |
|---|---|
isPGTRForwarder() | 部署時計算 |
pgtrSender() | 部署時計算 |
isTrustedForwarder(address) | 部署時計算 |
在 Solidity 中計算:
bytes4 id = type(ITMPForwarder).interfaceId;
在鏈下計算 (JavaScript/viem):
import { toFunctionSelector, keccak256, toBytes } from 'viem';
const xor = (a: string, b: string) =>
'0x' + (parseInt(a, 16) ^ parseInt(b, 16)).toString(16).padStart(8, '0');
const id = [
toFunctionSelector('isPGTRForwarder()'),
toFunctionSelector('pgtrSender()'),
toFunctionSelector('isTrustedForwarder(address)'),
].reduce(xor);