ERC-712是一种通用的结构化签名标准,为离线签名和链上验证提供了高效工具。ERC-2612是基于ERC-712的扩展,专注于代币授权的优化,特别适用于DeFi和钱包应用场景。
1. 什么是ERC-712 ERC-712提供了一种对结构化数据进行离线签名的标准。它通过定义签名的格式和数据结构,确保签名的安全性和可验证性,并显著提高了用户交互的便利性。 2.核心内容 2.1 数据结构EIP-712允许开发者定义数据结构,并使用哈希算法将其转换为签名消息。
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.应用场景
<!--StartFragment-->
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;
参数说明:
功能:
2.2 状态变量
mapping(address => uint256) public nonces;
2.3 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-2612的改进协议,它扩展了ERC-20的授权模型,并为DeFi协议提供了更灵活和高效的授权机制。与ERC-2612类似,Permit2也利用了离线签名(基于EIP-712)来完成代币授权,但其功能更强大,支持批量授权、时间限制和转账功能。 1. Permit2的核心目标
2. 核心功能 2.1 授权代币(permit)扩展的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}
2.2 限时授权(restricted transfer) 添加时间范围限制和授权金额限制,确保授权仅在特定时间内有效。
function transferFrom(
address token,
address from,
address to,
uint256 amount
) external;
功能
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 状态变量
3.2 安全机制
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.应用场景
对比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;
在permit基础上进行了扩展,支持批量授权和时间范围限制
批量授权
function permit(
address owner,
PermitDetails[] calldata details,
bytes calldata signature
) external;
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合约实现
// 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);
}
}
// 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.部署脚本
// 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.测试脚本
// 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);
}
}
// 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);
}
}
6. 总结 该项目实现了 Permit 和 Permit2 的完整逻辑,并展示了如何使用 Foundry 进行测试和部署。Permit 提供单代币授权,而 Permit2 支持批量操作。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!