NFT合约分析:ERC721A

本文详述了ERC721A合约的实现,重点在于其批量铸造的惰性初始化机制如何降低gas消耗。相较于传统ERC721,ERC721A在批量铸造时避免了频繁更新存储,从而节约成本。虽然首次转账成本增加,但后续转账成本降低,更适合大规模铸造场景。文章还分析了合约的存储结构、铸造、授权、转账和销毁等关键功能。
摘要由CSDN通过智能技术生成

概述

读者可前往我的博客获得更好的阅读体验。

本文主要介绍标准NFT实现的一个变体,即ERC721A合约实现的相关细节。ERC721A是由著名NFT系列Azuki提出,该系列NFT是著名的蓝筹NFT。本文主要聚焦于Azuki提出的ERC721A合约的代码细节分析。

与传统的ERC721实现相比,ERC721A在批量铸造(batch mint)方面具有显著的gas优势,这得益于ERC721A的惰性初始化方面的设计。关于ERC721A与普通ERC721实现的对比,我们将会在下文展开说明。

本文要求读者具有基础的solidity知识,希望读者对标准ERC721有所了解。

读者可在阅读本文前,酌情阅读以下参考材料:

本文基于目前的最新版本(4.2.3)合约代码进行分析。

ERC721实现

由于下文涉及到ERC721AERC721的技术对比,考虑到部分读者可以对ERC721合约实现并不清楚,本节简要的介绍ERC721正常实现的铸造功能,本节主要基于solmate的实现版本。

solmate实现都较为短小精悍且经过gas优化,我个人较为推崇。solmateERC721实现仅有 231 行,读者可自行阅读。

solmate合约中,我们可以看到核心数据结构为:

mapping(uint256 => address) internal _ownerOf;
mapping(address => uint256) internal _balanceOf;

其中,各映射功能如下:

  • _ownerOf 记录 tokenId 与持有者的关系
  • _balanceOf 记录持有人所持有的 NFT 数量

其铸造方法定义如下:

function _mint(address to, uint256 id) internal virtual {
    require(to != address(0), "INVALID_RECIPIENT");

    require(_ownerOf[id] == address(0), "ALREADY_MINTED");

    // Counter overflow is incredibly unrealistic.
    unchecked {
        _balanceOf[to]++;
    }

    _ownerOf[id] = to;

    emit Transfer(address(0), to, id);
}

通过此函数,我们更新了_ownerOf_balanceOf实现用户铸造 NFT 的功能。我们可以发现用户每次铸造NFT都需要更新_ownerOf_balanceOf映射。众所周知,在操作码gas消耗中,更新存储需要消耗大量gas。如果用户批量铸造,会在此过程中消耗大量gas

根据数据(PDF警告),在ETH价格为 1500 美元时,更新存储的价格为 7.5 美元,而写入存储的价格为 30 美元。这意味着仅在mint过程中,更新映射会浪费大量资产。

转账函数定义如下:

function transferFrom(
    address from,
    address to,
    uint256 id
) public virtual {
    require(from == _ownerOf[id], "WRONG_FROM");

    require(to != address(0), "INVALID_RECIPIENT");

    require(
        msg.sender == from || isApprovedForAll[from][msg.sender] || msg.sender == getApproved[id],
        "NOT_AUTHORIZED"
    );

    // Underflow of the sender's balance is impossible because we check for
    // ownership above and the recipient's balance can't realistically overflow.
    unchecked {
        _balanceOf[from]--;

        _balanceOf[to]++;
    }

    _ownerOf[id] = to;

    delete getApproved[id];

    emit Transfer(from, to, id);
}

由于对于每个tokenId都维护有一个mapping映射,所以转账逻辑实现也较为简单。

总体来看,对于每一个NFT,在solmate实现的智能合约中,都维持有以下两个映射:

  • mapping(uint256 => address) internal _ownerOf; 标识NFT的拥有者
  • mapping(uint256 => address) public getApproved; 记录NFT的授权情况

优势

在上一节中,我们介绍了常规NFT实现的基本情况,正如上文所述,常规实现在批量mint铸造阶段会消耗大量gas。为了解决这一问题,ERC721A引入惰性初始化机制。简单来说,在批量铸造时,不再记录tokenId与用户地址的映射关系,而是记录起始tokenId和数量与用户的映射关系。在本节中,我们不对此实现的技术细节进行分析,我们会在本文稍后部分对此进行讨论。

在批量铸造阶段,ERC721AOpenZeppelin实现的对比如下:

ERC721 ERC721A
批量铸造 5 个 NFT 155949 gas 63748 gas
转移 5 个 NFT 226655 gas 334450 gas
铸造的 Base Fee 200 gwei 200 gwei
转移的 Base Fee 40 gwei 40 gwei
总花费 0.0403 ether 0.0261 ether

如果读者对于此处的gas计算的细节感兴趣,可以阅读以太坊机制详解:Gas Price计算。我们在此处不详细讨论计算方式。我们可以注意到铸造阶段的Base fee较高,这考虑到了NFT铸造导致的网络拥堵情况。

显然,惰性初始化机制对于批量铸造阶段的gas节省是具有明显优势的,但惰性加载将初始化的成本转移到了转账部分,我们可以看到在转移NFT时的成本有所上升。但需要注意,第一次转账后由于彻底完成了初始化,所有后续转账的成本会降低,如下:

ERC721 ERC721A
First transfer 45331 gas 92822 gas
Subsequent transfers 45331 gas 44499 gas

通过表格可以看出,除第一次转账消耗的gas明显增多,但随后转账的价格与常规的NFT转账并无区别。

总结来说,ERC721A实现了低成本的批量铸造,但将部分成本转移到了第一次转账中。这种设计充分考虑到了铸造阶段可能出现的以太坊网络拥堵而造成gas价格飙升的情况,而用户后期转账是偶发的且不会导致网络拥堵的。通过这种特殊的成本转嫁机制,ERC721A降低用户的总成本。

换言之,如果您认为您的NFT项目不存在批量铸造的情况或不会导致以太坊网络拥堵,可以选择常规NFT实现。

具体实现

在讨论了ERC721A的基本内容后,为进一步增加我们对ERC721A的理解,我们将对其合约进行阅读分析。ERC721A的开源仓库位于github。此处,我们仅讨论ERC721A的主合约,而暂不讨论extensions部分。

对于NFT合约的分析,存储数据结构和_mint函数是一个很好的入手点。我们首先关注存储数据结构。

在NFT数据存储中,我们可以看到solmate等常规实现都使用了mapping(uint256 => address) internal _ownerOf将单个tokenId与持有者对应。但ERC721A是对批量铸造进行特殊优化的,开发者认为在批量铸造过程中,用户持有的NFT的tokenId往往是连续的,如下图:

ERC721A TokenId

基本数据结构

在批量铸造过程中,用户铸造连续的NFT是极其常见的。为了实现连续分配tokenID以降低gas消耗的目的,我们需要一些更加复杂的数据结构设计,具体代码设计如下:

// The next token ID to be minted.
uint256 private _currentIndex;

// The number of tokens burned.
uint256 private _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 {_packedOwnershipOf} implementation for 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;

// Mapping from token ID to approved address.
mapping(uint256 => TokenApprovalRef) private _tokenApprovals;

// Mapping from owner to operator approvals
mapping(address => mapping(address => bool)) private _operatorApprovals;

与其他简单参数相比,我们主要关注复杂的参数:

  1. _packedOwnerships 类似常规NFT实现中的_ownerOf,我们通过此映射查询某 tokenID 的拥有者,但此结构是打包方式的,即我们并不
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WongSSH

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值