
ERC 721 是非同质化通证(token) NFT 的实现标准。
非同质化,意味着它是唯一的,不可替代的,比如区块链应用里的一块土地/一首歌/一张图/一段话,甚至你也可以将现实世界里的物理资产 token 化,它还可以代表一辆车/一间房。
相较于 ERC 20 这种同质化通证的标准,ERC 721 协议必然更加复杂,通常会由若干个合约组成。分开来看,每个合约都定义一定的职责或者功能,组合起来,共同完成 NFT 的实现和管理。
接下来,我们来讨论其在 OpenZeppelin 中的实现。
@openzeppelin 版本 4.5.0
核心实现接口包括:
- IERC721: 定义了 Token 层面的通用属性,包括持有查询/余额查询/转账/转账授权等。
- IERC165: 判断合约是否实现了某个接口。
- ERC721Metadata: Metadata 层面的通用属性,包括token名称/存储地址(metadata URI)等。
- ERC721: 实现上述三个接口,实现 ERC721 协议。
- ERC721URIStorage: 实现上述所有接口,同时,是 tokenId 到 tokenURI 映射关系的实际持有者。
这些实现中,更多的,是通过 tokenId 将各类关系关联起来。
- tokenId 和 OwnerAddress 组成的 map,解决是谁的问题。可以查询指定 tokenId 的 NFT 是否属于某个账户。
- tokenId 和 tokenURI 组成的 map 中,主要解决 NFT 在哪的问题。可以通过 tokenId 获取 NFT 的信息(创建者/metadata地址等);
- tokenId 和 ApprovedAdress 组成的 map 中,解决谁可以被授权转账的问题。可以查询指定 tokenId 的 NFT 是否授权给某个账户进行操作;

下面,我们来看具体的接口定义和实现。
1 IERC721
符合 ERC 721 标准的合约必须实现的接口。
1.1 balanceOf(address owner) → uint256 balance
获取 owner 账户所拥有的 NFT 的数量。
1.2 ownerOf(uint256 tokenId) → address owner
通过 tokenId 查询持有特定 NFT 的账户。
1.3 transferFrom(address from, address to, uint256 tokenId)
将 from 账户上指定 tokenId 的 NFT 转给 to 账户。注意,这里 from 和 to 都不能是零地址,tokenId 必须属于 from,如果该方法的调用者不是 from 账户,它必须是通过 approve 或者 setApprovalForAll 被授权过允许转移 NFT 的账户。
1.4 safeTransferFrom(address from, address to, uint256 tokenId)
与 transferFrom 相同,但是需要注意的是,
在本函数中,如果 to 是合约,则其必须实现 IERC721Receiver.onERC721Received ,在这种情况下,onERC721Received 将被执行,且需要返回 IERC721Receiver.onERC721Received.selector(即bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))),交易才能执行成功。
1.5 safeTransferFrom(from, to, tokenId, data)
上面的函数实际上将调用本函数执行,只不过其 data 字段为空字符串。如果 to 是合约,数据负载 data 将通过 to 合约的 onERC721Received 方法由 from 发送到 to 合约。转账完成之后,to 合约的onERC721Received 被调用,这就是 safe 前缀的含义。
具体代码可以 查看这里。
1.6 approve(to, tokenId)
由 NFT 的实际持有者授权 to 账户可以调用 transferFrom 或者 safeTransferFrom,代替持有者完成 tokenId 的 NFT 的交易。
1.7 getApproved(tokenId)
通过 tokenId 查询授权操作账户。所有授权操作账户和 tokenId 的关系,在 OpenZeppelin 的实现中,记录在 _tokenApprovals map 中。
1.8 setApprovalForAll(operator, _approved)
由 NFT 的实际持有者授权 operator 账户是否可以通过调用 transferFrom 或者 safeTransferFrom,代替持有者完成其账户下所有 NFT 的转账。授权关系记录在 _operatorApprovals map 中。
1.9 isApprovedForAll(owner, operator)
以 owner 和 operator 为 key,查询 _operatorApprovals map 中存储的数据是否为 true,即持有者是否授权 operator 可以操作自己所有的 NFT。是否授权所有 token 的交易权,是存储在 _operatorApprovals 这个 map 中,该 map 的具体形式为 mapping(address => mapping(address => bool)),第一个地址是 owner,第二个地址是 operator,变量 bool是是否全部授权。
2 IERC165
该协议标准中,判断合约是否实现了某个接口。具体实现
2.1 supportsInterface(interfaceId)
外部可以通过调用该方法,确认该合约是否支持某些标准(或者说实现了某些接口),比如 ERC 721.
注意,该方法是 ERC 165 标准中的方法。
A bytes4 value containing the EIP-165 interface identifier of the given interface I. This identifier is defined as the XOR of all function selectors defined within the interface itself - excluding all inherited functions.
引用来源:type(I).interfaceId
3 IERC721Metadata
该接口主要定义 NFT 的属性信息。
3.1 name() → string
合约 token 的名字。
3.2 symbol() → string
合约 token 名字的简写。
3.3 tokenURI(uint256 tokenId) → string
通过指定的 tokenId 返回其 URI。如果 baseURI 不为空,则 tokenURI 应该为 abi.encodePacked(baseURI, tokenId.toString()),否则该方法将返回空字符串。注意,通常在其实现类中,会 override 该方法,当 baseURI 为空时,直接返回 tokenURI。这里,URI 通常指向了符合 ERC721 Metadata 的 Json 格式文件,如下所示:
1 | { |
4 ERC721
ERC721 同时实现了 IERC721/IERC165/IERC721Metadata 接口,也有一些内部方法的实现。
4.1 _exists(uint256 tokenId) → bool
通过 tokenId 查询 NFT 是否存在。tokenId 到 账户地址的映射关系存储在 _owners map 中。
4.2 _isApprovedOrOwner(address spender, uint256 tokenId) → bool
查询指定的 spender 账户是否被允许交易指定 tokenId 的 NFT. tokeId 到授权账户的映射关系存储在 _tokenApprovals map 中。
4.3 _mint(address to, uint256 tokenId)
本质上来讲,就是将 tokenId 和账户 to 通过 _owners(一个map) 关联起来,tokenId 为 key,to 为 value。在关联之前,此 tokenId 应为 _owners 中未曾有过的。确保 tokenId 的唯一性。同时,将账户 to 的持有量自增。
4.4 _safeMint(address to, uint256 tokenId, bytes _data)
该方法将调用上述 _mint 方法,但同时,所谓 safe,也将调用 _checkOnERC721Received 方法。在 _checkOnERC721Received 中,如果 to 是合约账户,则 to 必须实现了 IERC721Receiver 接口,其 onERC721Received 将被调用,data 作为参数通过 onERC721Received 传递给 to 合约。
4.5 _safeMint(address to, uint256 tokenId)
将调用 2.2.4 的方法,只不过 _data 是个缺省值,用空字符串表示。
4.6 _burn(uint256 tokenId)
销毁指定 tokenId 的 NFT。所谓销毁,将移除 tokenId 的授权操作关系,且在 _owners 账户关系中将 tokenId 指向零地址。’owner’ 持有量自减。
4.7 _burn(address owner, uint256 tokenId)
销毁指定 tokenId 的 NFT。该方法已经被弃用,推荐 2.2.6 中的方法。
4.8 _baseURI()
baseURI 主要用于参与 tokenURI 的计算。默认情况下,它是空字符串。默认情况下,tokenId 到 tokenURI 之间的映射关系存储在 _tokenURIs 中。如果 baseURI 为空,则方法 tokenURI() 将直接从 _tokenURIs 中取值;否则, tokenURI() 方法中,将通过 abi.encodePacked 方法拼接 baseURI 和 tokenURI 并返回。
5 ERC721URIStorage
实现了上述所有接口。同时,是 tokenId 和 tokenURI 映射关系的实际持有者。
5.1 tokenURI(uint256 tokenId)
在 4.8 中已经做了介绍。
5.2 _setTokenURI(uint256 tokenId, string _tokenURI)
通过 _tokenURIs 这个 map 来关联 tokenId 和 tokenURI.
5.3 _burn(uint256 tokenId)
如上 _burn() 方法,该类中,同时删除了 _tokenURIs 中所存储的对应的内容。
参考链接
ERC 721
ERC721
EIP-721: Non-Fungible Token Standard
openzeppelin-ERC721.sol
type(I).interfaceId
ERC721Metadata
Why doesn’t OpenZeppelin ERC721 contain setTokenURI?
Why doesn’t OpenZeppelin ERC721 contain setTokenURI?-Answer