x402 erc20ApprovalGasSponsoring 扩展详解:面向 exact EVM 方案的 ERC-20 无 Gas 审批与原子结算
【免费下载链接】x402A payments protocol for the internet. Built on HTTP.项目地址: https://gitcode.com/GitHub_Trending/x4/x402
本文围绕 x402 协议规范中的erc20ApprovalGasSponsoring扩展,讲解如何在 HTTP 402 支付流程中实现 ERC-20 代币的"无 Gas"审批:Client 如何离线签名一笔approve(Permit2, amount)交易、Facilitator 如何校验并垫付 Gas 完成结算,以及该扩展在 Python 与 Go SDK 中的具体实现(交易构造、字段校验、原子批量执行接口)。读完本文,你将能够理解该扩展的完整消息格式、校验与结算逻辑,并能在 specs/schemes/exact/scheme_exact_evm.md 方案之上为不支持 EIP-2612 的 ERC-20 代币接入无 Gas 审批能力。
为什么需要这个扩展:exact 方案下的审批难题
在 scheme_exact(EVM 链)方案中,当extra.assetTransferMethod为permit2时,付款方需要先把代币授权给 Canonical Permit2 合约,Facilitator 才能代表用户调用 x402Permit2Proxy 完成结算(其 Solidity 实现见 contracts/evm/src/x402BasePermit2Proxy.sol、contracts/evm/src/x402UptoPermit2Proxy.sol)。
问题在于:
- 支持 EIP-2612 的代币可以用一条链下
permit签名替代链上审批,这是另一个扩展 eip2612_gas_sponsoring.md 覆盖的场景; - 但大量 ERC-20 代币并不实现 EIP-2612,用户必须先发起一笔消耗原生 Gas 的
approve交易。对于纯钱包地址(EOA)或移动端轻客户端来说,要求用户先充值 Gas 再付 API 费用,会直接打断支付体验。
erc20ApprovalGasSponsoring扩展正是为此设计:它让Client 签好但不上链的审批交易随PaymentPayload一起提交,由Facilitator负责垫付 Gas、转发交易并完成结算。按 Go SDK 包注释的表述(go/extensions/erc20approvalgassponsor/types.go):"这是面向不实现 EIP-2612的 ERC-20 代币的无 Gas 审批扩展。与链下签名不同,Client 创建一笔已签名(但未广播)的approve(Permit2, MaxUint256)交易,Facilitator 在调用 settle() 之前将其广播。"
核心流程:三方职责与原子性要求
规范(specs/extensions/erc20_gas_sponsoring.md)定义的分工如下:
| 角色 | 职责 |
|---|---|
| Client | 构造并离线签名的普通 EVM 交易,调用token.approve(Permit2, amount) |
| Facilitator | 1) 若 Client 原生 Gas 余额不足,向其钱包补足足够的原生代币;2)广播Client 签好名的审批交易;3) 审批确认后立即通过x402Permit2Proxy完成结算 |
其中有一个关键的安全约束:整个流程必须使用**原子批量交易(atomic batch transaction)**执行。原因在于时序风险——从 Facilitator 向用户打 Gas 到最终结算之间存在时间窗口,恶意行为者可能抢跑(front-run)并在两者之间截走资金;原子化打包消除了这个窗口。
声明支持:402 Payment Required 响应中的 extensions 条目
Facilitator 通过在402 Payment Required响应的extensions对象中包含erc20ApprovalGasSponsoring条目来声明支持该扩展。规范给出的完整声明示例如下:
{ "x402Version": "2", "accepts": [ { "scheme": "exact", "network": "eip155:84532", "amount": "10000", "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", "maxTimeoutSeconds": 60, "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "extra": { "assetTransferMethod": "permit2", "name": "USDC", "version": "2" } } ], "extensions": { "erc20ApprovalGasSponsoring": { "info": { "description": "The facilitator accepts a raw signed approval transaction and will sponsor the gas fees.", "version": "1" // 这里没有其它字段,因为其余字段全部由 Client 填写 }, "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "from": { "type": "string", "pattern": "^0x[a-fA-F0-9]{40}$", "description": "The address of the sender." }, "asset": { "type": "string", "pattern": "^0x[a-fA-F0-9]{40}$", "description": "The ERC-20 token contract address to approve." }, "spender": { "type": "string", "pattern": "^0x[a-fA-F0-9]{40}$", "description": "The address of the spender (Canonical Permit2)." }, "amount": { "type": "string", "pattern": "^[0-9]+$", "description": "Approval amount (uint256). Typically MaxUint." }, "signedTransaction": { "type": "string", "pattern": "^0x[a-fA-F0-9]+$", "description": "RLP-encoded signed transaction calling ERC20.approve()." }, "version": { "type": "string", "pattern": "^[0-9]+(\\.[0-9]+)*$", "description": "Schema version identifier." } }, "required": [ "from", "asset", "spender", "amount", "signedTransaction", "version" ] } } } }各字段的语义与约束(与 SDK 中的 JSON Schema 实现一一对应,见 python/x402/extensions/erc20_approval_gas_sponsoring/schema.py):
| 字段 | 类型/约束 | 说明 |
|---|---|---|
from | ^0x[a-fA-F0-9]{40}$ | 发送方地址(代币持有者) |
asset | ^0x[a-fA-F0-9]{40}$ | 被审批的 ERC-20 代币合约地址 |
spender | ^0x[a-fA-F0-9]{40}$ | 被授权地址,必须是 Canonical Permit2 |
amount | ^[0-9]+$ | 审批额度(uint256 十进制字符串),通常为 MaxUint256 |
signedTransaction | ^0x[a-fA-F0-9]+$ | RLP 编码的已签名approve交易 hex |
version | ^[0-9]+(\.[0-9]+)*$ | Schema 版本标识(如 "1"、"1.0"、"1.2.3") |
注意info与schema的分工:info在 402 响应中只携带description和version("其余字段全部由 Client 填写");而 Client 实际提交的PaymentPayload.extensions.erc20ApprovalGasSponsoring对象中,info就是按上述 schema 填满的六字段对象。Go SDK 的声明函数 go/extensions/erc20approvalgassponsor/declare.go 返回的就是该结构,其描述文本为 "The facilitator accepts a pre-signed ERC-20 approve(Permit2, MaxUint256) transaction for tokens without EIP-2612",并附Version: "1"常量(go/extensions/erc20approvalgassponsor/types.go)。
Client 侧流程:构造、签名并挂载审批交易
规范规定的 Client 侧步骤:
- 构造一笔普通的以太坊交易,调用
token.approve(Permit2, amount); - 在链下对该交易签名;
- 将原始签名交易的 hex放入
PaymentPayload的:
extensions.erc20ApprovalGasSponsoringClient 实现要点
规范明确要求 Client 保证两点,否则签名交易将无效:
maxFee与maxPriorityFee必须与当前网络价格匹配(EIP-1559 费率);nonce必须等于该钱包当前链上 nonce。
Python SDK 中的参考实现 python/x402/extensions/erc20_approval_gas_sponsoring/client.py 完整展示了这一步。sign_erc20_approval_transaction(signer, token_address, chain_id)的关键行为(client.py#L17-L87):
- calldata:用 ERC-20
approveABI 编码approve(PERMIT2_ADDRESS, MAX_UINT256),其中MAX_UINT256 = 2**256 - 1,即规范所说"通常为 MaxUint";若运行环境没有 Web3,则回退到手工拼接——approve 函数选择器095ea7b3加 32 字节补齐的 Permit2 地址与 MaxUint256(client.py#L50-L53); - nonce:实时读取
signer.get_transaction_count(signer.address); - 费率:优先调用 signer 的
estimate_fees_per_gas()获取网络当前价格;若不可用,回退到保守默认值maxFeePerGas = 1_000_000_000wei(1 Gwei)、maxPriorityFeePerGas = 100_000_000wei(0.1 Gwei)(client.py#L57-L65); - 交易类型:固定为
type: 2(EIP-1559 动态费率交易),gas取常量ERC20_APPROVE_GAS_LIMIT(定义于 python/x402/mechanisms/evm/constants.py); - signer 契约:signer 需提供
address、sign_transaction(tx_dict) -> hex、get_transaction_count(address) -> int,可选estimate_fees_per_gas(); - 返回值是
Erc20ApprovalGasSponsoringInfo数据类(python/x402/extensions/erc20_approval_gas_sponsoring/types.py#L11-L41),to_dict()输出的键名恰好就是 schema 要求的from / asset / spender / amount / signedTransaction / version,spender固定为 Canonical Permit2 地址,amount为 MaxUint256 的十进制字符串。
PaymentPayload 示例
规范给出的完整PaymentPayload示例(注意:payload.permit2Authorization走的是常规 permit2 授权路径,而extensions.erc20ApprovalGasSponsoring携带的是那笔已签名的approve交易,两者同包提交):
{ "x402Version": "2", "accepts": [ { "scheme": "exact", "network": "eip155:84532", "amount": "10000", "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", "maxTimeoutSeconds": 60, "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "extra": { "assetTransferMethod": "permit2", "name": "USDC", "version": "2" } } ], "payload": { "signature": "0x2d6a7588d6acca505cbf0d9a4a227e0c52c6c34008c8e8986a1283259764173608a2ce6496642e377d6da8dbbf5836e9bd15092f9ecab05ded3d6293af148b571c", "permit2Authorization": { "permitted": { "token": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "amount": "10000" }, "from": "0x857b06519E91e3A54538791bDbb0E22373e36b66", "spender": "0x402085c248EeA27D92E8b30b2C58ed07f9E20001", "nonce": "33247007178036348590600198031289925668252061821958005840077069883511451257277", "deadline": "1740672154", "witness": { "to": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", "validAfter": "1740672089" } } }, "extensions": { "erc20ApprovalGasSponsoring": { "info": { "from": "0x857b06519E91e3A54538791bDbb0E22373e36b66", "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "spender": "0x000000000022D473030F116dDEE9F6B43aC78BA3", "amount": "115792089237316195423570985008687907853269984665640564039457584007913129639935", "signedTransaction": "0x505cbf0d9a4a227e0c52c6c2d6a7588d6acca34008c8e8986a12832597641d6293af148b571c73608a2ce6496642e377d6da8dbbf5836e9bd15092f9ecab05ded3", "version": "1" } } } }示例中amount即 MaxUint256(2^256 - 1)的十进制表示,spender为 Canonical Permit2 合约地址(其权威地址见 specs/schemes/exact/scheme_exact_evm.md 的 Canonical Permit2 一节)。
Facilitator 校验逻辑:四步验证
规范规定,收到携带erc20ApprovalGasSponsoring的PaymentPayload后,Facilitator 必须执行以下验证:
1. 解码原始签名交易
对signedTransaction执行 RLP 解码。
2. 校验交易字段
- 恢复出的签名者必须与
from一致; to地址必须等于asset代币合约;- calldata必须对应
approve(spender, amount)调用; - 关键:Facilitator必须验证扩展数据中的
spender与解码交易中 calldata 里的 spender 一致,且等于期望合约(Canonical Permit2); - nonce必须等于用户当前链上 nonce;
- maxFee / maxPriorityFee必须与当前网络价格匹配。
Python SDK 的校验实现 python/x402/extensions/erc20_approval_gas_sponsoring/facilitator.py 将上述逻辑落成了一条清晰的错误码链,可作为 Facilitator 实现的参照:
validate_erc20_approval_for_payment(facilitator.py#L60-L90)依次检查:- schema 格式(六个字段正则)→ 失败返回
invalid_erc20_approval_extension_format; info.from与本次付款人(payer)一致 →erc20_approval_from_mismatch;info.asset与本次支付使用的代币地址一致 →erc20_approval_asset_mismatch;info.spender等于PERMIT2_ADDRESS(常量定义在 python/x402/mechanisms/evm/constants.py)→erc20_approval_spender_not_permit2。
- schema 格式(六个字段正则)→ 失败返回
_validate_signed_approval_tx(facilitator.py#L93-L154)进一步解码交易:- 用
Account.recover_transaction恢复签名者,与 payer 比对 →erc20_approval_tx_signer_mismatch; - 用
TypedTransaction.from_bytes解析 typed 交易,确认to指向代币合约 →erc20_approval_tx_wrong_target; - 确认 calldata 以 approve 选择器
095ea7b3开头 →erc20_approval_tx_wrong_selector; - 从 calldata 中解析第 32–72 个 hex 字符(即 bytes 4..36 的 32 字节补齐地址)作为 spender,与 Permit2 地址比对 →
erc20_approval_tx_wrong_spender; - 解析失败则返回
erc20_approval_tx_parse_failed/erc20_approval_tx_invalid_signature。
- 用
Go SDK 提供了同层的格式提取与校验:ExtractInfo从PaymentPayload的extensions中取出六字段并检查完整性(go/extensions/erc20approvalgassponsor/extract.go#L21-L60),ValidateInfo执行与 JSON Schema 一致的正则校验(extract.go#L62-L70),测试覆盖见 extract_test.go。
3. 检查用户 Gas 余额
检查from地址是否有足够的原生代币覆盖这笔审批交易的成本:
- 余额充足 →跳过打款步骤;
- 余额不足 → Facilitator计算缺口并垫付差额。
4. 模拟完整执行序列
Facilitator 必须在单个原子批量交易中模拟:
- Funding:向用户发送原生 Gas 代币(仅在需要时);
- Approval Relay:广播用户签好名的审批交易;
- Settlement:调用
x402Permit2Proxy.settle。
结算逻辑:Facilitator 的原子打包
规范最后定义的结算逻辑即 Facilitator 构造的原子 bundle的三个有序操作:
- Gas Funding:若用户原生 Gas 不足,向用户(
from)发送足以支付审批交易 Gas 的原生代币; - Broadcast Approval:广播 Client 提供的
signedTransaction(调用ERC20.approve(Permit2, amount)); - x402Permit2Proxy Settlement:调用
x402Permit2Proxy.settle()完成结算,把代币划转给payTo。
从源码结构看,SDK 把"如何保证原子性"留给了 Facilitator 的 signer 实现。Go SDK 定义了专门的多交易执行接口(go/extensions/erc20approvalgassponsor/types.go#L62-L95):
TransactionRequest:单个执行单元,Serialized为已签名的 raw 交易 hex(直接sendRawTransaction转发),或Call(未签名的合约写调用,由 signer 签名执行)——前者对应转发用户的审批交易,后者对应打款与 settle 调用;Erc20ApprovalGasSponsoringSigner:在FacilitatorEvmSigner基础上扩展SendTransactions(ctx, []TransactionRequest) ([]string, error),注释明确"signer 拥有执行策略(顺序执行、批量执行,或通过 Flashbots、multicall、智能合约账户批处理实现原子打包)";Erc20ApprovalGasSponsoringSimulator:可选的SimulateTransactions能力,对应规范中"模拟完整执行序列"这一步;Erc20ApprovalFacilitatorExtension:持有 signer 并注册到 Facilitator,支持SignerForNetwork按网络选择不同结算 signer(优先于默认 signer),该优先级行为有单测覆盖(resolve_signer_test.go)。
Python SDK 的接口与之对应(python/x402/extensions/erc20_approval_gas_sponsoring/types.py#L57-L104):Erc20ApprovalGasSponsoringSigner协议要求send_transactions(list[TransactionRequest]) -> list[str]("发送一批交易,元素可以是已签名 raw hex 或WriteContractCall,返回逐笔交易哈希")以及wait_for_transaction_receipt;Erc20ApprovalFacilitatorExtension以key = "erc20ApprovalGasSponsoring"标识自身,并支持signer_for_network解析器,resolve_signer(network)优先使用按网络的解析结果。
仓库中的实现与验证入口
围绕该规范,仓库中可直接深入阅读的对应物:
| 内容 | 路径 |
|---|---|
| 扩展规范(本文主体) | specs/extensions/erc20_gas_sponsoring.md |
| 所依赖的 exact EVM 方案与 Canonical Permit2 | specs/schemes/exact/scheme_exact_evm.md |
| 姊妹扩展:EIP-2612 permit 场景 | specs/extensions/eip2612_gas_sponsoring.md |
| Python 实现(类型/客户端签名/Facilitator 校验/Schema/声明) | python/x402/extensions/erc20_approval_gas_sponsoring/ |
| Go 实现(声明/提取/校验/Signer 接口) | go/extensions/erc20approvalgassponsor/ |
| settle 的链上实现(Permit2 代理合约族) | contracts/evm/src/x402BasePermit2Proxy.sol、contracts/evm/src/x402ExactPermit2Proxy.sol |
适用前提与限制
- 该扩展仅服务于
assetTransferMethod: "permit2"的 exact EVM 方案:accepts条目中必须能匹配该 scheme/network/asset;若代币支持 EIP-2612,规范建议优先考虑 eip2612_gas_sponsoring.md 扩展的纯链下签名路径; spender被硬校验为 Canonical Permit2(Python 实现直接比对PERMIT2_ADDRESS常量),不是任意 spender;- Client 的
maxFee/maxPriorityFee与nonce若与链上状态脱节,签名交易将直接作废——这是 Client 侧必须实时获取网络价格与 nonce 的根本原因; - 原子性是安全边界而非优化项:规范明确以原子批量交易来对抗"打款与结算之间被抢跑截资"的风险,Facilitator 若无法提供批量/打包执行能力(Go 接口中的
SendTransactions/SimulateTransactions),则不应声明支持该扩展。
【免费下载链接】x402A payments protocol for the internet. Built on HTTP.项目地址: https://gitcode.com/GitHub_Trending/x4/x402
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考