ERC721A优化解读

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项目采用此合约标准。

post image