# Solidity项目分析之 PXN 合约解读

By [dami.eth](https://paragraph.com/@yidakoumi) · 2022-05-08

---

前言
--

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

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

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

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

> 我的推特：[https://twitter.com/dami2333](https://twitter.com/dami2333)
> 
> 黑魔法老师推特：[https://twitter.com/MrsZaaa](https://twitter.com/MrsZaaa)
> 
> NFT黑魔法频道：[https://www.youtube.com/c/NFT黑魔法](https://www.youtube.com/c/NFT%E9%BB%91%E9%AD%94%E6%B3%95)
> 
> 黑魔法discord：[https://discord.gg/CTfK9fH3aQ](https://discord.gg/CTfK9fH3aQ)

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

![](https://storage.googleapis.com/papyrus_images/9cadb75b3e4406c315301662d13001478d2205ce0653c8c9fb6ee3cc7f83c572.png)

随便点进一个挂单的 NFT ，点击 detail ：

![](https://storage.googleapis.com/papyrus_images/f4a99f2dd19c3d611312ba5763ef5332e2068be8a5a59cf416bf28a675639a37.png)

合约地址如下： [https://etherscan.io/address/0x160c404b2b49cbc3240055ceaee026df1e8497a0](https://etherscan.io/address/0x160c404b2b49cbc3240055ceaee026df1e8497a0)

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

![](https://storage.googleapis.com/papyrus_images/5a2de0153d41d55147634f8ccc6efcd26c2cf6f437254e5ac3f367ff61f2e288.png)

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

[https://etherscan.deth.net/address/0x160c404b2b49cbc3240055ceaee026df1e8497a0#writeContract](https://etherscan.deth.net/address/0x160c404b2b49cbc3240055ceaee026df1e8497a0#writeContract)

截图如下：

![](https://storage.googleapis.com/papyrus_images/06e3b6d13ff4df02d02c0bebc1abefe64c29e29d849edd907a4bd8b565a8a965.png)

正文
--

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

### 合约初印象

继承了 ERC-721A 的合约，同时，继承了 Ownable 合约：

![](https://storage.googleapis.com/papyrus_images/db34524a80291b1b43066a3d2753c1d8001c017d3285f2c3227cddd275608813.png)

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

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

![](https://storage.googleapis.com/papyrus_images/ff212d5b2a6c80ede235bc44cb3d02eb70568cfb7d395a80f9e67a3fd6e432a7.png)

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

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

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

![](https://storage.googleapis.com/papyrus_images/f56cd73e859357ed4fa6ad92a292813ce4bb1546018fad0313dc5e0e340813ab.png)

![](https://storage.googleapis.com/papyrus_images/ca832442d8074f554b2172af2a6e0913255a019c672d86bc6f21afd91e448a82.png)

### 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://storage.googleapis.com/papyrus_images/1118d23c3641fe5563813f504a4437c5c120734eb2587020cad695509a5368c5.png)

原文地址：[https://davidkathoh.medium.com/tx-origin-vs-msg-sender-93db7f234cb9](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 成功的交易，是下面这笔：

![](https://storage.googleapis.com/papyrus_images/b0358c2d7b897e552034799a231ecc104bbe1d49661bd25b8b92569792144b98.png)

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

![](https://storage.googleapis.com/papyrus_images/6fdcf16d35d7fad410dc4df1a13da0f433973c233cb5c06d6aa544952db95357.png)

最后一个荷拍 mint 者，是这笔交易：

![](https://storage.googleapis.com/papyrus_images/8b0f47576274372191257b9626331e5d77762851525f392341e16ddc7b24081b.png)

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

![](https://storage.googleapis.com/papyrus_images/4c3294c2d361a60898e2a5417391234e1885eb71ab3763ba2ee159ce7698266a.png)

开始的 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 成功的交易为：

![](https://storage.googleapis.com/papyrus_images/fa6669e9478443fbb36b6b07b38e999fecfda22174e1d3db6c6dfc9a23bf79e5.png)

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

最后一笔白单交易为：

![](https://storage.googleapis.com/papyrus_images/44e566216630392cadf819b8709bd83b24d6f09dc1bd814f56eaf6011b653e87.png)

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

![](https://storage.googleapis.com/papyrus_images/aa219817e85c6a29f388c1b58e0db51ec35d5e25456ca452674f9f64b392626c.png)

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

![](https://storage.googleapis.com/papyrus_images/a57c43697f2d2b3445cda079906111e6615786c9ed141d41ee89b251eb0fc834.png)

地址： [https://dashboard.tenderly.co/tx/mainnet/0x71bca7dcb99cffde7c6cbe364aab0411a61dbeb364a5fe585f519a524839a690](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 交易为：

![](https://storage.googleapis.com/papyrus_images/26da65c42ff831ce74b2ee76ca89e4fd89bdeede2b7feb56f9d510a7a89a7bb6.png)

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。

![](https://storage.googleapis.com/papyrus_images/8011ecf02adeea62b2c6efa620292078b1757a1cb95b84fbfc57cf1256bd394a.png)

代码校验逻辑：

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

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

### 安全疑问

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

![](https://storage.googleapis.com/papyrus_images/1baf3700795923d779a042336b43f1d6d6197c328d3ba50eef084f83e8a7e2b7.png)

反观 Azuki 的提现函数，确实有加 nonReentrant 防止：

![](https://storage.googleapis.com/papyrus_images/0ff1e4b9297c49e4535bad2fb86a5b8ddecdae7ccfb79d3245ddacc3a52db96c.png)

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

结语
--

智能合约和正常 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：

[https://twitter.com/dami2333](https://twitter.com/dami2333)

---

*Originally published on [dami.eth](https://paragraph.com/@yidakoumi/solidity-pxn)*
