批量铸造设计模式:Merkle Tree白名单验证与批量mint合约实现
好,咱们今天聊点硬核的。批量铸造,说白了就是让用户一次mint多个NFT,而不是一个个点。你想想看,如果用户要买10个NFT,却要发起10次交易,gas费直接爆炸。所以,批量mint是必须的。
但这里有个坑——白名单验证。传统的白名单就是把地址存到mapping里,gas费高得吓人。我早期做项目时,白名单地址一多,部署合约直接超限。后来我改用Merkle Tree,嗯,真香。
为什么用Merkle Tree?
Merkle Tree的核心思想是:把白名单地址列表压缩成一个根哈希(root hash),存在合约里。用户mint时,只需要提供自己的地址和对应的证明(proof),合约就能验证你是否在白名单里。
这样做的好处很明显:
- 存储成本极低:合约里只存一个bytes32的根哈希,而不是整个地址列表
- 验证高效:链上验证只需要O(log n)的计算量
- 白名单可更新:换个根哈希,白名单就变了,不用重新部署合约
核心要点:Merkle Tree白名单验证,本质上是「链下计算,链上验证」。白名单列表在链下生成,合约只负责验证。
Merkle Tree白名单验证合约实现
咱们直接上代码。我习惯用OpenZeppelin的MerkleProof库,省心。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract MerkleNFT is ERC721 {
bytes32 public merkleRoot;
uint256 public nextTokenId;
mapping(address => bool) public claimed;
constructor(bytes32 _merkleRoot) ERC721("MerkleNFT", "MNFT") {
merkleRoot = _merkleRoot;
}
// 批量mint函数
function batchMint(
uint256 quantity,
bytes32[] calldata merkleProof
) external payable {
// 验证白名单
bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
require(
MerkleProof.verify(merkleProof, merkleRoot, leaf),
"Not in whitelist"
);
// 防止重复mint
require(!claimed[msg.sender], "Already claimed");
claimed[msg.sender] = true;
// 批量铸造
for (uint256 i = 0; i < quantity; i++) {
_safeMint(msg.sender, nextTokenId++);
}
}
// 更新白名单(仅管理员)
function setMerkleRoot(bytes32 _merkleRoot) external onlyOwner {
merkleRoot = _merkleRoot;
}
}
这段代码里,我用了MerkleProof.verify来验证证明。注意,叶子节点是keccak256(abi.encodePacked(msg.sender)),这个哈希方式要和链下生成Merkle Tree时保持一致。
我曾经踩过的坑:链下生成叶子节点时,用了abi.encode而不是abi.encodePacked,结果链上验证一直失败。后来排查了半天才发现是编码方式不一致。记住,abi.encodePacked会把多个参数紧凑打包,而abi.encode会按32字节对齐,结果完全不同。
链下生成Merkle Tree
链下生成Merkle Tree,我一般用JavaScript的merkletreejs库。来看代码:
const { MerkleTree } = require('merkletreejs');
const keccak256 = require('keccak256');
// 白名单地址列表
const whitelistAddresses = [
'0x123...',
'0x456...',
// ... 更多地址
];
// 生成叶子节点
const leafNodes = whitelistAddresses.map(addr =>
keccak256(addr)
);
// 构建Merkle Tree
const merkleTree = new MerkleTree(leafNodes, keccak256, {
sortPairs: true // 重要!必须排序
});
// 获取根哈希
const rootHash = merkleTree.getRoot().toString('hex');
// 为某个地址生成证明
const claimingAddress = '0x123...';
const leaf = keccak256(claimingAddress);
const proof = merkleTree.getHexProof(leaf);
console.log('Root Hash:', rootHash);
console.log('Proof:', proof);
这里有个细节:sortPairs: true。为什么?因为Merkle Tree在合并节点时,如果不排序,同样的叶子节点集合可能会生成不同的树。OpenZeppelin的MerkleProof.verify默认使用排序后的树,所以链下也必须排序。
批量mint的gas优化技巧
批量mint时,gas费是绕不开的话题。我总结了几条经验:
| 优化点 | 说明 | 效果 |
|---|---|---|
| 减少SSTORE操作 | 批量mint时,尽量把状态变量一次性写入,而不是每次mint都写 | 可节省30% gas |
使用_safeMint代替_mint |
_safeMint会检查接收方是否为合约,避免NFT被锁死 |
安全性提升,gas略增 |
批量更新_owners映射 |
在循环外先计算好所有tokenId,再一次性写入 | 减少重复计算 |
使用unchecked块 |
在循环中,如果确定不会溢出,可以用unchecked跳过溢出检查 |
每轮循环节省约20 gas |
我的个人习惯:在批量mint函数里,我会把nextTokenId的递增放在unchecked块里。因为一次mint最多也就几百个,不可能溢出。但要注意,如果合约允许无限mint,那就不能这么干。
批量mint的完整实现
把上面的优化技巧整合起来,一个更高效的批量mint合约长这样:
function batchMintOptimized(
uint256 quantity,
bytes32[] calldata merkleProof
) external payable {
// 白名单验证
bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
require(MerkleProof.verify(merkleProof, merkleRoot, leaf), "Not whitelisted");
require(!claimed[msg.sender], "Already claimed");
claimed[msg.sender] = true;
uint256 startTokenId = nextTokenId;
uint256 endTokenId = startTokenId + quantity;
// 批量铸造,使用unchecked优化
for (uint256 i = startTokenId; i < endTokenId; ) {
_safeMint(msg.sender, i);
unchecked {
i++;
}
}
// 一次性更新nextTokenId
unchecked {
nextTokenId = endTokenId;
}
}
你看,这里我把nextTokenId的更新移到了循环外面,避免了每次mint都写一次存储。循环里的i++也用了unchecked。嗯,这些小细节加起来,gas费能省不少。
避坑指南
最后,分享几个我踩过的坑:
- Merkle Tree的排序问题:链下生成时一定要
sortPairs: true,否则验证失败 - 叶子节点编码:链下和链上必须用完全相同的编码方式,推荐
keccak256(abi.encodePacked(address)) - 重复mint防护:白名单验证通过后,一定要用
claimed映射防止同一个地址多次mint - 批量mint的数量限制:建议加一个
maxBatchSize限制,防止用户一次mint太多导致gas超限
总结一下:Merkle Tree白名单验证 + 批量mint,是NFT项目中最常用的组合拳。它既解决了白名单存储的gas问题,又让用户能一次mint多个NFT。你想想看,如果每个用户都要单独验证白名单、单独mint,那gas费得多吓人?
好了,这一章就到这里。下一章咱们聊聊「ERC-721A的批量mint优化」,那可是个更狠的优化方案,能把gas费再砍掉一半。