ERC-721A 是由 Azuki 研发的 ERC-721 实现。ERC-721A,已经运用到了他們发行的 Azuki NFT Collection 中。根据他们的文档介绍,铸造一个NFT 所消耗的 gas fee,相比 OZ ERC721,节省了一倍多,如果是一次mint 5 个,节省的 gas 甚至达到 7 倍。
gas费用对比

ERC-721A优化

优化 1 - 从 OpenZeppelin (OZ) ERC721Enumerable 中删除重复存储
IERC721Enumerable 的广泛使用的 OZ 实现包括每个令牌元数据的冗余存储。这种非规范化的方法以编写函数的巨大成本优化读取函数,考虑到用户不太可能为读取函数付费,这并不理想。此外,我们的令牌从 0 开始编号,这一事实让我们从基本实现中删除了一些冗余存储。我们强烈建议所有新发布的产品在寻找大赢家时仔细检查此文件。
ERC721Enumerable接口提供了读取NFT发现量,持有者,TokenID等情况,由智能合约来实现:

tokenByIndex(): //返回指定位置NFT的TokenID
tokenOfOwnerByIndex(): //返回指定地址的所有TokenID

首先 ERC721a 对 ERC721Enumerable 中的实现做了优化,去除了一些不必要的存储。
OZ ERC721Enumerable:

uint256[] private _allTokens;
/**
  * @dev See {IERC721Enumerable-totalSupply}.
  */
function totalSupply() public view virtual override returns (uint256) {
   return _allTokens.length;
}

Azuki ERC721a:

/**
  * @dev See {IERC721Enumerable-totalSupply}.
  */
function totalSupply() public view override returns (uint256) {
    return currentIndex;
}

可以看到相比 OZ ERC721Enumerable, Azuki ERC721a 沒有使用昂贵的 array 存储空间來保存 allTokens, 而是直接用 currentIndex 來返回(之所以能这样做是因为 Azuki ERC721a 的所有 tokenID 都是从 0 开始,逐个增长)。
Storage 存储空间的优化
在 @openzeppelin/ERC721Enumerable 实现中,为了方便读取 NFT 的所有者信息,做了许多冗余的元数据存储,作为代价,在 mint 函数内,则需要额外的开销来存储这些信息。而 ERC721A 实现则相反,将所占的必须存储压缩到了最小,这样虽然增加了读取操作的复杂度,但是,读取是免费的。
@openzeppelin/ERC721Enumerable 中所用的存储:

    //存储钱包地址==>(tokenIDindex==>tokenID)
    mapping(address => mapping(uint256 => uint256)) private _ownedTokens;
    //保存了该 NFT ID 到用户拥有索引的映射,如_ownedTokensIndex[201] = 0 表示 ID 为      201 的该 NFT 是所属用户的拥有列表中的第一个。
    mapping(uint256 => uint256) private _ownedTokensIndex;
    //表示了所以被 mint 出来的该 NFT 的 ID 列表。
    uint256[] private _allTokens;
    //表示了具体某个 ID 的 NFT 在 _allTokens 列表中的位置
    mapping(uint256 => uint256) private _allTokensIndex;
    // ...
}

而在 ERC721A 的实现中,去除了那两个冗余索引:

    Context,
    ERC165,
    IERC721,
    IERC721Metadata,
    IERC721Enumerable
{
  struct TokenOwnership {
      address addr;
      uint64 startTimestamp;
  }

  struct AddressData {
      uint128 balance;
      uint128 numberMinted;
  }
//ID => 钱包地址
  mapping(uint256 => TokenOwnership) private _ownerships;
//钱包地址 => 所有数量
  mapping(address => AddressData) private _addressData;

  // ...
}

优化 2 - 每个批次铸币请求更新所有者的余额一次,而不是每个铸币 NFT
假设 Alice 有 2 个代币并想再购买 5 个。在 Solidity 中,更新储值需要消耗 gas。因此,如果我们在存储中跟踪 Alice 拥有多少代币,那么通过一次更新将 Alice 的持有量从 2 直接更新到 7 会更便宜,而不是将该值更新 5 次(在OZ中需要逐个mint,并且每个额外的代币增加一次,从 2 到 3、3到 4 等)。
虽然这是一个相对简单的概念,但 NFT 领域的绝大多数批量铸币厂还没有采用这一点,因为 OZ 默认实现不包括批量铸币厂 API,并且很容易在不调整的情况下从现成的现有解决方案中获取它. 我们强烈建议所有支持批量铸币的项目都考虑这个技巧。
Azuki ERC-721A mint function:

    address to,
    uint256 quantity,
    bytes memory _data
) internal {
    uint256 startTokenId = currentIndex;
    
    ...
    AddressData memory addressData = _addressData[to];
    _addressData[to] = AddressData(
        addressData.balance + uint128(quantity),
        addressData.numberMinted + uint128(quantity)
    );
    _ownerships[startTokenId] = TokenOwnership(to, uint64(block.timestamp));
    uint256 updatedIndex = startTokenId;
    for (uint256 i = 0; i < quantity; i++) {
        emit Transfer(address(0), to, updatedIndex);
        require(
            _checkOnERC721Received(address(0), to, updatedIndex, _data),
            "ERC721A: transfer to non ERC721Receiver implementer"
        );
        updatedIndex++;
    }
    currentIndex = updatedIndex;
    _afterTokenTransfers(address(0), to, startTokenId, quantity);
}

这样一来,ERC721A 做到了就把对 storage 的写入从 O(N) 优化到了 O(1) 。单次 mint 的数量越多,优化效果则越明显。
优化 3 - 每个批次铸币请求更新一次所有者数据,而不是每个铸币 NFT
这在精神上类似于优化 2。假设 Alice 想购买 3 个代币 - 代币 #100、#101 和 #102。与其将 Alice 保存为所有者 3 次(每次都要花费我们的 gas),我们可以改为只保存一次所有者值,这种方式在语义上意味着 Alice 拥有所有 3 个令牌。
如何?假设 Alice 铸造代币 #100、#101 和 #102,而 Bob 铸造代币 #103 和 #104。内部所有者跟踪器如下所示:
token队列.png
这里的关键是,如果我们想查看谁拥有 #102,我们实际上不需要将 Alice 明确设置为 #102 的明确所有者。我们可以更改 ownerOf 函数来执行以下操作:
OwnerOf.png
关键见解:如果我们将其实现更改为递减,直到它找到明确的所有者集,ownerOf 仍然按预期工作。
虽然如果代币不是 HODL 的话,这些延迟的所有者写入可能仍会在代币生命周期的后期发生,但我们仍然期望从整体上节省大量净成本,因为这可以减少铸币厂的 gas 消耗,从而降低集中 gas 的严重性薄荷时间整个生态系统的峰值。这种优化涉及一些额外的逻辑,特别是在传输方面,

ERC-721A存在的问题

因为Azuki ERC-721A中TokenID必须要保持连续性为基础,所以目前Azuki ERC-721A并没有提供 burn Token 功能。所以要使用类似销毁NFT的功能,Azuki ERC-721A则不试用。如果你的需求是TokenID是随机数,那么Azuki ERC-721A也不适用。