# 如何更優雅的實現 NFT 智能合約的白名單功能

By [BlockGames](https://paragraph.com/@981225) · 2022-05-12

---

如果玩過 NFT，相信你對「白名單」制度不會陌生。NFT公開發售之前，都會讓一小部分被授權的地址，可以提前購買。因為能夠保證取得購買資格，無需與其他人瘋搶，所以往往大家都非常想擠進這個白名單中去。

本文不討論 marketing 的策略，而是說說 NFT 智能合約如何更優雅的處理白名單。

聰明的你肯定想到，可以用一個 mapping，來儲存所有 whitelist 的地址，當 presale 時檢查這個 mapping 中是否有這個地址即可。

    //SPDX-License-Identifier: GPL-3.0
    pragma solidity ^0.8.4;
    import "@openzeppelin/contracts/access/Ownable.sol";
    
    contract Whitelist is Ownable {
        mapping (address => bool) userAddr;
    
        function addToWhitelist (address[] calldata users) external onlyOwner {
            for (uint i = 0; i < users.length; i++) {
                userAddr[users[i]] = true;
            }
        }
    
        function preSale() external payable 
        {
            require(whiteList[msg.sender] == true, "STOP: not in whitelist");
            ...
        }
    
    } 
    

       當然，這個方法絕對沒錯。很多 NFT 專案都是使用這個方法。 可是，這種的做法的一大問題就是成本太貴。 我做了[一個實驗](https://rinkeby.etherscan.io/tx/0xc1bf9bb1e6e9c055f9058c9e9a4a7f1e633d7d6e831f79c314640affda463b49)，假如按照 gas price 89 gwei 計算， 一次過儲存 1,000 個白名單地址，消耗 502,774 gwei gas， 需要支付 2.06 ETH，約合 67,885 港幣的費用。如果分開多次存儲，花費更高。
    
        當然如果你「不差錢」，那麼就可以不要再看下去了。但如果不捨得這六萬元，我們可以看看有沒有更好的方法。
    
         答案當然是有的。我們看看下面一段 solidity 程式：
    

    function presale(
            bytes memory _ticket, // 伺服器發出的「票據」
            bytes memory _signature // 伺服器發出的「簽名」
        ) public payable {
             
            // 如果你想的話，我們可以在智能合約中檢查「票據」是否被使用過。
            require(!_ticketUsed[_ticket], "FRANK: ticket has already been used");
            // 驗證「票據」和「簽名」是否有效
            require(
                isAuthorized(
                    msg.sender,
                    _ticket,
                    _signature,
                    signerAddress
                ),
                "FRANK: ticket is invalid"
            );
        
            _mint();
        }
    

      相信聰明的你看完後已經有點頭緒了。 這個做法就是引入了一個「票據」和「簽名」的概念。當用戶在你的網頁上 mint NFT 時，不僅僅是直接同智能合約互動，而是需要預先從 web 伺服器憑藉自己的地址，取得一個授權，這個授權包括一個「票據」和一個「簽名」。
    
       從伺服器的角度來看，伺服器保管有一個簽名私鑰，可以根據用戶的地址，加上一個隨機生成的有意義或無意義數據，即「票據」，組合後進行數位簽署，從而得到一個「簽名」。
    
        然後，用戶可以憑藉「票據」和「簽名」，向智能合約發起購買請求。當智能合約收到購買請求後，會對「票據」和「簽名」進行認證。
    
       認證的步驟，我們會使用 solidity 的 ecrecover 方法。ecrecover 可以根據「簽名」，得出簽名者的公鑰地址。因此，在知道簽名者的公鑰地址的前提下，我們就可以透過核對這個地址，來判斷「簽名」是否由簽名者發出。
    
       我們附帶一個「票據」，則某種程度上是為了防止「回放攻擊」( replay attack )。我們可以在智能合約中檢查該 「票據」是否已經被使用過，從而執行不同的邏輯。
    

    function isAuthorized(
        address sender, // 發起 trx 人的地址；
        bytes memory ticket, // 伺服器發出的「票據」
        bytes memory signature, // 伺服器發出的「簽名」
        address signerAddress // 簽名人的地址
      ) private pure returns (bool) {
        bytes32 hash = keccak256(abi.encodePacked(sender, ticket));
        return signerAddress == hash.recover(signature);
      }
    

這種簽名認證的方法，好處非常明顯。我們完全不需要在智能合約中花費大量成本存儲任何白名單地址，就可以優雅的做到白名單效果，而且也可以隨時增加或減少白名單的數量。

不僅如此，使用這個方法，我們亦可以讓智能合約能夠輕易做到一些邏輯驗證。比如，一個購物的智能合約，需要驗證傳入的 ETH 是否和商品價格符合，一個做法是在智能合約中存儲一個商品ID和價格的列表，每當交易時進行比對。而使用簽名認證，則可以使用上述相同邏輯，透過傳入一個「簽名」，智能合約則可以驗證數據的真確性，而無需在智能合約中保存海量昂貴數據。

---

*Originally published on [BlockGames](https://paragraph.com/@981225/nft)*
