设置白名单
设置白名单的一种方式是将白名单地址直接存储在合约中。
import "@openzeppelin/contracts/access/Ownable.sol";
contract NFTWhite is Ownable {
mapping(address => bool) whiteLists;
constructor() ERC721('RoboItem', 'RTM') {}
// 添加白名单地址
function setWhiteList(address _to, bool _state) public onlyOwner {
whiteLists[_to] = _state;
}
}
如上代码所示,通过全局变量 whiteLists
存储白名单地址。通过调用 setWhiteList
来设置白名单。但每次只能设置一个地址。假设有 1000 个白名单地址,则需要调用 1000 次 setWhiteList
,无疑会耗费巨量的 gas
。当然也可以批量设置,但这并不能节省多少 gas
费。此时就可以通过默克尔树来极大的节省 gas
费。
使用默克尔树设置白名单
如果你还不知道什么是 默克尔树,可以直接查看 这里
默克尔证明(Merkle Proof)
假设有下面的默克尔树
在已知根节点,即Top Hash
的值,需要证明 0x02
是否在原始数据中。只需要提供节点 Hash 0-0
和 Hash 1
的值即可。
首先计算出 0x02
的哈希值,然后与已知的 Hash 0-0
经过哈希计算得到 Hash 0
, 再将 Hash 0
与 已知的 Hash 1
计算得到 Top Hash
。如果此时计算得到的 Top Hash
的值与原先的 Top Hash
一致,则证明 0x02
在原数据中。
这样做的优点在于不需要知道其他节点的值就可以证明数据在原始数据的集合中。
设置白名单
思路:
-
将白名单地址列表存储在前端
-
根据白名单地址列表生成默克尔树, 并将根节点的值保存在智能合约中
-
用户
mint
的时候,调用合约的mint
方法,并传入用户地址以及用户地址对应的proof
列表 -
合约端根据前端传入的信息进行
proof
验证,通过则可以mint
前端实现
import { MerkleTree } from 'merkletreejs'
import keccak256 from 'keccak256'
import { ethers } from 'ethers'
// 白名单列表
const whiteLists = [
'0xDB85A62a1a9Bb682E0e661e3E7bbEb4947BBBE07',
'0x9215E27C0800F3fF433A19064a4983325cA62F48',
'0xb23D56af43A669e2E5E5EB9Dbd4B3Ee3e2309F26',
'0x56E6aBb93832817f8FAeaDBB0c1aD561A6ea59da',
'0xE0a070e79e136050cd54d00b1840AA4096d2bD2b',
'0x32993d42Fc41940453C39F1491f9175cFD955904',
'0x865F6aBc0A7645F206a419612FE50FDB3c0D7329',
'0x827ce8028893839dE74cd6Dbf36A7D0DeBA43Dcf',
'0x3E20869eeC79D20388c0A02a72f255564fcC085c',
'0x22d3bc725aF70E007D0Fd87afaF1d72094fC3429'
]
const nodes = whiteLists.map((addr) => keccak256(addr))
// 创建 merkle tree
const merkleTree = new MerkleTree(nodes, keccak256, { sortPairs: true })
async function mint() {
const provider = new ethers.providers.Web3Provider(window.ethereum)
// 获取地址
const [account] = await provider.send('eth_requestAccounts', [])
// 前端验证
if (!whiteLists.includes(account)) {
alert('you are not in white list')
return
}
const signer = provider.getSigner()
// 创建合约实例
const contract = new ethers.Contract(contractAddress, contractABI, signer)
// 获取地址对应的 proof 列表
const proof = merkleTree.getHexProof(keccak256(account))
// 调用合约的 mint 方法
const tx = await contract.mint(account, proof, {
gasLimit: 3000000
})
tx.wait()
}
合约端
节点验证使用 openzeppelin
的 MerkleProof
库进行验证,具体代码如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
library MerkleProof {
// 验证方法
// leaf:proof 列表
// root:根节点的值
// leaf: 需要验证的节点值
function verify(
bytes32[] memory proof,
bytes32 root,
bytes32 leaf
) internal pure returns (bool) {
// 计算结果与 root 比较
return processProof(proof, leaf) == root;
}
// 将 leaf 节点与 proof 中的列表中的第0个元素进行 hash 运算,并将运算结果与第1个元素继续进行 hash 运算,直到运算到 proof 列表的最后一个元素,得到的值就是 根节点的值
function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) {
bytes32 computedHash = leaf;
// 循环进行 hash 运算
for (uint256 i = 0; i < proof.length; i++) {
computedHash = _hashPair(computedHash, proof[i]);
}
return computedHash;
}
function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) {
return a < b ? _efficientHash(a, b) : _efficientHash(b, a);
}
// 将 a和b 进行 hash 运算
function _efficientHash(bytes32 a, bytes32 b) private pure returns (bytes32 value) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, a)
mstore(0x20, b)
value := keccak256(0x00, 0x40)
}
}
}
在我们的合约中只需要调用 MerkleProof.verify
即可实现验证。
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
// 带白名单功能NFT
contract RoboNFT is ERC721URIStorage, Ownable {
// 存储默克尔树根节点的值
bytes32 public root;
constructor(bytes32 _root) ERC721('RoboNFT', 'robo') {
root = _root;
}
// 设置跟节点的值(防止白名单变化的情况)
function setRoot(bytes32 _root) external onlyOwner {
root = _root;
}
// mint 函数
function mint(address _to, uint tokenId, bytes32[] calldata proof) external {
// 验证 _to 地址是否属于白名单
require(MerkleProof.verify(proof, root, keccak256(abi.encodePacked(_to))), 'not in white list');
_mint(_to, tokenId);
}
}
通过默克尔树避免了链上存储大量的白名单地址信息,而只需要存储一个白名单地址列表生成的默克尔树的根节点信息。这样便节省了大量设置白名单的费用。同时这样的方式还可以用来发放代币空投等