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,通过:

  1. 用户对授权信息进行离线签名(基于 EIP-712)
  2. 第三方提交签名到链上,一笔交易完成授权+转移
  3. 实现 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

  1. NFT 交易市场的 gasless 上架

    • 用户签名授权市场合约转移 NFT
    • 买家购买时一笔交易完成授权+转移
  2. 批量 NFT 操作

    • 用户批量签名多个 NFT 的授权
    • 第三方批量执行转移操作
  3. 第三方代理转移

    • 游戏或应用代用户管理 NFT
    • 用户只需签名,无需支付 Gas
  4. 拍卖和竞价

    • 用户签名授权拍卖合约
    • 拍卖结束时自动转移 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 等场景