本文作者:bixia1994[1]
在一个典型的 NFT 中,通常会利用 OZ 的 EIP721 模板来做如下实现:
function mintNFT(uint256 numberOfNfts) public payable {
//检查totalsupply不能超过
require(totalSupply() < MAX_NFT_SUPPLY);
require(numberOfNfts.add(totalSupply()) < MAX_NFT_SUPPLY);
//检查numberOfNFT在(0,20]
require(numberOfNfts > 0 && numberOfNfts <=20);
//检查价格*numberOfNFT==msg.value
require(numberOfNfts.mul(getNFTPrice()) == msg.value);
//执行for循环,每个循环里都触发mint一次,写入一个全局变量
for (uint i = 0; i < numberOfNfts; i++) {
uint index = totalSupply();
_safeMint(msg.sender, index);
}
}
其中,_safeMint 是 OZ 中提供的 mint API 函数,其具体调用如下:
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);
}
从上述的实现过程中可以看到,对于普通的 NFT mint 过程,其算法复杂度是 O(N),即用户需要 mint N 个 NFT,则需要循环调用 N 次单独的 mint 方法。
其最核心的部分在于:OZ 的实现中,在 mint 方法内部,维护了两个全局的 mapping。
分别是用于记录用户拥有的 NFT 数量的 balance 和记录 tokenID 到用户映射的 owners。不管是 mint 还是 transfer,其内部都需要去更新这两个全局变量。单就 mint 来讲,mint 1 个 NFT 就需要进行至少 2 次 SSTORE。而 mint N 个 NFT 则需要进行至少 2N 次 SSTORE。
从 Openzeppelin 的实现缺点来看,其主要缺点在于没有提供批量 Mint 的 API,使得用户批量 Mint 时,其算法复杂度达到 O(N).故 ERC721A 提出了一种批量 Mint 的 API,使得其算法复杂度降为 O(1).
最简单的想法莫过于直接修改_mint 函数,将批量 mint 的数量也作为参数传入,然后在_mint 函数里面修改 balance 和 owners 两个全局变量。由于是批量 mint,与 OZ 的单独 mint 方式不同的是,其需要在 mint 函数内部维护一个全局递增的 tokenID。另一个需要注意的事情是:根据 EIP721 规范,当任何 NFT 的 ownership 发生变化时,都需要发出一个 Transfer 事件。故这里也需要通过 For 循环的方式来批量发出 Transfer 事件。
function _mint(address to, uint256 quantity) internal virtual {
...//checks
uint256 tokenId = _currIndex;
_balances[to] += quantity;
_owners[tokenId] = to;
···//emit Event
for (uint256 i = 0; i < quantity; i++) {
emit Transfer(address(0),to,tokenId);
tokenId++;
}
//update index
_currIndex = tokenId;
}
对该简单想法的分析:
20220211151047.png
该最简单想法的问题:
20220211151107.png
function _exists(uint256 tokenId) internal view returns (bool) {
tokenId < _currentIndex;
}
function ownershipOf(uint256 tokenId) internal view returns (TokenOwnership memory) {
//check exists
require(_exists(tokenId),"OwnerQueryForNonexistentToken");
//遍历 递减
for (uint256 curr = tokenId;curr >= 0;curr--) {
address owner = _owners[curr];
if (owner != address(0)) {
return owner;
}
}
revert("Ownership Error");
}
function ownerOf(uint256 _tokenId) external view returns (address) {
return ownershipOf(_tokenId).addr;
}
具体实现逻辑如下:
function _transfer(address from,address to,uint256 tokenId) private {
//check ownership
TokenOwnership memory prevOwnership = ownershipOf(tokenId);
require(from == prevOwnership.addr);
//update from&to
balance[from] -= 1;
balance[to] += 1;
_owners[tokenId] = to;
uint256 nextTokenId = tokenId + 1;
if (_owners[nextTokenId] == address(0) && _exists(nextTokenId)) {
_owners[nextTokenId] = from;
}
emit Transfer(from,to,tokenId);
}
20220211151200.png
function tokenOfOwnerByIndex(address owner, uint256 index) public view override returns (uint256) {
//check index <= balance
require(index <= balanceOf(owner),"OwnerIndexOutOfBounds");
uint256 max = totalSupply();
uint256 tokenIdsIndex;
uint256 curr;
for (uint256 i = 0; i < max; i++) {
address alice = _ownes[i];
if (owner != address(0)) {
curr = alice;
}
if (curr == owner) {
if (index == tokenIdsIndex) return i;
tokenIdsIndex++;
}
}
revert("error");
}
从上面的分析可以看出,ERC721A 算法相较于 Openzeppelin 的 EIP721 实现有比较大的突破,但是也有自身的局限性。还有部分我暂未理解清楚:
局限性:
ERC721A 针对的 NFT 批量铸造过程,需要 tokenId 从 0 开始连续单调递增,如果 tokenId 是不连续的正整数,比如用 timestamp 来作为 tokenId,该算法其实就会失效。
没看懂的部分:
为什么需要一个 timestamp?
struct TokenOwnership {
address addr;
uint64 startTimestamp;
}
这个 startTimestamp 有什么用?
[1]
bixia1994: https://learnblockchain.cn/people/3295
[2]
Openzeppelin的EIP721实现: https://learnblockchain.cn/article/3041
[3]
Azuki的EIP721A实现: https://www.azuki.com/erc721a