分析ERC721A源码节省Gas优化思路
Link: https://github.com/chiru-labs/ERC721A
"erc721a": "^4.3.0" 
    // Mapping from token ID to ownership 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;    function _mint(address to, uint256 quantity) internal virtual {
        uint256 startTokenId = _currentIndex;
        if (quantity == 0) _revert(MintZeroQuantity.selector);
        _beforeTokenTransfers(address(0), to, startTokenId, quantity);
        // Overflows are incredibly unrealistic.
        // `balance` and `numberMinted` have a maximum limit of 2**64.
        // `tokenId` has a maximum limit of 2**256.
        unchecked {
            // Updates:
            // - `address` to the owner.
            // - `startTimestamp` to the timestamp of minting.
            // - `burned` to `false`.
            // - `nextInitialized` to `quantity == 1`.
            _packedOwnerships[startTokenId] = _packOwnershipData(
                to,
                _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0)
            );
            // Updates:
            // - `balance += quantity`.
            // - `numberMinted += quantity`.
            //
            // We can directly add to the `balance` and `numberMinted`.
            _packedAddressData[to] += quantity * ((1 << _BITPOS_NUMBER_MINTED) | 1);
            // Mask `to` to the lower 160 bits, in case the upper bits somehow aren't clean.
            uint256 toMasked = uint256(uint160(to)) & _BITMASK_ADDRESS;
            if (toMasked == 0) _revert(MintToZeroAddress.selector);
            uint256 end = startTokenId + quantity;
            uint256 tokenId = startTokenId;
            if (end - 1 > _sequentialUpTo()) _revert(SequentialMintExceedsLimit.selector);
            do {
                assembly {
                    // Emit the `Transfer` event.
                    log4(
                        0, // Start of data (0, since no data).
                        0, // End of data (0, since no data).
                        _TRANSFER_EVENT_SIGNATURE, // Signature.
                        0, // `address(0)`.
                        toMasked, // `to`.
                        tokenId // `tokenId`.
                    )
                }
                // The `!=` check ensures that large values of `quantity`
                // that overflows uint256 will make the loop run out of gas.
            } while (++tokenId != end);
            _currentIndex = end;
        }
        _afterTokenTransfers(address(0), to, startTokenId, quantity);
    }    uint256 startTokenId = _currentIndex;
    if (quantity == 0) _revert(MintZeroQuantity.selector);
    _beforeTokenTransfers(address(0), to, startTokenId, quantity);startTokenId = 1;
        // Updates:
        // - `address` to the owner.
        // - `startTimestamp` to the timestamp of minting.
        // - `burned` to `false`.
        // - `nextInitialized` to `quantity == 1`.
        _packedOwnerships[startTokenId] = _packOwnershipData(
            to,
            _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0)
        );nextInitialized = (quantity == 1); 下一个tokenId有没有初始化。
_packedOwnerships[1] = _packed(AAAA, block.timestamp, false, nextInitialized, extraData);
将接收地址、铸造时间戳等信息打包后赋值给 _packedOwnerships[1]。
            // Updates:
            // - `balance += quantity`.
            // - `numberMinted += quantity`.
            //
            // We can directly add to the `balance` and `numberMinted`.
            _packedAddressData[to] += quantity * ((1 << _BITPOS_NUMBER_MINTED) | 1);更新 接收者 持有token的数量以及铸造token的数量。
Transfer Event            uint256 end = startTokenId + quantity;
            uint256 tokenId = startTokenId;
            if (end - 1 > _sequentialUpTo()) _revert(SequentialMintExceedsLimit.selector);
            do {
                assembly {
                    // Emit the `Transfer` event.
                    log4(
                        0, // Start of data (0, since no data).
                        0, // End of data (0, since no data).
                        _TRANSFER_EVENT_SIGNATURE, // Signature.
                        0, // `address(0)`.
                        toMasked, // `to`.
                        tokenId // `tokenId`.
                    )
                }
                // The `!=` check ensures that large values of `quantity`
                // that overflows uint256 will make the loop run out of gas.
            } while (++tokenId != end);    _currentIndex = end;
    _afterTokenTransfers(address(0), to, startTokenId, quantity);_currentIndex = 11;
无论铸造几个 NFT,都只更新 3 个 Slot,外加 N 个 Transfer 事件(必须),这就是 ERC721 批量 mint 节省 GAS 的精髓。
    function ownerOf(uint256 tokenId) public view virtual override returns (address) {
        return address(uint160(_packedOwnershipOf(tokenId)));
    }    function _packedOwnershipOf(uint256 tokenId) private view returns (uint256 packed) {
        if (_startTokenId() <= tokenId) {
            packed = _packedOwnerships[tokenId];
            if (tokenId > _sequentialUpTo()) {
                if (_packedOwnershipExists(packed)) return packed;
                _revert(OwnerQueryForNonexistentToken.selector);
            }
            // If the data at the starting slot does not exist, start the scan.
            if (packed == 0) {
                if (tokenId >= _currentIndex) _revert(OwnerQueryForNonexistentToken.selector);
                // Invariant:
                // There will always be an initialized ownership slot
                // (i.e. `ownership.addr != address(0) && ownership.burned == false`)
                // before an unintialized ownership slot
                // (i.e. `ownership.addr == address(0) && ownership.burned == false`)
                // Hence, `tokenId` will not underflow.
                //
                // We can directly compare the packed value.
                // If the address is zero, packed will be zero.
                for (;;) {
                    unchecked {
                        packed = _packedOwnerships[--tokenId];
                    }
                    if (packed == 0) continue;
                    if (packed & _BITMASK_BURNED == 0) return packed;
                    // Otherwise, the token is burned, and we must revert.
                    // This handles the case of batch burned tokens, where only the burned bit
                    // of the starting slot is set, and remaining slots are left uninitialized.
                    _revert(OwnerQueryForNonexistentToken.selector);
                }
            }
            // Otherwise, the data exists and we can skip the scan.
            // This is possible because we have already achieved the target condition.
            // This saves 2143 gas on transfers of initialized tokens.
            // If the token is not burned, return `packed`. Otherwise, revert.
            if (packed & _BITMASK_BURNED == 0) return packed;
        }
        _revert(OwnerQueryForNonexistentToken.selector);
    }_startTokenId() <= tokenId <  _currentIndex
        if (packed == 0) {
            if (tokenId >= _currentIndex) _revert(OwnerQueryForNonexistentToken.selector);
            for (;;) {
                unchecked {
                    packed = _packedOwnerships[--tokenId];
                }
                if (packed == 0) continue;
                if (packed & _BITMASK_BURNED == 0) return packed;
                _revert(OwnerQueryForNonexistentToken.selector);
            }
        }tokenId 的传参范围:1 <= tokenId <11;
获取 tokenId 的打包数据,packed = _packedOwnerships[5];
如果 packed 为空,依次往下获取 (tokenId - 1) 的打包数据,直到 packed不为空。
最终, packed = _packedOwnerships[1];解析packed 后,owner = AAAA。
查询 tokenId 的 owner 是一个循环往下遍历的过程,直到数据不为空。
    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public payable virtual override {
        uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId);
        // Mask `from` to the lower 160 bits, in case the upper bits somehow aren't clean.
        from = address(uint160(uint256(uint160(from)) & _BITMASK_ADDRESS));
        if (address(uint160(prevOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector);
        (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId);
        // The nested ifs save around 20+ gas over a compound boolean condition.
        if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A()))
            if (!isApprovedForAll(from, _msgSenderERC721A())) _revert(TransferCallerNotOwnerNorApproved.selector);
        _beforeTokenTransfers(from, to, tokenId, 1);
        // Clear approvals from the previous owner.
        assembly {
            if approvedAddress {
                // This is equivalent to `delete _tokenApprovals[tokenId]`.
                sstore(approvedAddressSlot, 0)
            }
        }
        // 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 {
            // We can directly increment and decrement the balances.
            --_packedAddressData[from]; // Updates: `balance -= 1`.
            ++_packedAddressData[to]; // Updates: `balance += 1`.
            // Updates:
            // - `address` to the next owner.
            // - `startTimestamp` to the timestamp of transfering.
            // - `burned` to `false`.
            // - `nextInitialized` to `true`.
            _packedOwnerships[tokenId] = _packOwnershipData(
                to,
                _BITMASK_NEXT_INITIALIZED | _nextExtraData(from, to, prevOwnershipPacked)
            );
            // If the next slot may not have been initialized (i.e. `nextInitialized == false`) .
            if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) {
                uint256 nextTokenId = tokenId + 1;
                // If the next slot's address is zero and not burned (i.e. packed value is zero).
                if (_packedOwnerships[nextTokenId] == 0) {
                    // If the next slot is within bounds.
                    if (nextTokenId != _currentIndex) {
                        // Initialize the next slot to maintain correctness for `ownerOf(tokenId + 1)`.
                        _packedOwnerships[nextTokenId] = prevOwnershipPacked;
                    }
                }
            }
        }
        // Mask `to` to the lower 160 bits, in case the upper bits somehow aren't clean.
        uint256 toMasked = uint256(uint160(to)) & _BITMASK_ADDRESS;
        assembly {
            // Emit the `Transfer` event.
            log4(
                0, // Start of data (0, since no data).
                0, // End of data (0, since no data).
                _TRANSFER_EVENT_SIGNATURE, // Signature.
                from, // `from`.
                toMasked, // `to`.
                tokenId // `tokenId`.
            )
        }
        if (toMasked == 0) _revert(TransferToZeroAddress.selector);
        _afterTokenTransfers(from, to, tokenId, 1);
    }        uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId);
        // Mask `from` to the lower 160 bits, in case the upper bits somehow aren't clean.
        from = address(uint160(uint256(uint160(from)) & _BITMASK_ADDRESS));
        if (address(uint160(prevOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector);uint256 prevOwnershipPacked = _packedOwnershipOf(5);
依次往下找,prevOwnershipPacked = _packedOwnerships[1];
解析数据,拿到 tokenId 的owner,与 from 地址做校验。
        (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId);
        // The nested ifs save around 20+ gas over a compound boolean condition.
        if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A()))
            if (!isApprovedForAll(from, _msgSenderERC721A())) _revert(TransferCallerNotOwnerNorApproved.selector);
        _beforeTokenTransfers(from, to, tokenId, 1);
        // Clear approvals from the previous owner.
        assembly {
            if approvedAddress {
                // This is equivalent to `delete _tokenApprovals[tokenId]`.
                sstore(approvedAddressSlot, 0)
            }
        }校验 from 是否将 tokenId 授权给 调用者;之后清除 tokenId 的授权信息。
        unchecked {
            // We can directly increment and decrement the balances.
            --_packedAddressData[from]; // Updates: `balance -= 1`.
            ++_packedAddressData[to]; // Updates: `balance += 1`.
            // Updates:
            // - `address` to the next owner.
            // - `startTimestamp` to the timestamp of transfering.
            // - `burned` to `false`.
            // - `nextInitialized` to `true`.
            _packedOwnerships[tokenId] = _packOwnershipData(
                to,
                _BITMASK_NEXT_INITIALIZED | _nextExtraData(from, to, prevOwnershipPacked)
            );
            // If the next slot may not have been initialized (i.e. `nextInitialized == false`) .
            if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) {
                uint256 nextTokenId = tokenId + 1;
                // If the next slot's address is zero and not burned (i.e. packed value is zero).
                if (_packedOwnerships[nextTokenId] == 0) {
                    // If the next slot is within bounds.
                    if (nextTokenId != _currentIndex) {
                        // Initialize the next slot to maintain correctness for `ownerOf(tokenId + 1)`.
                        _packedOwnerships[nextTokenId] = prevOwnershipPacked;
                    }
                }
            }
        }更新 AAAA, BBBB 的余额信息;balance_AAAA = 9, balance_BBBB = 1;
// Mapping from token ID to ownership details
// Bits Layout:
// - [0..159]   `addr`
// - [160..223] `startTimestamp`
// - [224]      `burned`
// - [225]      `nextInitialized`
// - [232..255] `extraData`更新tokenId = 5 的打包信息;_packedOwnerships[5] =(BBBB, block.timestamp, false, true, extraData); 此时,需要 nextInitialized = true。
如果 prevOwnershipPacked 的 nextInitialized = false,即 下一个tokenId的打包信息没有初始化;那么就要对现在tokenId = 5的下一个进行初始化。
_packedOwnerships[6] = prevOwnershipPacked。
(也可以这么理解,在转移TokenId为 N 的NFT时,如果TokenId为 N + 1 的NFT没有被初始化过,就要将要TokenId为 N 的打包信息赋值给TokenId为 N + 1;然后更新TokenId为 N 的打包信息)
为什要这样呢? 因为需要将 prevOwnershipPacked 赋值给 _packedOwnerships[6],初始化tokenId = 6 打包数据 。不然你查找owner(10)时将查到 _packedOwnerships[5]的 owner 是 BBBB; AAAA 的 NFT 无缘无故的丢了,肯定大哭不愿意啊!
Transfer Event    // Mask `to` to the lower 160 bits, in case the upper bits somehow aren't clean.
    uint256 toMasked = uint256(uint160(to)) & _BITMASK_ADDRESS;
    assembly {
        // Emit the `Transfer` event.
        log4(
            0, // Start of data (0, since no data).
            0, // End of data (0, since no data).
            _TRANSFER_EVENT_SIGNATURE, // Signature.
            from, // `from`.
            toMasked, // `to`.
            tokenId // `tokenId`.
        )
    }
    if (toMasked == 0) _revert(TransferToZeroAddress.selector);
    _afterTokenTransfers(from, to, tokenId, 1);在 NFT的一次转移过程中,最多更新两个token(N和 N + 1)的打包数据。
Foundry 执行日志: [PASS] test_Bob_Transfer_Owner_AscOrder() (gas: 333103) [PASS] test_Bob_Transfer_Owner_DescOrder() (gas: 342060)
[PASS] test_Bob_Transfer_Owner_AscOrder() (gas: 333103)
Traces:
  [333103] ERC721ATest::test_Bob_Transfer_Owner_AscOrder()
    ├─ [0] VM::startPrank(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e])
    │   └─ ← [Return]
    ├─ [62840] MockERC721A::transferFrom(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 11)
    │   ├─ emit Transfer(from: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], to: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], tokenId: 11)
    │   └─ ← [Stop]
    ├─ [29340] MockERC721A::transferFrom(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 12)
    │   ├─ emit Transfer(from: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], to: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], tokenId: 12)
    │   └─ ← [Stop]
    ├─ [29340] MockERC721A::transferFrom(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 13)
    │   ├─ emit Transfer(from: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], to: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], tokenId: 13)
    │   └─ ← [Stop]
    ├─ [29340] MockERC721A::transferFrom(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 14)
    │   ├─ emit Transfer(from: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], to: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], tokenId: 14)
    │   └─ ← [Stop]
    ├─ [29340] MockERC721A::transferFrom(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 15)
    │   ├─ emit Transfer(from: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], to: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], tokenId: 15)
    │   └─ ← [Stop]
    ├─ [29340] MockERC721A::transferFrom(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 16)
    │   ├─ emit Transfer(from: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], to: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], tokenId: 16)
    │   └─ ← [Stop]
    ├─ [29340] MockERC721A::transferFrom(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 17)
    │   ├─ emit Transfer(from: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], to: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], tokenId: 17)
    │   └─ ← [Stop]
    ├─ [29340] MockERC721A::transferFrom(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 18)
    │   ├─ emit Transfer(from: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], to: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], tokenId: 18)
    │   └─ ← [Stop]
    ├─ [29340] MockERC721A::transferFrom(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 19)
    │   ├─ emit Transfer(from: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], to: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], tokenId: 19)
    │   └─ ← [Stop]
    ├─ [9125] MockERC721A::transferFrom(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 20)
    │   ├─ emit Transfer(from: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], to: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], tokenId: 20)
    │   └─ ← [Stop]
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return]
    └─ ← [Stop]
[PASS] test_Bob_Transfer_Owner_DescOrder() (gas: 342060)
Traces:
  [342060] ERC721ATest::test_Bob_Transfer_Owner_DescOrder()
    ├─ [0] VM::startPrank(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e])
    │   └─ ← [Return]
    ├─ [80004] MockERC721A::transferFrom(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 20)
    │   ├─ emit Transfer(from: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], to: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], tokenId: 20)
    │   └─ ← [Stop]
    ├─ [29063] MockERC721A::transferFrom(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 19)
    │   ├─ emit Transfer(from: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], to: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], tokenId: 19)
    │   └─ ← [Stop]
    ├─ [28822] MockERC721A::transferFrom(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 18)
    │   ├─ emit Transfer(from: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], to: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], tokenId: 18)
    │   └─ ← [Stop]
    ├─ [28581] MockERC721A::transferFrom(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 17)
    │   ├─ emit Transfer(from: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], to: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], tokenId: 17)
    │   └─ ← [Stop]
    ├─ [28340] MockERC721A::transferFrom(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 16)
    │   ├─ emit Transfer(from: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], to: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], tokenId: 16)
    │   └─ ← [Stop]
    ├─ [28099] MockERC721A::transferFrom(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 15)
    │   ├─ emit Transfer(from: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], to: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], tokenId: 15)
    │   └─ ← [Stop]
    ├─ [27858] MockERC721A::transferFrom(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 14)
    │   ├─ emit Transfer(from: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], to: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], tokenId: 14)
    │   └─ ← [Stop]
    ├─ [27617] MockERC721A::transferFrom(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 13)
    │   ├─ emit Transfer(from: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], to: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], tokenId: 13)
    │   └─ ← [Stop]
    ├─ [27376] MockERC721A::transferFrom(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 12)
    │   ├─ emit Transfer(from: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], to: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], tokenId: 12)
    │   └─ ← [Stop]
    ├─ [9925] MockERC721A::transferFrom(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 11)
    │   ├─ emit Transfer(from: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], to: owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], tokenId: 11)
    │   └─ ← [Stop]
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return]
    └─ ← [Stop] 
                如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!