# NFT智能合约白名单机制

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

---

介绍
==

白部是推广和奖励的主要内容。现在有实验的3种实现方法，有几种方法介绍了他们自己的项目。我在本文中，并结合了自己的优点。

最原始的方式——储存在合同里
==============

其他编程语言和现代计算系统的开发者可以说，将数据存储的方式和语言描述是非常简单的数据处理方式。

这种方式可能会导致你产生大量有效的气体，是非常低的方式。这种方式也有优势，通过etherscan或行来测试他是否在白上会更容易。

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.11;
    
    import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
    import "@openzeppelin/contracts/access/Ownable.sol";
    
    contract PrimitiveWhiteList is ERC721Enumerable, Ownable {
    
      uint256 public constant MINT_PRICE = 0.1 ether;
      mapping(address => bool) public whitelist;
      
      constructor() ERC721("Primitive Whitelist", "PW") {}
      
      function whitelistMint(uint256 amount) external payable {
        require(msg.value == amount * MINT_PRICE, "Ether send below price");
        require(whitelist[msg.sender], "Not in whitelist");
        
        // start minting
        uint256 currentSupply = totalSupply();
    
        for (uint256 i = 1; i <= amount; i++) {
            _safeMint(msg.sender, currentSupply + i);
        }
      }
      
      function addWhitelist(address _newEntry) external onlyOwner {
        whitelist[_newEntry] = true;
      }
      
      function removeWhitelist(address _newEntry) external onlyOwner {
        require(whitelist[_newEntry], "Previous not in whitelist");
        whitelist[_newEntry] = false;
      }
    
    }
    

优点：简单有效，容易编码，容易添加地址或删除地址

缺点：效率真的很低，对发行方来说真的很贵

聪明的方法 I — Merkle Tree Airdrop
=============================

执行白名单的另一种方式是利用Merkle树。Merkle树是区块链中的一个重要角色。Merkle树利用了哈希的特性，即输入的轻微变化将导致完全不同的输出，以及两个输入导致相同输出的概率几乎不可能。

Merkle树的结构如下所示。当前节点的哈希值等于其左侧子节点、右侧子节点及其数据的哈希值。因此，从哈希的属性来看，任何改变都会导致完全不同的输出；我们可以利用这个属性来实现白名单。

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

哈希规则，如果哈希的输出与 Top Hash 进行比较一样，你可以确保 L1 同一个输入。

要完成这一天，需要生成一棵merkle树，你对你的圈子过程上拿走，以造点气。我们将使用javascript来生成merkle树。如果ether.js比较，你也很像也可以使用ethersutils .solidityK存储下面的k256哈希来为这另外的内容。在将JSON的包含在中。，把数据改成另一种类型来调整。

    import fs from "fs";
    import { MerkleTree } from "merkletreejs";
    import Web3 from "web3";
    import keccak256 from "keccak256";
    import dotenv from "dotenv";
    
    // Establish web3 provider
    dotenv.config();
    const web3 = new Web3(process.env.MAINNET_RPC_URL);
    
    // hashing function for solidity keccak256
    const hashNode = (account, amount) => {
        return Buffer.from(
            web3.utils
                .soliditySha3(
                    { t: "address", v: account },
                    { t: "uint256", v: amount }
                )
                .slice(2),
            "hex"
        );
    };
    
    // read list, Note: the root path is at cwd
    // the json file structure: {"<address>": <amount>, "<address>": <amount>, ...}
    const readRawList = (path) => {
        const rawdata = fs.readFileSync(path);
        const data = JSON.parse(rawdata);
    
        return data;
    };
    
    const generateMerkleTree = (data) => {
        const leaves = Object.entries(data).map((node) => hashNode(...node));
    
        const merkleTree = new MerkleTree(leaves, keccak256, { sortPairs: true });
        const merkleRoot = merkleTree.getHexRoot();
    
        return [merkleRoot, merkleTree];
    };
    
    const checkTree = (pairs, tree, root) => {
        for (const [key, value] of Object.entries(pairs)) {
            const leaf = hashNode(key, value);
            const proof = tree.getProof(leaf);
    
            // hex proof for solidity byte32[] input
            // const hexProof = tree.getHexProof(leaf);
    
            if (!tree.verify(proof, leaf, root)) {
                console.err("Verification failed");
                return false;
            }
        }
    
        return true;
    };
    
    function main(filepath, outputPath) {
        const rawData = readRawList(filepath);
        const [merkleRoot, merkleTree] = generateMerkleTree(rawData);
    
        if (checkTree(rawData, merkleTree, merkleRoot)) {
            fs.writeFileSync(
                outputPath,
                JSON.stringify({
                    root: merkleRoot,
                    tree: merkleTree,
                })
            );
    
            console.log(`Successfully generate merkle tree to ${outputPath}.`);
        } else {
            console.err("Generate merkle tree failed.");
        }
    }
    
    main("./db/freeClaimList.json", "./db/freeClaimMerkle.json");
    

需要通过solidity函数将根存储在合约中。然后用MerkleProof库进行链上验证。每当有人想mint时，需要在前台或者后台为用户生成证明，用tree.getHexProof函数生成bytes32\[\]证明。查看checkTree函数中的注释，了解更详细的实现。

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.11;
    
    import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
    import "@openzeppelin/contracts/access/Ownable.sol";
    import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
    import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
    
    contract PrimitiveWhiteList is ERC721Enumerable, Ownable {
      
      using ECDSA for bytes32;
      
      uint256 public constant MINT_PRICE = 0.1 ether;
      bytes32 private _whitelistMerkleRoot;
      
      constructor() ERC721("Merkle Tree Whitelist", "MTW") {}
      
      function whitelistSale(bytes32[] memory proof, uint256 amount) external payable {
            // merkle tree list related
            require(_whitelistMerkleRoot != "", "Free Claim merkle tree not set");
            require(
                MerkleProof.verify(
                    proof,
                    _whitelistMerkleRoot,
                    keccak256(abi.encodePacked(msg.sender, amount))
                ),
                "Free Claim validation failed"
            );
    
            // start minting
            uint256 currentSupply = totalSupply();
    
            for (uint256 i = 1; i <= amount; i++) {
                _safeMint(msg.sender, currentSupply + i);
            }
        }
      
        function setWhitelistMerkleRoot(bytes32 newMerkleRoot_) external onlyOwner {
            _whitelistMerkleRoot = newMerkleRoot_;
        }
    
    }
    

每当调整白名单时，必须更新`_freeClaimMerkleRoot`。

优点：经济效益高，易于使用

缺点：对用户来说，mint时gas的用量略大。每次希望改变白名单时都需要重置Root。

聪明的方法二 — 后台签名
=============

最后一种方式也比第一种方式更便宜。然而，最后一种方式比前一种方式更中心化。这种方法利用了签署信息的机制。需要在后端设置一个地址，并保持它的可信度。每当一个白名单上的用户希望mint需要首先验证它。验证后，可以签署信息并将其传回给用户。然后，用户就可以使用签名的信息，并进行mint。

但你可能会问，如何确保没有人可以伪造签名信息？原因是，如果你用你的私钥签署信息，你将得到一个哈希的信息。然后，你可以用哈希的信息和哈希前的信息生成公钥。因此，如果你在合约处存储公钥，在后端用私钥签署消息，你可以确保没有人可以伪造消息。

然而，为了防止重放攻击，你可以使用一个nonce来确保签署的消息不会被恶意使用。因此，整个签名过程将如下所示：

    import Web3 from "web3";
    import dotenv from "dotenv";
    
    // Establish web3 provider
    dotenv.config();
    const web3 = new Web3(process.env.MAINNET_RPC_URL);
    
    const generateNonce = () => {
      return crypto.randomBytes(16).toString("hex");
    };
    
    // Hash message
    const mintMsgHash = (recipient, amount, newNonce, contract) => {
      return (
        web3.utils.soliditySha3(
          { t: "address", v: recipient },
          { t: "uint256", v: amount },
          { t: "string", v: newNonce },
          { t: "address", v: contract }
        ) || ""
      );
    };
    
    const signMessage = (msgHash, privateKey) => {
        return web3.eth.accounts.sign(msgHash, privateKey);
    };
    
    // Signing the message at backend.
    // You can store the data at database or check for Nonce conflict 
    export const Signing = (address, amount) => {
      const newNonce = generateNonce();
     
      const hash = mintMsgHash(
        address,
        amount,
        newNonce,
        config.ContractAddress
      );
    
      const signner = signMessage(hash, config.PrivateKey);
      
      return {
        amount: amount,
        nonce: newNonce,
        hash: signner.message,
        signature: signner.signature,
      };
      
    }
    

因此在生成哈希信息后，即在返回数据处的哈希和签名，可以将其传递给前端，供用户mint NFT。因此必须在链上设置验证。

    //SPDX-License-Identifier: MIT
    pragma solidity ^0.8.11;
    
    import "@openzeppelin/contracts/token/ERC721/ERC721";
    import "@openzeppelin/contracts/access/Ownable.sol";
    import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
    
    contract SignatureWhitelist is ERC721, Ownable {
      using ECDSA for bytes32;
      
      address private _systemAddress;
      mapping(string => bool) public _usedNonces;
    
      function publicSale(
        uint256 amount,
        string memory nonce,
        bytes32 hash,
        bytes memory signature
      ) external payable nonReentrant {
     
        // signature realted
        require(matchSigner(hash, signature), "Plz mint through website");
        require(!_usedNonces[nonce], "Hash reused");
        require(
          hashTransaction(msg.sender, amount, nonce) == hash,
          "Hash failed"
        );
    
        _usedNonces[nonce] = true;
    
        // start minting
        uint256 currentSupply = totalSupply();
    
        for (uint256 i = 1; i <= amount; i++) {
          _safeMint(msg.sender, currentSupply + i);
        }
        
      }
      
      function matchSigner(bytes32 hash, bytes memory signature) public view returns (bool) {
        return _systemAddress == hash.toEthSignedMessageHash().recover(signature);
      }
    
      function hashTransaction(
        address sender,
        uint256 amount,
        string memory nonce
      ) public view returns (bytes32) {
      
        bytes32 hash = keccak256(
          abi.encodePacked(sender, amount, nonce, address(this))
        );
    
        return hash;
      }
    
    }
    

优点：对开发者来说更便宜，在后端更容易管理白名单。

缺点：造币需要更多的gas，不那么去中心化。

总结
==

有很多方法来实现白名单机制。每种方式都有其优点和缺点。因此，开发者应该仔细考虑需求，并在每种方式之间找到平衡。

原文：[https://coinsbench.com/smart-contract-whitelist-mechanism-fbe3464159ed](https://coinsbench.com/smart-contract-whitelist-mechanism-fbe3464159ed)

---

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