Розробка Merkle-Tree Whitelist для NFT
Колекція з 10 000 токенів з 3 000 whitelist-адрес. Зберігання whitelist на on-chain mapping — це 3 000 операцій SSTORE під час setup, приблизно 0.5-0.8 ETH газу лише на налаштування. Merkle Tree вирішує це за одну транзакцію: root хеш у контракті, proof для кожної адреси off-chain. Газ на верифікацію однієї адреси під час мінтингу — близько 3-5k замість повного SLOAD з mapping.
Як працює верифікація Merkle Proof
Побудова дерева
Листи дерева — це хеші keccak256 адрес (іноді з додатковими даними: keccak256(abi.encodePacked(address, maxMintAmount))). Дерево будується знизу вгору: хеші сусідніх листів комбінуються та хешуються. Корінь — один 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 хешів. Вартість верифікації газу зростає повільно.
Вразливість Double-Leaf та захист
Класична проблема Merkle Tree у смарт-контрактах: якщо лист не унікальний — один proof може верифікувати кілька листів. Атака: створити адресу, keccak256 якої збігається з конкатенацією двох листів наступного рівня.
Захист в OpenZeppelin MerkleProof: бібліотека перевіряє, що leaf != internal_node, тобто лист ніколи не приймається як проміжний вузол. Це вбудовано з OZ 4.7. Якщо використовуєте старішу версію або кастомну реалізацію — додайте явну перевірку.
Додатковий захист: хешувати лист двічі keccak256(keccak256(abi.encodePacked(addr))). Це робить практично неможливим збіг листа з внутрішнім вузлом.
Розширений whitelist: тиери та кількості
Простий whitelist — лише перевірка «адреса існує / не існує». Для багаторівневих whitelist (tier 1: 2 NFT, tier 2: 1 NFT) — включіть дані у лист:
bytes32 leaf = keccak256(abi.encodePacked(msg.sender, maxAmount));
require(MerkleProof.verify(proof, merkleRoot, leaf), "Invalid proof");
require(amount <= maxAmount, "Exceeds allocation");
Тепер proof верифікує не лише адресу, але й максимальну кількість для цієї адреси. Одне дерево, один root, різні аллокації.
Захист від подвійного мінтингу
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..."] } — статичний словник адреса → proof. Генерується один раз при створенні дерева, публікується на CDN. Користувач робить GET запит з адресою, отримує proof. Швидко, дешево, без бекенду.
Backend API. GET /api/whitelist/proof?address=0xABC. Дозволяє додавати адреси без перебудови всього JSON. Але вимагає або перерахування дерева при додаваннях, або зберігання всіх листів у БД для динамічної побудови proof. Якщо whitelist змінюється після публікації root — потрібно оновити root у контракті (якщо контракт дозволяє).
On-chain события. Якщо додавання до whitelist відбувається через смарт-контракт (наприклад, через Premint або proof-of-hold NFT) — события можна індексувати через The Graph та будувати дерево динамічно.
Оновлення Whitelist після розгортання
Якщо контракт дозволяє змінювати merkleRoot (через onlyOwner), whitelist можна оновити без повторного розгортання. Сценарій: основний whitelist + останні додавання. Побудуйте нове дерево з додаваннями, оновіть root через setMerkleRoot().
Важливо: після зміни root старі proof'и перестають працювати. Користувачі з кешованими proof'ами отримають revert. Потрібна комунікація про оновлення.
Процес розробки
Розробка (2-3 дні). Генератор дерева (Node.js скрипт), контракт з верифікацією, API або статичний JSON для proof'ів. Foundry тести: перевіряємо, що коректні proof'и проходять, неправильні — ні, double-mint заблокований.
Інтеграція з фронтендом. wagmi для виклику mint функції, merkletreejs для верифікації proof'а на клієнтській стороні перед відправкою транзакції (оптимізація UX).
Оцінка часу
Базова реалізація Merkle whitelist — 2-3 дні. З тиерами, API для proof'ів та інтеграцією з фронтендом — до 5 днів.
Вартість розраховується після уточнення розміру whitelist та структури тиерів.







