# ERC721A优化解读

By [franx.eth](https://paragraph.com/@franx-2) · 2022-02-28

---

NFT白单和公售mint时，有些项目允许一个tx交易mint多个NFT，以太坊标准的ERC721在mint多个时gas成本成倍增加，对用户来说不太友好。Azuki对此进行了优化，发布了ERC721A合约，这个合约也在跟随着最新市场变化而不断优化，现对当前最新版本v3.0.0进行解读。

合约源码见

[https://github.com/chiru-labs/ERC721A](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项目采用此合约标准。

![](https://storage.googleapis.com/papyrus_images/c3d78cdde1efe70a526937a6c27e2aa98250e0639e104b401edb27e318c0d63e.png)

---

*Originally published on [franx.eth](https://paragraph.com/@franx-2/erc721a)*
