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