使用 ERC-7730 为你的协议添加明文签名
大多数重大的以太坊漏洞利用都有相同的最后一步:用户授权了一笔他们无法真正理解的交易。硬件钱包显示原始的十六进制调用数据,更糟糕的是,它们迫使你开启盲签。软件钱包会显示解码后的字段,但前提是它们能识别该合约。当它们无法识别时(无论是因为协议是新的、应用程序被攻破,还是设备离线),用户只能盲签。
ERC-7730 (opens in a new tab) 定义了一种标准的 JSON 格式,用于描述你的合约函数调用的含义。
支持 ERC-7730 的钱包会读取你的描述符并显示:
兑换
发送:1,000 USDC
最低接收:0.42 WETH
协议:尤尼斯瓦普 V3 (Uniswap V3)
或者是一句人类和代理都能读懂的完整句子:
将 1,000 USDC 兑换为至少 0.42 WETH
而不是一个函数选择器和一堆原始整数值。
这就是明文签名 (opens in a new tab)——“所见即所签”。本教程将引导你为自己的合约编写描述符,使用官方 CLI 工具进行验证,并将其提交到开放注册表。
前提条件
- 熟悉 Solidity 和智能合约 ABI
- 一个已部署且具有已验证 ABI 的智能合约(在描述符被注册表接受之前,需要进行 Sourcify (opens in a new tab) 验证)
- 用于验证 CLI 的 Python 3.12+
- 基本的 JSON 知识
什么是 ERC-7730 描述符?
描述符是一个包含三个部分的单一 JSON 文件:
| 部分 | 目的 |
|---|---|
context | 通过链 ID 和地址将描述符绑定到特定的合约部署 |
metadata | 命名项目并定义可重用的常量 |
display | 将每个函数签名映射到人类可读的标签和字段格式 |
由于描述符与合约本身是分离的,你可以为任何现有协议添加明文签名支持,而无需重新部署。钱包从注册表中检索描述符,并在签名时使用它们。
第 1 步:创建文件骨架
创建一个名为 calldata-<contractname>-<descriptorversion>.json 的文件。calldata- 前缀告诉注册表此描述符涵盖合约函数调用,而不是用于类型化数据消息的 eip712-。descriptorversion 告诉注册表描述符文件的版本,如果未提供版本,则默认为 0。
{
"$schema": "https://eips.ethereum.org/assets/eip-7730/erc7730-v2.schema.json",
"context": {},
"metadata": {},
"display": {
"formats": {}
}
}
第 2 步:编写上下文部分
context 部分将描述符绑定到一个或多个合约部署。钱包使用它来将传入的交易与正确的描述符进行匹配。
"context": {
"$id": "uniswap-v3-router-mainnet",
"contract": {
"deployments": [
{ "chainId": 1, "address": "0xYourContractAddressOnMainnet" },
{ "chainId": 137, "address": "0xYourContractAddressOnPolygon" }
]
}
}
上下文字段
context.$id— 此描述符文档或部署配置的唯一标识符。contract.deployments— 此描述符适用的部署集合。deployments[].chainId— 部署的 EVM 链 ID。包括你的合约部署的每条链。deployments[].address— 钱包应与此描述符关联的合约地址。使用包含执行逻辑的实现地址。
第 3 步:编写元数据部分
元数据部分提供有关此文件描述的项目和合约的人类可读信息。钱包可能会在签名期间使用此信息来显示协议名称、链接和其他上下文详细信息。
"metadata": {
"owner": "Example Swap Protocol",
"info": { "url": "https://example.xyz" },
"contractName": "SwapRouter"
}
元数据字段
owner— 负责此描述符的项目、协议、组织或维护者。info.url— 钱包可能向用户显示的规范项目或文档 URL,以提供额外的上下文。contractName— 此文件描述的合约或实现名称,通常与已验证的源代码或 ABI 匹配。
如果你的 ERC-7730 文件描述的是一个 ERC-20 合约,你也应该添加一个代币对象。
第 4 步:编写显示格式部分
display.formats 对象将函数签名映射到人类可读的签名指令。这就是钱包在用户授权交易之前向用户显示你的函数的方式!
每个键都是一个人类可读的 ABI 片段——包含参数名称和参数类型的函数签名,与它们在你的 ABI 中出现的方式完全一致。
示例:描述代币兑换
"display": {
"formats": {
"swapExactTokensForTokens(uint256 amountIn,uint256 amountOutMin,address[] path,address to,uint256 deadline)": {
"intent": "Swap",
"interpolatedIntent": "Swap {amountIn} for at least {amountOutMin}",
"fields": [
{
"path": "#.amountIn",
"label": "Send",
"format": "tokenAmount",
"params": {
"tokenPath": "#.path[0]"
}
},
{
"path": "#.amountOutMin",
"label": "Receive minimum",
"format": "tokenAmount",
"params": {
"tokenPath": "#.path[1]"
}
},
{
"path": "#.to",
"label": "Recipient",
"format": "addressName",
"params": {
"types": ["eoa", "contract"],
"sources": ["local", "ens"]
}
},
{
"path": "#.deadline",
"label": "Expires",
"format": "date",
"params": {
"encoding": "timestamp"
}
}
]
}
}
}
显示字段
intent— (必填) 操作的简短、用户友好的描述,例如“兑换”。interpolatedIntent— (推荐) 嵌入格式化字段值的更丰富的句子模板,例如"Swap {amountIn} for at least {amountOutMin}"。将其与intent一起包含,以提供更用户友好的描述符,钱包可以根据任何显示限制选择显示。fields— (必填) 钱包应向用户显示的交易字段的有序列表。-
path— (必填) 对交易数据的引用。#.fieldName指向按 ABI 中名称解码的调用数据参数。@.value指随交易发送的 ETH 值。 -
label— (必填) 显示在值旁边的人类可读标签。 -
format— (推荐) 控制应如何渲染该值。常见格式包括:tokenAmountaddressNamedate
当不需要额外的格式化时,使用
raw。某些格式接受额外的params配置。例如:tokenAmount可以使用tokenPath来识别哪个代币地址提供小数位数和代码元数据。date可以使用encoding来描述时间戳的编码方式。
如果所选格式不需要额外信息,请省略
params。
-
完整的描述符
{
"$schema": "https://eips.ethereum.org/assets/eip-7730/erc7730-v2.schema.json",
"context": {
"$id": "uniswap-v3-router-mainnet",
"contract": {
"deployments": [
{
"chainId": 1,
"address": "0xYourContractAddressOnMainnet"
},
{
"chainId": 137,
"address": "0xYourContractAddressOnPolygon"
}
]
}
},
"metadata": {
"owner": "Example Swap Protocol",
"info": {
"url": "https://example.xyz"
},
"contractName": "SwapRouter"
},
"display": {
"formats": {
"swapExactTokensForTokens(uint256 amountIn,uint256 amountOutMin,address[] path,address to,uint256 deadline)": {
"intent": "Swap",
"interpolatedIntent": "Swap {amountIn} for at least {amountOutMin}",
"fields": [
{
"path": "#.amountIn",
"label": "Send",
"format": "tokenAmount",
"params": {
"tokenPath": "#.path[0]"
}
},
{
"path": "#.amountOutMin",
"label": "Receive minimum",
"format": "tokenAmount",
"params": {
"tokenPath": "#.path[1]"
}
},
{
"path": "#.to",
"label": "Recipient",
"format": "addressName",
"params": {
"types": ["eoa", "contract"],
"sources": ["local", "ens"]
}
},
{
"path": "#.deadline",
"label": "Expires",
"format": "date",
"params": {
"encoding": "timestamp"
}
}
]
}
}
}
}
第 5 步:提交到注册表
ERC-7730 注册表 (opens in a new tab)是一个由以太坊基金会作为中立管理者托管的开放存储库。任何人都可以自由克隆并自行托管它——钱包独立决定它们信任哪些注册表实例。
- 在 GitHub 上复刻 (Fork) 该存储库
- 在
registry/<your-project-name>/处创建一个文件夹 - 将你的文件放入其中:
registry/myproject/calldata-mycontract-0_0.json - 将
$schema字段更新为存储库中使用的相对路径:"../../specs/erc7730-v2.schema.json" - 开启一个拉取请求 (Pull request)
当你开启 PR 时,CI 会自动运行模式验证,检查函数签名是否生成有效的选择器,确认合约地址已在 Sourcify 上验证,并标记 ABI 不一致之处。检查结果会内联显示在 PR 上。注册表维护者会筛选提交的内容,以查找格式错误或潜在恶意的描述符。包含在注册表中并不意味着经过审计或认可。
合并后会发生什么?
注册表中的所有描述符都对审计员开放。在你的 PR 合并后,任何审计员都可以审查你的描述符并发布加密证明(根据 ERC-8176 (opens in a new tab))以确认其准确性。
这些证明信号让钱包能够应用自己的信任策略——具有多个独立证明的描述符比没有证明的描述符更有分量。你可以通过 clearsigning.org (opens in a new tab) 联系审计员社区。
钱包选择它们将支持的注册表。一旦你的描述符进入注册表,支持 ERC-7730 的钱包如果其注册表中包含该描述符,就会开始获取它,并在用户与你的合约交互时显示人类可读的数据。
延伸阅读
- ERC-7730 规范 (opens in a new tab)
- ERC-7730 注册表 (opens in a new tab)
- clearsigning.org (opens in a new tab) — 工具、生态系统状态和治理
- Sourcify 合约验证 (opens in a new tab)
- 万亿美元安全倡议 (Trillion Dollar Security initiative) (opens in a new tab)
页面最后更新: 2026年5月12日