分别使用默克尔树和数字签名两种方式给NFT合约添加白名单
前言
本文分别采用默克默克尔树和数字签名两种方式给nft合约添加白名单,对比两者的优缺点,本文包含了合约的开发,测试,部署全流程。
基础概念
默克尔树:也称为哈希树,是一种树形数据结构,主要用于数据验证和同步,默克尔树的特点是每个非叶子节点是其子节点的哈希值,而叶子节点存储的是数据或数据的哈希
数字签名(ECDSA):以太坊采用的数字签名双椭圆曲线数字签名算法(ECDSA
);
开发
默克尔树白名单NFT合约
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.22;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
library MerkleProof {
/**
* @dev 当通过`proof`和`leaf`重建出的`root`与给定的`root`相等时,返回`true`,数据有效。
* 在重建时,叶子节点对和元素对都是排序过的。
*/
function verify(
bytes32[] memory proof,
bytes32 root,
bytes32 leaf
) internal pure returns (bool) {
return processProof(proof, leaf) == root;
}
/**
* @dev Returns 通过Merkle树用`leaf`和`proof`计算出`root`. 当重建出的`root`和给定的`root`相同时,`proof`才是有效的。
* 在重建时,叶子节点对和元素对都是排序过的。
*/
function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) {
bytes32 computedHash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
computedHash = _hashPair(computedHash, proof[i]);
}
return computedHash;
}
// Sorted Pair Hash
function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) {
return a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a));
}
}
contract MerkleTree is ERC721 {
bytes32 immutable public root; // Merkle树的根
mapping(address => bool) public mintedAddress; // 记录已经mint的地址
// 构造函数,初始化NFT合集的名称、代号、Merkle树的根
constructor(string memory name, string memory symbol, bytes32 merkleroot)
ERC721(name, symbol)
{
root = merkleroot;
}
// 利用Merkle树验证地址并完成mint
function mint(address account, uint256 tokenId, bytes32[] calldata proof)
external
{
require(_verify(_leaf(account), proof), "Invalid merkle proof"); // Merkle检验通过
require(!mintedAddress[account], "Already minted!"); // 地址没有mint过
_mint(account, tokenId); // mint
mintedAddress[account] = true; // 记录mint过的地址
}
// 计算Merkle树叶子的哈希值
function _leaf(address account)
internal pure returns (bytes32)
{
return keccak256(abi.encodePacked(account));
}
// Merkle树验证,调用MerkleProof库的verify()函数
function _verify(bytes32 leaf, bytes32[] memory proof)
internal view returns (bool)
{
return MerkleProof.verify(proof, root, leaf);
}
}
数字签名白名单NFT合约
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.22;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import "hardhat/console.sol";
contract SignatureNFT is ERC721 {
using ECDSA for bytes32;
using MessageHashUtils for bytes32;
address public signer; // 签名地址
mapping(address => bool) public mintedAddress; // 记录已经铸造的地址
constructor(string memory _name, string memory _symbol, address _signer)
ERC721(_name, _symbol)
{
signer = _signer;
}
// 验证签名并铸造 NFT
function mint(address _account, uint256 _tokenId, bytes memory _signature) external {
bytes32 messageHash = getMessageHash(_account, _tokenId);
bytes32 ethSignedMessageHash = messageHash.toEthSignedMessageHash();
console.log(verify(ethSignedMessageHash, _signature));
require(verify(ethSignedMessageHash, _signature), "Invalid signature");
require(!mintedAddress[_account], "Already minted!");
_mint(_account, _tokenId);
mintedAddress[_account] = true;
}
// 生成消息哈希
function getMessageHash(address _account, uint256 _tokenId) public pure returns (bytes32) {
return keccak256(abi.encodePacked(_account, _tokenId));
}
// 验证签名
function verify(bytes32 _msgHash, bytes memory _signature) public view returns (bool) {
console.log(signer);
console.log(_msgHash.recover(_signature));
return _msgHash.recover(_signature) == signer;
}
}
# 编译指令
# npx hardhat compile
测试
默克尔树白名单NFT合约
const {ethers,getNamedAccounts,deployments} = require("hardhat");
const { assert,expect } = require("chai");
describe("MerkleTreeNFT",async()=>{
let MerkleTreeNFT;//合约
let firstAccount//第一个账户
let secondAccount//第二个账户;
let addr1;//第一个账户
let addr2;//第二个账户
let addr3;//第三个账户
let addr4;//第四个账户
let Proof=[
"0x00314e565e0574cb412563df634608d76f5c59d9f817e85966100ec1d48005c0",
"0x7e0eefeb2d8740528b8f598997a219669f0842302d3c573e9bb7262be3387e63"
]//通过MerkleTree网页获取0 Proof下的数组
let Proof1=[
"0xe9707d0e6171f728f7473c24cc0432a9b07eaaf1efed6a137a4a8c12c79552d9",
"0x7e0eefeb2d8740528b8f598997a219669f0842302d3c573e9bb7262be3387e63"
]//通过MerkleTree网页获取1 Proof下的数组
let Proof2=[
"0x1ebaa930b8e9130423c183bf38b0564b0103180b7dad301013b18e59880541ae",
"0x070e8db97b197cc0e4a1790c5e6c3667bab32d733db7f815fbe84f5824c7168d"
]//通过MerkleTree网页获取2 Proof下的数组
let Proof3=[
"0x8a3552d60a98e0ade765adddad0a2e420ca9b1eef5f326ba7ab860bb4ea72c94",
"0x070e8db97b197cc0e4a1790c5e6c3667bab32d733db7f815fbe84f5824c7168d"
]//通过MerkleTree网页获取3 Proof下的数组
beforeEach(async()=>{
await deployments.fixture(["merkeTree"]);
firstAccount=(await getNamedAccounts()).firstAccount;
secondAccount=(await getNamedAccounts()).secondAccount;
[addr1,addr2,addr3,addr4]=await ethers.getSigners();
const MerkleTreeDeployment = await deployments.get("MerkleTree");
MerkleTreeNFT = await ethers.getContractAt("MerkleTree",MerkleTreeDeployment.address);//已经部署的合约交互
})
describe("MerkleTreeNFT测试",async()=>{
it("验证合约",async()=>{
//账户
//addr1,addr2.addr3,addr4
//白名单账号铸造
await MerkleTreeNFT.mint(addr1.address,0,Proof)
console.log( await MerkleTreeNFT.ownerOf(0))
//只能铸造一次
// await MerkleTreeNFT.mint(firstAccount,0,Proof)
// console.log('报错', await MerkleTreeNFT.ownerOf(0))
//白名单addr2 铸造
await MerkleTreeNFT.mint(addr2.address,1,Proof1)
console.log(await MerkleTreeNFT.ownerOf(1))
//白名单addr3 铸造
await MerkleTreeNFT.mint(addr3.address,2,Proof2)
console.log(await MerkleTreeNFT.ownerOf(2))
//白名单addr4 铸造
await MerkleTreeNFT.mint(addr4.address,3,Proof3)
console.log(await MerkleTreeNFT.ownerOf(3))
})
})
})
# 测试指令
# npx hardhat test ./test/xxx.js
数字签名白名单NFT合约
测试时铸造需要的数字签名获取
步骤如何下
- 把需要签名的钱包地址导入到Metamask钱包中
- 本地测试的情况使用opensea测试网
- 打开控制台输入如下代码
- await ethereum.enable()//链接网站
- accunt="要导入的地址"//导入账号的地址例如0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
- hash="生成的hash"//可以通过部署的合约中的getMessageHash方法生成
- await ethereum.request({method:"personal_sign",params:[account,hash]})//获取数字签名铸造时使用
const {ethers,getNamedAccounts,deployments} = require("hardhat");
const { assert,expect } = require("chai");
describe("数字签名",function(){
let Signature;//合约地址
let firstAccount//第一个账户
let secondAccount//第二个账户
//签名可以通过钱包签名,也可以通过合约签名这里主要用钱包签名
//步骤入下
//1.在metamask钱包导入账户
//2.打开opensea控制台
//3.输入ethereum.enable()//链接网站
//4.输入account=""//导入的钱包公钥
//5.输入hash=""//可以通过部署的合约地址获取
//6.输入await ethereum.request({method:"personal_sign",params:[account,hash]})//获取签名
let walletSignature="0x7c016a14819cd75ef2321cdf18f415fb54d8faf077e23b259a6d1033b530e5fb738021508e6c9e449e8c9b8f1503163ca327e518b2f6aa4b3ca9d5f9392cd3301c"
beforeEach(async function(){
await deployments.fixture(["SignatureNFT"]);
firstAccount=(await getNamedAccounts()).firstAccount;
secondAccount=(await getNamedAccounts()).secondAccount;
const SignatureDeployment = await deployments.get("SignatureNFT");
Signature = await ethers.getContractAt("SignatureNFT",SignatureDeployment.address);//已经部署的合约交互
});
describe("数字签名",function(){
it("SignatureNFT",async function(){
//获取hash 参数说明 地址,id
const Signaturehash=await Signature.getMessageHash(firstAccount,0);
console.log('获取hash',Signaturehash)
//白名单账户铸造nft 参数 地址,id,签名
await Signature.mint(firstAccount,0,walletSignature)
//验证该账号是否铸造成功 获取有一个nft
console.log(await Signature.balanceOf(firstAccount))
//再次铸造失败
await Signature.mint(firstAccount,0,walletSignature)
//获取有一个nft验证该账号是否铸造成功
console.log(await Signature.balanceOf(firstAccount))
});
})
})
# 测试指令
# npx hardhat test ./test/xxx.js
部署
默克尔树白名单NFT合约
部署参数说明和获取: 参数4个:nft的name ,nft的符号,默克尔树的根节点,初始化Owner
步骤如何:
- 打开hardhat项目在终端中输入 npx hardhat node 可以获取20个账户
- 打开默克尔树网站
- 把第一步获取的账号的公钥的地址添到input的数字中,选择hash方法为Keccak-256 options中选择hashLeaves和sortPairs选项
- 点击compute,
- 成功后就会生成OutputRoot中就会生成Roothash(例: 0xe9707d0e6171f728f7473c24cc0432a9b07eaaf1efed6a137a4a8c12c79552d9
)
module.exports=async function ({getNamedAccounts,deployments}){
const firstAccount= (await getNamedAccounts()).firstAccount;
const root="0xe9707d0e6171f728f7473c24cc0432a9b07eaaf1efed6a137a4a8c12c79552d9";//通过默克尔网页生成
const {deploy,log} = deployments;
const MerkeTree=await deploy("MerkleTree",{
from:firstAccount,
args: ["MerkeTree","MKTNFT",root],//参数 name,symble,root
log: true,
})
console.log('默克尔树合约地址',MerkeTree.address)
}
module.exports.tags=["all","merkeTree"]
# 部署指令
# npx hardhat deploy
数字签名白名单NFT合约
module.exports=async function ({getNamedAccounts,deployments}){
const firstAccount= (await getNamedAccounts()).firstAccount;
const {deploy,log} = deployments;
const Signature=await deploy("SignatureNFT",{
from:firstAccount,
args: ["Signature","SNFT",firstAccount],//参数 name,symble,签名者地址
log: true,
})
console.log('签名合约地址',Signature.address)
}
module.exports.tags=["all","SignatureNFT"]
# 部署指令
# npx hardhat deploy
区别
默克尔树
优点:Gas成本低、验证效率高、数据完整性保障;
缺点:生成和维护成本高、数据隐私性较差;
数字签名
优点:安全、灵活、易实现;
缺点:Gas 成本较高、验证效率较低、依赖于密钥管理;
总结
以上就是采用默克尔树和数字签名两种方式,实现NFT合约添加白名单的方法。两种方法各有优劣,根据情况酌情使用。
共有 0 条评论