Разработка merkle-tree whitelist для NFT
Коллекция на 10 000 токенов с 3 000 whitelist-адресов. Хранить whitelist в on-chain mapping — это 3 000 SSTORE операций при заполнении, около 0.5-0.8 ETH газа только на setup. Merkle Tree решает эту задачу за одну транзакцию: root хэш в контракте, proof для каждого адреса off-chain. Газ на верификацию одного адреса при минтинге — порядка 3-5k gas вместо полного SLOAD по mapping.
Как работает Merkle Proof верификация
Построение дерева
Листья дерева — keccak256 хэши адресов (иногда с дополнительными данными: keccak256(abi.encodePacked(address, maxMintAmount))). Дерево строится снизу вверх: хэши соседних листьев объединяются и хэшируются. Корень (root) — один bytes32, хранящийся в контракте.
import { MerkleTree } from 'merkletreejs'
import { keccak256, encodePacked } from 'viem'
const leaves = whitelist.map(addr =>
keccak256(encodePacked(['address'], [addr]))
)
const tree = new MerkleTree(leaves, keccak256, { sortPairs: true })
const root = tree.getHexRoot() // → bytes32 для контракта
sortPairs: true — критичный параметр. Он обеспечивает детерминированное построение дерева независимо от порядка листьев. Без него один и тот же whitelist даёт разные root-ы при разном порядке адресов.
Верификация в контракте
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
bytes32 public merkleRoot;
function mint(uint256 amount, bytes32[] calldata proof) external payable {
bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
require(MerkleProof.verify(proof, merkleRoot, leaf), "Invalid proof");
// ... mint logic
}
MerkleProof.verify() из OpenZeppelin — стандартная реализация с O(log n) сложностью. Для 3 000 адресов — proof из 12 хэшей (log2(3000) ≈ 12). Для 100 000 адресов — 17 хэшей. Gas cost верификации растёт медленно.
Double-leaf уязвимость и защита
Классическая проблема Merkle Tree в смарт-контрактах: если leaf не уникален — один proof может верифицировать несколько листьев. Атака: создать адрес, keccak256 которого совпадает с concat двух листьев следующего уровня.
Защита в OpenZeppelin MerkleProof: библиотека проверяет что leaf != internal_node, то есть leaf никогда не принимается как промежуточный узел. Это встроено в реализацию начиная с OZ 4.7. Если используешь старую версию или собственную реализацию — добавь явную проверку.
Дополнительная защита: хэшировать лист дважды keccak256(keccak256(abi.encodePacked(addr))). Это делает совпадение leaf с internal node практически невозможным.
Расширенный whitelist: тиеры и количества
Простой whitelist — только проверка «адрес есть / нет». Для многоуровневых whitelist (tier 1: 2 NFT, tier 2: 1 NFT) — включаем данные в leaf:
bytes32 leaf = keccak256(abi.encodePacked(msg.sender, maxAmount));
require(MerkleProof.verify(proof, merkleRoot, leaf), "Invalid proof");
require(amount <= maxAmount, "Exceeds allocation");
Теперь proof верифицирует не только адрес, но и максимальное количество для этого адреса. Одно дерево, один root, разные аллокации.
Защита от double-mint
Merkle Proof только верифицирует право минтить — не предотвращает повторный минт. Нужно отдельно трекать использованные аллокации:
mapping(address => uint256) public mintedAmount;
function mint(uint256 amount, uint256 maxAmount, bytes32[] calldata proof) external {
require(mintedAmount[msg.sender] + amount <= maxAmount, "Exceeds allocation");
mintedAmount[msg.sender] += amount;
// ...
}
mintedAmount занимает SSTORE только при первом минте пользователя — это O(unique_minters) storage, а не O(whitelist_size).
Off-chain дистрибуция proof-ов
Три подхода к тому, как пользователь получает свой proof:
JSON файл в IPFS/CDN. { "0xABC...": ["0x...", "0x..."] } — статический словарь address → proof. Генерируется один раз при создании дерева, публикуется в CDN. Пользователь делает GET запрос с адресом, получает proof. Быстро, дёшево, без бэкенда.
Backend API. GET /api/whitelist/proof?address=0xABC. Позволяет добавлять адреса без пересборки всего JSON. Но требует или пересчёта дерева при добавлениях, или хранения всех листьев в БД для динамического построения proof. При изменении whitelist после публикации root — нужно обновить root в контракте (если контракт позволяет).
On-chain events. Если добавление в whitelist происходит через смарт-контракт (например, через Premint или поставку NFT proof-of-hold) — события можно индексировать через The Graph и строить дерево динамически.
Обновление whitelist после деплоя
Если контракт позволяет смену merkleRoot (через onlyOwner), то whitelist можно обновить без передеплоя. Сценарий: основной whitelist + last-minute добавления. Строим новое дерево с дополнениями, обновляем root через setMerkleRoot().
Важно: после смены root старые proof-ы перестают работать. Пользователи с закешированными proof-ами получат revert. Нужна коммуникация об обновлении.
Процесс разработки
Разработка (2-3 дня). Генератор дерева (Node.js скрипт), контракт с верификацией, API или статический JSON для proof-ов. Foundry тесты: проверяем корректные proof-ы проходят, некорректные — нет, double-mint заблокирован.
Интеграция с frontend. wagmi для вызова mint функции, merkletreejs для клиентской верификации proof-а перед отправкой транзакции (UX оптимизация).
Ориентиры по срокам
Базовая Merkle whitelist реализация — 2-3 дня. С тиерами, API для proof-ов и frontend интеграцией — до 5 дней.
Стоимость рассчитывается после уточнения размера whitelist и структуры тиеров.







