一、前言
● ERC-712 是一种通用的结构化签名标准,为离线签名和链上验证提供了高效工具。
● ERC-2612 是基于 ERC-712 的扩展,专注于代币授权的优化,特别适用于 DeFi 和钱包应用场景。
二、ERC-712(EIP-712)—用于结构化数据的签名标准
1. 什么是ERC-712
ERC-712提供了一种对结构化数据进行离线签名的标准。它通过定义签名的格式和数据结构,确保签名的安全性和可验证性,并显著提高了用户交互的便利性。
2. 核心内容
2.1 数据结构
EIP-712允许开发者定义数据结构,并使用哈希算法将其转换为签名消息。
● 域分隔符(Domain Separator):用于区分不同的合约或网络环境,防止签名跨合约或跨链被滥用。
bytes32 DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes("TokenName")), // 合约名称
keccak256(bytes("1")), // 合约版本
chainId, // 链 ID
address(this) // 合约地址
)
);
● 消息结构:用户自定义的结构化数据,可以是交易信息、授权请求等。
struct Permit {
address owner;
address spender;
uint256 value;
uint256 nonce;
uint256 deadline;
}
2.2 签名步骤
● 计算消息哈希:
bytes32 hashStruct = keccak256(
abi.encode(
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"),
owner,
spender,
value,
nonce,
deadline
)
);
● 计算完整签名的哈希值:
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
hashStruct
)
);
● 通过签名工具(如钱包)生成签名
2.3. 验证签名
链上验证签名是否由合法地址生成:
address signer = ecrecover(digest, v, r, s);
require(signer == owner, "Invalid signature");
3.应用场景
● 代币授权(与ERC-2612结合):实现无Gas授权,通过离线签名完成代币授权。
● 去中心化身份认证(DID):利用结构化签名验证用户身份。
● 多重签名钱包:简化多重签名中的签名和验证流程。
三、EIP-2612—基于ERC-712的代币无Gas授权标准
1. 什么是ERC-2612
ERC-2612是对ERC-20的扩展,利用ERC-712的结构化签名标准引入permit方法,使得用户无需调用approve方法即可离线授权代币转账。它简化了授权流程,节省了交易费用。
2. 核心内容
2.1 permit方法
permit方法是ERC-2612的核心功能,用于离线完成代币授权
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external;
● 参数说明:
owner:授权代币的所有者
spender:被授权的地址
value:授权的代币数量
deadline:签名的有效时间
v,r,s:签名的分量。
● 功能:
验证签名是否有效
更新授权数据(触发Approval事件)
2.2 状态变量
● nonces:每个地址都有一个唯一的nonce,防止签名重放。
mapping(address => uint256) public nonces;
● DOMAIN_SEPARATOR:用于与EIP-712的签名格式对接,确保安全性。
2.3 ERC-2612的工作流程
● 用户构造一条授权数据,并离线签名(通过钱包工具)
● 用户将签名数据发送到链上合约,调用permit方法完成授权
● 合约验证签名的合法性,并记录授权信息
四、ERC-2612的实现代码示例
以下是一个符合ERC-2612的简单合约实现:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract ERC20Permit is ERC20 {
mapping(address => uint256) public nonces;
bytes32 public DOMAIN_SEPARATOR;
bytes32 public constant PERMIT_TYPEHASH =
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
uint256 chainId;
assembly {
chainId := chainid()
}
DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name)),
keccak256(bytes("1")),
chainId,
address(this)
)
);
}
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external {
require(block.timestamp <= deadline, "Permit: expired deadline");
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(
abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)
)
)
);
address recoveredAddress = ecrecover(digest, v, r, s);
require(recoveredAddress != address(0) && recoveredAddress == owner, "Permit: invalid signature");
_approve(owner, spender, value);
}
}
五、Permit2改进的ERC-20授权标准
Permit2是一种基于ERC-2612的改进协议,它扩展了ERC-20的授权模型,并为DeFi协议提供了更灵活和高效的授权机制。与ERC-2612类似,Permit2也利用了离线签名(基于EIP-712)来完成代币授权,但其功能更强大,支持批量授权、时间限制和转账功能。
1. Permit2的核心目标
● 增强授权灵活性
支持批量授权操作
支持时间范围限制,使授权更安全。
● 减少链上交互
通过离线签名完成批量授权,降低交易成本。
支持多次调用复用签名,提高效率。
● 改进安全性:限制授权的代币数量和时间范围,降低被滥用的风险。
● 适配多种代币:可以用于任何ERC-20代币,而无需代币原生支持。
2. 核心功能
2.1 授权代币(permit)
扩展的permit方法,允许通过签名授权代币使用,支持批量授权和时间限制。
function permit(
address owner,
PermitDetails[] calldata details,
bytes calldata signature
) external;
● 参数解析
owner:授权代币的持有人
details:包含代币授权信息的数组,支持批量授权
signature:基于 EIP-712 的签名数据
PermitDetails 结构:
struct PermitDetails {
address token; // 授权的代币地址
address spender; // 被授权地址
uint256 amount; // 授权金额
uint256 expiration; // 授权的过期时间
uint256 nonce; // 防止重放攻击的 nonce
}
2.2 限时授权(restricted transfer)
添加时间范围限制和授权金额限制,确保授权仅在特定时间内有效。
function transferFrom(
address token,
address from,
address to,
uint256 amount
) external;
● 功能
验证token的授权信息
确保调用发生在授权时间范围内
执行代币转账
2.3 签名验证
Permit2使用EIP-712签名规范,验证离线签名的合法性。
● 计算消息哈希
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(
abi.encode(
keccak256("Permit(address owner,PermitDetails[] details)"),
owner,
details
)
)
)
);
● 验证签名
address signer = ecrecover(digest, v, r, s);
require(signer == owner, "Invalid signature");
2.4 批量撤销授权
提供批量撤销授权的方法,提高安全性
function revoke(
address[] calldata tokens,
address[] calldata spenders
) external;
● 功能
清除指定代币的授权记录
确保敏感授权能够快速被撤销
3. 状态变量和安全机制
3.1 状态变量
● nonces:每个地址和授权记录都是唯一的nonce,防止签名被重复使用
● authorization:存储每个代币的授权记录,包括授权金额和有效时间
3.2 安全机制
● 签名验证:使用EIP-712验证签名,确保数据来源的可信性
● 时间限制:限制授权的时间范围,减少潜在的滥用风险
● 批量撤销:提供快速清除授权的方法,确保用户资产安全
4. permit2完整实例
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract Permit2 {
struct PermitDetails {
address token;
address spender;
uint256 amount;
uint256 expiration;
uint256 nonce;
}
mapping(address => uint256) public nonces;
mapping(address => mapping(address => uint256)) public allowances;
bytes32 public DOMAIN_SEPARATOR;
bytes32 public constant PERMIT_TYPEHASH = keccak256("Permit(address owner,PermitDetails[] details)");
constructor(string memory name, uint256 chainId) {
DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name)),
keccak256(bytes("1")),
chainId,
address(this)
)
);
}
function permit(
address owner,
PermitDetails[] calldata details,
uint8 v,
bytes32 r,
bytes32 s
) external {
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH, owner, keccak256(abi.encode(details))))
)
);
address signer = ecrecover(digest, v, r, s);
require(signer == owner, "Invalid signature");
for (uint256 i = 0; i < details.length; i++) {
PermitDetails memory detail = details[i];
require(block.timestamp <= detail.expiration, "Permit expired");
allowances[detail.token][detail.spender] = detail.amount;
}
}
function transferFrom(
address token,
address from,
address to,
uint256 amount
) external {
require(allowances[token][msg.sender] >= amount, "Insufficient allowance");
allowances[token][msg.sender] -= amount;
IERC20(token).transferFrom(from, to, amount);
}
function revoke(address[] calldata tokens, address[] calldata spenders) external {
require(tokens.length == spenders.length, "Mismatched input lengths");
for (uint256 i = 0; i < tokens.length; i++) {
allowances[tokens[i]][spenders[i]] = 0;
}
}
}
5.应用场景
● DeFi协议中的无Gas授权
用户通过离线签名授权协议操作代币,提升用户体验。
例如:Uniswap、SushiSwap
● 批量授权与撤销:支持一键批量授权和撤销操作,提高操作效率。
● 时间范围限制:设定授权时间范围,避免授权长期存在导致安全隐患。
● 多代币管理:在多资产场景中,一次性完成多个代币的授权和管理。
六、Permit和Permit2对比
Permit和Permit2是两种代币授权机制,旨在优化和增强ERC-20标准中的授权流程。它们通过离线签名(EIP-712)来减少链上交互,降低用户使用成本。以下是两者在功能、实现和应用场景方面的对比。
1. 基本概念
2. 功能对比
3. 方法对比
3.1 Permit(ERC-2612)
主要新增了permit方法,用于完成单一代币的授权操作
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external;
● 限制
只能用于单一代币
无法设定时间范围,授权有效期仅通过deadline控制
3.2 Permit2
在permit基础上进行了扩展,支持批量授权和时间范围限制
批量授权
function permit(
address owner,
PermitDetails[] calldata details,
bytes calldata signature
) external;
● PermitDetails结构
struct PermitDetails {
address token; // 授权的代币地址
address spender; // 授权的账户
uint256 amount; // 授权金额
uint256 expiration; // 授权过期时间
uint256 nonce; // 防止重放攻击的 nonce
}
转账功能
function transferFrom(
address token,
address from,
address to,
uint256 amount
) external;
● 功能
使用授权直接完成代币的转账操作
验证转账是否在授权范围和时间范围内
批量撤销
function revoke(
address[] calldata tokens,
address[] calldata spenders
) external;
● 功能:批量清除多个代币的授权记录
4.数据管理对比
5.应用场景对比
6. 优势与劣势对比
● Permit(ERC-2612)
优势
实现简单,适合单一代币的授权需求
使用离线签名,降低用户Gas成本
劣势
无法批量授权,适配性较弱
授权时间不可灵活控制
需要代币原生支持,限制较多
● Permit2
优势
支持批量授权和撤销操作,提高效率
引入时间范围限制,增强安全性
适配任何ERC-20代币,无需代币本身支持。
劣势
状态管理更复杂,可能增加存储和逻辑开销
实现成本更高,签名数据结构更加复杂
7. 总结
● 选择Permit(ERC-2612):适用于单一代币的简单授权场景,如普通钱包授权或单代币DeFi协议。
● 选择Permit2:适用于复杂DeFi场景,需要多代币管理、批量授权和撤销操作。
七、ERC-712和ERC-2612项目实战
以下是一个完整的基于 Foundry 的项目示例,包含 Permit(ERC-2612) 和 Permit2 的实现、部署脚本和测试脚本。该项目演示了如何同时实现和测试两种授权机制。
1. 项目目录结构
permit-foundry/
├── src/
│ ├── ERC20Permit.sol # ERC-2612 实现
│ ├── Permit2.sol # Permit2 实现
├── script/
│ ├── Deploy.s.sol # 部署脚本
├── test/
│ ├── ERC20Permit.t.sol # ERC-2612 测试脚本
│ ├── Permit2.t.sol # Permit2 测试脚本
├── foundry.toml # Foundry 配置文件
2.ERC-2612和Permit2合约实现
● 文件:src/ERC20Permit.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract ERC20Permit is ERC20 {
mapping(address => uint256) public nonces;
bytes32 public DOMAIN_SEPARATOR;
bytes32 public constant PERMIT_TYPEHASH =
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
constructor(string memory name, string memory symbol, uint256 initialSupply) ERC20(name, symbol) {
uint256 chainId;
assembly {
chainId := chainid()
}
DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name)),
keccak256(bytes("1")),
chainId,
address(this)
)
);
_mint(msg.sender, initialSupply);
}
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external {
require(block.timestamp <= deadline, "Permit: expired deadline");
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(
abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)
)
)
);
address recoveredAddress = ecrecover(digest, v, r, s);
require(recoveredAddress != address(0) && recoveredAddress == owner, "Permit: invalid signature");
_approve(owner, spender, value);
}
}
● 文件:src/Permit2.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract Permit2 {
struct PermitDetails {
address token;
address spender;
uint256 amount;
uint256 expiration;
uint256 nonce;
}
mapping(address => uint256) public nonces;
mapping(address => mapping(address => uint256)) public allowances;
bytes32 public DOMAIN_SEPARATOR;
bytes32 public constant PERMIT_TYPEHASH =
keccak256("Permit(address owner,PermitDetails[] details)");
constructor(string memory name, uint256 chainId) {
DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name)),
keccak256(bytes("1")),
chainId,
address(this)
)
);
}
function permit(
address owner,
PermitDetails[] calldata details,
uint8 v,
bytes32 r,
bytes32 s
) external {
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH, owner, keccak256(abi.encode(details))))
)
);
address signer = ecrecover(digest, v, r, s);
require(signer == owner, "Invalid signature");
for (uint256 i = 0; i < details.length; i++) {
PermitDetails memory detail = details[i];
require(block.timestamp <= detail.expiration, "Permit expired");
allowances[detail.token][detail.spender] = detail.amount;
}
}
function transferFrom(
address token,
address from,
address to,
uint256 amount
) external {
require(allowances[token][msg.sender] >= amount, "Insufficient allowance");
allowances[token][msg.sender] -= amount;
IERC20(token).transferFrom(from, to, amount);
}
}
3.部署脚本
● 文件:script/Deploy.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "../src/ERC20Permit.sol";
import "../src/Permit2.sol";
contract Deploy is Script {
function run() external {
vm.startBroadcast();
// 部署 ERC20Permit
ERC20Permit token = new ERC20Permit("MyToken", "MTK", 1000 * 10 ** 18);
console.log("ERC20Permit deployed at:", address(token));
// 部署 Permit2
Permit2 permit2 = new Permit2("Permit2", block.chainid);
console.log("Permit2 deployed at:", address(permit2));
vm.stopBroadcast();
}
}
4.测试脚本
● 文件:test/ERC20Permit.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/ERC20Permit.sol";
contract ERC20PermitTest is Test {
ERC20Permit public token;
address public owner;
address public spender;
function setUp() public {
token = new ERC20Permit("MyToken", "MTK", 1000 * 10 ** 18);
owner = address(1);
spender = address(2);
vm.prank(address(this));
token.transfer(owner, 500 * 10 ** 18);
}
function testPermit() public {
uint256 amount = 100 * 10 ** 18;
uint256 deadline = block.timestamp + 1 days;
uint256 nonce = token.nonces(owner);
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
token.DOMAIN_SEPARATOR(),
keccak256(
abi.encode(
token.PERMIT_TYPEHASH(),
owner,
spender,
amount,
nonce,
deadline
)
)
)
);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(1, digest);
vm.prank(spender);
token.permit(owner, spender, amount, deadline, v, r, s);
assertEq(token.allowance(owner, spender), amount);
}
}
● 文件:test/Permit2.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/Permit2.sol";
contract Permit2Test is Test {
Permit2 public permit2;
address public owner;
address public spender;
function setUp() public {
permit2 = new Permit2("Permit2", block.chainid);
owner = address(1);
spender = address(2);
}
function testPermit2() public {
Permit2.PermitDetails;
details[0] = Permit2.PermitDetails({
token: address(this),
spender: spender,
amount: 100,
expiration: block.timestamp + 1 days,
nonce: 0
});
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
permit2.DOMAIN_SEPARATOR(),
keccak256(abi.encode(permit2.PERMIT_TYPEHASH(), owner, keccak256(abi.encode(details))))
)
);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(1, digest);
vm.prank(spender);
permit2.permit(owner, details, v, r, s);
assertEq(permit2.allowances(address(this), spender), 100);
}
}
5. 测试与部署
● 安装依赖
forge install OpenZeppelin/openzeppelin-contracts
● 编译
forge build
● 运行测试
forge test
● 部署合约
forge script script/Deploy.s.sol --broadcast --rpc-url <YOUR_RPC_URL>
6. 总结
该项目实现了 Permit 和 Permit2 的完整逻辑,并展示了如何使用 Foundry 进行测试和部署。Permit 提供单代币授权,而 Permit2 支持批量操作。
声明:本网站所有相关资料如有侵权请联系站长删除,资料仅供用户学习及研究之用,不构成任何投资建议!