{"content":{"title":"分析 ERC721A 源码节省Gas优化思路","body":"# 分析 ERC721A 源码节省Gas优化思路\r\n\r\nLink: https://github.com/chiru-labs/ERC721A \r\n`\"erc721a\": \"^4.3.0\"` \r\n\r\n```solidity\r\n\r\n    // Mapping from token ID to ownership details\r\n    // Bits Layout:\r\n    // - [0..159]   `addr`\r\n    // - [160..223] `startTimestamp`\r\n    // - [224]      `burned`\r\n    // - [225]      `nextInitialized`\r\n    // - [232..255] `extraData`\r\n    mapping(uint256 => uint256) private _packedOwnerships;\r\n\r\n    // Mapping owner address to address data.\r\n    //\r\n    // Bits Layout:\r\n    // - [0..63]    `balance`\r\n    // - [64..127]  `numberMinted`\r\n    // - [128..191] `numberBurned`\r\n    // - [192..255] `aux`\r\n    mapping(address => uint256) private _packedAddressData;\r\n```\r\n\r\n\r\n\r\n##  1. 分析 _mint(to, quantity)\r\n\r\n```addsolidity\r\n    function _mint(address to, uint256 quantity) internal virtual {\r\n        uint256 startTokenId = _currentIndex;\r\n        if (quantity == 0) _revert(MintZeroQuantity.selector);\r\n\r\n        _beforeTokenTransfers(address(0), to, startTokenId, quantity);\r\n\r\n        // Overflows are incredibly unrealistic.\r\n        // `balance` and `numberMinted` have a maximum limit of 2**64.\r\n        // `tokenId` has a maximum limit of 2**256.\r\n        unchecked {\r\n            // Updates:\r\n            // - `address` to the owner.\r\n            // - `startTimestamp` to the timestamp of minting.\r\n            // - `burned` to `false`.\r\n            // - `nextInitialized` to `quantity == 1`.\r\n            _packedOwnerships[startTokenId] = _packOwnershipData(\r\n                to,\r\n                _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0)\r\n            );\r\n\r\n            // Updates:\r\n            // - `balance += quantity`.\r\n            // - `numberMinted += quantity`.\r\n            //\r\n            // We can directly add to the `balance` and `numberMinted`.\r\n            _packedAddressData[to] += quantity * ((1 << _BITPOS_NUMBER_MINTED) | 1);\r\n\r\n            // Mask `to` to the lower 160 bits, in case the upper bits somehow aren't clean.\r\n            uint256 toMasked = uint256(uint160(to)) & _BITMASK_ADDRESS;\r\n\r\n            if (toMasked == 0) _revert(MintToZeroAddress.selector);\r\n\r\n            uint256 end = startTokenId + quantity;\r\n            uint256 tokenId = startTokenId;\r\n\r\n            if (end - 1 > _sequentialUpTo()) _revert(SequentialMintExceedsLimit.selector);\r\n\r\n            do {\r\n                assembly {\r\n                    // Emit the `Transfer` event.\r\n                    log4(\r\n                        0, // Start of data (0, since no data).\r\n                        0, // End of data (0, since no data).\r\n                        _TRANSFER_EVENT_SIGNATURE, // Signature.\r\n                        0, // `address(0)`.\r\n                        toMasked, // `to`.\r\n                        tokenId // `tokenId`.\r\n                    )\r\n                }\r\n                // The `!=` check ensures that large values of `quantity`\r\n                // that overflows uint256 will make the loop run out of gas.\r\n            } while (++tokenId != end);\r\n\r\n            _currentIndex = end;\r\n        }\r\n        _afterTokenTransfers(address(0), to, startTokenId, quantity);\r\n    }\r\n```\r\n\r\n\r\n\r\n### 以 _mint(AAAA,10); _currentIndex = 1 为例,剖析执行流程\r\n\r\n### (1) 获取将要开始铸造的TokenId\r\n\r\n```solidity\r\n    uint256 startTokenId = _currentIndex;\r\n    if (quantity == 0) _revert(MintZeroQuantity.selector);\r\n\r\n    _beforeTokenTransfers(address(0), to, startTokenId, quantity);\r\n```\r\n\r\nstartTokenId = 1；\r\n\r\n### (2) 更新mapping(tokenId ==> (owner, startTime, ....))\r\n\r\n```solidity\r\n        // Updates:\r\n        // - `address` to the owner.\r\n        // - `startTimestamp` to the timestamp of minting.\r\n        // - `burned` to `false`.\r\n        // - `nextInitialized` to `quantity == 1`.\r\n        _packedOwnerships[startTokenId] = _packOwnershipData(\r\n            to,\r\n            _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0)\r\n        );\r\n```\r\n\r\nnextInitialized = (quantity == 1); 下一个tokenId有没有初始化。\r\n\r\n_packedOwnerships[1] = _packed(AAAA, block.timestamp, false, nextInitialized, extraData);\r\n\r\n\r\n\r\n将接收地址、铸造时间戳等信息打包后赋值给 _packedOwnerships[1]。\r\n\r\n### (3) 更新 mapping(owner => (balance, numberMinted, ...))\r\n\r\n```solidity\r\n            // Updates:\r\n            // - `balance += quantity`.\r\n            // - `numberMinted += quantity`.\r\n            //\r\n            // We can directly add to the `balance` and `numberMinted`.\r\n            _packedAddressData[to] += quantity * ((1 << _BITPOS_NUMBER_MINTED) | 1);\r\n```\r\n\r\n更新 接收者 持有token的数量以及铸造token的数量。\r\n\r\n### (4) 循环生成 `Transfer` Event\r\n\r\n```solidity\r\n            uint256 end = startTokenId + quantity;\r\n            uint256 tokenId = startTokenId;\r\n\r\n            if (end - 1 > _sequentialUpTo()) _revert(SequentialMintExceedsLimit.selector);\r\n\r\n            do {\r\n                assembly {\r\n                    // Emit the `Transfer` event.\r\n                    log4(\r\n                        0, // Start of data (0, since no data).\r\n                        0, // End of data (0, since no data).\r\n                        _TRANSFER_EVENT_SIGNATURE, // Signature.\r\n                        0, // `address(0)`.\r\n                        toMasked, // `to`.\r\n                        tokenId // `tokenId`.\r\n                    )\r\n                }\r\n                // The `!=` check ensures that large values of `quantity`\r\n                // that overflows uint256 will make the loop run out of gas.\r\n            } while (++tokenId != end);\r\n```\r\n\r\n### (5) 更新 _currentIndex\r\n\r\n```solidity\r\n    _currentIndex = end;\r\n    _afterTokenTransfers(address(0), to, startTokenId, quantity);\r\n```\r\n\r\n_currentIndex = 11；\r\n\r\n### 总结\r\n\r\n`无论铸造几个 NFT，都只更新 3 个 Slot，外加 N 个 Transfer 事件（必须），这就是 ERC721 批量 mint 节省 GAS 的精髓。`\r\n\r\n\r\n## 2. 分析 ownferOf(tokenId)\r\n\r\n```solidity\r\n    function ownerOf(uint256 tokenId) public view virtual override returns (address) {\r\n        return address(uint160(_packedOwnershipOf(tokenId)));\r\n    }\r\n```\r\n\r\n```solidity\r\n    function _packedOwnershipOf(uint256 tokenId) private view returns (uint256 packed) {\r\n        if (_startTokenId() <= tokenId) {\r\n            packed = _packedOwnerships[tokenId];\r\n\r\n            if (tokenId > _sequentialUpTo()) {\r\n                if (_packedOwnershipExists(packed)) return packed;\r\n                _revert(OwnerQueryForNonexistentToken.selector);\r\n            }\r\n\r\n            // If the data at the starting slot does not exist, start the scan.\r\n            if (packed == 0) {\r\n                if (tokenId >= _currentIndex) _revert(OwnerQueryForNonexistentToken.selector);\r\n                // Invariant:\r\n                // There will always be an initialized ownership slot\r\n                // (i.e. `ownership.addr != address(0) && ownership.burned == false`)\r\n                // before an unintialized ownership slot\r\n                // (i.e. `ownership.addr == address(0) && ownership.burned == false`)\r\n                // Hence, `tokenId` will not underflow.\r\n                //\r\n                // We can directly compare the packed value.\r\n                // If the address is zero, packed will be zero.\r\n                for (;;) {\r\n                    unchecked {\r\n                        packed = _packedOwnerships[--tokenId];\r\n                    }\r\n                    if (packed == 0) continue;\r\n                    if (packed & _BITMASK_BURNED == 0) return packed;\r\n                    // Otherwise, the token is burned, and we must revert.\r\n                    // This handles the case of batch burned tokens, where only the burned bit\r\n                    // of the starting slot is set, and remaining slots are left uninitialized.\r\n                    _revert(OwnerQueryForNonexistentToken.selector);\r\n                }\r\n            }\r\n            // Otherwise, the data exists and we can skip the scan.\r\n            // This is possible because we have already achieved the target condition.\r\n            // This saves 2143 gas on transfers of initialized tokens.\r\n            // If the token is not burned, return `packed`. Otherwise, revert.\r\n            if (packed & _BITMASK_BURNED == 0) return packed;\r\n        }\r\n        _revert(OwnerQueryForNonexistentToken.selector);\r\n    }\r\n```\r\n\r\n### 以 ownerOf(5) 为例\r\n\r\n`_startTokenId() <= tokenId <  _currentIndex`\r\n\r\n```solidity\r\n        if (packed == 0) {\r\n            if (tokenId >= _currentIndex) _revert(OwnerQueryForNonexistentToken.selector);\r\n   \r\n            for (;;) {\r\n                unchecked {\r\n                    packed = _packedOwnerships[--tokenId];\r\n                }\r\n                if (packed == 0) continue;\r\n                if (packed & _BITMASK_BURNED == 0) return packed;\r\n                _revert(OwnerQueryForNonexistentToken.selector);\r\n            }\r\n        }\r\n```\r\n\r\ntokenId 的传参范围：1 <= tokenId <11；\r\n\r\n获取 tokenId 的打包数据，packed = _packedOwnerships[5];\r\n\r\n如果 packed 为空，依次往下获取 （tokenId - 1） 的打包数据，直到 packed不为空。\r\n\r\n最终， packed = _packedOwnerships[1]；解析packed 后，owner = AAAA。\r\n\r\n### 总结\r\n\r\n`查询 tokenId 的 owner 是一个循环往下遍历的过程，直到数据不为空。`\r\n\r\n## 3. 分析 transferFrom(from, to, tokenId)\r\n\r\n```solidity\r\n    function transferFrom(\r\n        address from,\r\n        address to,\r\n        uint256 tokenId\r\n    ) public payable virtual override {\r\n        uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId);\r\n\r\n        // Mask `from` to the lower 160 bits, in case the upper bits somehow aren't clean.\r\n        from = address(uint160(uint256(uint160(from)) & _BITMASK_ADDRESS));\r\n\r\n        if (address(uint160(prevOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector);\r\n\r\n        (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId);\r\n\r\n        // The nested ifs save around 20+ gas over a compound boolean condition.\r\n        if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A()))\r\n            if (!isApprovedForAll(from, _msgSenderERC721A())) _revert(TransferCallerNotOwnerNorApproved.selector);\r\n\r\n        _beforeTokenTransfers(from, to, tokenId, 1);\r\n\r\n        // Clear approvals from the previous owner.\r\n        assembly {\r\n            if approvedAddress {\r\n                // This is equivalent to `delete _tokenApprovals[tokenId]`.\r\n                sstore(approvedAddressSlot, 0)\r\n            }\r\n        }\r\n\r\n        // Underflow of the sender's balance is impossible because we check for\r\n        // ownership above and the recipient's balance can't realistically overflow.\r\n        // Counter overflow is incredibly unrealistic as `tokenId` would have to be 2**256.\r\n        unchecked {\r\n            // We can directly increment and decrement the balances.\r\n            --_packedAddressData[from]; // Updates: `balance -= 1`.\r\n            ++_packedAddressData[to]; // Updates: `balance += 1`.\r\n\r\n            // Updates:\r\n            // - `address` to the next owner.\r\n            // - `startTimestamp` to the timestamp of transfering.\r\n            // - `burned` to `false`.\r\n            // - `nextInitialized` to `true`.\r\n            _packedOwnerships[tokenId] = _packOwnershipData(\r\n                to,\r\n                _BITMASK_NEXT_INITIALIZED | _nextExtraData(from, to, prevOwnershipPacked)\r\n            );\r\n\r\n            // If the next slot may not have been initialized (i.e. `nextInitialized == false`) .\r\n            if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) {\r\n                uint256 nextTokenId = tokenId + 1;\r\n                // If the next slot's address is zero and not burned (i.e. packed value is zero).\r\n                if (_packedOwnerships[nextTokenId] == 0) {\r\n                    // If the next slot is within bounds.\r\n                    if (nextTokenId != _currentIndex) {\r\n                        // Initialize the next slot to maintain correctness for `ownerOf(tokenId + 1)`.\r\n                        _packedOwnerships[nextTokenId] = prevOwnershipPacked;\r\n                    }\r\n                }\r\n            }\r\n        }\r\n\r\n        // Mask `to` to the lower 160 bits, in case the upper bits somehow aren't clean.\r\n        uint256 toMasked = uint256(uint160(to)) & _BITMASK_ADDRESS;\r\n        assembly {\r\n            // Emit the `Transfer` event.\r\n            log4(\r\n                0, // Start of data (0, since no data).\r\n                0, // End of data (0, since no data).\r\n                _TRANSFER_EVENT_SIGNATURE, // Signature.\r\n                from, // `from`.\r\n                toMasked, // `to`.\r\n                tokenId // `tokenId`.\r\n            )\r\n        }\r\n        if (toMasked == 0) _revert(TransferToZeroAddress.selector);\r\n\r\n        _afterTokenTransfers(from, to, tokenId, 1);\r\n    }\r\n```\r\n\r\n### 以 transferFrom(AAAA, BBBB, 5) 为例\r\n\r\n### (1) 获取 tokenId 的打包数据（owner）\r\n\r\n\r\n\r\n```solidity\r\n        uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId);\r\n\r\n        // Mask `from` to the lower 160 bits, in case the upper bits somehow aren't clean.\r\n        from = address(uint160(uint256(uint160(from)) & _BITMASK_ADDRESS));\r\n\r\n        if (address(uint160(prevOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector);\r\n```\r\n\r\n uint256 prevOwnershipPacked = _packedOwnershipOf(5)；\r\n\r\n依次往下找，prevOwnershipPacked = _packedOwnerships[1];\r\n\r\n解析数据，拿到 tokenId 的owner，与 from 地址做校验。\r\n\r\n\r\n\r\n### (2) 校验 from 是否将 tokenId 授权给 _msgSender\r\n\r\n```solidity\r\n        (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId);\r\n\r\n        // The nested ifs save around 20+ gas over a compound boolean condition.\r\n        if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A()))\r\n            if (!isApprovedForAll(from, _msgSenderERC721A())) _revert(TransferCallerNotOwnerNorApproved.selector);\r\n\r\n        _beforeTokenTransfers(from, to, tokenId, 1);\r\n\r\n        // Clear approvals from the previous owner.\r\n        assembly {\r\n            if approvedAddress {\r\n                // This is equivalent to `delete _tokenApprovals[tokenId]`.\r\n                sstore(approvedAddressSlot, 0)\r\n            }\r\n        }\r\n```\r\n\r\n校验 from 是否将 tokenId 授权给 调用者；之后清除 tokenId 的授权信息。\r\n\r\n\r\n\r\n### (3) 更新 form, to 的余额信息，及 tokenId 的打包信息\r\n\r\n```solidity\r\n        unchecked {\r\n            // We can directly increment and decrement the balances.\r\n            --_packedAddressData[from]; // Updates: `balance -= 1`.\r\n            ++_packedAddressData[to]; // Updates: `balance += 1`.\r\n\r\n            // Updates:\r\n            // - `address` to the next owner.\r\n            // - `startTimestamp` to the timestamp of transfering.\r\n            // - `burned` to `false`.\r\n            // - `nextInitialized` to `true`.\r\n            _packedOwnerships[tokenId] = _packOwnershipData(\r\n                to,\r\n                _BITMASK_NEXT_INITIALIZED | _nextExtraData(from, to, prevOwnershipPacked)\r\n            );\r\n\r\n            // If the next slot may not have been initialized (i.e. `nextInitialized == false`) .\r\n            if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) {\r\n                uint256 nextTokenId = tokenId + 1;\r\n                // If the next slot's address is zero and not burned (i.e. packed value is zero).\r\n                if (_packedOwnerships[nextTokenId] == 0) {\r\n                    // If the next slot is within bounds.\r\n                    if (nextTokenId != _currentIndex) {\r\n                        // Initialize the next slot to maintain correctness for `ownerOf(tokenId + 1)`.\r\n                        _packedOwnerships[nextTokenId] = prevOwnershipPacked;\r\n                    }\r\n                }\r\n            }\r\n        }\r\n```\r\n\r\n更新 AAAA, BBBB 的余额信息；balance_AAAA = 9, balance_BBBB = 1;\r\n\r\n    // Mapping from token ID to ownership details\r\n    // Bits Layout:\r\n    // - [0..159]   `addr`\r\n    // - [160..223] `startTimestamp`\r\n    // - [224]      `burned`\r\n    // - [225]      `nextInitialized`\r\n    // - [232..255] `extraData`\r\n\r\n更新tokenId = 5 的打包信息；_packedOwnerships[5] =(BBBB, block.timestamp, false, true, extraData); 此时，需要 nextInitialized = true。\r\n\r\n\r\n\r\n如果 prevOwnershipPacked 的 nextInitialized  = false，即 下一个tokenId的打包信息没有初始化；那么就要对现在tokenId = 5的下一个进行初始化。\r\n\r\n _packedOwnerships[6] = prevOwnershipPacked。\r\n\r\n\r\n\r\n（也可以这么理解，在转移TokenId为 N 的NFT时，如果TokenId为 N + 1 的NFT没有被初始化过，就要将要TokenId为 N 的打包信息赋值给TokenId为 N + 1；然后更新TokenId为 N 的打包信息）\r\n\r\n\r\n\r\n`为什要这样呢？ 因为需要将 prevOwnershipPacked 赋值给 _packedOwnerships[6]，初始化tokenId = 6 打包数据 。不然你查找owner(10)时将查到 _packedOwnerships[5]的 owner 是 BBBB; AAAA 的 NFT 无缘无故的丢了，肯定大哭不愿意啊!`\r\n\r\n\r\n\r\n### (4) 生成 `Transfer` Event\r\n\r\n```solidity\r\n    // Mask `to` to the lower 160 bits, in case the upper bits somehow aren't clean.\r\n    uint256 toMasked = uint256(uint160(to)) & _BITMASK_ADDRESS;\r\n    assembly {\r\n        // Emit the `Transfer` event.\r\n        log4(\r\n            0, // Start of data (0, since no data).\r\n            0, // End of data (0, since no data).\r\n            _TRANSFER_EVENT_SIGNATURE, // Signature.\r\n            from, // `from`.\r\n            toMasked, // `to`.\r\n            tokenId // `tokenId`.\r\n        )\r\n    }\r\n    if (toMasked == 0) _revert(TransferToZeroAddress.selector);\r\n\r\n    _afterTokenTransfers(from, to, tokenId, 1);\r\n```\r\n\r\n\r\n\r\n### 总结\r\n\r\n在 NFT的一次转移过程中，最多更新两个token（N和 N + 1）的打包数据。\r\n\r\n\r\n\r\n## 4. 有这么一个问题，CCCC批量铸造了5个NFT，tokenId 分别为 11,12,13,14,15。现在他要将这5个NFT分5次转给DDDD，那么他应该从tokenId为11还是15开始呢？\r\n\r\n首先肯定是要执行5次 transferFrom(CCCC, DDDD, tokenId)。\r\n\r\n假如从15 ==> 11,\r\ntransferFrom(CCCC, DDDD, 15),查询owner执行了 5次,初始化 15 的打包信息；\r\ntransferFrom(CCCC, DDDD, 14),查询owner执行了 4次,初始化 14 的打包信息；\r\ntransferFrom(CCCC, DDDD, 13),查询owner执行了 3次,初始化 13 的打包信息；\r\ntransferFrom(CCCC, DDDD, 12),查询owner执行了 2次,初始化 12 的打包信息；\r\ntransferFrom(CCCC, DDDD, 11),查询owner执行了 1次,更新 11 的打包信息。\r\n\r\n假如从11 ==> 15, \r\ntransferFrom(CCCC, DDDD, 11),查询owner执行了 1次,更新 11 的打包信息,初始化12的打包信息；\r\ntransferFrom(CCCC, DDDD, 12),查询owner执行了 1次,更新 12 的打包信息,初始化13的打包信息；\r\ntransferFrom(CCCC, DDDD, 13),查询owner执行了 1次,更新 13 的打包信息,初始化14的打包信息；\r\ntransferFrom(CCCC, DDDD, 14),查询owner执行了 1次,更新 14 的打包信息,初始化15的打包信息；\r\ntransferFrom(CCCC, DDDD, 15),查询owner执行了 1次,更新 15 的打包信息。"},"author":{"user":"https://learnblockchain.cn/people/5578","address":"0x0148ff91E06977093992EDdEf76F381E1EE5de2f"},"history":"bafkreifw5bcyl4qnbeapkdfamf6ji2xr7cr3zojosv6r4w3oiqwbzb4tga","timestamp":1711676323,"version":1}