概述
读者可前往我的博客获得更好的阅读体验。
本文主要介绍标准NFT实现的一个变体,即ERC721A合约实现的相关细节。ERC721A是由著名NFT系列Azuki提出,该系列NFT是著名的蓝筹NFT。本文主要聚焦于Azuki提出的ERC721A合约的代码细节分析。
与传统的ERC721实现相比,ERC721A在批量铸造(batch mint)方面具有显著的gas优势,这得益于ERC721A的惰性初始化方面的设计。关于ERC721A与普通ERC721实现的对比,我们将会在下文展开说明。
本文要求读者具有基础的solidity知识,希望读者对标准ERC721有所了解。
读者可在阅读本文前,酌情阅读以下参考材料:
本文基于目前的最新版本(4.2.3)合约代码进行分析。
ERC721实现
由于下文涉及到ERC721A与ERC721的技术对比,考虑到部分读者可以对ERC721合约实现并不清楚,本节简要的介绍ERC721正常实现的铸造功能,本节主要基于solmate的实现版本。
solmate实现都较为短小精悍且经过gas优化,我个人较为推崇。solmate的ERC721实现仅有 231 行,读者可自行阅读。
在solmate合约中,我们可以看到核心数据结构为:
mapping(uint256 => address) internal _ownerOf;
mapping(address => uint256) internal _balanceOf;
其中,各映射功能如下:
_ownerOf记录 tokenId 与持有者的关系_balanceOf记录持有人所持有的 NFT 数量
其铸造方法定义如下:
function _mint(address to, uint256 id) internal virtual {
require(to != address(0), "INVALID_RECIPIENT");
require(_ownerOf[id] == address(0), "ALREADY_MINTED");
// Counter overflow is incredibly unrealistic.
unchecked {
_balanceOf[to]++;
}
_ownerOf[id] = to;
emit Transfer(address(0), to, id);
}
通过此函数,我们更新了_ownerOf和_balanceOf实现用户铸造 NFT 的功能。我们可以发现用户每次铸造NFT都需要更新_ownerOf和_balanceOf映射。众所周知,在操作码gas消耗中,更新存储需要消耗大量gas。如果用户批量铸造,会在此过程中消耗大量gas。
根据数据(PDF警告),在ETH价格为 1500 美元时,更新存储的价格为 7.5 美元,而写入存储的价格为 30 美元。这意味着仅在
mint过程中,更新映射会浪费大量资产。
转账函数定义如下:
function transferFrom(
address from,
address to,
uint256 id
) public virtual {
require(from == _ownerOf[id], "WRONG_FROM");
require(to != address(0), "INVALID_RECIPIENT");
require(
msg.sender == from || isApprovedForAll[from][msg.sender] || msg.sender == getApproved[id],
"NOT_AUTHORIZED"
);
// Underflow of the sender's balance is impossible because we check for
// ownership above and the recipient's balance can't realistically overflow.
unchecked {
_balanceOf[from]--;
_balanceOf[to]++;
}
_ownerOf[id] = to;
delete getApproved[id];
emit Transfer(from, to, id);
}
由于对于每个tokenId都维护有一个mapping映射,所以转账逻辑实现也较为简单。
总体来看,对于每一个NFT,在solmate实现的智能合约中,都维持有以下两个映射:
mapping(uint256 => address) internal _ownerOf;标识NFT的拥有者mapping(uint256 => address) public getApproved;记录NFT的授权情况
优势
在上一节中,我们介绍了常规NFT实现的基本情况,正如上文所述,常规实现在批量mint铸造阶段会消耗大量gas。为了解决这一问题,ERC721A引入惰性初始化机制。简单来说,在批量铸造时,不再记录tokenId与用户地址的映射关系,而是记录起始tokenId和数量与用户的映射关系。在本节中,我们不对此实现的技术细节进行分析,我们会在本文稍后部分对此进行讨论。
在批量铸造阶段,ERC721A与OpenZeppelin实现的对比如下:
| ERC721 | ERC721A | |
|---|---|---|
| 批量铸造 5 个 NFT | 155949 gas | 63748 gas |
| 转移 5 个 NFT | 226655 gas | 334450 gas |
| 铸造的 Base Fee | 200 gwei | 200 gwei |
| 转移的 Base Fee | 40 gwei | 40 gwei |
| 总花费 | 0.0403 ether | 0.0261 ether |
如果读者对于此处的
gas计算的细节感兴趣,可以阅读以太坊机制详解:Gas Price计算。我们在此处不详细讨论计算方式。我们可以注意到铸造阶段的Base fee较高,这考虑到了NFT铸造导致的网络拥堵情况。
显然,惰性初始化机制对于批量铸造阶段的gas节省是具有明显优势的,但惰性加载将初始化的成本转移到了转账部分,我们可以看到在转移NFT时的成本有所上升。但需要注意,第一次转账后由于彻底完成了初始化,所有后续转账的成本会降低,如下:
| ERC721 | ERC721A | |
|---|---|---|
| First transfer | 45331 gas | 92822 gas |
| Subsequent transfers | 45331 gas | 44499 gas |
通过表格可以看出,除第一次转账消耗的gas明显增多,但随后转账的价格与常规的NFT转账并无区别。
总结来说,ERC721A实现了低成本的批量铸造,但将部分成本转移到了第一次转账中。这种设计充分考虑到了铸造阶段可能出现的以太坊网络拥堵而造成gas价格飙升的情况,而用户后期转账是偶发的且不会导致网络拥堵的。通过这种特殊的成本转嫁机制,ERC721A降低用户的总成本。
换言之,如果您认为您的NFT项目不存在批量铸造的情况或不会导致以太坊网络拥堵,可以选择常规NFT实现。
具体实现
在讨论了ERC721A的基本内容后,为进一步增加我们对ERC721A的理解,我们将对其合约进行阅读分析。ERC721A的开源仓库位于github。此处,我们仅讨论ERC721A的主合约,而暂不讨论extensions部分。
对于NFT合约的分析,存储数据结构和_mint函数是一个很好的入手点。我们首先关注存储数据结构。
在NFT数据存储中,我们可以看到solmate等常规实现都使用了mapping(uint256 => address) internal _ownerOf将单个tokenId与持有者对应。但ERC721A是对批量铸造进行特殊优化的,开发者认为在批量铸造过程中,用户持有的NFT的tokenId往往是连续的,如下图:

基本数据结构
在批量铸造过程中,用户铸造连续的NFT是极其常见的。为了实现连续分配tokenID以降低gas消耗的目的,我们需要一些更加复杂的数据结构设计,具体代码设计如下:
// The next token ID to be minted.
uint256 private _currentIndex;
// The number of tokens burned.
uint256 private _burnCounter;
// Token name
string private _name;
// Token symbol
string private _symbol;
// Mapping from token ID to ownership details
// An empty struct value does not necessarily mean the token is unowned.
// See {_packedOwnershipOf} implementation for details.
//
// Bits Layout:
// - [0..159] `addr`
// - [160..223] `startTimestamp`
// - [224] `burned`
// - [225] `nextInitialized`
// - [232..255] `extraData`
mapping(uint256 => uint256) private _packedOwnerships;
// 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;
// Mapping from token ID to approved address.
mapping(uint256 => TokenApprovalRef) private _tokenApprovals;
// Mapping from owner to operator approvals
mapping(address => mapping(address => bool)) private _operatorApprovals;
与其他简单参数相比,我们主要关注复杂的参数:
_packedOwnerships类似常规NFT实现中的_ownerOf,我们通过此映射查询某tokenID的拥有
ERC721A合约深度解析:批量铸造与惰性初始化

本文详述了ERC721A合约的实现,重点在于其批量铸造的惰性初始化机制如何降低gas消耗。相较于传统ERC721,ERC721A在批量铸造时避免了频繁更新存储,从而节约成本。虽然首次转账成本增加,但后续转账成本降低,更适合大规模铸造场景。文章还分析了合约的存储结构、铸造、授权、转账和销毁等关键功能。
最低0.47元/天 解锁文章
275

被折叠的 条评论
为什么被折叠?



