Розробка системи антибот-захисту при мінтингу
Azuki у січні 2022: 8700 NFT за 3 хвилини, 30 ETH газу в піці. Боти сминтили 2/3 колекції раніше реальних користувачів. OpenSea через тиждень: одні адреси перепродають по 10x. Це класична ситуація без антибот-захисту. Наступні великі запуски — BAYC, CryptoPunks — вчилися на помилках та впровадили різні механізми. Який з них правильний для вашої колекції — залежить від розміру аудиторії та бажаного розподілу.
Механізми захисту та їх trade-offs
Merkle whitelist: найпоширеніша захист
Merkle tree з адрес вайтлісту. Кожна адреса з листа може довести своє членство, надавши proof з O(log n) хешів. Контракт зберігає тільки один root (32 байти), не весь список.
bytes32 public merkleRoot;
function whitelistMint(uint256 quantity, bytes32[] calldata proof) external payable {
bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
require(MerkleProof.verify(proof, merkleRoot, leaf), "Not whitelisted");
require(!_whitelistClaimed[msg.sender], "Already claimed");
_whitelistClaimed[msg.sender] = true;
_mint(msg.sender, quantity);
}
Генерація Merkle tree — off-chain TypeScript скрипт через merkletreejs. Root оновлюється перед мінтом через setMerkleRoot() (onlyOwner). Proofs користувачі отримують через API або заранее публікуються в IPFS.
Уязвимість: якщо frontend компрометований, атакуючий може запросити proof для будь-якої адреси з листа через API. Захист: proof видаватись тільки wallet, який його запитує (signature-gated API), або весь список публікується заранее (повна відкритість).
Commit-reveal: захист від frontrunning при random mint
Без commit-reveal: бот аналізує mempool, бачить транзакцію з параметрами, робить точну копію з більш високим gas — frontrunning. З commit-reveal: користувач спочатку публікує keccak256(secret + address), потім через N блоків розкриває secret. За ці N блоків копіювати бесмисленно — secret невідомий.
Двухетапний процес незручний для користувачів. Використовуємо тільки де random розподіл критичний та користувачі готові до двох транзакцій.
Per-address ліміти: необхідний мінімум
Найбазовіша захист — ліміт на адресу:
mapping(address => uint256) public mintedByAddress;
uint256 public constant MAX_PER_ADDRESS = 3;
function mint(uint256 quantity) external {
require(mintedByAddress[msg.sender] + quantity <= MAX_PER_ADDRESS, "Limit exceeded");
mintedByAddress[msg.sender] += quantity;
_mint(msg.sender, quantity);
}
Не захищає від Sybil — один бот створює тисячі адрес. Але підвищує вартість атаки: потрібно більше гаманців, газ на переміщення ETH між ними. У комбінації з іншими методами — ефективно.
Глибоке занурення: proof-of-work при мінтингу
Найрідше застосовуємий, але цікавий механізм. Ідея: перед мінтом потрібно вирішити обчислювальну задачу — знайти nonce такий, що keccak256(address + nonce) < difficulty. Це CPU/GPU робота, яку бот робить швидше, але вона створює resource constraint.
uint256 public mintDifficulty = type(uint256).max / 1000; // 0.1% хешів проходять
function mint(uint256 nonce) external {
bytes32 hash = keccak256(abi.encodePacked(msg.sender, nonce, block.number / 100));
require(uint256(hash) < mintDifficulty, "Invalid proof of work");
_mint(msg.sender, 1);
}
block.number / 100 — вікно в ~100 блоків (~20 хвилин). Nonce валідний тільки в цьому вікні, нельзя обчислити заранее. Складність настроюється через mintDifficulty.
Проблема: мобільні користувачі витрачають 10-30 секунд на обчислення. Боти з GPU — 0.1 секунди. Асиметрія не на користь звичайних користувачів. Proof-of-work ефективний тільки у комбінації з вайтлістом, де боти спочатку не у листі.
Час очікування та batch ліміти
Додаткова механіка проти ботів: максимальний mint у перші N блоків від початку — 1 токен. Після N блоків — до MAX_PER_ADDRESS. Бот, який б'є в першу секунду, отримує тільки 1 токен. Користувачі, які прийшли через хвилину, можуть взяти більше.
uint256 public publicMintStartBlock;
function maxMintForBlock(uint256 _block) public view returns (uint256) {
if (_block < publicMintStartBlock + 50) return 1; // перші ~10 хв
return MAX_PER_ADDRESS;
}
Порівняння механізмів
| Механізм | Вартість атаки | UX для користувача | Складність реалізації |
|---|---|---|---|
| Per-address ліміт | Низька (Sybil) | Чудово | Мінімальна |
| Merkle whitelist | Висока | Хорошо | Середня |
| Commit-reveal | Висока | Погано (2 txs) | Висока |
| Proof-of-work | Середня | Нормально | Середня |
| Batch ліміт за часом | Середня | Чудово | Низька |
Рекомендовані комбінації
Мала колекція (<1000), закрита спільнота: Merkle whitelist + per-address ліміт 2-3.
Середня колекція (1000-10000), відкритий мінт: Whitelist фаза (Merkle) → public фаза з batch лімітом за часом + per-address ліміт.
Велика колекція (>10000), високий попит: Whitelist фаза + public фаза з proof-of-work або raffle через VRF.
Процес розробки
Аналітика (1 день). Визначаємо механіку: є ли whitelist, скільки фаз, ліміти на адресу.
Розробка (1-3 дні). Контракт з обраними механізмами. Off-chain скрипт генерації Merkle tree. API для видачі proofs.
Тестування. Окремо тестуємо кожен механізм: whitelist proof перевірка, ліміти, timing механіки. Foundry fuzz тест: testMintLimit(address,uint256) — будь-яка комбінація не повинна перевищувати ліміт.
Орієнтири за часом
Система з Merkle whitelist + per-address ліміт — 1-2 дні. Повна багатофазна система з proof-of-work та commit-reveal — 3-5 днів.







