白名單機制

白名單機制

目前有三種做法,以下先談到搭配後端與合約的簽名驗證做法

1. 後端簽名,合約驗證

1.生成私鑰和對應地址:首先,你需要生成一對以太坊私鑰和公鑰,然後從公鑰導出對應的以太坊地址。這個地址將被用於識別簽名者。重要的是,私鑰必須保密,只能由授權的個人或系統持有。

2.將地址設置為合約中的root:在部署或初始化智能合約時,你需要將這個生成的以太坊地址設置為合約中的root變量。這個地址代表了被授權進行特定操作(如白名單鑄幣)的實體。

3.在後端使用私鑰進行簽名:在後端,當用戶請求鑄造代幣時,你需要使用私鑰對用戶的地址或其他相關數據進行簽名。這個簽名後續會被用戶提交給智能合約。

4.在智能合約中驗證簽名:當智能合約接收到用戶提交的簽名時,它會使用hash.recover(signature)方法從簽名中恢覆出地址,並檢查這個地址是否與存儲在root中的地址相匹配。如果匹配,則驗證成功,表明簽名是由持有對應私鑰的實體生成的,用戶便被授權進行鑄幣操作。

後端

生成簽名

function backendSign(address, contractName) {
  try {
    const messageHash = web3.utils.soliditySha3(
      { type: "address", value: address }, // 發出交易的人的地址
      { type: "string", value: contractName } // 合約的名稱
    );
    const signature = EthCrypto.sign(
      identity.privateKey, // privateKey
      messageHash // hash of message
    );
    return signature; // 填入 mint 交易的簽名
  } catch (err) {
    return "address not valid"
  }
}

合約端

呼叫 mint 時傳入後端生成的簽名

function initialize(address root_, uint256 reserveAmount, uint256 price_, string calldata unrevealedURI) external onlyOwner{
  require(!initialized, "only initialized once");
  initialized = true;
  root = root_;
}

function mint(bytes memory _signature) external nonReentrant payable{
  require(msg.value == price, "Price not correct");
  require(totalSupply() + devReserve < collectionSize, "Reach max supply");
  _verifyAndMint(_signature, msg.sender);
}

function _verifyAndMint(bytes memory _signature, address account) internal{
  bytes32 msgHash = keccak256(abi.encodePacked(account, name()));
  require(isValidSignature(msgHash, _signature), "Not authorized to mint");
  ...
}

function isValidSignature(bytes32 hash, bytes memory signature) public view returns (bool isValid) {
  return hash.recover(signature) == root;
}

Merkle Tree 方式

產生 root

const { MerkleTree } = require('merkletreejs');
const keccak256 = require('keccak256');

// List of whitelisted addresses
const whitelistAddresses = [
  '0xAddress1...',
  '0xAddress2...',
  '0xAddress3...',
  // ... more addresses
];

// Generate leaf nodes
const leaves = whitelistAddresses.map(addr => keccak256(addr));
const tree = new MerkleTree(leaves, keccak256, { sortPairs: true });

// Get the root hash of the Merkle tree (to be used in the smart contract)
const rootHash = tree.getRoot().toString('hex');

產生 proof

用戶發送 mint 前,瀏覽器用個別地址跟 API Server 獲取 proof

const address = '0xAddress1...'; // Address to generate proof for
const leaf = keccak256(address);
const proof = tree.getHexProof(leaf);

console.log(bytes32Proof); // This will be used in the smart contract for verification

contract

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

contract WhitelistedNFT {
  bytes32 public merkleRoot;

  constructor(bytes32 _merkleRoot) {
    merkleRoot = _merkleRoot;
  }

  function mint(bytes32[] calldata _merkleProof) public {
    // Verify the provided address against the Merkle root
    bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
    require(MerkleProof.verify(_merkleProof, merkleRoot, leaf), "Not in whitelist");

    // Mint NFT logic here...
  }
}

可用以下 Node.js 腳本測試

記得 leaves 至少要一個以上,不然 getProof 會回傳空 array

在 Node 端 verify 時要用 getHexProof 只用 getProof,但如果是要給合約的 proof 要用 byte32 格式,所以要用 getHexProof

const { MerkleTree } = require("merkletreejs");
const keccak256 = require("keccak256");
const leaves = ['0xaa2eAbb245944168705e3Ad21C9D266131E296E7', '0xaaA0ea4E952C2a9bB6FDaDf7cBa1a08eb20EE157'].map(x => keccak256(x))
const tree = new MerkleTree(leaves, keccak256)
const root = tree.getRoot().toString('hex')
const leaf = keccak256('0xaa2eAbb245944168705e3Ad21C9D266131E296E7')
const proof = tree.getProof(leaf)
const rootHash = tree.getRoot().toString("hex");
console.log(tree.verify(proof, leaf, rootHash)) // true

實際合約與測試

Hardhat 測試

whitelist.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

contract Whitelist {
    bytes32 public merkleRoot;

    constructor(bytes32 _merkleRoot) {
        merkleRoot = _merkleRoot;
    }

    function isWhitelisted(
        bytes32[] calldata _merkleProof,
        address _address
    ) public view returns (bool) {
        bytes32 leaf = keccak256(abi.encodePacked(_address));
        return MerkleProof.verify(_merkleProof, merkleRoot, leaf);
    }
}

test.js

const { expect } = require("chai");
const { ethers } = require("hardhat");
const { MerkleTree } = require("merkletreejs");
const keccak256 = require("keccak256");

describe("Whitelist Contract", function () {
  let Whitelist;
  let whitelistContract;
  let accounts;
  let whitelistAddresses;
  let tree;
  let rootHash;

  before(async function () {
    Whitelist = await ethers.getContractFactory("Whitelist");
    accounts = await ethers.getSigners();

    // Set up whitelist addresses
    whitelistAddresses = [accounts[0].address, accounts[1].address];
    const leaves = whitelistAddresses.map((addr) => keccak256(addr));
    tree = new MerkleTree(leaves, keccak256, { sortPairs: true });
    rootHash = tree.getHexRoot();
    whitelistContract = await Whitelist.deploy(rootHash);
  });

  it("should allow whitelisted address", async function () {
    const address = whitelistAddresses[0];
    const leaf = keccak256(address);
    const proof = tree.getHexProof(leaf);

    expect(await whitelistContract.isWhitelisted(proof, address)).to.be.true;
  });

  it("should not allow non-whitelisted address", async function () {
    const nonWhitelistedAddress = accounts[2].address;
    const leaf = keccak256(nonWhitelistedAddress);
    const proof = tree.getHexProof(leaf);

    expect(await whitelistContract.isWhitelisted(proof, nonWhitelistedAddress))
      .to.be.false;
  });
});

Last updated