Skip to content

最近看到到EIP712 发现这个可以permit可以在线下签名的方式代替approve,减少链上gas

Notifications You must be signed in to change notification settings

0xlayerghost/EIP712Test

Repository files navigation

EIP712 代币合约学习项目

🎯 项目概述

这是一个完整的EIP712标准学习和实践项目,实现了基于EIP712标准的代币合约,支持permit功能。通过这个项目,你可以深入理解EIP712签名标准的工作原理,以及如何在实际项目中应用它来优化用户体验和降低gas费用。

📚 什么是EIP712?

EIP712(Ethereum Improvement Proposal 712)是以太坊的一个改进提案,定义了一种标准化的方式来对结构化数据进行哈希和签名。它解决了传统签名方式的几个关键问题: image

🔍 核心概念

1. 域分隔符(Domain Separator)

确保签名只在特定的合约和区块链上有效,防止跨链重放攻击。

DOMAIN_SEPARATOR = keccak256(
    abi.encode(
        DOMAIN_TYPEHASH,
        keccak256(bytes("EIP712 Test Token")),  // 合约名称
        keccak256(bytes("1")),                  // 版本号
        block.chainid,                          // 链ID
        address(this)                           // 合约地址
    )
);

2. 类型哈希(Type Hash)

定义结构化数据的格式,确保数据结构的一致性。

// Permit类型哈希
bytes32 private constant PERMIT_TYPEHASH = keccak256(
    "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
);

3. 结构化数据哈希

将用户数据按照EIP712标准进行哈希处理。

bytes32 structHash = keccak256(
    abi.encode(
        PERMIT_TYPEHASH,
        owner,
        spender,
        value,
        nonce,
        deadline
    )
);

4. 最终消息哈希

结合域分隔符和结构化数据哈希生成最终的签名消息。

bytes32 digest = keccak256(
    abi.encodePacked(
        "\x19\x01",         // EIP712魔法值
        DOMAIN_SEPARATOR,   // 域分隔符
        structHash          // 结构化数据哈希
    )
);

🚀 项目实现的功能

1. 基础ERC20代币功能

  • 标准的转账、授权、代理转账功能
  • 完整的余额和授权管理
  • 事件日志记录

2. EIP712 Permit功能

  • 通过离线签名进行代币授权
  • 避免传统的approve + transferFrom两步操作
  • 显著降低gas费用(节省约47%)

3. 代理转账功能

  • 支持通过签名进行代理转账
  • 第三方可以代替用户支付gas费用
  • 适用于DeFi协议集成

4. 批量操作

  • 支持批量转账功能
  • 提高操作效率

5. 完整的测试套件

  • Solidity测试合约
  • 前端集成示例
  • 交互式HTML演示

💡 EIP712的优势

传统方式 vs EIP712方式

传统授权流程(需要2笔交易):

  1. 用户调用 approve(spender, amount) - 消耗gas
  2. 第三方调用 transferFrom(user, recipient, amount) - 消耗gas

EIP712流程(只需要1笔交易):

  1. 用户在链下生成EIP712签名 - 免费
  2. 第三方调用 permit(...) 或直接执行业务逻辑 - 只消耗1次gas

Gas费用对比

操作 传统方式 EIP712方式 节省
授权 ~46,000 gas 0 gas (链下签名) 100%
转账 ~51,000 gas ~51,000 gas 0%
总计 ~97,000 gas ~51,000 gas 47%

🔧 技术实现细节

核心安全机制

1. Nonce防重放攻击

每个用户维护独立的nonce,每次使用后自动递增:

mapping(address => uint256) public nonces;

2. 时间限制

每个签名都有过期时间,防止长期有效的签名被滥用:

require(block.timestamp <= deadline, "EIP712: permit expired");

3. 签名验证

使用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                     # 项目说明文档(本文件)

🛠️ 快速开始

1. 环境准备

确保你已经安装了以下工具:

  • Node.js (v14+)
  • npm 或 yarn
  • MetaMask 钱包
  • Remix IDE 或 Hardhat/Truffle

2. 安装依赖

npm install ethers
#
yarn add ethers

3. 部署合约

使用Remix IDE:

  1. 打开 Remix IDE
  2. 创建新文件并复制 test.sol 的内容
  3. 编译合约(Solidity 0.8.7+)
  4. 连接MetaMask到测试网络(如Sepolia)
  5. 部署合约

使用部署脚本:

node deploy-and-verify.js

4. 运行测试

Solidity测试:

在Remix中部署 EIP712Test.sol 并调用:

testContract.runAllTests();
testContract.demonstrateEIP712Workflow();

前端测试:

node eip712-frontend-example.js

交互式演示:

直接在浏览器中打开 step-by-step-demo.html

💻 代码示例

前端签名生成(JavaScript)

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);

Python集成示例

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()
    }

🎯 实际应用场景

1. DeFi协议集成

传统方式:

// 需要两笔交易
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
);  // 一笔交易完成授权+交换

2. 代理支付(Meta Transactions)

用户可以生成签名,由第三方代替支付gas费用:

// 用户生成签名(免费)
const signature = await user.signPermit(permitData);

// 代理方执行交易(支付gas)
await relayer.executePermit(signature);

3. 批量操作优化

// 生成多个permit签名
const signatures = await Promise.all(
    permits.map(permit => generatePermitSignature(permit))
);

// 批量执行,减少交互次数
await contract.batchPermit(permits, signatures);

🔒 安全最佳实践

1. 私钥管理

  • ❌ 永远不要在前端代码中硬编码私钥
  • ✅ 使用MetaMask等钱包进行签名
  • ✅ 在生产环境中使用环境变量管理敏感信息

2. 签名验证

  • ✅ 始终验证签名的有效性
  • ✅ 检查签名是否过期
  • ✅ 确保nonce的正确性
  • ✅ 验证域分隔符匹配

3. 合约安全

  • ✅ 实现重入攻击保护
  • ✅ 使用SafeMath防止溢出
  • ✅ 添加访问控制机制
  • ✅ 进行充分的测试

4. 前端安全

// 验证签名参数
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');
    }
}

🧪 测试指南

单元测试

项目包含完整的测试套件,涵盖以下测试场景:

  1. 基础功能测试

    • 代币基本功能(转账、授权)
    • 域分隔符计算
    • 类型哈希验证
  2. EIP712功能测试

    • Permit签名生成和验证
    • Transfer签名功能
    • Nonce管理机制
  3. 安全性测试

    • 重放攻击防护
    • 签名过期检查
    • 边界条件处理
  4. 集成测试

    • 前端集成流程
    • 批量操作功能
    • 错误处理机制

运行测试

# Solidity测试
# 在Remix中部署EIP712Test.sol并调用
testContract.runAllTests();

# 前端测试
node eip712-frontend-example.js

# 交互式测试
# 在浏览器中打开step-by-step-demo.html

📊 性能分析

Gas消耗对比

功能 传统方式 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%
失败风险 高(两步操作) 低(一步完成) 显著降低
用户操作 复杂 简单 显著简化

🔧 开发工具和资源

推荐工具

  1. 开发环境

  2. 前端库

  3. 测试工具

学习资源

  1. 官方文档

  2. 实用教程

🚀 部署指南

测试网部署

  1. 准备工作

    # 获取测试网ETH
    # Sepolia: https://sepoliafaucet.com/
    # Goerli: https://goerlifaucet.com/
  2. 配置网络

    // 网络配置
    const networks = {
        sepolia: {
            url: 'https://sepolia.infura.io/v3/YOUR_PROJECT_ID',
            chainId: 11155111,
            accounts: [process.env.PRIVATE_KEY]
        }
    };
  3. 部署合约

    npx hardhat run scripts/deploy.js --network sepolia

主网部署

⚠️ 主网部署前必须完成:

  • 充分的测试网测试
  • 安全审计
  • 代码审查
  • 备份和恢复计划

🤝 贡献指南

我们欢迎社区贡献!请遵循以下步骤:

  1. Fork项目

    git clone https://github.com/your-username/EIP712Test.git
    cd EIP712Test
  2. 创建功能分支

    git checkout -b feature/your-feature-name
  3. 提交更改

    git commit -m "Add: your feature description"
    git push origin feature/your-feature-name
  4. 创建Pull Request

贡献要求

  • ✅ 代码符合Solidity最佳实践
  • ✅ 添加充分的测试用例
  • ✅ 更新相关文档
  • ✅ 通过所有现有测试
  • ✅ 添加适当的注释

🐛 常见问题

Q1: 签名验证失败怎么办?

A: 检查以下几点:

  • 域分隔符是否正确(合约地址、链ID)
  • 类型哈希是否匹配
  • 参数顺序是否正确
  • nonce是否是最新的

Q2: 如何在不同链上使用?

A: 修改域分隔符中的chainId:

const domain = {
    name: 'EIP712 Test Token',
    version: '1',
    chainId: 1, // 主网
    verifyingContract: contractAddress
};

Q3: 如何处理签名过期?

A: 设置合理的过期时间并在前端检查:

const deadline = Math.floor(Date.now() / 1000) + 3600; // 1小时后过期

// 检查是否过期
if (Math.floor(Date.now() / 1000) > deadline) {
    throw new Error('Signature expired');
}

Q4: 如何优化gas费用?

A:

  • 使用批量操作
  • 合理设置gas price
  • 在gas费用较低时执行交易
  • 考虑Layer 2解决方案

📄 许可证

本项目采用 MIT 许可证 - 详见 LICENSE 文件

🙏 致谢

感谢以下项目和资源的启发:

📞 联系方式

如果你有任何问题或建议,欢迎通过以下方式联系:


⭐ 如果这个项目对你有帮助,请给我们一个星标!

🔄 持续更新中,欢迎关注最新进展!

About

最近看到到EIP712 发现这个可以permit可以在线下签名的方式代替approve,减少链上gas

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published