2、ERC721标准深度解析:接口定义、核心函数详解

好,咱们正式开始啃ERC721这块硬骨头。说实话,这个标准我前前后后看了不下二十遍,每次重读都能发现一些之前忽略的细节。ERC721定义了NFT最基本的操作规范,你想想看,没有它,现在这些动辄几十万的数字艺术品根本没法在链上流转。

2.1 接口定义:IERC721

ERC721的核心接口定义在IERC721.sol中。它规定了所有NFT合约必须实现的功能。我习惯把接口理解成一份合同——你只要说「我实现了IERC721」,别人就知道你的合约能做什么。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IERC721 {
    // 查询余额
    function balanceOf(address owner) external view returns (uint256 balance);
    // 查询所有者
    function ownerOf(uint256 tokenId) external view returns (address owner);
    // 安全转账
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
    // 普通转账
    function transferFrom(address from, address to, uint256 tokenId) external;
    // 授权
    function approve(address to, uint256 tokenId) external;
    // 批量授权
    function setApprovalForAll(address operator, bool _approved) external;
    // 查询授权地址
    function getApproved(uint256 tokenId) external view returns (address operator);
    // 查询批量授权状态
    function isApprovedForAll(address owner, address operator) external view returns (bool);
}

嗯,这里要注意,接口里只定义了函数签名,没有实现。具体怎么存数据、怎么校验,那是实现合约的事。

2.2 核心函数详解

2.2.1 balanceOf:查余额

balanceOf(address owner)返回某个地址持有的NFT数量。逻辑很简单,但有个坑——如果传入的地址是零地址(address(0)),必须revert。我在项目中遇到过有人传错参数,结果合约直接崩了。

function balanceOf(address owner) public view virtual override returns (uint256) {
    require(owner != address(0), "ERC721: balance query for the zero address");
    return _balances[owner];
}

2.2.2 ownerOf:查主人

ownerOf(uint256 tokenId)返回某个tokenId的当前持有者。如果这个tokenId不存在(比如从未铸造过),也必须revert。说白了,每个NFT必须有一个明确的主人,不能悬空。

关键点:ownerOf查询的是当前所有者,不是历史所有者。如果你想查历史记录,得自己额外维护。

2.2.3 safeTransferFrom vs transferFrom

这两个函数都是转账,但区别很大。我刚开始写合约时总搞混,后来吃了亏才记住。

函数 特点 适用场景
transferFrom 直接转账,不检查接收方 接收方是EOA(普通地址)
safeTransferFrom 转账后检查接收方是否支持ERC721 接收方可能是合约

safeTransferFrom会调用接收方合约的onERC721Received函数。如果接收方没实现这个函数,转账会失败。为什么要这样?你想想看,如果把NFT转给一个不支持ERC721的合约,那这个NFT就永远锁在里面了——我曾经帮一个项目方追回过这种「死资产」,过程极其痛苦。

function safeTransferFrom(
    address from,
    address to,
    uint256 tokenId,
    bytes memory data
) public virtual override {
    require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner nor approved");
    _safeTransfer(from, to, tokenId, data);
}

2.2.4 approve与setApprovalForAll

这两个授权函数,一个针对单个NFT,一个针对全部NFT。

  • approve:授权某个地址操作你持有的某一个特定NFT。比如你有一张编号为42的猴子图片,想卖给某个市场,就调用approve(marketAddress, 42)
  • setApprovalForAll:授权某个地址操作你持有的所有NFT。比如你信任OpenSea,就调用setApprovalForAll(openseaAddress, true),这样OpenSea可以帮你转走你钱包里的任何NFT。

警告:setApprovalForAll是双刃剑。我曾经见过一个用户授权了恶意合约,结果所有NFT被一次性转走。建议只授权给经过审计的合约,并且定期检查授权列表。

2.2.5 getApproved与isApprovedForAll

这两个是查询函数,用来检查授权状态。

  • getApproved(tokenId):返回某个NFT被授权给了哪个地址。如果没有授权,返回零地址。
  • isApprovedForAll(owner, operator):返回某个操作员是否被授权管理某个地址的所有NFT。

我习惯在写前端时先调用这两个函数,确认授权状态后再发起交易。这样可以避免用户因为授权不足而交易失败,白白浪费gas。

2.3 事件定义

ERC721定义了三个事件,它们就像区块链上的广播信号,告诉全世界「发生了什么」。

event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

2.3.1 Transfer事件

每次NFT被转移时,必须触发Transfer事件。铸造时from填零地址,销毁时to填零地址。这个事件是NFT生命周期的完整记录——从出生到死亡,每一步都有迹可循。

2.3.2 Approval事件

当调用approve函数时触发。注意,setApprovalForAll不会触发这个事件,它触发的是ApprovalForAll。这两个事件是分开的,别搞混了。

2.3.3 ApprovalForAll事件

当调用setApprovalForAll时触发。参数中的approved布尔值表示是授权(true)还是取消授权(false)。

小技巧:事件中的indexed关键字可以让参数被索引,方便链下查询。比如你可以通过Transfer事件的from参数,快速查到某个地址转出了哪些NFT。我在做数据分析时经常利用这个特性。

2.4 避坑指南

最后分享几个我踩过的坑:

  • 零地址检查:所有涉及地址参数的函数,都要检查零地址。否则可能造成资产永久锁定。
  • 重复授权approve同一个地址多次,会覆盖之前的授权。但不会报错,所以前端要做好提示。
  • 授权后转账:授权后,被授权者可以调用transferFrom转走NFT。但注意,授权者自己也可以转走——授权不是转移所有权。
  • 事件必须触发:ERC721标准规定,任何状态变更都必须触发对应事件。这是链上数据完整性的保证。

嗯,ERC721的核心内容差不多就这些。下一章我们会深入ERC1155,看看它和ERC721有什么不同,以及什么时候该用哪个标准。