Solidity项目分析之 PXN 合约解读

前言

大家好啊,我是大咪,现在是北京时间的2022年05月07日11:03:25,最近在参与 youtuber 【nft黑魔法】老师的 discord 课程作业。

这周的作业是以读合约的视角,分析近期大热项目 PXN 的合约,除了代码方面的分析,我还加上了 etherscan 上的一些链上数据作为分析,区块链有意思的点就是在于链上追溯数据,一切都是公开透明的交易。

目前我也还是在学习阶段,希望本篇复盘可以对你在学习的路上有所帮助,若有问题,还望多多交流。

PS:PXN已经发售完毕,本次合约是分析的主网合约。

我的推特:https://twitter.com/dami2333

黑魔法老师推特:https://twitter.com/MrsZaaa

NFT黑魔法频道:https://www.youtube.com/c/NFT黑魔法

黑魔法discord:https://discord.gg/CTfK9fH3aQ

首先,先来根据 opensea 上的官方项目,找到主网上的合约地址:

随便点进一个挂单的 NFT ,点击 detail :

合约地址如下:
https://etherscan.io/address/0x160c404b2b49cbc3240055ceaee026df1e8497a0

进入区块链浏览器后,点击 contract - write ,便可以看到合约的代码函数了:

这里分享一个在社群中学到的知识,通过修改 etherscan.io -> eterscan.deth.net ,可以直接把网址变为 vscode 的在线浏览模式,便于代码阅读。

截图如下:

正文

前置环境准备就绪,接下来按照以下几个结构进行分析。

合约初印象

继承了 ERC-721A 的合约,同时,继承了 Ownable 合约:

ERC-721A 由 Azuki 团队创建,与 ERC-721 相比,它在同时铸造多个 NFT 时可节省很多 gas fee。

而 Ownable 合约的核心作用,是为项目限制了某些函数只能合约部署者(项目方自己)进行调用,比如下面的提现函数 withdrawFunds(),后面跟了一个修改器 onlyOwner :

如果好奇具体是如何实现的,可以移步 github 自行查看:
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol

初印象结论:通过这两个继承合约可以看出来,一些设置类的函数被限制仅由项目方可调用,这点没问题,而 721A 可以为大家节省多个 NFT 同时 mint 时产生的 gas。

通过 etherscan ,点击 Write Contract 可以清晰的查找 mint 的函数,涉及了 4 个:

Mint 函数分析

从名字上不难看出,4 个 mint 函数对应着 4 类不同参与人群,按照正常业务顺序一个个来解读一下。

devMint : 开发者 mint 函数 

mintDutchAuction:荷兰拍卖 mint 函数 

mintWL:白名单 mint 函数 

teamMint:团队成员 mint 函数

正常的业务流程,一般先是荷拍 mint ,之后进入白名单 mint。至于开发者和团队 mint 的时间,不确定,后面看源码去分析。

1. mintDutchAuction() 分析

先说根据这个函数,看出来的业务相关的信息。

合约的业务逻辑:
荷拍的 PNX 总发行数量为 4000 个,正式荷拍开始时间为北京时间 2022-05-05 11:00:00(CST),对应 UTC 的时间为 2022-05-05 03:00:00(凌晨),我们国家比世界标准要快 8 个小时。

荷拍,起始价格 2 ETH 起拍,每 15 分钟价格降低 0.05 ETH,意味着,1小时价格降低 0.2 ETH ,荷拍最低的价格为 0.1 ETH。

通过荷拍结束的过程,对白名单的定价也有影响,如果荷拍的最终价格定在了小于 0.7 ETH 以内,那么白单的价格则为【当前荷拍价格 / 2】,举个例子,比如最终荷拍的价格定在了 0.6 ETH ,那么白单则为 0.3 ETH ,而白单默认的价格设定的为 0.35 ETH。

再来从代码层面解读一下:
这个函数中,加上了修改器 callerIsUser ,限制了合约调用者只能为钱包地址进行调用,而不能通过合约地址调用。

modifier callerIsUser() {
  require(tx.origin == msg.sender, "The caller is another contract");
  _;
}

这里有个 tx.origin 和 msg.sender 知识点,可以通过一张图看明白上述代码的作用:

原文地址:https://davidkathoh.medium.com/tx-origin-vs-msg-sender-93db7f234cb9

校验的业务逻辑:

  1. 荷拍的状态是否激活,true 为激活,后续代码才能继续运行。【状态校验】
  2. 调用者的签名,通过项目前端网页调用后端动态生成了荷拍签名,再去和智能合约对当前调用者的地址加签后进行验证,这样可以防止"合约机器人"调用。【合约调用者校验】
  3. 当前 mint 数量 + 已经 mint 的数量是否小于等于荷拍的数量(4000个),防止超发【数量校验】
  4. 荷拍时间是否大于等于开启时间【时间校验】
  5. 当前区块链时间戳是否大于白名单开启时间,如果大于了白名单的开启时间,说明荷拍的时间已经结束,禁止后续代码的运行。【时间校验】
  6. 调用合约传入的 mint 数量,荷拍 mint 的最大数量小于等于 2,防止个人超出 mint 数量【数量校验】
  7. 当前调用者已经 mint 的数量 + 调用合约的 mint 数量小于等于2,防止个人超出 mint 数量【数量校验】
  8. 合约调用者的钱包剩余金额是否大于等于你需要 mint 的个数 * 当前荷拍的价格【金额校验】

以上校验逻辑都通过,可以荷拍 mint 。

真正第一个荷拍出价 mint 成功的交易,是下面这笔:

可以看下他的出价时间 May-05-2022 03:03:24 AM,mint数量2个,荷拍交易价格 4 ETH ,他给的 max priority 非常高,以致于成了第一个 mint 成功的人:

最后一个荷拍 mint 者,是这笔交易:

详情,10 分钟荷拍 4000 个 PXN 全部售罄,可以看最后这笔 gas fee,总共才 $55 :

开始的 gas war 和 结束的 gas fee,有着天壤之别啊。。。差了好多钱。。

源码不帖了,自行去上面的去看就好。

2. mintWL() 分析

依然是先说根据这个函数,看出来的业务相关的信息。

白单的总发行数量为 6000,开始时间为荷拍开始时间的 24h 之后,也就是 正式荷拍开始时间为北京时间 2022-05-06 11:00:00(CST),对应 UTC 的时间为 2022-05-06 03:00:00(凌晨),持续时间为 24 小时,即白单 mint 结束时间为 UTC 的时间为 2022-05-07 03:00:00 (凌晨)。

白单的起始价格默认 0.35 ETH,而上面提到了,如果最终荷拍最低价 mint 小于了 0.7 ETH ,则会按照【当前荷拍最低价 / 2 】算出白单价格。

再来从代码层面解读一下:
校验逻辑:

  1. 荷拍最终价格需要大于0,否则意味着荷拍还没有结束,不能进行白单mint【价格校验】
  2. 调用者的签名,通过项目前端网页调用后端动态生成了签名,再去和智能合约对当前调用者的地址加签后进行验证,这样可以防止"合约机器人"调用。【合约调用者校验】
  3. 已经 Mint 的白单数量 + 当前该交易 mint 的数量,必须小于 6000【防止超发 mint】
  4. 当前调用者是否已经白单 mint 过一次了,一个白单地址只能 mint 一次【防止超发 mint】
  5. 当前区块链时间戳大于白单开启时间,小于白单结束时间【时间校验】
  6. 合约调用者的钱包剩余金额是否大于等于你需要当前白单的价格【金额校验】

以上校验都过了,调用者可以开始mint。

第一个白单 mint 成功的交易为:

UTC时间为 2022-05-05 03:00:13 ,gas fee大约 0.8 ETH 左右。

最后一笔白单交易为:

时间:May-07-2022 02:02:33 AM +UTC。
可以看到,这里有三笔交易失败的:

我们把交易txid复制到tendly,可以看一下失败的详情:

地址:
https://dashboard.tenderly.co/tx/mainnet/0x71bca7dcb99cffde7c6cbe364aab0411a61dbeb364a5fe585f519a524839a690

提示,仅能在白单时间 mint ,显然是超了时间了,他这笔 mint 的时间为:May-07-2022 08:15:56 AM +UTC,白单结束时间为:2022-05-07 03:00:00 ,都超了 5 小时了,才想起来,亏死了。

3. teamMint()分析

这部分 mint ,是给项目的团队贡献者的 mint 奖励,项目方维护了一个 _teamList 的列表,根据这个列表,对团队成员所贡献的不同,可以 mint 的数量也不同。

而团队 mint 的开始时间也和白名单开始时间一样,但对于结束时间没有限制,意味着团队成员只要在白单开启时间后,想什么时候 mint 都可以。mint的价格和白单价格一致,都为 0.35 ETH。

校验逻辑:

  1. 当前区块链时间戳大于白单开启时间【时间校验】
  2. 团队成员地址对应的 mint 数量校验【超发mint校验】
  3. 当前钱包的价格大于 mint 的数量 * 价格 校验【金额】
  4. 当前 mint 的数量不能大于总发行量 10000 (4000荷拍 + 6000白单)【超发校验】

以上校验都通过后,可以正常 mint 。

看到的第一笔 team mint 交易为:

Mint 了 4 个,时间:May-06-2022 03:00:13 AM +UTC。

4. devMint()分析

最后,这个函数是合约部署者的 mint ,以上所有人 mint 完之后,合约部署者可以在白单开始后的 24h以后 mint ,即 2022-05-07 03:00:00 以后的所有时间。

但合约部署者有意思的是,总发行数量 10000 ,减去当前 mint 的总数量,只要当调用了这个函数,剩下的所有 NFT ,都会打给这个开发者的地址去。目前链上还没有看到有调用这个函数,可能是因为团队成员还没有 mint 完。

刚才在分析 mintwl 的时候,有列出最后一笔交易,tokenid 是 9868 个,剩余 130+ 个 NFT。

代码校验逻辑:

  1. 当前区块链时间戳大于等于白单开启时间后过了24小时【时间校验】
  2. 团队成员地址对应的 mint 数量校验【超发mint校验】
  3. 当前钱包的价格大于 mint 的数量 * 价格 校验【金额】
  4. 当前 mint 的数量不能大于总发行量 10000 (4000荷拍 + 6000白单)【超发校验】

以上校验都通过后,可以正常 mint ,这里 mint 的逻辑是,按 10 个为一组,批量 mint ,最终如果还剩下个位数,在将剩余的个位数 NFT 发送至合约部署者钱包。

安全疑问

对于重入攻击方面的安全方面,可以看到,这次 PXN 没有对提现函数进行 nonReentrant 的修改器装饰,根本原因是因为他们把发送的地址写死了吗?

反观 Azuki 的提现函数,确实有加 nonReentrant 防止:

后面也许等我的知识完善起来,就能明白了....

结语

智能合约和正常 web2 世界里的代码不同,一旦部署,无法修改。
而安全方面的漏洞大多出现在 mint 函数中,要么是校验漏洞,要么是业务的逻辑漏洞。

而对于校验规则,可以形成一套 SOP ,就像 PXN 项目中的一样,上次分析气球人的合约也是类似的模板:

  1. 荷拍最终价格需要大于0,否则意味着荷拍还没有结束,不能进行白单mint【价格校验】
  2. 调用者的签名,通过项目前端网页调用后端动态生成了签名,再去和智能合约对当前调用者的地址加签后进行验证,这样可以防止"合约机器人"调用。【合约调用者校验】
  3. 已经 Mint 的白单数量 + 当前该交易 mint 的数量,必须小于 6000【防止超发 mint】
  4. 当前调用者是否已经白单 mint 过一次了,一个白单地址只能 mint 一次【防止超发 mint】
  5. 当前区块链时间戳大于白单开启时间,小于白单结束时间【时间校验】
  6. 合约调用者的钱包剩余金额是否大于等于你需要当前白单的价格【金额校验】

如果没有荷拍环节,就去掉荷拍部分的校验。

区块链有意思的地方在于公开,透明,代码只要是好项目,大部分都是会开源的,我们可以从优秀的项目中学习如何去写代码,如何闭坑,而对于智能合约而言,越简单,越清晰的逻辑反而越好。能避免复杂逻辑,就尽量避免吧。

还记得4月发生的事件吗,Akutar NFT 因为写错1个单词,导致 3400万 美金锁死在合约里,再也无法提现,不止是项目方哭😭,参与者也哭😭。

好,以上就是完整的分享了....希望大家可以有所收获,有问题也欢迎随时交流探讨!
💎 |币圈萌新|NFT学习中|成为科学家的路上|💎

我的Twitter:

Subscribe to dami.eth
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.