Розробка Whitelist/Allowlist для мінтингу NFT
Типова ситуація: колекція з 10 000 NFT з whitelist з 3 000 адрес на 12 годин до публічного мінтингу. Якщо зберігати whitelist on-chain як mapping(address => bool)—розгортання контракту з 3 000 адрес коштує приблизно 15-20M газу лише на операції SSTORE. При 20 gwei це $300-400 газу лише на setup. Merkle proof вирішує це за одну транзакцію: root хеш у контракті, proof для кожної адреси off-chain. Газ на верифікацію однієї адреси під час мінтингу — близько 3-5k газу замість повного SLOAD з mapping.
Merkle Proof: Як це працює та де люди помиляються
Merkle дерево будується off-chain: кожна адреса хешується через keccak256(abi.encodePacked(address)), дерево будується попарним хешуванням листів. Результат — 32-байтний merkleRoot. Цей root розгортається у контракті. При мінтингу користувач передає proof[] — масив сімейних хешів вздовж шляху від його листка до кореня. Контракт верифікує через MerkleProof.verify() з OpenZeppelin.
Поширена помилка — подвійний мінтинг. Якщо контракт не має mapping(address => bool) public hasMinted (або mapping(address => uint256) public mintedCount), користувач з whitelist може мінтити необмежену кількість разів. Proof залишається дійсним. Контракт не знає, що адреса вже робила мінтинг.
Друга помилка — кодування листа. OpenZeppelin MerkleProof очікує, що лист — це keccak256(keccak256(data)) (подвійний хеш) для захисту від preimage атак у разі збігу вузлів з листями. Якщо генеруєте дерево через merkletreejs з одиничним хешем, але контракт використовує _leaf = keccak256(abi.encodePacked(account)) без подвійного хешу — можлива collision атака при певних конфігураціях. Використовуйте keccak256(bytes.concat(keccak256(abi.encode(addr)))) або стандартну пару: @openzeppelin/merkle-tree JS бібліотека + MerkleProof.sol.
Варіанти реалізації
Merkle Proof (основний)
Підходить для: списків від 100 до мільйонів адрес. Вартість розгортання не залежить від розміру списку. Proof передається користувачем під час мінтингу (frontend генерує автоматично).
Обмеження: не можна додати адресу після розгортання без перебудови дерева та оновлення root. Якщо контракт upgradeable або owner може викликати setMerkleRoot(bytes32) — це вирішується. Якщо immutable — ні.
Підпис від бекенду (ECDSA)
Owner контракту тримає приватний ключ. Для кожної whitelist-адреси бекенд підписує keccak256(abi.encodePacked(address, nonce)). Контракт верифікує підпис через ECDSA.recover() та порівнює з адресою підписувача.
Перевага: динамічне управління — можна додавати адреси без зміни контракту, можна видавати пріоритети, різні квоти. Недолік: централізований signer — єдина точка відмови та довіри. Якщо ключ витекти — будь-хто може заминтити.
Застосування: gaming мінт (бекенд знає досягнення користувача), динамічні кампанії.
On-chain Mapping
Тільки для дуже малих списків (<100 адрес) або коли список відомий заздалегідь і не зміниться. addToWhitelist(address[]) з модифікатором onlyOwner. 20 000 газу на адресу.
Додаткова механіка
Багаторівневий whitelist — різні квоти для різних рівнів. Merkle дерево містить листи keccak256(abi.encode(address, maxMintAmount)). Користувач передає proof + свій maxMintAmount, контракт верифікує обидва параметри разом.
Часові фази — WL → Allowlist → Public. Контракт тримає enum SalePhase { PAUSED, WHITELIST, ALLOWLIST, PUBLIC }. Різні root'и для кожної фази, різні ціни. owner змінює фазу через setPhase().
Batch мінт з WL — користувач може заминтити N NFT в одній транзакції, якщо його квота дозволяє. mintedCount[msg.sender] += amount замість bool флага.
Процес розробки
Реалізація (1-2 дні). Контракт з Merkle verify + hasMinted mapping + phase management. Тести в Foundry з edge cases: повторний мінт, невалідний proof, вичерпана квота.
Інтеграція з фронтендом (1 день). Генерація proof через @openzeppelin/merkle-tree, wagmi hook useMint().
Розгортання. Foundry скрипт з автоматичною верифікацією Etherscan.
Оцінка часу
Whitelist контракт з Merkle proof — 2-3 дні включаючи тести та інтеграцію з фронтендом.
Вартість розраховується індивідуально.







