🔵这是NETICI LABS的第一篇文章分享。NL试图打造一个有温度的 Web3 【玩具】试验室。这里有硬核技术分析文章,也有市场观察分析,希望大家一起来玩。
🟡The next big thing will start out looking like a toy.
🖌作者:arc0xc9
团队:Chiru Labs,位于洛杉矶,团队成员有守望先锋的前角色艺术总监、Facebook前工程师、谷歌的前工程师等。
1. 项目方链下准备好图片和白名单。图片上传到IPFS上。
2. 项目方部署合约:在2022年1月10日,项目方部署了合约,并携带如下参数: a) batchsize:5。表示每个用户最多挖5个 b) collectionSize:10000 c) amountForDev: 200 d) amountForAuctionAndDev:8900
3. 项目方设置baseUri:设置基础URL为“https://azuki-prereveal.s3-us-west-1.amazonaws.com/metadata”
注意这个时候,每个token对应哪张图片,还不是确定的,根据这里面访问具体的token,发现图片url都是一个bean.gif:

4. 项目方设置拍卖起始时间:通过setAuctionSaleStartTime函数,将售卖启动时间设置为2022-1-13: 02:00:00 北京时间
5. 项目方获取保留token:通过devMint函数,为自己保留200个token。
6. 用户开始竞拍:项目开售后,立即有大量用户调用auctionMint,挖掘token。而且是批量挖掘。甚至可以看到很多人在等着上线那一瞬间开抢,产生了许多失败的交易:

关于auction模式的拍卖价格,采用下降式荷兰式拍卖,即价格随时间推移,线性衰减到一个终止价格。如果当前时间已经过了拍卖时间,则采用最终价格。价格下降的公式为: startPrice - Δt * (startPrice - endPrice)/auctionInterval 具体参数如下:
a) 起始售价:1 ether(彼时约3300U) b) 终止售价:0.15 ether c) 拍卖时长:340分钟,即5小时零40分钟。 d) 价格下降机制:在通常的荷兰式拍卖中,每下降1s,那么价格也会随之下降。在Azuki中,对此稍有不同,它的价格是每20分钟下降一次。也就是说,340分钟的拍卖时间被均分为17份,每一份时间值20分钟。那么每经过这么一份时间,价格就会下降(1-0.15)/17 = 0.05 ether。
这一次拍卖非常成功,在340分钟内,所有拍卖的NFT都已经售卖完毕,售罄后仍有大量失败的auctionMint请求,报错:

7. 项目方设置白名单。于2022-1-14日 北京时间 凌晨两点前,多次调用seedAllowList,将数百个白名单账户设置到合约中。某一次调用产生了大概99307153个gas,对应gas price(190gwei),这笔交易的gas消耗约1.77 ether(当时5700刀)。相比之下普通的auctionMint个87393个gas,结合gas price(5669 gwei),仅产生0.5 ether(当时1500 u)
8. 项目方结束拍卖,开启白名单和公售。于2022-1-14 凌晨两点调用endAuctionAndSetupNonAuctionSaleInfo。开启白名单售卖和公售。附带参数:
mintlistPriceWei:“500000000000000000”,即0.5 ether。这是白名单用户的mint价格
publicPriceWei:“1000000000000000000”,即1 ether。这是公售价格
publicSaleStartTime:“1642269600”,即2022年1月16日凌晨2点
9. 白名单用户开始mint:调用allowlistMint。一次性只能挖一个,挖完后,链上会删除该账户信息。
10. 用户开始public sale mint:调用public sale mint.
11.项目方关闭公售。在2022-1-19 凌晨1点,调用endAuctionAndSetupNonAuctionSaleInfo,附带参数:
mintlistPriceWei:“500000000000000000”,即0.5 ether。这是白名单用户的mint价格。
publicPriceWei:“0”,即1 ether。这是公售价格。
publicSaleStartTime:“1674145770”,即2023年1月20日.
12. 项目方setBaseUri:setBaseUri,设置为“https://ikzttp.mypinata.cloud/ipfs/QmeBWSnYPEnUimvpPfNHuvgcK9wFH9Sa6cZ4KDfgkfJJis/”。
用户可以通过这个地址访问自己的元数据了:

13. 项目方提现:项目调用withdrawMoney来提现。
14. 接入二级市场:项目方通过OpenSea的wyvern,接入交易市场。
15. 用户查看自己的图片:调用getTokenUri,查到自己的元数据URL,访问后得到元数据JSON。然后顺着JSON里的链接,找到自己的图片。
1.ERC721A:号称使用了更剩gas的ERC721A标准,也就是一次gas费,铸造多个NFT。
2.售卖分四个阶段:项目方(免费)、拍卖(1 ether to 0.15 ether)、白名单售卖(0.5 ether)、公售(1 ether)。
3.tokenId和图片的映射由项目方自己说了算:不像无聊猿猴那样在链上计算了(由合约说了算)
4.数据存储:售卖前,元数据和图片(也就是那个红豆)放到中心化服务器上。售卖结束后,元数据和图片都存在IPFS上(借助pianata这个专用的NFT服务)
ERC721A和ERC721的差别不在于接口,而在于实现。
从IERC721A.sol的接口文件来看,包含的接口和通常的ERC721Enumerable无差别: balanceOf、ownerOf、safeTransferFrom、transferFrom、approve、setApproveForAll 、getApproved、isApprovedForAll、name、symbol、tokenURI,在早期版本还包括totalSupply, tokenByIndex, tokenOfOwnerByIndex(目前版本则干脆直接砍掉了) 实现的差别主要体现在ERC721A.sol,也就是实现层面。
💡优化1:砍掉了ERC721Enumerable的多余存储
首先从存储的内容来看,核心状态只包括如下:
用户的信息:userAddress -> (balance, numberMinted)
NFT的信息:tokenId -> (addr, startMintTime)。在新版本还陆续增加了一些信息,例如24位的extraData
NFT许可授权:tokenId->addr
用户授权信息:address->(address->allowed)
这里面,去除了ERC721Enumerable中的大数组,这些数组用于支持tokenByIndex,tokenOfOwnerByIndex等操作,维护它们,就意味着每次操作都要多写状态变量,从而消耗额外的gas。

那么早期721A是怎么实现tokenByIndex和tokenOfOwnerByIndex的?对于前者,由于tokenId和index一直相等(因为不支持burn),因此返回index即可。对于tokenOfOwnerByIndex,则会遍历所有token,直到查找到特定用户的特定token。由于这个操作在查询端,不计入交易,所以性能虽然低,但也可以接受。注意,新版本中已经移除了这几个函数,同时增加了burn的支持。
💡优化2:mint支持批操作
从mint的方式来看:mint的id由合约通过计数器生成,而不是ERC721那样由外部指定。此外,mint支持传入quantity,这样数据更新可以一次性完成。而对于OZ,如果想一次性挖出多个token,则需要调用多次_safeMint。下图是OZ的_mint函数:

而对于ERC721A,只需直接给相关数据结构更新1次即可:

💡优化3:最大限度减少更新数据的频率
假如Alice挖掘了5个token,那么只有第一个token会记录它的属主信息。如果想查找剩余4个token的属主,则向前找到第一个属主不为空的token,取它的属主。

回看的逻辑如下:

如果发生了token所有权转让,则该token下一个的属主进行更新:

**🔵优点:**ERC721A的主要优势是降低了mint的成本。
存储优化:在ERC721Enumerable中,存储了allTokens,在ERC721A中去除了这些存储只用一个index记录目前总共的供应量。721A砍掉了这部分数据结构,节省了更新它们的gas。
批量操作:mint支持传入quantity,这样数据更新可以一次性完成。 节省更新:此外,每个用户挖一组token的时候,只有第一个token保存属主信息。
🔴缺点 :
token由ERC721A合约维护,且必须连续递增。在ERC721中,tokenId是用户自己维护的,可以任意指定方式
砍掉了enumerable的功能
📍编码细节
有很多callerIsUser修饰符,用于确保不是合约在调用,而是外部账户在调用自己的函数
转账操作采用call,不像BAYC采用transfer。这是符合业界要求的做法

transfer的三大特点:如果接受者是合约,则必须定义一个payable 的fallback函数(or receive());如果转账失败,会自动回滚; 为fallback提供了2300gas,仅够丢出事件,用于防止重入攻击。


call的特点:可以自定义调用哪个函数,而不仅仅是fallback函数;如果转账失败,不会自动回滚,需通过success返回值回滚;可以自定义gas(默认还是2300)。

尽可能的压缩存储,比如时间戳就用32位存储:

注意,压缩存储不仅对struct有效,对状态的存储也有效,两个uint128总共占据1个slot。

管理类合约,和mint本身都不抛出事件,事件依赖ERC721A自己来抛
ERC721A方面,大量运用assembly,包括shl等位操作
ERC721A方面,采用了定义error + revert的方式,比较优雅的抛出错误
azuki合约:https://etherscan.io/address/0xed5af388653567af2f388e6224dc7c4b3241c544#code
erc721A解读:https://mirror.xyz/xyyme.eth/BSamlJFCpc1NyCNlaLPTC9zGN87qAuktBPEeR3jDBhs transaction fee vs gas fee: https://cryptobriefing.com/understanding-ethereums-gas-transaction-fees/
ERC721A: https://github.com/chiru-labs/ERC721A/blob/main/contracts/IERC721A.sol https://www.frank.hk/blog/azuki-erc721a/?hmsr=joyk.com&utm_source=joyk.com&utm_medium=referral
🟡🔵🔴
Twitter: neticilabs
即刻:NeticiLabs

