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有什么不同,以及什么时候该用哪个标准。