EIP-712的"结构化革命":让签名变得人类友好

从"天书"到"白话文"
还记得上一篇我们聊的EIP-191吗?它虽然解决了签名标准化的问题,但还有一个严重的痛点:用户根本不知道自己在签什么!
想象一下这样的场景:你打开MetaMask,屏幕上显示的是一串像天书一样的十六进制字符串:
0x1901f2d857b4a3edcb9731cdf7edb72424f123456789abcdef...0x1901f2d857b4a3edcb9731cdf7edb72424f123456789abcdef...然后钱包弹出提示:"请签名确认"。
你敢签吗?这就像有人递给你一份全是乱码的合同说"来,签个字"一样——谁知道这里面是什么内容!
EIP-712:签名界的"翻译官"
EIP-712就是为了解决这个问题而生的。它让复杂的结构化数据变得人类可读,就像给签名装上了一个"翻译官"。
核心思想:结构化+类型化+可读化
EIP-712的三大核心理念:
- 结构化(Structured):不再是一坨乱码,而是有层次的数据结构
- 类型化(Typed):每个字段都有明确的类型定义
- 可读化(Human-readable):钱包可以友好地显示签名内容
实际效果对比
没有EIP-712之前:
MetaMask显示:
"Sign this message: 0x1901f2d857b4a3edcb9731cdf..."
用户心理:这是啥?敢签吗?🤔MetaMask显示:
"Sign this message: 0x1901f2d857b4a3edcb9731cdf..."
用户心理:这是啥?敢签吗?🤔有了EIP-712之后:
MetaMask显示:
"向 张三 (0x8ba1f109551bD432803012645Hac136c22C501e1) 转账
代币:USDT
数量:100.00
手续费:0.1 ETH
截止时间:2025-08-05 15:30:00"
用户心理:这个我看得懂,可以签!✅MetaMask显示:
"向 张三 (0x8ba1f109551bD432803012645Hac136c22C501e1) 转账
代币:USDT
数量:100.00
手续费:0.1 ETH
截止时间:2025-08-05 15:30:00"
用户心理:这个我看得懂,可以签!✅差别很明显吧?
EIP-712的技术原理
1. 域分隔符(Domain Separator)
每个DApp都有自己的"身份证":
struct EIP712Domain {
string name; // DApp名称,比如"Uniswap V3"
string version; // 版本号,比如"1.0.0"
uint256 chainId; // 链ID,比如1(以太坊主网)
address verifyingContract; // 验证合约地址
bytes32 salt; // 可选的盐值(用于去重)
}struct EIP712Domain {
string name; // DApp名称,比如"Uniswap V3"
string version; // 版本号,比如"1.0.0"
uint256 chainId; // 链ID,比如1(以太坊主网)
address verifyingContract; // 验证合约地址
bytes32 salt; // 可选的盐值(用于去重)
}这就像每个DApp都有自己的"营业执照",确保签名不会被恶意网站冒用。
2. 类型哈希(Type Hash)
定义数据结构的"指纹":
// 转账操作的结构定义
struct Transfer {
address from; // 发送方
address to; // 接收方
address token; // 代币合约地址
uint256 amount; // 转账数量
uint256 nonce; // 防重放随机数
uint256 deadline; // 截止时间
}
// 计算类型哈希
bytes32 TRANSFER_TYPEHASH = keccak256(
"Transfer(address from,address to,address token,uint256 amount,uint256 nonce,uint256 deadline)"
);// 转账操作的结构定义
struct Transfer {
address from; // 发送方
address to; // 接收方
address token; // 代币合约地址
uint256 amount; // 转账数量
uint256 nonce; // 防重放随机数
uint256 deadline; // 截止时间
}
// 计算类型哈希
bytes32 TRANSFER_TYPEHASH = keccak256(
"Transfer(address from,address to,address token,uint256 amount,uint256 nonce,uint256 deadline)"
);3. 结构哈希(Struct Hash)
把具体数据打包成哈希:
function hashTransfer(Transfer memory transfer) internal pure returns (bytes32) {
return keccak256(abi.encode(
TRANSFER_TYPEHASH,
transfer.from,
transfer.to,
transfer.token,
transfer.amount,
transfer.nonce,
transfer.deadline
));
}function hashTransfer(Transfer memory transfer) internal pure returns (bytes32) {
return keccak256(abi.encode(
TRANSFER_TYPEHASH,
transfer.from,
transfer.to,
transfer.token,
transfer.amount,
transfer.nonce,
transfer.deadline
));
}4. 最终签名哈希
按照EIP-191的0x01版本格式组装:
function getTransferHash(Transfer memory transfer) public view returns (bytes32) {
return keccak256(abi.encodePacked(
"\x19\x01", // EIP-191 版本0x01前缀
DOMAIN_SEPARATOR, // 域分隔符
hashTransfer(transfer) // 结构哈希
));
}function getTransferHash(Transfer memory transfer) public view returns (bytes32) {
return keccak256(abi.encodePacked(
"\x19\x01", // EIP-191 版本0x01前缀
DOMAIN_SEPARATOR, // 域分隔符
hashTransfer(transfer) // 结构哈希
));
}实战案例:Permit的"免费转账"魔法
最经典的EIP-712应用就是ERC-20的Permit功能。传统的代币转账需要两步:
// 传统方式:需要两笔交易
token.approve(spender, amount); // 第一笔:授权(需要gas费)
token.transferFrom(user, to, amount); // 第二笔:转账(需要gas费)// 传统方式:需要两笔交易
token.approve(spender, amount); // 第一笔:授权(需要gas费)
token.transferFrom(user, to, amount); // 第二笔:转账(需要gas费)有了EIP-712的Permit,只需要一笔交易:
// EIP-712 Permit结构
struct Permit {
address owner; // 代币持有者
address spender; // 授权使用者
uint256 value; // 授权数量
uint256 nonce; // 防重放
uint256 deadline; // 过期时间
}// EIP-712 Permit结构
struct Permit {
address owner; // 代币持有者
address spender; // 授权使用者
uint256 value; // 授权数量
uint256 nonce; // 防重放
uint256 deadline; // 过期时间
}// 前端:用户签名(免费!)
const permit = {
owner: userAddress,
spender: contractAddress,
value: amount,
nonce: await token.nonces(userAddress),
deadline: Math.floor(Date.now() / 1000) + 3600 // 1小时后过期
};
const signature = await signer._signTypedData(domain, types, permit);// 前端:用户签名(免费!)
const permit = {
owner: userAddress,
spender: contractAddress,
value: amount,
nonce: await token.nonces(userAddress),
deadline: Math.floor(Date.now() / 1000) + 3600 // 1小时后过期
};
const signature = await signer._signTypedData(domain, types, permit);// 合约:一次性完成授权+转账
function transferWithPermit(
Permit memory permit,
bytes memory signature
) external {
// 验证签名并授权
token.permit(
permit.owner,
permit.spender,
permit.value,
permit.deadline,
v, r, s // 从signature解析出来
);
// 立即转账
token.transferFrom(permit.owner, msg.sender, permit.value);
}// 合约:一次性完成授权+转账
function transferWithPermit(
Permit memory permit,
bytes memory signature
) external {
// 验证签名并授权
token.permit(
permit.owner,
permit.spender,
permit.value,
permit.deadline,
v, r, s // 从signature解析出来
);
// 立即转账
token.transferFrom(permit.owner, msg.sender, permit.value);
}好处显而易见:
- 用户只需要签名(免费)
- 合约支付所有gas费
- 体验丝滑,一步到位
EIP-712的安全特性
1. 域绑定防冒用
每个DApp都有独特的域分隔符,签名无法跨域使用:
// Uniswap的域
DOMAIN_SEPARATOR = keccak256(abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256("Uniswap V3"),
keccak256("1.0.0"),
1, // 以太坊主网
address(this)
));
// 恶意网站无法伪造相同的域分隔符// Uniswap的域
DOMAIN_SEPARATOR = keccak256(abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256("Uniswap V3"),
keccak256("1.0.0"),
1, // 以太坊主网
address(this)
));
// 恶意网站无法伪造相同的域分隔符2. 链绑定防重放
chainId确保签名只在特定链上有效:
const domain = {
name: "MyDApp",
version: "1.0.0",
chainId: 1, // 只在以太坊主网有效
verifyingContract: contractAddress
};const domain = {
name: "MyDApp",
version: "1.0.0",
chainId: 1, // 只在以太坊主网有效
verifyingContract: contractAddress
};在BSC(chainId: 56)上,这个签名就无效了。
3. 时间限制防滥用
通过deadline字段设置过期时间:
require(block.timestamp <= deadline, "Signature expired");require(block.timestamp <= deadline, "Signature expired");4. nonce防重放
每个用户都有递增的nonce:
mapping(address => uint256) public nonces;
function verify(address user, uint256 nonce, bytes memory signature) external {
require(nonce == nonces[user], "Invalid nonce");
nonces[user]++; // 使用后立即递增
// 验证签名...
}mapping(address => uint256) public nonces;
function verify(address user, uint256 nonce, bytes memory signature) external {
require(nonce == nonces[user], "Invalid nonce");
nonces[user]++; // 使用后立即递增
// 验证签名...
}钱包如何显示EIP-712签名
MetaMask等钱包是如何把复杂的数据结构变成用户友好的界面的?
1. 解析类型定义
const types = {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" }
],
Transfer: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "token", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" }
]
};const types = {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" }
],
Transfer: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "token", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" }
]
};2. 渲染用户界面
钱包会把types和实际数据结合,生成类似这样的界面:
🔒 签名请求
📱 DApp: Uniswap V3 (v1.0.0)
🌐 网络: 以太坊主网
📄 合约: 0x1234...5678
📝 操作详情:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
发送方: 0x8ba1...501e1 (你的地址)
接收方: 0x742d...4E4A2
代币: USDT (0xdAC1...35Ec7)
数量: 100.00 USDT
随机数: 42
截止时间: 2025-08-05 15:30:00
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⚠️ 只在授权可信的DApp时才签名
✅ 取消 ✅ 签名🔒 签名请求
📱 DApp: Uniswap V3 (v1.0.0)
🌐 网络: 以太坊主网
📄 合约: 0x1234...5678
📝 操作详情:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
发送方: 0x8ba1...501e1 (你的地址)
接收方: 0x742d...4E4A2
代币: USDT (0xdAC1...35Ec7)
数量: 100.00 USDT
随机数: 42
截止时间: 2025-08-05 15:30:00
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⚠️ 只在授权可信的DApp时才签名
✅ 取消 ✅ 签名3. 特殊字段处理
钱包还会对特定类型做友好化处理:
// 地址字段:显示ENS名称或简化地址
"0x8ba1f109551bD432803012645Hac136c22C501e1" → "vitalik.eth"
// 时间戳:转换为可读时间
1722870600 → "2025-08-05 15:30:00"
// 大数字:添加单位和格式化
"100000000000000000000" → "100.00 TOKEN"
// 合约地址:显示已知合约名称
"0xA0b86a33E6441d8178E72C34b0A8d9227071a7A8" → "USDT Token Contract"// 地址字段:显示ENS名称或简化地址
"0x8ba1f109551bD432803012645Hac136c22C501e1" → "vitalik.eth"
// 时间戳:转换为可读时间
1722870600 → "2025-08-05 15:30:00"
// 大数字:添加单位和格式化
"100000000000000000000" → "100.00 TOKEN"
// 合约地址:显示已知合约名称
"0xA0b86a33E6441d8178E72C34b0A8d9227071a7A8" → "USDT Token Contract"EIP-712在DeFi中的广泛应用
1. DEX的订单簿
struct Order {
address trader; // 交易者
address baseToken; // 基础代币
address quoteToken; // 计价代币
uint256 baseAmount; // 基础代币数量
uint256 quoteAmount; // 计价代币数量
uint256 expiry; // 过期时间
uint256 salt; // 防重复盐值
}struct Order {
address trader; // 交易者
address baseToken; // 基础代币
address quoteToken; // 计价代币
uint256 baseAmount; // 基础代币数量
uint256 quoteAmount; // 计价代币数量
uint256 expiry; // 过期时间
uint256 salt; // 防重复盐值
}2. 借贷协议的授权
struct LendingPermit {
address borrower; // 借款人
address lender; // 出借人
address asset; // 资产
uint256 amount; // 金额
uint256 interestRate; // 利率
uint256 duration; // 借款期限
uint256 nonce;
}struct LendingPermit {
address borrower; // 借款人
address lender; // 出借人
address asset; // 资产
uint256 amount; // 金额
uint256 interestRate; // 利率
uint256 duration; // 借款期限
uint256 nonce;
}3. NFT的批量操作
struct BatchTransfer {
address from;
address to;
uint256[] tokenIds; // 批量NFT ID
uint256 deadline;
uint256 nonce;
}struct BatchTransfer {
address from;
address to;
uint256[] tokenIds; // 批量NFT ID
uint256 deadline;
uint256 nonce;
}安全陷阱与防范
虽然EIP-712让签名变得更安全、更友好,但还是有一些需要注意的安全陷阱:
1. 假冒的"友好"界面
恶意DApp可能会在类型定义中撒谎:
// 恶意类型定义:把转账伪装成查询
const maliciousTypes = {
BalanceQuery: [ // 看起来像查询余额
{ name: "user", type: "address" },
{ name: "token", type: "address" },
{ name: "amount", type: "uint256" } // 实际上是转账金额!
]
};// 恶意类型定义:把转账伪装成查询
const maliciousTypes = {
BalanceQuery: [ // 看起来像查询余额
{ name: "user", type: "address" },
{ name: "token", type: "address" },
{ name: "amount", type: "uint256" } // 实际上是转账金额!
]
};防范方法:
- 只在信任的DApp上签名
- 仔细检查签名内容,特别是金额字段
- 使用知名钱包,它们通常有额外的安全检查
2. 钓鱼网站的域名欺骗
恶意网站可能使用相似的域名:
// 正版Uniswap
const legitimateDomain = {
name: "Uniswap V3",
verifyingContract: "0x1F98431c8aD98523631AE4a59f267346ea31F984"
};
// 钓鱼网站
const phishingDomain = {
name: "Uniswap V3", // 名字一样!
verifyingContract: "0x1234567890123456789012345678901234567890" // 但合约不同
};// 正版Uniswap
const legitimateDomain = {
name: "Uniswap V3",
verifyingContract: "0x1F98431c8aD98523631AE4a59f267346ea31F984"
};
// 钓鱼网站
const phishingDomain = {
name: "Uniswap V3", // 名字一样!
verifyingContract: "0x1234567890123456789012345678901234567890" // 但合约不同
};防范方法:
- 检查verifyingContract地址是否正确
- 使用书签访问DApp,不要点击可疑链接
- 在签名前确认当前网站URL
3. 过长的截止时间
某些恶意DApp可能设置很长的deadline:
const maliciousData = {
deadline: 9999999999 // 2286年才过期!
};const maliciousData = {
deadline: 9999999999 // 2286年才过期!
};这意味着如果你的私钥泄露,攻击者可以在很长时间内使用这个签名。
防范方法:
- 检查deadline字段,确保过期时间合理
- 对于重要操作,选择较短的有效期
EIP-712的后续发展
EIP-712成功后,社区又基于它开发了很多扩展:
1. EIP-5267:域信息检索
允许合约动态返回自己的EIP-712域信息:
interface IERC5267 {
function eip712Domain() external view returns (
bytes1 fields,
string memory name,
string memory version,
uint256 chainId,
address verifyingContract,
bytes32 salt,
uint256[] memory extensions
);
}interface IERC5267 {
function eip712Domain() external view returns (
bytes1 fields,
string memory name,
string memory version,
uint256 chainId,
address verifyingContract,
bytes32 salt,
uint256[] memory extensions
);
}2. EIP-7730:清晰签名格式
进一步改进签名显示,让用户更容易理解:
{
"message": "Transfer 100 USDT to Alice",
"risk_level": "medium",
"gas_estimate": "21000",
"equivalent_usd": "$100.00"
}{
"message": "Transfer 100 USDT to Alice",
"risk_level": "medium",
"gas_estimate": "21000",
"equivalent_usd": "$100.00"
}3. EIP-7739:智能账户可读签名
为智能钱包优化的签名格式,支持更复杂的签名逻辑。
实际开发中的最佳实践
1. 合理设计数据结构
// ✅ 好的设计:语义清晰
struct SwapOrder {
address trader;
address tokenIn;
address tokenOut;
uint256 amountIn;
uint256 minAmountOut; // 明确表示最小接收量
uint256 deadline;
uint256 nonce;
}
// ❌ 不好的设计:容易混淆
struct Order {
address user;
address token1; // 哪个是输入,哪个是输出?
address token2;
uint256 amount1; // 到底是多少?
uint256 amount2;
}// ✅ 好的设计:语义清晰
struct SwapOrder {
address trader;
address tokenIn;
address tokenOut;
uint256 amountIn;
uint256 minAmountOut; // 明确表示最小接收量
uint256 deadline;
uint256 nonce;
}
// ❌ 不好的设计:容易混淆
struct Order {
address user;
address token1; // 哪个是输入,哪个是输出?
address token2;
uint256 amount1; // 到底是多少?
uint256 amount2;
}2. 提供清晰的类型描述
// ✅ 好的命名
const types = {
SwapOrder: [
{ name: "trader", type: "address" },
{ name: "tokenIn", type: "address" },
{ name: "tokenOut", type: "address" },
{ name: "amountIn", type: "uint256" },
{ name: "minAmountOut", type: "uint256" },
{ name: "deadline", type: "uint256" },
{ name: "nonce", type: "uint256" }
]
};
// ❌ 不好的命名
const types = {
Order: [
{ name: "a", type: "address" },
{ name: "b", type: "address" },
{ name: "c", type: "uint256" }
]
};// ✅ 好的命名
const types = {
SwapOrder: [
{ name: "trader", type: "address" },
{ name: "tokenIn", type: "address" },
{ name: "tokenOut", type: "address" },
{ name: "amountIn", type: "uint256" },
{ name: "minAmountOut", type: "uint256" },
{ name: "deadline", type: "uint256" },
{ name: "nonce", type: "uint256" }
]
};
// ❌ 不好的命名
const types = {
Order: [
{ name: "a", type: "address" },
{ name: "b", type: "address" },
{ name: "c", type: "uint256" }
]
};3. 实现适当的安全检查
contract SecureContract {
mapping(address => uint256) public nonces;
function executeOrder(Order memory order, bytes memory signature) external {
// 1. 验证签名
bytes32 hash = getOrderHash(order);
address signer = ECDSA.recover(hash, signature);
require(signer == order.trader, "Invalid signature");
// 2. 检查nonce防重放
require(order.nonce == nonces[order.trader], "Invalid nonce");
nonces[order.trader]++;
// 3. 检查deadline防过期
require(block.timestamp <= order.deadline, "Order expired");
// 4. 执行业务逻辑
// ...
}
}contract SecureContract {
mapping(address => uint256) public nonces;
function executeOrder(Order memory order, bytes memory signature) external {
// 1. 验证签名
bytes32 hash = getOrderHash(order);
address signer = ECDSA.recover(hash, signature);
require(signer == order.trader, "Invalid signature");
// 2. 检查nonce防重放
require(order.nonce == nonces[order.trader], "Invalid nonce");
nonces[order.trader]++;
// 3. 检查deadline防过期
require(block.timestamp <= order.deadline, "Order expired");
// 4. 执行业务逻辑
// ...
}
}写在最后
EIP-712真正实现了签名的"结构化革命",它不仅让签名变得更安全,更重要的是让普通用户能够理解自己在签什么。这种"人类友好"的设计理念,正是区块链技术走向大众化的重要基石。
从EIP-191的"交通规则"到EIP-712的"翻译官",我们可以看到以太坊生态是如何一步步变得更加完善和用户友好的。每一个EIP的背后,都是无数开发者的智慧结晶和对用户体验的不懈追求。
如果你是开发者,强烈建议在项目中使用EIP-712标准——你的用户会因为清晰的签名界面而感谢你。如果你是普通用户,记住这个原则:看不懂的签名,绝对不要签!
关于作者们
作者基本来自井畅公司的技术工程师和社区热心小伙伴,热衷于探索前沿技术并分享实践经验。通过系统化的学习和多年项目实践,井畅希望能帮助更多开发者快速掌握核心技能,少走弯路。
加入技术社区
🔹 技术交流QQ群 : 568285439
🔹 GitHub:jccdex - 关注获取示例代码和开源项目
🔹 公众号:[井畅] - 每周更新,分享行业动态与技术心得
有任何问题或建议,欢迎在评论区留言,我会尽快回复。
关于我们这群"代码考古学家"
我们基本来自井畅公司的技术工程师和社区热心小伙伴,平时就喜欢挖掘这些技术背后的故事。通过系统化学习和多年项目实践,我们希望能帮助更多开发者快速掌握核心技能,少走弯路(毕竟踩坑不好玩)。
加入我们的"考古队"
🔹 技术交流QQ群:568285439(欢迎来聊技术,也欢迎来吐槽)
🔹 GitHub:jccdex - 代码示例和开源项目都在这里
🔹 公众号:[井畅] - 每周更新,分享行业动态与技术心得
下期预告:EIP-1271:当智能合约也想要"签名"
下一篇我们要聊EIP-1271:智能合约签名验证标准。既然合约没有私钥,它们是如何"签名"的?多签钱包、DAO投票、代理合约都是如何验证签名的?EIP-1271如何让合约也拥有了"签名"的能力?
敬请期待这场"合约签名"的技术探索!
有任何问题或建议,欢迎在评论区留言。我们承诺:技术问题必回复,吐槽也欢迎!