什么是非同质化代币(NFT)?
在了解 ERC721 规范之前,我们需要弄懂 同质化代币 和 非同质化代币 的含义。
- 同质化代币(FT):可以简单类比游戏中的金币 or 点券,我拥有的 100点券和你拥有的 100 点券 本质上并没有什么区别,都可以购买皮肤。
- 非同质化代币(NFT):唯一标识具体某一个人或者物品,例如:我拥有齐白石的《群虾图》真品,全球仅此一份。
什么是 ERC721 规范?
ERC721 规范是为了实现 非同质化代币(NFT) 而创建的标准;换言之,实现了 ERC721 规范的代币,每一份都是独一无二的存在。
1. 合约结构
我们已经了解了 ERC721 规范的意义,那么我们就来看看它的结构~
如上图所示,我们可以清晰的看到 ERC721 的继承关系;从上往下来分析一下各个合约接口/抽象合约的功能:
- IERC165:要求合约提供其实现了哪些接口;在与合约进行交互的时候可以先调用此接口进行查询,了解合约具体实现了哪些接口。
- ERC165:抽象合约,官方对 IERC165 提供的默认实现。
- IERC721:发行 NFT 的标准合约规范;定义了 NFT 合约的各类行为接口,诸如:转移、授权…。
- ERC721:官方对 IERC721 提供的默认实现。
- IERC721Metadata:定义合约的元数据信息;诸如:合约名字、标志、以及每个代币的 tokenURI。
- Context:抽象合约,上下文;主要对
msg 对象做一些封装;例如: msg.Sender 代表当前调用合约的用户。 - IERC721Enumerable:提高合约的可访问性(非必须实现,但是一般我们的合约都会实现以提高可访问性);主要提供:当前发行
NFT 总量、通过索引获取用户所拥有的TokenID… - ERC721Enumerable: 抽象合约,官方对 IERC721Enumerable 提供的默认实现。
- Ownable:主要提供合约
owner 的校验以及 owner 的转移;例如合约中的有些函数必须得有owner 进行调用。 - IERC721Receiver:当进行 NFT 转移的时候,如果接收的地址是一个
合约地址 的话,那么接收的合约必须实现该接口。 - Address:
Address 是 solidity 特有的一种类型,项目中的 libarary Address 主要是对该类型做了一些简易工具的封装。 - Strings:同上,工具类的存在,提供将
uint256 转换成 string 的能力。
接下来我们就来看看合约的源码~
1.1 IERC165 接口合约
IERC165 是一个合约标准,这个标准要求合约提供其实现了哪些接口,这样在与合约进行交互的时候可以先调用此接口进行查询。
pragma solidity ^0.8.0;
interface IERC165 {
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
1.2 ERC165 抽象合约
ERC165 是官方为 IERC165 提供的默认实现。
pragma solidity ^0.8.0;
import "./IERC165.sol";
abstract contract ERC165 is IERC165 {
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IERC165).interfaceId;
}
}
1.3 IERC721 接口合约
pragma solidity ^0.8.0;
import "./utils/introspection/IERC165.sol";
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) external;
function transferFrom(address from, address to, uint256 tokenId) external;
function approve(address to, uint256 tokenId) external;
function getApproved(uint256 tokenId) external view returns (address operator);
function setApprovalForAll(address operator, bool _approved) external;
function isApprovedForAll(address owner, address operator) external view returns (bool);
function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
}
1.4 ERC721 实现合约
官方对 IERC721 接口合约提供的默认实现,我们编写的 NFT 合约一般继承于该合约;在其基础上编写自己的合约逻辑。官方实现主要提供了如下能力:
- 基础的查询能力,诸如代币的owner、合约的名字、标志…
- 代币的授权/回收、转移
- 代币的生成/销毁(注意:销毁函数无检查所属权,需要我们自行检查)
pragma solidity ^0.8.0;
import "./IERC721.sol";
import "./IERC721Receiver.sol";
import "./extensions/IERC721Metadata.sol";
import "../../utils/Address.sol";
import "../../utils/Context.sol";
import "../../utils/Strings.sol";
import "../../utils/introspection/ERC165.sol";
contract ERC721 is Context, ERC165, IERC721, IERC721Metadata {
using Address for address;
using Strings for uint256;
string private _name;
string private _symbol;
mapping(uint256 => address) private _owners;
mapping(address => uint256) private _balances;
mapping(uint256 => address) private _tokenApprovals;
mapping(address => mapping(address => bool)) private _operatorApprovals;
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
return
interfaceId == type(IERC721).interfaceId ||
interfaceId == type(IERC721Metadata).interfaceId ||
super.supportsInterface(interfaceId);
}
function balanceOf(address owner) public view virtual override returns (uint256) {
require(owner != address(0), "ERC721: balance query for the zero address");
return _balances[owner];
}
function ownerOf(uint256 tokenId) public view virtual override returns (address) {
address owner = _owners[tokenId];
require(owner != address(0), "ERC721: owner query for nonexistent token");
return owner;
}
function name() public view virtual override returns (string memory) {
return _name;
}
function symbol() public view virtual override returns (string memory) {
return _symbol;
}
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
string memory baseURI = _baseURI();
return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
}
function _baseURI() internal view virtual returns (string memory) {
return "";
}
function approve(address to, uint256 tokenId) public virtual override {
address owner = ERC721.ownerOf(tokenId);
require(to != owner, "ERC721: approval to current owner");
require(
_msgSender() == owner || isApprovedForAll(owner, _msgSender()),
"ERC721: approve caller is not owner nor approved for all"
);
_approve(to, tokenId);
}
function _approve(address to, uint256 tokenId) internal virtual {
_tokenApprovals[tokenId] = to;
emit Approval(ERC721.ownerOf(tokenId), to, tokenId);
}
function getApproved(uint256 tokenId) public view virtual override returns (address) {
require(_exists(tokenId), "ERC721: approved query for nonexistent token");
return _tokenApprovals[tokenId];
}
function setApprovalForAll(address operator, bool approved) public virtual override {
_setApprovalForAll(_msgSender(), operator, approved);
}
function _setApprovalForAll(
address owner,
address operator,
bool approved
) internal virtual {
require(owner != operator, "ERC721: approve to caller");
_operatorApprovals[owner][operator] = approved;
emit ApprovalForAll(owner, operator, approved);
}
function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
return _operatorApprovals[owner][operator];
}
function transferFrom(
address from,
address to,
uint256 tokenId
) public virtual override {
require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");
_transfer(from, to, tokenId);
}
function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) {
require(_exists(tokenId), "ERC721: operator query for nonexistent token");
address owner = ERC721.ownerOf(tokenId);
return (spender == owner || getApproved(tokenId) == spender || isApprovedForAll(owner, spender));
}
function _transfer(
address from,
address to,
uint256 tokenId
) internal virtual {
require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner");
require(to != address(0), "ERC721: transfer to the zero address");
_beforeTokenTransfer(from, to, tokenId);
_approve(address(0), tokenId);
_balances[from] -= 1;
_balances[to] += 1;
_owners[tokenId] = to;
emit Transfer(from, to, tokenId);
_afterTokenTransfer(from, to, tokenId);
}
function safeTransferFrom(
address from,
address to,
uint256 tokenId
) public virtual override {
safeTransferFrom(from, to, tokenId, "");
}
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes memory _data
) public virtual override {
require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");
_safeTransfer(from, to, tokenId, _data);
}
function _safeTransfer(
address from,
address to,
uint256 tokenId,
bytes memory _data
) internal virtual {
_transfer(from, to, tokenId);
require(_checkOnERC721Received(from, to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer");
}
function _exists(uint256 tokenId) internal view virtual returns (bool) {
return _owners[tokenId] != address(0);
}
function _safeMint(address to, uint256 tokenId) internal virtual {
_safeMint(to, tokenId, "");
}
function _safeMint(
address to,
uint256 tokenId,
bytes memory _data
) internal virtual {
_mint(to, tokenId);
require(
_checkOnERC721Received(address(0), to, tokenId, _data),
"ERC721: transfer to non ERC721Receiver implementer"
);
}
function _mint(address to, uint256 tokenId) internal virtual {
require(to != address(0), "ERC721: mint to the zero address");
require(!_exists(tokenId), "ERC721: token already minted");
_beforeTokenTransfer(address(0), to, tokenId);
_balances[to] += 1;
_owners[tokenId] = to;
emit Transfer(address(0), to, tokenId);
_afterTokenTransfer(address(0), to, tokenId);
}
function _burn(uint256 tokenId) internal virtual {
address owner = ERC721.ownerOf(tokenId);
_beforeTokenTransfer(owner, address(0), tokenId);
_approve(address(0), tokenId);
_balances[owner] -= 1;
delete _owners[tokenId];
emit Transfer(owner, address(0), tokenId);
_afterTokenTransfer(owner, address(0), tokenId);
}
function _checkOnERC721Received(
address from,
address to,
uint256 tokenId,
bytes memory _data
) private returns (bool) {
if (to.isContract()) {
try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval) {
return retval == IERC721Receiver.onERC721Received.selector;
} catch (bytes memory reason) {
if (reason.length == 0) {
revert("ERC721: transfer to non ERC721Receiver implementer");
} else {
assembly {
revert(add(32, reason), mload(reason))
}
}
}
} else {
return true;
}
}
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId
) internal virtual {}
function _afterTokenTransfer(
address from,
address to,
uint256 tokenId
) internal virtual {}
}
1.5 IERC721Metadata 接口合约
定义合约的元数据信息;诸如:合约名字、标志、以及每个代币的 tokenURI。那么 tokenURI 是什么呢?
TokenURI 是 ERC721 规范提出用以描述 NFT 资源;我们可以通过 NFT 的 TokenURI 获取到该 NFT 对应的描述资源。假如有一款宠物养成类游戏,在对宠物数据进行上链的时候并不会完整的将整个宠物信息进行上链。一方面是成本问题,链上存储的数据越多,所需要的成本就越高;另外一方面则是诸如宠物等级等信息会随着宠物的成长而不断的变化,如果上链则会频繁的修改该数据,也不合适。因此 TokenURI 的存在就是为了解决这类问题,它是对 NFT 资源的扩充元描述。
那比如退一步说:我的上链元数据不存在更新的场景并且数据量不大;那还存在维护 TokenURI 的必要吗?其实上,将所有的元数据存储在链上是正确的方式,但是目前很多 NFT 市场(例如OpenSea)不知道如何读取链上元数据(或者说是不支持);所以目前来说,使用 TokenURI 的链下元数据来可视化我们的 NFT,同时拥有所有的链上元数据是最理想的,这样你的代币就可以互相交互。
那如何维护 TokenURI 呢?比较常见的方式有 2种。
中心化存储,我们可以采用中心化存储系统,将 tokenURI 维护到我们自身的系统中;优点是操作简单,缺点也比较明显;当中心化系统故障后则无法响应。
IPFS 是一种点对点的超媒体协议,旨在使网络更快、更安全、更开放。它允许任何人上传一个文件,并且该文件是经过哈希校验的,所以如果文件发生改变,它的哈希值也会改变。这对于存储图片来说是非常理想的,因为这意味着每次图片更新时,链上的哈希/tokenURI也要改变,同时这意味着我们可以拥有元数据的历史记录。将图像添加到 IPFS 上也非常简单,而且不需要运行服务器。
pragma solidity ^0.8.0;
import "../IERC721.sol";
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);
}
1.6 IERC721Enumerable 接口合约
提高合约的可访问性(非必须实现,但是一般我们的合约都会实现以提高可访问性);主要提供:当前发行 NFT 总量、通过索引获取用户所拥有的TokenID…
pragma solidity ^0.8.0;
import "../IERC721.sol";
interface IERC721Enumerable is IERC721 {
function totalSupply() external view returns (uint256);
function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256 tokenId);
function tokenByIndex(uint256 index) external view returns (uint256);
}
1.7 ERC721Enumerable 抽象合约
官方对 IERC721Enumerable 提供的默认实现。代码比较简单,不做过度解析,自行阅读~
pragma solidity ^0.8.0;
import "../ERC721.sol";
import "./IERC721Enumerable.sol";
abstract contract ERC721Enumerable is ERC721, IERC721Enumerable {
mapping(address => mapping(uint256 => uint256)) private _ownedTokens;
mapping(uint256 => uint256) private _ownedTokensIndex;
uint256[] private _allTokens;
mapping(uint256 => uint256) private _allTokensIndex;
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC721) returns (bool) {
return interfaceId == type(IERC721Enumerable).interfaceId || super.supportsInterface(interfaceId);
}
function tokenOfOwnerByIndex(address owner, uint256 index) public view virtual override returns (uint256) {
require(index < ERC721.balanceOf(owner), "ERC721Enumerable: owner index out of bounds");
return _ownedTokens[owner][index];
}
function totalSupply() public view virtual override returns (uint256) {
return _allTokens.length;
}
function tokenByIndex(uint256 index) public view virtual override returns (uint256) {
require(index < ERC721Enumerable.totalSupply(), "ERC721Enumerable: global index out of bounds");
return _allTokens[index];
}
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId
) internal virtual override {
super._beforeTokenTransfer(from, to, tokenId);
if (from == address(0)) {
_addTokenToAllTokensEnumeration(tokenId);
} else if (from != to) {
_removeTokenFromOwnerEnumeration(from, tokenId);
}
if (to == address(0)) {
_removeTokenFromAllTokensEnumeration(tokenId);
} else if (to != from) {
_addTokenToOwnerEnumeration(to, tokenId);
}
}
function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private {
uint256 length = ERC721.balanceOf(to);
_ownedTokens[to][length] = tokenId;
_ownedTokensIndex[tokenId] = length;
}
function _addTokenToAllTokensEnumeration(uint256 tokenId) private {
_allTokensIndex[tokenId] = _allTokens.length;
_allTokens.push(tokenId);
}
function _removeTokenFromOwnerEnumeration(address from, uint256 tokenId) private {
uint256 lastTokenIndex = ERC721.balanceOf(from) - 1;
uint256 tokenIndex = _ownedTokensIndex[tokenId];
if (tokenIndex != lastTokenIndex) {
uint256 lastTokenId = _ownedTokens[from][lastTokenIndex];
_ownedTokens[from][tokenIndex] = lastTokenId;
_ownedTokensIndex[lastTokenId] = tokenIndex;
}
delete _ownedTokensIndex[tokenId];
delete _ownedTokens[from][lastTokenIndex];
}
function _removeTokenFromAllTokensEnumeration(uint256 tokenId) private {
uint256 lastTokenIndex = _allTokens.length - 1;
uint256 tokenIndex = _allTokensIndex[tokenId];
uint256 lastTokenId = _allTokens[lastTokenIndex];
_allTokens[tokenIndex] = lastTokenId;
_allTokensIndex[lastTokenId] = tokenIndex;
delete _allTokensIndex[tokenId];
_allTokens.pop();
}
}
1.8 IERC721Receiver 接口合约
上面讲到在做 NFT 的转移时,如果 _to 是一个合约的话,那么其必须实现 IERC721Receiver 接口才可以接收。
pragma solidity ^0.8.0;
interface IERC721Receiver {
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4);
}
2. 工具合约
在合约结构中,还有一些诸如 Context、Ownerable 等工具合约,代码都比较简单,这里就不做过多的解析。
3. 总结
本文主要对 ERC721 规范进行了解析,有了 OpenZeeppline 提供的默认实现;我们在编写自己的合约时其实所需要做的事情并不多。有几个比较核心的点:
- 确认上链数据。
- TokenURI 的维护。
- 合理的 Mint 和 Burn:TokenID 由于要求唯一性,可采用自增的方式来维护;内部提供的 _burn 函数并没有做权限校验,需要我们外层保证。
- 事件的处理,事件是连接去中心化系统和中心化系统的桥梁;我们对链上的事件进行监听,捕获到即使不做任何操作也可以做日志流水记录到 DB 中。
后台开发的童鞋可能会有疑问(一开始我也有):在诸如 _transfer 函数中做了多个操作,并且没有做事务保证;是否会存在问题呢?
链上在执行的时候是串行化的,无并发问题;并且在执行函数时,如果发生了错误;之前所有的执行都会回滚。因而我们可以不用考虑并发和事务等问题。
另:本文是从技术角度进行分析,我们应严格遵守国家法律法规;不做任何违法犯罪的事情!
笔芯~
|