WTF Solidity极简入门: 34. ERC721

我最近在重新学 Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。

推特:@0xAA_Science@WTFAcademy_

社区:Discord微信群官网 wtf.academy

所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity


BTCETH这类代币都属于同质化代币,矿工挖出的第1BTC与第10000BTC并没有不同,是等价的。但世界中很多物品是不同质的,其中包括房产、古董、虚拟艺术品等等,这类物品无法用同质化代币抽象。因此,以太坊EIP721提出了ERC721标准,来抽象非同质化的物品。这一讲,我们将介绍ERC721标准,并基于它发行一款NFT

EIP与ERC

这里有一个点需要理解,本节标题是ERC721,这里又提到了EIP721,这两个是什么关系呢?

EIP全称 Ethereum Improvement Proposals(以太坊改进建议), 是以太坊开发者社区提出的改进建议, 是一系列以编号排定的文件, 类似互联网上IETF的RFC。

EIP可以是 Ethereum 生态中任意领域的改进, 比如新特性、ERC、协议改进、编程工具等等。

ERC全称 Ethereum Request For Comment (以太坊意见征求稿), 用以记录以太坊上应用级的各种开发标准和协议。如典型的Token标准(ERC20, ERC721)、名字注册(ERC26, ERC13), URI范式(ERC67), Library/Package格式(EIP82), 钱包格式(EIP75,EIP85)。

ERC协议标准是影响以太坊发展的重要因素, 像ERC20, ERC223, ERC721, ERC777等, 都是对以太坊生态产生了很大影响。

所以最终结论:EIP包含ERC

在这一节学习完成后,才能明白为什么上来讲ERC165而不是ERC721,想要看结论可直接移动到最下面

ERC165

通过ERC165标准,智能合约可以声明它支持的接口,供其他合约检查。简单的说,ERC165就是检查一个智能合约是不是支持了ERC721ERC1155的接口。

IERC165接口合约只声明了一个supportsInterface函数,输入要查询的interfaceId接口id,若合约实现了该接口id,则返回true

1
2
3
4
5
6
7
8
interface IERC165 {
/**
* @dev 如果合约实现了查询的`interfaceId`,则返回true
* 规则详见:https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
*
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

我们可以看下ERC721是如何实现supportsInterface()函数的:

1
2
3
4
5
6
function supportsInterface(bytes4 interfaceId) external pure override returns (bool)
{
return
interfaceId == type(IERC721).interfaceId ||
interfaceId == type(IERC165).interfaceId;
}

当查询的是IERC721IERC165的接口id时,返回true;反之返回false

IERC721

IERC721ERC721标准的接口合约,规定了ERC721要实现的基本函数。它利用tokenId来表示特定的非同质化代币,授权或转账都要明确tokenId;而ERC20只需要明确转账的数额即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* @dev ERC721标准接口.
*/
interface IERC721 is IERC165 {
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);

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 safeTransferFrom(
address from,
address to,
uint256 tokenId
) 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);
}

IERC721事件

IERC721包含3个事件,其中TransferApproval事件在ERC20中也有。

  • Transfer事件:在转账时被释放,记录代币的发出地址from,接收地址totokenid
  • Approval事件:在授权时释放,记录授权地址owner,被授权地址approvedtokenid
  • ApprovalForAll事件:在批量授权时释放,记录批量授权的发出地址owner,被授权地址operator和授权与否的approved

IERC721函数

  • balanceOf:返回某地址的NFT持有量balance
  • ownerOf:返回某tokenId的主人owner
  • transferFrom:普通转账,参数为转出地址from,接收地址totokenId
  • safeTransferFrom:安全转账(如果接收方是合约地址,会要求实现ERC721Receiver接口)。参数为转出地址from,接收地址totokenId
  • approve:授权另一个地址使用你的NFT。参数为被授权地址approvetokenId
  • getApproved:查询tokenId被批准给了哪个地址。
  • setApprovalForAll:将自己持有的该系列NFT批量授权给某个地址operator
  • isApprovedForAll:查询某地址的NFT是否批量授权给了另一个operator地址。
  • safeTransferFrom:安全转账的重载函数,参数里面包含了data

IERC721Receiver

如果一个合约没有实现ERC721的相关函数,转入的NFT就进了黑洞,永远转不出来了。为了防止误转账,ERC721实现了safeTransferFrom()安全转账函数,目标合约必须实现了IERC721Receiver接口才能接收ERC721代币,不然会revertIERC721Receiver接口只包含一个onERC721Received()函数。

1
2
3
4
5
6
7
8
9
// ERC721接收者接口:合约必须实现这个接口来通过安全转账接收ERC721
interface IERC721Receiver {
function onERC721Received(
address operator,
address from,
uint tokenId,
bytes calldata data
) external returns (bytes4);
}

我们看下ERC721利用_checkOnERC721Received来确保目标合约实现了onERC721Received()函数(返回onERC721Receivedselector):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function _checkOnERC721Received(
address operator,
address from,
address to,
uint256 tokenId,
bytes memory data
) internal {
if (to.code.length > 0) {
try IERC721Receiver(to).onERC721Received(operator, from, tokenId, data) returns (bytes4 retval) {
if (retval != IERC721Receiver.onERC721Received.selector) {
// Token rejected
revert IERC721Errors.ERC721InvalidReceiver(to);
}
} catch (bytes memory reason) {
if (reason.length == 0) {
// non-IERC721Receiver implementer
revert IERC721Errors.ERC721InvalidReceiver(to);
} else {
/// @solidity memory-safe-assembly
assembly {
revert(add(32, reason), mload(reason))
}
}
}
}
}

IERC721Metadata

IERC721MetadataERC721的拓展接口,实现了3个查询metadata元数据的常用函数:

  • name():返回代币名称。
  • symbol():返回代币代号。
  • tokenURI():通过tokenId查询metadata的链接urlERC721特有的函数。
1
2
3
4
5
6
7
interface IERC721Metadata is IERC721 {
function name() external view returns (string memory);

function symbol() external view returns (string memory);

function tokenURI(uint256 tokenId) external view returns (string memory);
}

ERC721主合约

ERC721主合约实现了IERC721IERC165IERC721Metadata定义的所有功能,包含4个状态变量和17个函数。实现都比较简单,每个函数的功能见代码注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
// SPDX-License-Identifier: MIT
// by 0xAA
pragma solidity ^0.8.21;

import "./IERC165.sol";
import "./IERC721.sol";
import "./IERC721Receiver.sol";
import "./IERC721Metadata.sol";
import "./String.sol";

contract ERC721 is IERC721, IERC721Metadata{
using Strings for uint256; // 使用Strings库,

// Token名称
string public override name;
// Token代号
string public override symbol;
// tokenId 到 owner address 的持有人映射
mapping(uint => address) private _owners;
// address 到 持仓数量 的持仓量映射
mapping(address => uint) private _balances;
// tokenID 到 授权地址 的授权映射
mapping(uint => address) private _tokenApprovals;
// owner地址 到 operator地址 的批量授权映射
mapping(address => mapping(address => bool)) private _operatorApprovals;

// 错误 无效的接收者
error ERC721InvalidReceiver(address receiver);

/**
* 构造函数,初始化`name` 和`symbol` .
*/
constructor(string memory name_, string memory symbol_) {
name = name_;
symbol = symbol_;
}

// 实现IERC165接口supportsInterface
function supportsInterface(bytes4 interfaceId)
external
pure
override
returns (bool)
{
return
interfaceId == type(IERC721).interfaceId ||
interfaceId == type(IERC165).interfaceId ||
interfaceId == type(IERC721Metadata).interfaceId;
}

// 实现IERC721的balanceOf,利用_balances变量查询owner地址的balance。
function balanceOf(address owner) external view override returns (uint) {
require(owner != address(0), "owner = zero address");
return _balances[owner];
}

// 实现IERC721的ownerOf,利用_owners变量查询tokenId的owner。
function ownerOf(uint tokenId) public view override returns (address owner) {
owner = _owners[tokenId];
require(owner != address(0), "token doesn't exist");
}

// 实现IERC721的isApprovedForAll,利用_operatorApprovals变量查询owner地址是否将所持NFT批量授权给了operator地址。
function isApprovedForAll(address owner, address operator)
external
view
override
returns (bool)
{
return _operatorApprovals[owner][operator];
}

// 实现IERC721的setApprovalForAll,将持有代币全部授权给operator地址。调用_setApprovalForAll函数。
function setApprovalForAll(address operator, bool approved) external override {
_operatorApprovals[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}

// 实现IERC721的getApproved,利用_tokenApprovals变量查询tokenId的授权地址。
function getApproved(uint tokenId) external view override returns (address) {
require(_owners[tokenId] != address(0), "token doesn't exist");
return _tokenApprovals[tokenId];
}

// 授权函数。通过调整_tokenApprovals来,授权 to 地址操作 tokenId,同时释放Approval事件。
function _approve(
address owner,
address to,
uint tokenId
) private {
_tokenApprovals[tokenId] = to;
emit Approval(owner, to, tokenId);
}

// 实现IERC721的approve,将tokenId授权给 to 地址。条件:to不是owner,且msg.sender是owner或授权地址。调用_approve函数。
function approve(address to, uint tokenId) external override {
address owner = _owners[tokenId];
require(
msg.sender == owner || _operatorApprovals[owner][msg.sender],
"not owner nor approved for all"
);
_approve(owner, to, tokenId);
}

// 查询 spender地址是否可以使用tokenId(需要是owner或被授权地址)
function _isApprovedOrOwner(
address owner,
address spender,
uint tokenId
) private view returns (bool) {
return (spender == owner ||
_tokenApprovals[tokenId] == spender ||
_operatorApprovals[owner][spender]);
}

/*
* 转账函数。通过调整_balances和_owner变量将 tokenId 从 from 转账给 to,同时释放Transfer事件。
* 条件:
* 1. tokenId 被 from 拥有
* 2. to 不是0地址
*/
function _transfer(
address owner,
address from,
address to,
uint tokenId
) private {
require(from == owner, "not owner");
require(to != address(0), "transfer to the zero address");

_approve(owner, address(0), tokenId);

_balances[from] -= 1;
_balances[to] += 1;
_owners[tokenId] = to;

emit Transfer(from, to, tokenId);
}

// 实现IERC721的transferFrom,非安全转账,不建议使用。调用_transfer函数
function transferFrom(
address from,
address to,
uint tokenId
) external override {
address owner = ownerOf(tokenId);
require(
_isApprovedOrOwner(owner, msg.sender, tokenId),
"not owner nor approved"
);
_transfer(owner, from, to, tokenId);
}

/**
* 安全转账,安全地将 tokenId 代币从 from 转移到 to,会检查合约接收者是否了解 ERC721 协议,以防止代币被永久锁定。调用了_transfer函数和_checkOnERC721Received函数。条件:
* from 不能是0地址.
* to 不能是0地址.
* tokenId 代币必须存在,并且被 from拥有.
* 如果 to 是智能合约, 他必须支持 IERC721Receiver-onERC721Received.
*/
function _safeTransfer(
address owner,
address from,
address to,
uint tokenId,
bytes memory _data
) private {
_transfer(owner, from, to, tokenId);
_checkOnERC721Received(from, to, tokenId, _data);
}

/**
* 实现IERC721的safeTransferFrom,安全转账,调用了_safeTransfer函数。
*/
function safeTransferFrom(
address from,
address to,
uint tokenId,
bytes memory _data
) public override {
address owner = ownerOf(tokenId);
require(
_isApprovedOrOwner(owner, msg.sender, tokenId),
"not owner nor approved"
);
_safeTransfer(owner, from, to, tokenId, _data);
}

// safeTransferFrom重载函数
function safeTransferFrom(
address from,
address to,
uint tokenId
) external override {
safeTransferFrom(from, to, tokenId, "");
}

/**
* 铸造函数。通过调整_balances和_owners变量来铸造tokenId并转账给 to,同时释放Transfer事件。铸造函数。通过调整_balances和_owners变量来铸造tokenId并转账给 to,同时释放Transfer事件。
* 这个mint函数所有人都能调用,实际使用需要开发人员重写,加上一些条件。
* 条件:
* 1. tokenId尚不存在。
* 2. to不是0地址.
*/
function _mint(address to, uint tokenId) internal virtual {
require(to != address(0), "mint to zero address");
require(_owners[tokenId] == address(0), "token already minted");

_balances[to] += 1;
_owners[tokenId] = to;

emit Transfer(address(0), to, tokenId);
}

// 销毁函数,通过调整_balances和_owners变量来销毁tokenId,同时释放Transfer事件。条件:tokenId存在。
function _burn(uint tokenId) internal virtual {
address owner = ownerOf(tokenId);
require(msg.sender == owner, "not owner of token");

_approve(owner, address(0), tokenId);

_balances[owner] -= 1;
delete _owners[tokenId];

emit Transfer(owner, address(0), tokenId);
}

// _checkOnERC721Received:函数,用于在 to 为合约的时候调用IERC721Receiver-onERC721Received, 以防 tokenId 被不小心转入黑洞。
function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory data) private {
if (to.code.length > 0) {
try IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data) returns (bytes4 retval) {
if (retval != IERC721Receiver.onERC721Received.selector) {
revert ERC721InvalidReceiver(to);
}
} catch (bytes memory reason) {
if (reason.length == 0) {
revert ERC721InvalidReceiver(to);
} else {
/// @solidity memory-safe-assembly
assembly {
revert(add(32, reason), mload(reason))
}
}
}
}
}

/**
* 实现IERC721Metadata的tokenURI函数,查询metadata。
*/
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
require(_owners[tokenId] != address(0), "Token Not Exist");

string memory baseURI = _baseURI();
return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
}

/**
* 计算{tokenURI}的BaseURI,tokenURI就是把baseURI和tokenId拼接在一起,需要开发重写。
* BAYC的baseURI为ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/
*/
function _baseURI() internal view virtual returns (string memory) {
return "";
}
}

写一个免费铸造的APE

我们来利用ERC721来写一个免费铸造的WTF APE,总量设置为10000,只需要重写一下mint()baseURI()函数即可。由于baseURI()设置的和BAYC一样,元数据会直接获取无聊猿的,类似RRBAYC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// SPDX-License-Identifier: MIT
// by 0xAA
pragma solidity ^0.8.21;

import "./ERC721.sol";

contract WTFApe is ERC721{
uint public MAX_APES = 10000; // 总量

// 构造函数
constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_){
}

//BAYC的baseURI为ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/
function _baseURI() internal pure override returns (string memory) {
return "ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/";
}

// 铸造函数
function mint(address to, uint tokenId) external {
require(tokenId >= 0 && tokenId < MAX_APES, "tokenId out of range");
_mint(to, tokenId);
}
}

发行ERC721NFT

有了ERC721标准后,在ETH链上发行NFT变得非常简单。现在,我们发行属于我们的NFT。

Remix上编译好ERC721合约和WTFApe合约(按照顺序),在部署栏点击下按钮,输入构造函数的参数,name_symbol_都设为WTF,然后点击transact键进行部署。

NFT信息如何重点
部署合约

这样,我们就创建好了WTFNFT。我们需要运行mint()函数来给自己铸造一些代币。在mint函数那一栏点开右侧的下按钮输入账户地址,和tokenid,并点击mint按钮,为自己铸造0WTFNFT。

可以点开右侧的Debug按钮,具体查看下面的logs。

里面包含四个关键信息:

  • 事件Transfer
  • 铸造地址0x0000000000000000000000000000000000000000
  • 接收地址0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
  • tokenid0

铸造NFT

我们利用balanceOf()函数来查询账户余额。输入我们当前的账户,可以看到有一个NFT,铸造成功。

账户信息如图左侧,右侧标注为函数执行的具体信息。

查询NFT详情

我们也可以利用ownerOf()函数来查询NFT属于哪个账户。输入tokenid,可以我们的地址,查询无误。

tokenid查询拥有者详情

ERC165与ERC721详解

上面说到,为了防止NFT被转到一个没有能力操作NFT的合约中去,目标必须正确实现ERC721TokenReceiver接口:

1
2
3
interface ERC721TokenReceiver {
function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4);
}

拓展到编程语言的世界中去,无论是Java的interface,还是Rust的Trait(当然solidity中和trait更像的是library),只要是和接口沾边的,都在透露着一种这样的意味:接口是某些行为的集合(在solidity中更甚,接口完全等价于函数选择器的集合),某个类型只要实现了某个接口,就表明该类型拥有这样的一种功能。因此,只要某个contract类型实现了上述的ERC721TokenReceiver接口(更具体而言就是实现了onERC721Received这个函数),该contract类型就对外表明了自己拥有管理NFT的能力。当然操作NFT的逻辑被实现在该合约其他的函数中。
ERC721标准在执行safeTransferFrom的时候会检查目标合约是否实现了onERC721Received函数,这是一种利用ERC165思想进行的操作。
那究竟什么是ERC165呢?
ERC165是一种对外表明自己实现了哪些接口的技术标准。就像上面所说的,实现了一个接口就表明合约拥有种特殊能力。有一些合约与其他合约交互时,期望目标合约拥有某些功能,那么合约之间就能够通过ERC165标准对对方进行查询以检查对方是否拥有相应的能力。
以ERC721合约为例,当外部对某个合约进行检查其是否是ERC721时,怎么做? 。按照这个说法,检查步骤应该是首先检查该合约是否实现了ERC165, 再检查该合约实现的其他特定接口。此时该特定接口是IERC721. IERC721的是ERC721的基本接口(为什么说基本,是因为还有其他的诸如ERC721Metadata ERC721Enumerable 这样的拓展):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/// 注意这个**0x80ac58cd**
/// **⚠⚠⚠ Note: the ERC-165 identifier for this interface is 0x80ac58cd. ⚠⚠⚠**
interface ERC721 /* is ERC165 */ {
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);

function balanceOf(address _owner) external view returns (uint256);

function ownerOf(uint256 _tokenId) external view returns (address);

function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;

function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

function approve(address _approved, uint256 _tokenId) external payable;

function setApprovalForAll(address _operator, bool _approved) external;

function getApproved(uint256 _tokenId) external view returns (address);

function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}

0x80ac58cd=
bytes4(keccak256(ERC721.Transfer.selector) ^ keccak256(ERC721.Approval.selector) ^ ··· ^keccak256(ERC721.isApprovedForAll.selector)),这是ERC165规定的计算方式。

那么,类似的,能够计算出ERC165本身的接口(它的接口里只有一个
function supportsInterface(bytes4 interfaceID) external view returns (bool); 函数,对其进行bytes4(keccak256(supportsInterface.selector)) 得到0x01ffc9a7。此外,ERC721还定义了一些拓展接口,比如ERC721Metadata ,长这样:

1
2
3
4
5
6
///  Note: the ERC-165 identifier for this interface is 0x5b5e139f.
interface ERC721Metadata /* is ERC721 */ {
function name() external view returns (string _name);
function symbol() external view returns (string _symbol);
function tokenURI(uint256 _tokenId) external view returns (string); // 这个很重要,前端展示的小图片的链接都是这个函数返回的
}

这个0x5b5e139f 的计算就是:

1
IERC721Metadata.name.selector ^ IERC721Metadata.symbol.selector ^ IERC721Metadata.tokenURI.selector

solmate实现的ERC721.sol是怎么完成这些ERC165要求的特性的呢?

1
2
3
4
5
6
function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
return
interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165
interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721
interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata
}

没错就这么简单。当外界按照link1 的步骤去做检查的时候,如果外界想检查这个合约是否实现了165,好说,就是supportsInterface函数在入参是0x01ffc9a7时必须返回true,在入参是0xffffffff时,返回值必须是false。上述实现完美达成要求。

当外界想检查这个合约是否是ERC721的时候,好说,入参是0x80ac58cd 的时候表明外界想做这个检查。返回true。

当外界想检查这个合约是否实现ERC721的拓展ERC721Metadata接口时,入参是0x5b5e139f。好说,返回了true。

并且由于该函数是virtual的。因此该合约的使用者可以继承该合约,然后继续实现ERC721Enumerable 接口。实现完里面的什么totalSupply 啊之类的函数之后,把继承的supportsInterface重实现为

1
2
3
4
5
6
7
function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
return
interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165
interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721
interfaceId == 0x5b5e139f || // ERC165 Interface ID for ERC721Metadata
interfaceId == 0x780e9d63; // ERC165 Interface ID for ERC721Enumerable
}

优雅,简洁,可拓展性拉满。

总结

这一讲,我介绍了ERC721标准、接口及其实现,并在合约代码进行了中文注释。并且我们利用ERC721做了一个免费铸造的WTF APE NFT,元数据直接调用于BAYCERC721标准仍在不断发展中,目前比较流行的版本为ERC721Enumerable(提高NFT可访问性)和ERC721A(节约铸造gas)。