智能合约实现白名单的3个机制

对比使用mapping、默克尔树、及离线签名 3 中方式处理白名单的优缺点。

简介

白名单是推广NFT项目和奖励早期进入及热情参与者的好方法。有很多方法可以实现白名单机制,每种方法都有自己的优势和劣势。现在主要有3种实现白名单机制的方法,本文介绍它们,并谈谈它们的优点和缺点。

最原始的方式--将白名单保存在存储中

对于熟悉其他语言和现代计算系统的开发者来说,将数据存储在堆或存储器中似乎是处理一系列数据的一个相当合理和简单的方法。

因此,要将白名单存储在存储器中,你可以简单地声明一个mapping映射,记录所有符合白名单条件的有效地址。然而,在 EVM 中,使用这种方式将消耗你大量的Gas,而且是一种非常低效的方法。不过,对于任何人来说,通过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;
  }

}

优点:容易验证,编码简单,容易添加地址或删除地址

缺点:效率真的很低,对于发布者(项目方)来说真的很贵

聪明的方法一 - Merkle Tree Airdrop

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

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

Merkle

所以,让我们想象一下,你想知道L1是否等于一个地址,你可以提取Hash 0-1,Hash 1,以及被测试的地址。然后,你按照hash规则,将hash的输出与Top Hash进行比较。如果结果相同,你可以确保L1等于输入地址。

然而,要做到这一点,你需要生成一棵merkle树,并将一半的构建树过程从链上拿走,以节省Gas。我们将使用javascript来生成merkle树。如果你对ether.js比较熟悉,你也可以使用ethers.utils.solidityKeccak256来哈希配对。另外,记得要将JSON文件存储为以下内容:{&lt;address>: <amount>, &lt;address>: <amount>}。你也可以根据你的需要,把数据改成另一种类型来调整。


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: {"&lt;address>": &lt;amount>, "&lt;address>": &lt;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);
    co...

剩余50%的内容订阅专栏后可查看

0 条评论

请先 登录 后评论
翻译小组
翻译小组

首席翻译官

131 篇文章, 21947 学分