NFT白单和公售mint时,有些项目允许一个tx交易mint多个NFT,以太坊标准的ERC721在mint多个时gas成本成倍增加,对用户来说不太友好。Azuki对此进行了优化,发布了ERC721A合约,这个合约也在跟随着最新市场变化而不断优化,现对当前最新版本v3.0.0进行解读。
合约源码见
https://github.com/chiru-labs/ERC721A
核心优化是不再每个tokenId都存储对应的所有者地址,对于mint多个的地址只在第一个tokenId上存储所有者信息,但这样依赖于tokenId的连续性。也就是说mint多个时,tokenId是连续的,对于某些项目的tokenId不是连续的来说就不太适用。比如meebits的tokenId是随机去除获取的。
下面对关键代码进行了中文的补充注释
// Compiler will pack this into a single 256bit word.
struct TokenOwnership {
// The address of the owner.
address addr;//所有者地址 160位
// Keeps track of the start time of ownership with minimal overhead for tokenomics.
uint64 startTimestamp;//连续片段的第一个token归属给用户的时间戳
// Whether the token has been burned.
bool burned;//是否燃烧 8位
}//160+64+8=212,对齐到264位
// Compiler will pack this into a single 256bit word.
struct AddressData {
// Realistically, 2**64-1 is more than enough.
uint64 balance;//token余额
// Keeps track of mint count with minimal overhead for tokenomics.
uint64 numberMinted;//mint数量
// Keeps track of burn count with minimal overhead for tokenomics.
uint64 numberBurned;//燃烧数量
// For miscellaneous variable(s) pertaining to the address
// (e.g. number of whitelist mint slots used).
// If there are multiple variables, please pack them into a uint64.
uint64 aux;//附加参数
}
// The tokenId of the next token to be minted.
uint256 internal _currentIndex;
// The number of tokens burned.
uint256 internal _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 ownershipOf implementation for details.
mapping(uint256 => TokenOwnership) internal _ownerships;//tokenId与所有者地址的映射关系
// Mapping owner address to address data
mapping(address => AddressData) private _addressData;//地址与地址token的映射关系
主要用了TokenOwnership来存储所有者信息,AddressData存储所有者对应的token信息
/**
* Gas spent here starts off proportional to the maximum mint batch size.
* It gradually moves to O(1) as tokens get transferred around in the collection over time.
*/
function ownershipOf(uint256 tokenId) internal view returns (TokenOwnership memory) {
uint256 curr = tokenId;
unchecked {
if (_startTokenId() <= curr && curr < _currentIndex) {//tokenId在当前mint的数量范围内
TokenOwnership memory ownership = _ownerships[curr];
if (!ownership.burned) {//未燃烧
if (ownership.addr != address(0)) {
return ownership;
}
// Invariant:
// There will always be an ownership that has an address and is not burned
// before an ownership that does not have an address and is not burned.
// Hence, curr will not underflow.
while (true) {//tokenId对应的所有者地址是空,表示此tokenId在存储时不是连续块的第一个token,因此要向前遍历直接找到非0地址
curr--;
ownership = _ownerships[curr];
if (ownership.addr != address(0)) {
return ownership;
}
}
}
}
}
revert OwnerQueryForNonexistentToken();
}
因为不再每个tokenId都存储所有者信息,所以在根据tokenId查找所有者时,有可能要遍历查找,但读交易是不需要gas的
/**
* @dev Mints `quantity` tokens and transfers them to `to`.
*
* Requirements:
*
* - `to` cannot be the zero address.
* - `quantity` must be greater than 0.
*
* Emits a {Transfer} event.
*/
function _mint(
address to,
uint256 quantity,
bytes memory _data,
bool safe
) internal {
uint256 startTokenId = _currentIndex;//当前tokenId索引
if (to == address(0)) revert MintToZeroAddress();//不能mint到0地址
if (quantity == 0) revert MintZeroQuantity();//数量不能=0
_beforeTokenTransfers(address(0), to, startTokenId, quantity);
// Overflows are incredibly unrealistic.
// balance or numberMinted overflow if current value of either + quantity > 1.8e19 (2**64) - 1
// updatedIndex overflows if _currentIndex + quantity > 1.2e77 (2**256) - 1
unchecked {
_addressData[to].balance += uint64(quantity);//目标地址token余额+1
_addressData[to].numberMinted += uint64(quantity);//目标地址mint数量+1
_ownerships[startTokenId].addr = to;//设置tokenId所有者
_ownerships[startTokenId].startTimestamp = uint64(block.timestamp);//设置连续片段的第一个token归属给用户的时间戳
uint256 updatedIndex = startTokenId;
uint256 end = updatedIndex + quantity;//最后一个tokenId
if (safe && to.isContract()) {//目标地址是合约
do {
emit Transfer(address(0), to, updatedIndex);//事件日志
if (!_checkContractOnERC721Received(address(0), to, updatedIndex++, _data)) {//校验是否符合IERC721Receiversf标准
revert TransferToNonERC721ReceiverImplementer();
}
} while (updatedIndex != end);
// Reentrancy protection
if (_currentIndex != startTokenId) revert();
} else {
do {
emit Transfer(address(0), to, updatedIndex++);//事件日志
} while (updatedIndex != end);
}
_currentIndex = updatedIndex;//更新当前tokenId索引,供下次mint使用
}
_afterTokenTransfers(address(0), to, startTokenId, quantity);
}
/**
* @dev Transfers `tokenId` from `from` to `to`.
*
* Requirements:
*
* - `to` cannot be the zero address.
* - `tokenId` token must be owned by `from`.
*
* Emits a {Transfer} event.
*/
function _transfer(
address from,
address to,
uint256 tokenId
) private {
TokenOwnership memory prevOwnership = ownershipOf(tokenId);//获取tokenId所有者地址
bool isApprovedOrOwner = (_msgSender() == prevOwnership.addr ||
isApprovedForAll(prevOwnership.addr, _msgSender()) ||
getApproved(tokenId) == _msgSender());//tokenId是函数调用方自己的 或者 已授权给函数调用方
if (!isApprovedOrOwner) revert TransferCallerNotOwnerNorApproved();
if (prevOwnership.addr != from) revert TransferFromIncorrectOwner();//tokenId所有者与from参数要一样
if (to == address(0)) revert TransferToZeroAddress();//不能转到0地址
_beforeTokenTransfers(from, to, tokenId, 1);
// Clear approvals from the previous owner
_approve(address(0), tokenId, prevOwnership.addr);//转出地址的tokenId授权记录要清空
// Underflow of the sender's balance is impossible because we check for
// ownership above and the recipient's balance can't realistically overflow.
// Counter overflow is incredibly unrealistic as tokenId would have to be 2**256.
unchecked {
_addressData[from].balance -= 1;//转出地址余额-1
_addressData[to].balance += 1;//转入地址余额+1
_ownerships[tokenId].addr = to;//更新tokenId所有者地址为目标地址
_ownerships[tokenId].startTimestamp = uint64(block.timestamp);//更新连续片段的第一个token归属给用户的时间戳
// If the ownership slot of tokenId+1 is not explicitly set, that means the transfer initiator owns it.
// Set the slot of tokenId+1 explicitly in storage to maintain correctness for ownerOf(tokenId+1) calls.
uint256 nextTokenId = tokenId + 1;
if (_ownerships[nextTokenId].addr == address(0)) {//下一个tokenId的所有者信息是空时,表示下一个tokenId也是当前tokenId拥有的,因此要更新所有者信息
// This will suffice for checking _exists(nextTokenId),
// as a burned slot cannot contain the zero address.
if (nextTokenId < _currentIndex) {
_ownerships[nextTokenId].addr = prevOwnership.addr;//更新下一个tokenId所有者地址为转出地址
_ownerships[nextTokenId].startTimestamp = prevOwnership.startTimestamp;//更新连续片段的第一个token归属给用户的时间戳
}
}
}
emit Transfer(from, to, tokenId);
_afterTokenTransfers(from, to, tokenId, 1);
}
/**
* @dev Destroys `tokenId`.
* The approval is cleared when the token is burned.
*
* Requirements:
*
* - `tokenId` must exist.
*
* Emits a {Transfer} event.
*/
function _burn(uint256 tokenId) internal virtual {
TokenOwnership memory prevOwnership = ownershipOf(tokenId);//获取tokenId所有者地址
_beforeTokenTransfers(prevOwnership.addr, address(0), tokenId, 1);
// Clear approvals from the previous owner
_approve(address(0), tokenId, prevOwnership.addr);//地址的tokenId授权记录要清空
// Underflow of the sender's balance is impossible because we check for
// ownership above and the recipient's balance can't realistically overflow.
// Counter overflow is incredibly unrealistic as tokenId would have to be 2**256.
unchecked {
_addressData[prevOwnership.addr].balance -= 1;//地址余额-1
_addressData[prevOwnership.addr].numberBurned += 1;//地址煅烧数量+1
// Keep track of who burned the token, and the timestamp of burning.
_ownerships[tokenId].addr = prevOwnership.addr;//更新tokenid的所有者地址,因为可能tokenId是属于连续块中的非第一个,之前mint时没有做所有者记录
_ownerships[tokenId].startTimestamp = uint64(block.timestamp);//更新连续片段的第一个token归属给用户的时间戳
_ownerships[tokenId].burned = true;//更新tokenId状态为已燃烧
// If the ownership slot of tokenId+1 is not explicitly set, that means the burn initiator owns it.
// Set the slot of tokenId+1 explicitly in storage to maintain correctness for ownerOf(tokenId+1) calls.
uint256 nextTokenId = tokenId + 1;
if (_ownerships[nextTokenId].addr == address(0)) {//下一个tokenId的所有者信息是空时,表示下一个tokenId也是当前tokenId拥有的,因此要更新所有者信息
// This will suffice for checking _exists(nextTokenId),
// as a burned slot cannot contain the zero address.
if (nextTokenId < _currentIndex) {
_ownerships[nextTokenId].addr = prevOwnership.addr;//更新下一个tokenId所有者地址
_ownerships[nextTokenId].startTimestamp = prevOwnership.startTimestamp;//更新连续片段的第一个token归属给用户的时间戳
}
}
}
emit Transfer(prevOwnership.addr, address(0), tokenId);
_afterTokenTransfers(prevOwnership.addr, address(0), tokenId, 1);
// Overflow not possible, as _burnCounter cannot be exceed _currentIndex times.
unchecked {
_burnCounter++;
}
}
多个mint时,只存储了第一个tokenId对应的_addressData和_ownerships,这样可以省去重复的gas消耗;transfer时如果不是第一个tokenId的transfer,需要更新tokenId+1的_ownerships;burn时与transfer类似。
项目官方有对mint的gas消耗与ERC721进行了对比,可以说在gas消耗上节省了许多,往后应该会有越来越多的NFT项目采用此合约标准。

