WTF Solidity极简入门: 34. ERC721
我最近在重新学 Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。
推特:@0xAA_Science |@WTFAcademy_
社区:Discord |微信群 |官网 wtf.academy
所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity
BTC
和ETH
这类代币都属于同质化代币,矿工挖出的第1
枚BTC
与第10000
枚BTC
并没有不同,是等价的。但世界中很多物品是不同质的,其中包括房产、古董、虚拟艺术品等等,这类物品无法用同质化代币抽象。因此,以太坊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就是检查一个智能合约是不是支持了ERC721
,ERC1155
的接口。
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; }
当查询的是IERC721
或IERC165
的接口id时,返回true
;反之返回false
。
IERC721
IERC721
是ERC721
标准的接口合约,规定了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个事件,其中Transfer
和Approval
事件在ERC20
中也有。
Transfer
事件:在转账时被释放,记录代币的发出地址from
,接收地址to
和tokenid
。
Approval
事件:在授权时释放,记录授权地址owner
,被授权地址approved
和tokenid
。
ApprovalForAll
事件:在批量授权时释放,记录批量授权的发出地址owner
,被授权地址operator
和授权与否的approved
。
IERC721函数
balanceOf
:返回某地址的NFT持有量balance
。
ownerOf
:返回某tokenId
的主人owner
。
transferFrom
:普通转账,参数为转出地址from
,接收地址to
和tokenId
。
safeTransferFrom
:安全转账(如果接收方是合约地址,会要求实现ERC721Receiver
接口)。参数为转出地址from
,接收地址to
和tokenId
。
approve
:授权另一个地址使用你的NFT。参数为被授权地址approve
和tokenId
。
getApproved
:查询tokenId
被批准给了哪个地址。
setApprovalForAll
:将自己持有的该系列NFT批量授权给某个地址operator
。
isApprovedForAll
:查询某地址的NFT是否批量授权给了另一个operator
地址。
safeTransferFrom
:安全转账的重载函数,参数里面包含了data
。
IERC721Receiver
如果一个合约没有实现ERC721
的相关函数,转入的NFT
就进了黑洞,永远转不出来了。为了防止误转账,ERC721
实现了safeTransferFrom()
安全转账函数,目标合约必须实现了IERC721Receiver
接口才能接收ERC721
代币,不然会revert
。IERC721Receiver
接口只包含一个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()
函数(返回onERC721Received
的selector
):
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
是ERC721
的拓展接口,实现了3个查询metadata
元数据的常用函数:
name()
:返回代币名称。
symbol()
:返回代币代号。
tokenURI()
:通过tokenId
查询metadata
的链接url
,ERC721
特有的函数。
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
主合约实现了IERC721
,IERC165
和IERC721Metadata
定义的所有功能,包含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); } }
发行ERC721
NFT
有了ERC721
标准后,在ETH
链上发行NFT变得非常简单。现在,我们发行属于我们的NFT。
在Remix
上编译好ERC721
合约和WTFApe
合约(按照顺序),在部署栏点击下按钮,输入构造函数的参数,name_
和symbol_
都设为WTF
,然后点击transact
键进行部署。
这样,我们就创建好了WTF
NFT。我们需要运行mint()
函数来给自己铸造一些代币。在mint
函数那一栏点开右侧的下按钮输入账户地址,和tokenid,并点击mint
按钮,为自己铸造0
号WTF
NFT。
可以点开右侧的Debug按钮,具体查看下面的logs。
里面包含四个关键信息:
事件Transfer
铸造地址0x0000000000000000000000000000000000000000
接收地址0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
tokenid0
我们利用balanceOf()
函数来查询账户余额。输入我们当前的账户,可以看到有一个NFT
,铸造成功。
账户信息如图左侧,右侧标注为函数执行的具体信息。
我们也可以利用ownerOf()
函数来查询NFT属于哪个账户。输入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,元数据直接调用于BAYC
。ERC721
标准仍在不断发展中,目前比较流行的版本为ERC721Enumerable
(提高NFT可访问性)和ERC721A
(节约铸造gas
)。