ERC721A - 批量 mint gas 优化
最近在面试的过程中,有被问到这么一个问题:有了解过 ERC721A
吗,知道它和普通的 ERC721
有什么不同吗?当时答不出来,挺尴尬。。。
虽然后面还是顺利拿到了 offer
,但总不能下次也答不出来吧。
下面,咱就来详细学习一下 ERC721A
在合约里面做了什么优化
什么是 ERC721A
ERC721A
是 Azuki
项目方提出的一个新的 ERC721
的改进标准,主要用于针对一次性 mint
多个 NFT
的时候 Gas
费优化。其本质是基于 ERC721
进行的优化,如果是顺序 mint
的 NFT
,则只在首位存储归属权信息,通过节约链上存储空间从而达到节约 Gas
费的效果。
为什么需要 ERC721A
因为标准的 ERC721
合约在批量 mint NFT
的时候,gas
费较高,效率极低。ERC721A
主要是针对这个场景进行优化,原理见下面图示。
原理图示
鉴于 ERC721
合约的核心存储结构是 mapping
的结构,不保存 key
值,原理是利用 keccak256
哈希函数将 value
数据存放在一个很大的空间(可以看做不存在哈希碰撞) 实际上 mapping
并不是有序的,但为简洁说明,下图采用顺序排列来简化。
标准 ERC721 批量 mint
在标准的
ERC721
合约中(如 OpenZepplin
实现),NFT
信息的存储是通过一个 mapping
的结构来保存的。每次创建一个 NFT
,都必须在这个 mapping
结构中放入归属地址。
mapping(uint256 tokenId => address) private _owners;
ERC721A 的优化 mint
对于
ERC721
的优化,实际上就是将重复的地址只保存在连续的 tokenId
的首位,重复的地址实际上只会保存一份。这样整体的 mapping
结构占用的存储空间则会变小,起到 gas
优化的作用。
ERC721A 是怎么查找的
对于
ERC721A
这样的优化,很容易发现一个问题:在查找空位置的 tokenId
时,该怎么定位这个 tokenId
的归属权呢?
如图所示,定位某个 tokenId
的归属权可以通过算法来进行定位。
- 查找
tokenId =2
的NFT
,查到这个位置为空 - 往前回溯
tokenId = 1
的NFT
,这个位置依旧为空 - 往前回溯
tokenId = 0
的NFT
,这个位置归属于0x0001
- 可认为
tokenId = 2
的NFT
归属于0x0001
地址
ERC721A 转让 NFT 怎么办
对于
ERC721
这个优化,我们很容易又会发现一个问题,假如我们要转让一个空位置的 NFT
,不就打破这种空位置的连续性了吗,该如何处理呢?
例如在 tokenId = 1
的位置转让给 0x0004
,如果往 tokenId = 1 的位置写入 0x0004
的数据,显然 tokenId = 2
的位置按照查找规则会被认为是 0x0004
所拥有的,显然不对。
其实解决起来这个问题也很简单,如果转让的位置为空,则不仅将这个位置转让出去,如果这个位置的后面也有空数据,则主动将后面的空数据填充。例如:
tokenId = 1
的归属权从0x0001
转让给0x0004
,直接往token = 1
的位置写入0x0004
- 发现
tokenId= 1
的后续位置仍然有空位tokenId = 2
,主动将后续的空位填充0x0001
的归属权。
ERC721A 是如何记录某人拥有多少 NFT 的
ERC721 的做法
在标准的 ERC721
合约中,记录某人拥有多少 NFT
的数据结构非常简单,是一个 mapping
的结构。
mapping(address owner => uint256) private _balances;
ERC721A 的做法
// Mapping owner address to address data.
//
// Bits Layout:
// - [0..63] `balance`
// - [64..127] `numberMinted`
// - [128..191] `numberBurned`
// - [192..255] `aux`
mapping(address => uint256) private _packedAddressData;
对于 ERC721A
记录某人拥有多少 NFT
数量的数据结构中的话,同样是使用一个 mapping
的数据结构来记录某人拥有多少 NFT
数量。但这个数据结构相比于普通 ERC721
合约而言,更复杂。因为它不仅记录某人拥有多少 NFT
,还记录了 mint
的数量,burn
的数量、额外参数。(通过 uint256
进行分区记录,256
位分为四个区,每个区 64
个位,分别记录了数量、mint
数量、burn
数量、额外参数)
ERC721A是如何确定 mint 的位置的
ERC721 的做法
function _mint(address to, uint256 tokenId) internal {
if (to == address(0)) {
revert ERC721InvalidReceiver(address(0));
}
address previousOwner = _update(to, tokenId, address(0));
if (previousOwner != address(0)) {
revert ERC721InvalidSender(address(0));
}
}
对于标准版本的 ERC721
而言,它并没有维护一个实际的变量区记录mint
的位置。如果我们需要 mint
某个 id
的 NFT
,它会在 mint
函数中主动检查这个 NFT
是否已经在 mapping
中存在,如果不存在则可以进行 mint
。
ERC721A 的做法
// The next token ID to be minted.
uint256 private _currentIndex;
在 ERC721A
的合约中,维护了一个整数来保存NFT
的 mint
的位置。也就是说,我们在调用 mint
函数时,这个 Index
会自增。相比与普通版本的 ERC721
合约而言,因为是使用自增 Index
的方式递增,ERC721A
中的 NFT
必须是顺序 mint
的( tokenId
是有序增加,实际物理存储仍然是无序的),而普通 ERC721
的 NFT
因为只在 mint
的时候检查 tokenId
是否存在,所以 tokenId
仍不一定是有序的。
真的需要 ERC721A 吗
如果我们的合约在部署前可以确定一定会出现很多批量 mint
的操作,那么 ERC721A
是很好的选择,因为它可以节省很多 gas
。
但如果我们并没有批量 mint
的需求,普通的 ERC721
合约则更好,因为它的实现非常简单,我们并没有必要用到 ERC721A
。
参考链接
ERC721 合约:https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/token/ERC721 ERC721A 合约:https://github.com/chiru-labs/ERC721A/tree/main Azuki 对于 ERC721A 的优化文档:https://www.azuki.com/erc721a