以太坊Permit(EIP-2612):离线签名授权的革命性标准1.引言:重新定义代币授权1.1传统授权的问题在传统的ERC20代币授权模式中,用户必须进行两步操作:调用approve(spender,amount)交易,支付Gas并等待确认执行实际的目标操作(如兑换、存款等)
<!--StartFragment-->
在传统的ERC20代币授权模式中,用户必须进行两步操作:
approve(spender, amount)交易,支付Gas并等待确认这种模式导致用户体验割裂、Gas成本翻倍、操作延迟增加。Permit的出现彻底改变了这一范式。
Permit是以太坊改进提案EIP-2612定义的标准接口,允许用户通过离线签名的方式授权第三方使用其ERC20代币,而无需预先发送链上approve交易。这是离线签名技术在代币授权场景下的标准化实现。
传统流程:
用户 → approve交易(支付Gas) → 等待确认 → 目标操作(支付Gas)
Permit流程:
用户 → 离线签名(无需Gas) → 提交签名 → 单笔交易完成授权+目标操作(支付一次Gas)
// EIP-2612标准接口
interface IERC20Permit {
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external;
function nonces(address owner) external view returns (uint256);
function DOMAIN_SEPARATOR() external view returns (bytes32);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
contract ERC20WithPermit is ERC20, EIP712 {
using ECDSA for bytes32;
// 为每个地址维护一个nonce,防止签名重放
mapping(address => uint256) private _nonces;
// Permit类型哈希,符合EIP-712标准
bytes32 private constant _PERMIT_TYPEHASH =
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
// 域名分隔符,防止跨链和跨合约重放
bytes32 private immutable _CACHED_DOMAIN_SEPARATOR;
uint256 private immutable _CACHED_CHAIN_ID;
address private immutable _CACHED_THIS;
constructor(
string memory name,
string memory symbol,
uint256 initialSupply
) ERC20(name, symbol) EIP712(name, "1") {
_mint(msg.sender, initialSupply);
// 缓存域分隔符以提高gas效率
_CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator();
_CACHED_CHAIN_ID = block.chainid;
_CACHED_THIS = address(this);
}
// 核心permit函数实现
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual {
require(block.timestamp <= deadline, "ERC20Permit: expired deadline");
// 使用当前nonce
uint256 currentNonce = _nonces[owner];
// 构建符合EIP-712的哈希
bytes32 structHash = keccak256(
abi.encode(
_PERMIT_TYPEHASH,
owner,
spender,
value,
currentNonce,
deadline
)
);
bytes32 hash = _hashTypedDataV4(structHash);
// 从签名中恢复地址
address signer = ECDSA.recover(hash, v, r, s);
require(signer == owner, "ERC20Permit: invalid signature");
// 验证通过,增加nonce并执行授权
_nonces[owner] = currentNonce + 1;
_approve(owner, spender, value);
}
// 查询地址的当前nonce
function nonces(address owner) public view virtual returns (uint256) {
return _nonces[owner];
}
// 域分隔符(供外部查询)
function DOMAIN_SEPARATOR() external view returns (bytes32) {
return _domainSeparatorV4();
}
// 内部函数:构建域分隔符
function _buildDomainSeparator() private view returns (bytes32) {
return keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name())),
keccak256(bytes("1")),
block.chainid,
address(this)
)
);
}
// 重写域分隔符计算以使用缓存
function _domainSeparatorV4() internal view override returns (bytes32) {
if (block.chainid == _CACHED_CHAIN_ID && address(this) == _CACHED_THIS) {
return _CACHED_DOMAIN_SEPARATOR;
} else {
return _buildDomainSeparator();
}
}
}
import { ethers } from 'ethers';
class PermitSigner {
constructor(tokenAddress, provider, signer) {
this.tokenAddress = tokenAddress;
this.provider = provider;
this.signer = signer;
this.tokenContract = new ethers.Contract(
tokenAddress,
[
'function nonces(address owner) view returns (uint256)',
'function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)',
'function name() view returns (string)',
'function DOMAIN_SEPARATOR() view returns (bytes32)'
],
signer
);
}
async createPermitSignature(spender, value, deadlineMinutes = 30) {
// 1. 获取必要参数
const owner = await this.signer.getAddress();
const nonce = await this.tokenContract.nonces(owner);
const deadline = Math.floor(Date.now() / 1000) + (deadlineMinutes * 60);
// 2. 获取代币名称
const name = await this.tokenContract.name();
// 3. 构建EIP-712域数据
const domain = {
name: name,
version: '1',
chainId: await this.provider.getNetwork().then(n => n.chainId),
verifyingContract: this.tokenAddress
};
// 4. 构建Permit类型定义
const types = {
Permit: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
]
};
// 5. 构建消息值
const message = {
owner: owner,
spender: spender,
value: value,
nonce: nonce,
deadline: deadline
};
// 6. 请求用户签名
const signature = await this.signer._signTypedData(domain, types, message);
// 7. 拆分签名
const sig = ethers.utils.splitSignature(signature);
return {
owner,
spender,
value,
deadline,
v: sig.v,
r: sig.r,
s: sig.s,
signature: sig
};
}
}
// 场景1:直接调用permit函数
async function executePermit(signatureData) {
const { owner, spender, value, deadline, v, r, s } = signatureData;
const tx = await tokenContract.permit(
owner,
spender,
value,
deadline,
v,
r,
s
);
const receipt = await tx.wait();
console.log(`Permit executed. Allowance set: ${value}`);
return receipt;
}
// 场景2:组合交易 - 在单笔交易中完成授权+操作
async function executePermitAndTransfer(signatureData, recipient, amount) {
const { owner, spender, value, deadline, v, r, s } = signatureData;
// 编码多调用数据
const permitData = tokenContract.interface.encodeFunctionData('permit', [
owner, spender, value, deadline, v, r, s
]);
const transferData = tokenContract.interface.encodeFunctionData('transferFrom', [
owner, recipient, amount
]);
// 使用Multicall或自定义合约执行批量调用
const multicallContract = new ethers.Contract(
MULTICALL_ADDRESS,
['function aggregate((address target, bytes callData)[] calls) returns (uint256 blockNumber, bytes[] returnData)'],
signer
);
const tx = await multicallContract.aggregate([
{ target: tokenContract.address, callData: permitData },
{ target: tokenContract.address, callData: transferData }
]);
return await tx.wait();
}
// 用户无Gas兑换流程
class GaslessDEX {
async createGaslessSwap(userSignature, swapParams) {
// 1. 验证用户签名
const isValid = await this.verifyPermitSignature(userSignature);
if (!isValid) throw new Error('Invalid signature');
// 2. 构建包含permit和swap的元交易
const metaTx = {
from: this.relayerAddress,
to: this.routerAddress,
data: this.encodeSwapWithPermit(userSignature, swapParams),
gasLimit: 500000,
nonce: await this.provider.getTransactionCount(this.relayerAddress)
};
// 3. 中继器支付Gas并发送交易
const tx = await this.relayer.sendTransaction(metaTx);
// 4. 监听交易结果
const receipt = await tx.wait();
return {
txHash: receipt.transactionHash,
relayer: this.relayerAddress,
userPaidGas: false
};
}
encodeSwapWithPermit(signature, swap) {
// 编码:permit + swapExactTokensForTokens
// 实际实现需根据具体DEX路由合约
}
}
// 单笔交易完成多个代币授权
class BatchPermit {
constructor(multiPermitContract) {
this.multiPermit = multiPermitContract;
}
async batchPermit(permitDataArray) {
// 编码多个permit调用
const calls = permitDataArray.map(data => {
return {
target: data.tokenAddress,
callData: this.encodePermitCall(data)
};
});
const tx = await this.multiPermit.batchPermit(calls);
return await tx.wait();
}
encodePermitCall(data) {
const iface = new ethers.utils.Interface([
'function permit(address,address,uint256,uint256,uint8,bytes32,bytes32)'
]);
return iface.encodeFunctionData('permit', [
data.owner,
data.spender,
data.value,
data.deadline,
data.v,
data.r,
data.s
]);
}
}
// 增强的签名验证逻辑
function _validatePermit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint256 nonce,
bytes32 r,
bytes32 s,
uint8 v
) internal {
// 1. 检查deadline
require(deadline >= block.timestamp, "Permit: expired signature");
// 2. 检查签名格式
require(uint256(s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0,
"Permit: invalid signature 's' value");
require(v == 27 || v == 28, "Permit: invalid signature 'v' value");
// 3. 构建消息哈希
bytes32 structHash = keccak256(
abi.encode(
PERMIT_TYPEHASH,
owner,
spender,
value,
nonce,
deadline
)
);
// 4. 计算EIP-712哈希
bytes32 hash = _hashTypedDataV4(structHash);
// 5. 恢复签名者
address signer = ECDSA.recover(hash, v, r, s);
require(signer == owner, "Permit: invalid signature");
require(signer != address(0), "Permit: invalid signature");
}
class SecurePermitHandler {
constructor() {
this.pendingSignatures = new Map();
}
async signWithSafetyChecks(user, token, spender, amount) {
// 1. 检查代币合约是否支持Permit
const supportsPermit = await this.checkPermitSupport(token);
if (!supportsPermit) {
throw new Error('Token does not support Permit');
}
// 2. 检查代币余额
const balance = await token.balanceOf(user);
if (balance.lt(amount)) {
throw new Error('Insufficient balance');
}
// 3. 设置合理的deadline(默认30分钟)
const deadline = Math.floor(Date.now() / 1000) + 1800;
// 4. 在UI中明确显示授权信息
this.displayPermitDetails({
token: await token.name(),
spender: spender,
amount: ethers.utils.formatUnits(amount, await token.decimals()),
deadline: new Date(deadline * 1000).toLocaleString(),
spenderType: await this.getSpenderType(spender) // DEX, Lending, etc.
});
// 5. 要求用户确认
const confirmed = await this.requestUserConfirmation();
if (!confirmed) {
throw new Error('User rejected permit signature');
}
// 6. 生成签名
return await this.createPermitSignature(user, token, spender, amount, deadline);
}
}
// 检查代币是否支持Permit
async function checkPermitSupport(tokenAddress, provider) {
try {
const token = new ethers.Contract(
tokenAddress,
[
'function permit(address,address,uint256,uint256,uint8,bytes32,bytes32)',
'function DOMAIN_SEPARATOR() view returns (bytes32)',
'function nonces(address) view returns (uint256)'
],
provider
);
// 尝试调用只读函数
await Promise.all([
token.DOMAIN_SEPARATOR(),
token.nonces(ethers.constants.AddressZero)
]);
return true;
} catch (error) {
console.warn('Token does not support Permit:', error.message);
return false;
}
}
// 批量Permit合约,减少Gas开销
contract BatchPermitExecutor {
using ECDSA for bytes32;
struct PermitData {
address token;
address owner;
address spender;
uint256 value;
uint256 deadline;
uint8 v;
bytes32 r;
bytes32 s;
}
function batchPermit(PermitData[] calldata permits) external {
for (uint256 i = 0; i < permits.length; i++) {
PermitData memory permit = permits[i];
// 调用各个代币的permit函数
(bool success, ) = permit.token.call(
abi.encodeWithSignature(
"permit(address,address,uint256,uint256,uint8,bytes32,bytes32)",
permit.owner,
permit.spender,
permit.value,
permit.deadline,
permit.v,
permit.r,
permit.s
)
);
// 继续执行即使某个失败(或根据需求revert)
if (!success) {
// 记录失败但继续
emit PermitFailed(permit.token, i);
}
}
}
}
// 使用Hardhat测试
const { expect } = require("chai");
describe("ERC20WithPermit", function() {
let token, owner, spender, other;
beforeEach(async function() {
[owner, spender, other] = await ethers.getSigners();
const Token = await ethers.getContractFactory("ERC20WithPermit");
token = await Token.deploy("Test Token", "TEST", ethers.utils.parseEther("1000"));
await token.deployed();
});
it("应该允许通过Permit设置授权", async function() {
const value = ethers.utils.parseEther("100");
const deadline = (await ethers.provider.getBlock('latest')).timestamp + 3600;
// 获取当前nonce
const nonce = await token.nonces(owner.address);
// 构建签名
const domain = {
name: "Test Token",
version: "1",
chainId: await owner.getChainId(),
verifyingContract: token.address
};
const types = {
Permit: [
{ name: "owner", type: "address" },
{ name: "spender", type: "address" },
{ name: "value", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
]
};
const message = {
owner: owner.address,
spender: spender.address,
value: value,
nonce: nonce,
deadline: deadline
};
const signature = await owner._signTypedData(domain, types, message);
const { v, r, s } = ethers.utils.splitSignature(signature);
// 通过spender账户执行permit
await token.connect(spender).permit(
owner.address,
spender.address,
value,
deadline,
v, r, s
);
// 验证授权已设置
const allowance = await token.allowance(owner.address, spender.address);
expect(allowance).to.equal(value);
});
});
ERC20Permit基础实现Permit (EIP-2612) 是以太坊生态中离线签名技术最成功的应用之一,它:
随着DeFi和Web3应用的普及,Permit已成为现代以太坊DApp的必备功能。它不仅是一项技术实现,更是用户体验革命的代表,展示了密码学如何从根本上改进区块链交互方式。
对于开发者而言,理解并正确实现Permit意味着能为用户提供更流畅、更经济的DApp体验。对于用户而言,支持Permit的代币和DApp代表着更友好的交互方式和更低的参与门槛。
<!--EndFragment-->
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!