ERC721A - 批量 mint NFT 时候的 gas 优化合约

ERC721A - 批量 mint gas 优化

最近在面试的过程中,有被问到这么一个问题:有了解过 ERC721A 吗,知道它和普通的 ERC721 有什么不同吗?当时答不出来,挺尴尬。。。 虽然后面还是顺利拿到了 offer,但总不能下次也答不出来吧。 下面,咱就来详细学习一下 ERC721A 在合约里面做了什么优化

什么是 ERC721A

ERC721AAzuki 项目方提出的一个新的 ERC721 的改进标准,主要用于针对一次性 mint 多个 NFT 的时候 Gas 费优化。其本质是基于 ERC721 进行的优化,如果是顺序 mintNFT,则只在首位存储归属权信息,通过节约链上存储空间从而达到节约 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 的归属权可以通过算法来进行定位。

  1. 查找 tokenId =2NFT,查到这个位置为空
  2. 往前回溯 tokenId = 1NFT,这个位置依旧为空
  3. 往前回溯 tokenId = 0NFT,这个位置归属于 0x0001
  4. 可认为 tokenId = 2NFT 归属于 0x0001 地址

ERC721A 转让 NFT 怎么办

对于 ERC721 这个优化,我们很容易又会发现一个问题,假如我们要转让一个空位置的 NFT,不就打破这种空位置的连续性了吗,该如何处理呢?

例如在 tokenId = 1 的位置转让给 0x0004,如果往 tokenId = 1 的位置写入 0x0004 的数据,显然 tokenId = 2 的位置按照查找规则会被认为是 0x0004 所拥有的,显然不对。

其实解决起来这个问题也很简单,如果转让的位置为空,则不仅将这个位置转让出去,如果这个位置的后面也有空数据,则主动将后面的空数据填充。例如:

  1. tokenId = 1 的归属权从 0x0001 转让给 0x0004,直接往 token = 1 的位置写入 0x0004
  2. 发现 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 某个 idNFT,它会在 mint 函数中主动检查这个 NFT 是否已经在 mapping 中存在,如果不存在则可以进行 mint

ERC721A 的做法

// The next token ID to be minted.
uint256 private _currentIndex;

ERC721A 的合约中,维护了一个整数来保存NFTmint 的位置。也就是说,我们在调用 mint 函数时,这个 Index 会自增。相比与普通版本的 ERC721 合约而言,因为是使用自增 Index 的方式递增,ERC721A 中的 NFT 必须是顺序 mint 的( tokenId 是有序增加,实际物理存储仍然是无序的),而普通 ERC721NFT 因为只在 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

全部评论(0)