# NFT Development 102 — 公平发售

By [Jacob Wei](https://paragraph.com/@jacobwei) · 2022-02-14

---

写在开头
====

大家好，我是 Jacob Wei，17 年入圈起一直从事区块链相关工作，专注智能合约与服务端的开发。最近 NFT 领域的兴起让我兴奋不已，因此我想在这里和大家分享一些 NFT 教程中缺失的部分即：NFT 项目的开发、部署、上线的完整流程。

本文需对对智能合约及 ERC721 有简单的了解，同时不会包含具体的代码实现，相关的开发资源都附上了对应的教程，本文更多的是分享一些开发思路。

关于 NFT 的章节，计划分为 3 个部分：

1.  NFT Development 102 —— 公平发售：即本文，发售阶段如何保证公平性
    
2.  NFT Development 102 —— 测试、部署及上架：讲解 NFT 开发阶段如何进行测试、部署及合约管理
    
3.  NFT Development 102 —— NFT 生成及随机性证明：随机性证明验证属性随机及相关衍生品设计
    

* * *

本文将从合约、后端、前端三个方面讲述如何在 NFT 发售阶段做到尽量公平，尽可能让真实用户 Mint 成功。文章结构如下：

*   合约
    
    *   禁止合约调用
        
    *   白名单验证
        
    *   验证Merkle Proof
        
    *   签名参数验证
        
*   后端
    
    *   API防护
        
    *   私钥防护
        
*   前端
    
    *   源码防护
        
    *   模拟器及群控检测
        
    *   Cloudflare 设置
        

合约
==

NFT 的发售中大家讨论最多的问题归纳一下分为以下几种：

*   禁止合约调用
    
*   白名单验证
    
*   调用参数验证
    
*   合约闭源
    

禁止合约调用
------

如果合约允许其他合约调用 Mint ，那大概率就会变成科学家秀操作的舞台。即使每个地址限量也可通过工程合约创建出期望数量的地址进行 Mint。

防范手段： 通过判断 TX 的发出方和发售合约收到请求的来源是否一致来判断，是否来自于其他合约的调用。如 Azuki 中的[验证](https://etherscan.io/address/0xed5af388653567af2f388e6224dc7c4b3241c544#code#F1#L42)如下：

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

注：关于 `msg.sender` and `tx.origin` 的区别示例可以查看 [Phishing with tx.origin](https://solidity-by-example.org/hacks/phishing-with-tx-origin/)

白名单验证
-----

常见的白名单验证方式分为两种：

### 合约记录白名单

对于白名单数量较少或对 Merkle tree 不太熟悉的项目方可能会采用这种方式，优点就是逻辑简单，可以随时添加或删除。缺点也很明显，就是项目方需要支付修改白名单数据的费用。如 Azuki 中可以批量设置白名单地址及对应的数量，gas 消耗数量可以参考 [seedAllowlist Tx](https://etherscan.io/tx/0x2f4a69b4ccf4f7f6c6c0d7cd5abf0260ac8724e421827e8e41de107ff08ad8c3) 。

示例：[Azuki seedAllowlist](https://etherscan.io/address/0xed5af388653567af2f388e6224dc7c4b3241c544#code)

    function seedAllowlist(address[] memory addresses, uint256[] memory numSlots)
        external
        onlyOwner
      {
        require(
          addresses.length == numSlots.length,
          "addresses does not match numSlots length"
        );
        for (uint256 i = 0; i < addresses.length; i++) {
          allowlist[addresses[i]] = numSlots[i];
        }
      }
    

### 验证Merkle Proof

比较常见同时推荐使用的的方式是合约验证 Merkle Proof，通过提交链下生成的 Proof 在合约中进行验证。这个方案也是经过了很多次验证，能够充分满足实际需求，除了验证白名单地址外还可以添加 Mint 数量、空投数量等信息。

Mint 过程中用户获取 Proof 的方法可以使用 API 或将 Merkle Tree 公开让用户自行获取，下面的资源中提供了 API 的示例。

这里有很多现成的方案可以参考，资源如下：

1.  视频教程: [OpenZeppelin Building an NFT Merkle Airdrop](https://blog.openzeppelin.com/workshop-recap-building-an-nft-merkle-drop/)
    
2.  Library: [Merkletreejs Library](https://www.npmjs.com/package/merkletreejs)
    
3.  Demo: [nft-merkle-drop](https://github.com/OpenZeppelin/workshops/tree/master/06-nft-merkle-drop)
    

注意事项：

1.  白名单地址判断：有的项目白名单形同虚设在合约中并没有验证。
    
2.  白名单 Mint 数量判断：Mint 数量应记录在合约中而不应使用地址的 balanceOf 进行判断，否则会出现白名单重复 Mint 的问题。
    

### 签名参数验证

对于公开发售阶段（非白名单），一般会采用参数验证的方式来检验用户数据是否合法，从过往的案例中经常出现的问题有以下几个：

1.  统一的参数值所有用户使用同一个参数值进行验证，这将降低机器人批量操作的难度。
    
2.  参数提前暴露顾名思义当科学家们提前获取到验证参数，就有足够的时间准备机器人。
    
3.  私钥泄漏这种情况虽然很少出现但是也应该引起项目方的重视，不要将私钥在前端暴露，将签名过程及参数的生成过程放在后端执行。
    

参数验证环节，推荐的开发模式：**在参数验证环节推荐的开发模式为每个地址的生成不同的调用参数，以防止重放攻击。**

参考资源：

*   Openzeppelin 的 `ECDSA` library: [Checking Signatures On-Chain](https://docs.openzeppelin.com/contracts/2.x/utilities#checking_signatures_on_chain)
    
*   solidity-by-example: [signature-replay](https://solidity-by-example.org/hacks/signature-replay/)
    

针对后续两个问题我们放在后端章节进行讨论

### 合约闭源

项目方为了降低合约被攻击或滥用的可能选择不公开合约源代码，不单单是在 NFT 领域在 GameFi 项目中也是如此。如果将此作为防御手段并不能够很好的防范机器人操作，举例说明：

[SuperGucci](https://opensea.io/collection/superplastic-supergucci) 采用[合约闭源](https://etherscan.io/address/0x78d61c684a992b0289bbfe58aaa2659f667907f8#code)的方式进行发售，其参数较为简单所以被聪明的科学家朋友攻破是必然的事情，只是时间的早晚而已。最终由于被科学家拿走了太多筹码，而不得不将最后一轮调整为了抽签发售。从链上调用数据可以看出参数和地址无关，没有其他特别的参数，因此对于此类合约存在使用相同参数重放调用的可能。

个人并不推荐这种方式进行发售，对于没有知名 IP 的项目来说未开源的项目并不能够给用户足够的信心，同样这样一点也不 Crypto 。

后端
==

在参数验证环节提到了参数提前暴露及私钥暴露两个问题，相应的解决方案为：

### API防护

为防止参数提前暴露，API 端对参数返回条件进行控制：

*   按照自身项目需求根据时间戳或起始区块返回签名数据
    
*   动态调整返回值结构，或可以尝试对数据进行二次加密等
    

### 私钥防护

私钥防护可以从两个方面入手：

*   由服务端提供 API 用来完成参数构建及签名的过程，避免签名私钥在前端暴露
    
*   发售前均适用测试地址进行签名，发售时启用生产地址进行签名
    

如果完成了以上两个步骤是否就万无一失了呢？对于大部分热门项目来说是的，因为对于热门项目来说公开发售基本上几分钟就销售一空，夸张的可能在几个区块里就卖空，所以留给科学家进行破解的时间就不是特别充足。那么对于采用荷兰拍机制存在长时间的降价等待时，此方法也就失效了，因为科学家们有足够的时间对接 API 获取验证参数来调用合约，最终在期望的价位把库存一扫而空。

### IP 限制

另外未了防止被同一用户获取过多可以针对同一 IP 限制签名数量，防止同一用户使用不同地址参与发售。

前端
==

前端方面将从源码防护、模拟器及群控检测、Cloudflare 设置三个方面聊起。以下方案都是一些思路，具体执行上最终都是项目方和科学家们斗智斗勇，就看谁更技高一筹了，看各位大神表演。

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

### 源码防护

完成了合约和后端的防护，那么对于前端来说想要提高业务的安全性大致可以通过加密、混淆、编译、打包等方式，这方面前端的小伙伴可能就比较熟悉了。有个思路如下 [JavaScript obfuscator](https://github.com/javascript-obfuscator/javascript-obfuscator) + [bytenode](https://github.com/bytenode/bytenode) + [node-packer](https://github.com/pmq20/node-packer) 。

同时逻辑部分的代码在发售前几分钟再执行部署，这样让科学家来不及从前端里面扒出更多信息。别忘记部署后刷新下CDN的缓存，防止用户受到旧的缓存影响。下图为 Cloudflare 中清楚缓存的页面：

![image](https://storage.googleapis.com/papyrus_images/51fffa37a04aff3e7af19c91d3b22ee95a1787c59cdff9a56bb235473b2672b9.png)

image

另外需要注意的是针对合约地址和合约 ABI 可以使用一些加密手段进行隐藏或混淆，如拆分多端然后 base64，避免科学家从混淆后的 Js 中通过正则提取出来。

### 模拟器及群控检测

除了源码部分前端还应该对于群控或模拟器做些判断，如检查浏览器屏幕大小，判断 Selenium 这类自动化插件，一般来说这类插件都会在 js 全局变量里面插入一些函数和变量，有很多检测方案可以尝试，网上资源也比较多这里就不再赘述。

### Cloudflare 设置

Cloudflare 中一些配置可以帮助我们提高对 Bot 的防护，当然破解方式还是有的，不存在什么万全之策，能做的只能是尽自己最大的努力进行防护。

### Bot Fight Mode

需购买 Cloudflare Pro 版本，价格为 $20 / month 。

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

image

### Under Attack Mode

发售阶段开启 Under Attack Mode 模式，也就是常见的五秒盾用来验证请求合法性。

![image](https://storage.googleapis.com/papyrus_images/733eaf71bed64117cade8ee663e3ecbcf0548895a3ba1ed3ef9e02d0c7ea8be9.png)

image

### Legacy Captcha

如需要提高防护等级，可以开启 legacy captcha，但是这样会影响一些用户体验，要和用户提前说明可能会遇到验证码而且有些验证码比较难识别。

开启路径为：Firewall Rules → Managed Challenge → Legacy CAPTCHA

写在最后
====

想要实现真正公平的发售是个挺复杂的问题，项目方可能也没办法把大部分精力放在技术层面。在可以遇见的将来，会出现比较完善的发售平台使得艺术家或项目方只需关注作品层面，而不需要直面技术中的种种困难。

NFT 的发售其实只是 NFT 项目其中一部分，除了上述的注意事项外还有很多细节值得重视，比如版税的设置、合约及前后端的测试、NFT Metadata 的部署、属性的随机性和稀有性、开图的公平性、还有 NFT 质押机制的设计、NFT 衍生品的设计等，如果大家感兴趣可以在后续的文章中继续和大家深入讨论。

祝愿大家都能够 Mint 到自己喜欢的 NFT。

---

*Originally published on [Jacob Wei](https://paragraph.com/@jacobwei/nft-development-102)*
