ERC 721 协议解析

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 账户。注意,这里 fromto 都不能是零地址,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)

owneroperator 为 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"title": "Asset Metadata",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Identifies the asset to which this NFT represents"
},
"description": {
"type": "string",
"description": "Describes the asset to which this NFT represents"
},
"image": {
"type": "string",
"description": "A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive."
}
}
}

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 方法拼接 baseURItokenURI 并返回。


5 ERC721URIStorage

实现了上述所有接口。同时,是 tokenIdtokenURI 映射关系的实际持有者。

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

© 2024 YueGS