EIP-4494 Link to heading
EIP-4494 解决的核心问题:NFT 的离线授权 Link to heading
传统 ERC-721 NFT 的授权转移需要两步操作:
- 用户调用
approve()
或setApprovalForAll()
授权某个地址 - 被授权方调用
transferFrom()
转移 NFT
这导致:
- 需要两笔交易,用户体验差
- 用户必须持有 ETH 支付 Gas 费
- 无法实现 gasless 的 NFT 交易体验
EIP-4494 将 ERC-2612 的 permit 机制扩展到 NFT,通过:
- 用户对授权信息进行离线签名(基于 EIP-712)
- 第三方提交签名到链上,一笔交易完成授权+转移
- 实现 gasless NFT 操作,提升用户体验
EIP-4494 通用签名结构 Link to heading
基于 EIP-712 的结构化签名:
struct Permit {
address spender; // 被授权者地址
uint256 tokenId; // NFT ID
uint256 nonce; // 防重放攻击
uint256 deadline; // 签名截止时间
}
Domain Separator 包含:
- name:NFT 合约名称
- version:版本号
- chainId:链 ID
- verifyingContract:NFT 合约地址
示例 Link to heading
Solidity 合约示例 Link to heading
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract MyNFT is ERC721, EIP712 {
// Permit 结构体
struct Permit {
address spender;
uint256 tokenId;
uint256 nonce;
uint256 deadline;
}
// TypeHash
bytes32 private constant PERMIT_TYPEHASH =
keccak256("Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)");
// 每个 tokenId 的 nonce
mapping(uint256 => uint256) private _nonces;
constructor() ERC721("MyNFT", "MNFT") EIP712("MyNFT", "1") {}
// 获取 tokenId 的当前 nonce
function nonces(uint256 tokenId) public view returns (uint256) {
return _nonces[tokenId];
}
// 计算 permit 签名摘要
function _hashPermit(Permit memory permit) internal view returns (bytes32) {
return _hashTypedDataV4(keccak256(
abi.encode(
PERMIT_TYPEHASH,
permit.spender,
permit.tokenId,
permit.nonce,
permit.deadline
)
));
}
// permit 函数:通过签名授权
function permit(
address spender,
uint256 tokenId,
uint256 deadline,
bytes memory signature
) external {
require(block.timestamp <= deadline, "ERC4494: expired deadline");
address owner = ownerOf(tokenId);
require(owner != spender, "ERC4494: approval to current owner");
Permit memory permitData = Permit({
spender: spender,
tokenId: tokenId,
nonce: _nonces[tokenId],
deadline: deadline
});
bytes32 digest = _hashPermit(permitData);
address signer = ECDSA.recover(digest, signature);
require(signer == owner, "ERC4494: invalid signature");
// 增加 nonce 防止重放
_nonces[tokenId]++;
// 执行授权
_approve(spender, tokenId);
}
// 带 permit 的转移函数
function transferWithPermit(
address from,
address to,
uint256 tokenId,
uint256 deadline,
bytes memory signature
) external {
// 先执行 permit
permit(msg.sender, tokenId, deadline, signature);
// 再执行转移
transferFrom(from, to, tokenId);
}
}
前端(ethers.js)签名示例 Link to heading
import { ethers } from "ethers";
async function signNFTPermit() {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
const domain = {
name: "MyNFT",
version: "1",
chainId: await signer.getChainId(),
verifyingContract: "0xYourNFTContractAddress",
};
const types = {
Permit: [
{ name: "spender", type: "address" },
{ name: "tokenId", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" }
]
};
const value = {
spender: "0xSpenderAddress",
tokenId: 123,
nonce: 0, // 从合约获取当前 nonce
deadline: Math.floor(Date.now() / 1000) + 3600 // 1小时后过期
};
// 签名
const signature = await signer._signTypedData(domain, types, value);
console.log("NFT Permit 签名:", signature);
return { value, signature };
}
应用场景 Link to heading
-
NFT 交易市场的 gasless 上架
- 用户签名授权市场合约转移 NFT
- 买家购买时一笔交易完成授权+转移
-
批量 NFT 操作
- 用户批量签名多个 NFT 的授权
- 第三方批量执行转移操作
-
第三方代理转移
- 游戏或应用代用户管理 NFT
- 用户只需签名,无需支付 Gas
-
拍卖和竞价
- 用户签名授权拍卖合约
- 拍卖结束时自动转移 NFT
安全注意事项 Link to heading
- Nonce 机制:每个 tokenId 独立的 nonce 防止重放攻击
- Deadline 检查:签名必须在截止时间前使用
- 所有者验证:只有 NFT 所有者的签名才有效
- 链 ID 绑定:防止跨链重放攻击
小结 Link to heading
- EIP-4494 将 ERC-2612 的 permit 机制扩展到 NFT 领域
- 通过 EIP-712 结构化签名实现 NFT 的离线授权
- 显著提升 NFT 交易的用户体验,实现 gasless 操作
- 广泛应用于 NFT 市场、游戏、DeFi 等场景