这是一个完整的EIP712标准学习和实践项目,实现了基于EIP712标准的代币合约,支持permit功能。通过这个项目,你可以深入理解EIP712签名标准的工作原理,以及如何在实际项目中应用它来优化用户体验和降低gas费用。
EIP712(Ethereum Improvement Proposal 712)是以太坊的一个改进提案,定义了一种标准化的方式来对结构化数据进行哈希和签名。它解决了传统签名方式的几个关键问题:

确保签名只在特定的合约和区块链上有效,防止跨链重放攻击。
DOMAIN_SEPARATOR = keccak256(
abi.encode(
DOMAIN_TYPEHASH,
keccak256(bytes("EIP712 Test Token")), // 合约名称
keccak256(bytes("1")), // 版本号
block.chainid, // 链ID
address(this) // 合约地址
)
);定义结构化数据的格式,确保数据结构的一致性。
// Permit类型哈希
bytes32 private constant PERMIT_TYPEHASH = keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
);将用户数据按照EIP712标准进行哈希处理。
bytes32 structHash = keccak256(
abi.encode(
PERMIT_TYPEHASH,
owner,
spender,
value,
nonce,
deadline
)
);结合域分隔符和结构化数据哈希生成最终的签名消息。
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01", // EIP712魔法值
DOMAIN_SEPARATOR, // 域分隔符
structHash // 结构化数据哈希
)
);- 标准的转账、授权、代理转账功能
- 完整的余额和授权管理
- 事件日志记录
- 通过离线签名进行代币授权
- 避免传统的approve + transferFrom两步操作
- 显著降低gas费用(节省约47%)
- 支持通过签名进行代理转账
- 第三方可以代替用户支付gas费用
- 适用于DeFi协议集成
- 支持批量转账功能
- 提高操作效率
- Solidity测试合约
- 前端集成示例
- 交互式HTML演示
传统授权流程(需要2笔交易):
- 用户调用
approve(spender, amount)- 消耗gas - 第三方调用
transferFrom(user, recipient, amount)- 消耗gas
EIP712流程(只需要1笔交易):
- 用户在链下生成EIP712签名 - 免费
- 第三方调用
permit(...)或直接执行业务逻辑 - 只消耗1次gas
| 操作 | 传统方式 | EIP712方式 | 节省 |
|---|---|---|---|
| 授权 | ~46,000 gas | 0 gas (链下签名) | 100% |
| 转账 | ~51,000 gas | ~51,000 gas | 0% |
| 总计 | ~97,000 gas | ~51,000 gas | 47% |
每个用户维护独立的nonce,每次使用后自动递增:
mapping(address => uint256) public nonces;每个签名都有过期时间,防止长期有效的签名被滥用:
require(block.timestamp <= deadline, "EIP712: permit expired");使用ecrecover恢复签名者地址并验证:
address recoveredSigner = ecrecover(digest, v, r, s);
return recoveredSigner == signer && recoveredSigner != address(0);EIP712Test/
├── test.sol # 主要的EIP712代币合约实现
├── EIP712Test.sol # Solidity测试合约
├── eip712-frontend-example.js # 前端集成示例(Node.js)
├── step-by-step-demo.html # 交互式HTML演示页面
├── EIP712TokenABI.json # 合约ABI文件
├── deploy-and-verify.js # 部署和验证脚本
├── remix-testing-guide.md # Remix IDE测试指南
├── EIP712_USAGE_GUIDE.md # 详细使用指南
└── README.md # 项目说明文档(本文件)
确保你已经安装了以下工具:
- Node.js (v14+)
- npm 或 yarn
- MetaMask 钱包
- Remix IDE 或 Hardhat/Truffle
npm install ethers
# 或
yarn add ethers- 打开 Remix IDE
- 创建新文件并复制
test.sol的内容 - 编译合约(Solidity 0.8.7+)
- 连接MetaMask到测试网络(如Sepolia)
- 部署合约
node deploy-and-verify.js在Remix中部署 EIP712Test.sol 并调用:
testContract.runAllTests();
testContract.demonstrateEIP712Workflow();node eip712-frontend-example.js直接在浏览器中打开 step-by-step-demo.html
const { ethers } = require('ethers');
// EIP712域定义
const domain = {
name: 'EIP712 Test Token',
version: '1',
chainId: 11155111, // Sepolia
verifyingContract: contractAddress
};
// Permit类型定义
const types = {
Permit: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' }
]
};
// 生成签名
async function generatePermitSignature(signer, permitData) {
const signature = await signer.signTypedData(domain, types, permitData);
const { v, r, s } = ethers.Signature.from(signature);
return { v, r, s };
}
// 使用示例
const permitData = {
owner: userAddress,
spender: spenderAddress,
value: ethers.parseEther('1000'),
nonce: 0,
deadline: Math.floor(Date.now() / 1000) + 3600
};
const { v, r, s } = await generatePermitSignature(signer, permitData);
await contract.permit(owner, spender, value, deadline, v, r, s);from web3 import Web3
from eth_account.messages import encode_structured_data
# EIP712域定义
domain = {
"name": "EIP712 Test Token",
"version": "1",
"chainId": 11155111,
"verifyingContract": contract_address
}
# 类型定义
types = {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"}
],
"Permit": [
{"name": "owner", "type": "address"},
{"name": "spender", "type": "address"},
{"name": "value", "type": "uint256"},
{"name": "nonce", "type": "uint256"},
{"name": "deadline", "type": "uint256"}
]
}
def generate_permit_signature(private_key, permit_data):
message = {
"domain": domain,
"types": types,
"primaryType": "Permit",
"message": permit_data
}
encoded_message = encode_structured_data(message)
signed_message = Web3().eth.account.sign_message(
encoded_message, private_key=private_key
)
return {
"v": signed_message.v,
"r": signed_message.r.hex(),
"s": signed_message.s.hex()
}传统方式:
// 需要两笔交易
await token.approve(dexContract.address, amount); // 第一笔交易
await dexContract.swap(tokenA, tokenB, amount); // 第二笔交易EIP712方式:
// 只需要一笔交易
const permitSig = await generatePermitSignature(...);
await dexContract.swapWithPermit(
tokenA, tokenB, amount, deadline, v, r, s
); // 一笔交易完成授权+交换用户可以生成签名,由第三方代替支付gas费用:
// 用户生成签名(免费)
const signature = await user.signPermit(permitData);
// 代理方执行交易(支付gas)
await relayer.executePermit(signature);// 生成多个permit签名
const signatures = await Promise.all(
permits.map(permit => generatePermitSignature(permit))
);
// 批量执行,减少交互次数
await contract.batchPermit(permits, signatures);- ❌ 永远不要在前端代码中硬编码私钥
- ✅ 使用MetaMask等钱包进行签名
- ✅ 在生产环境中使用环境变量管理敏感信息
- ✅ 始终验证签名的有效性
- ✅ 检查签名是否过期
- ✅ 确保nonce的正确性
- ✅ 验证域分隔符匹配
- ✅ 实现重入攻击保护
- ✅ 使用SafeMath防止溢出
- ✅ 添加访问控制机制
- ✅ 进行充分的测试
// 验证签名参数
function validateSignature(v, r, s) {
if (v !== 27 && v !== 28) {
throw new Error('Invalid v parameter');
}
if (r === '0x' || s === '0x') {
throw new Error('Invalid r or s parameter');
}
}
// 验证过期时间
function validateDeadline(deadline) {
const now = Math.floor(Date.now() / 1000);
if (deadline <= now) {
throw new Error('Signature expired');
}
}项目包含完整的测试套件,涵盖以下测试场景:
-
基础功能测试
- 代币基本功能(转账、授权)
- 域分隔符计算
- 类型哈希验证
-
EIP712功能测试
- Permit签名生成和验证
- Transfer签名功能
- Nonce管理机制
-
安全性测试
- 重放攻击防护
- 签名过期检查
- 边界条件处理
-
集成测试
- 前端集成流程
- 批量操作功能
- 错误处理机制
# Solidity测试
# 在Remix中部署EIP712Test.sol并调用
testContract.runAllTests();
# 前端测试
node eip712-frontend-example.js
# 交互式测试
# 在浏览器中打开step-by-step-demo.html| 功能 | 传统方式 | EIP712方式 | 节省比例 |
|---|---|---|---|
| 单次授权 | 46,000 gas | 0 gas | 100% |
| 批量授权(5个) | 230,000 gas | 0 gas | 100% |
| DeFi交互 | 97,000 gas | 51,000 gas | 47% |
| 代理转账 | 102,000 gas | 56,000 gas | 45% |
| 指标 | 传统方式 | EIP712方式 | 改善 |
|---|---|---|---|
| 交易次数 | 2次 | 1次 | 50% |
| 等待时间 | 30-60秒 | 15-30秒 | 50% |
| 失败风险 | 高(两步操作) | 低(一步完成) | 显著降低 |
| 用户操作 | 复杂 | 简单 | 显著简化 |
-
开发环境
-
前端库
-
测试工具
- Ganache - 本地区块链
- Tenderly - 调试和监控
- OpenZeppelin Test Helpers - 测试辅助工具
-
准备工作
# 获取测试网ETH # Sepolia: https://sepoliafaucet.com/ # Goerli: https://goerlifaucet.com/
-
配置网络
// 网络配置 const networks = { sepolia: { url: 'https://sepolia.infura.io/v3/YOUR_PROJECT_ID', chainId: 11155111, accounts: [process.env.PRIVATE_KEY] } };
-
部署合约
npx hardhat run scripts/deploy.js --network sepolia
- 充分的测试网测试
- 安全审计
- 代码审查
- 备份和恢复计划
我们欢迎社区贡献!请遵循以下步骤:
-
Fork项目
git clone https://github.com/your-username/EIP712Test.git cd EIP712Test -
创建功能分支
git checkout -b feature/your-feature-name
-
提交更改
git commit -m "Add: your feature description" git push origin feature/your-feature-name -
创建Pull Request
- ✅ 代码符合Solidity最佳实践
- ✅ 添加充分的测试用例
- ✅ 更新相关文档
- ✅ 通过所有现有测试
- ✅ 添加适当的注释
A: 检查以下几点:
- 域分隔符是否正确(合约地址、链ID)
- 类型哈希是否匹配
- 参数顺序是否正确
- nonce是否是最新的
A: 修改域分隔符中的chainId:
const domain = {
name: 'EIP712 Test Token',
version: '1',
chainId: 1, // 主网
verifyingContract: contractAddress
};A: 设置合理的过期时间并在前端检查:
const deadline = Math.floor(Date.now() / 1000) + 3600; // 1小时后过期
// 检查是否过期
if (Math.floor(Date.now() / 1000) > deadline) {
throw new Error('Signature expired');
}A:
- 使用批量操作
- 合理设置gas price
- 在gas费用较低时执行交易
- 考虑Layer 2解决方案
本项目采用 MIT 许可证 - 详见 LICENSE 文件
感谢以下项目和资源的启发:
- OpenZeppelin Contracts
- ethers.js
- EIP-712 标准
- 以太坊开发者社区
如果你有任何问题或建议,欢迎通过以下方式联系:
⭐ 如果这个项目对你有帮助,请给我们一个星标!
🔄 持续更新中,欢迎关注最新进展!