分析 ERC721A 源码节省Gas优化思路

  • Meta
  • 更新于 2024-03-29 09:38
  • 阅读 328

分析ERC721A源码节省Gas优化思路

分析 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;

1. 分析 _mint(to, quantity)

    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);
    }

以 _mint(AAAA,10); _currentIndex = 1 为例,剖析执行流程

(1) 获取将要开始铸造的TokenId

    uint256 startTokenId = _currentIndex;
    if (quantity == 0) _revert(MintZeroQuantity.selector);

    _beforeTokenTransfers(address(0), to, startTokenId, quantity);

startTokenId = 1;

(2) 更新mapping(tokenId ==> (owner, startTime, ....))

        // 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]。

(3) 更新 mapping(owner => (balance, numberMinted, ...))

            // Updates:
            // - `balance += quantity`.
            // - `numberMinted += quantity`.
            //
            // We can directly add to the `balance` and `numberMinted`.
            _packedAddressData[to] += quantity * ((1 << _BITPOS_NUMBER_MINTED) | 1);

更新 接收者 持有token的数量以及铸造token的数量。

(4) 循环生成 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);

(5) 更新 _currentIndex

    _currentIndex = end;
    _afterTokenTransfers(address(0), to, startTokenId, quantity);

_currentIndex = 11;

总结

无论铸造几个 NFT,都只更新 3 个 Slot,外加 N 个 Transfer 事件(必须),这就是 ERC721 批量 mint 节省 GAS 的精髓。

2. 分析 ownferOf(tokenId)

    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);
    }

以 ownerOf(5) 为例

_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 是一个循环往下遍历的过程,直到数据不为空。

3. 分析 transferFrom(from, to, tokenId)

    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);
    }

以 transferFrom(AAAA, BBBB, 5) 为例

(1) 获取 tokenId 的打包数据(owner)

        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 地址做校验。

(2) 校验 from 是否将 tokenId 授权给 _msgSender

        (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 的授权信息。

(3) 更新 form, to 的余额信息,及 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 无缘无故的丢了,肯定大哭不愿意啊!

(4) 生成 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)的打包数据。

4. 有这么一个问题,CCCC批量铸造了5个NFT,tokenId 分别为 11,12,13,14,15。现在他要将这5个NFT分5次转给DDDD,那么他应该从tokenId为11还是15开始呢?

首先肯定是要执行5次 transferFrom(CCCC, DDDD, tokenId)。

假如从15 ==> 11, transferFrom(CCCC, DDDD, 15),查询owner执行了 5次,初始化 15 的打包信息; transferFrom(CCCC, DDDD, 14),查询owner执行了 4次,初始化 14 的打包信息; transferFrom(CCCC, DDDD, 13),查询owner执行了 3次,初始化 13 的打包信息; transferFrom(CCCC, DDDD, 12),查询owner执行了 2次,初始化 12 的打包信息; transferFrom(CCCC, DDDD, 11),查询owner执行了 1次,更新 11 的打包信息。

假如从11 ==> 15, transferFrom(CCCC, DDDD, 11),查询owner执行了 1次,更新 11 的打包信息,初始化12的打包信息; transferFrom(CCCC, DDDD, 12),查询owner执行了 1次,更新 12 的打包信息,初始化13的打包信息; transferFrom(CCCC, DDDD, 13),查询owner执行了 1次,更新 13 的打包信息,初始化14的打包信息; transferFrom(CCCC, DDDD, 14),查询owner执行了 1次,更新 14 的打包信息,初始化15的打包信息; transferFrom(CCCC, DDDD, 15),查询owner执行了 1次,更新 15 的打包信息。

点赞 1
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Meta
Meta
0x0148...de2f
Solidity