Разработка системы антибот-защиты при минтинге
Azuki в январе 2022 года: 8700 NFT за 3 минуты, 30 ETH газ в пике. Боты сминтили 2/3 коллекции раньше реальных пользователей. OpenSea через неделю: одни адреса перепродают по 10x. Это классическая ситуация без антибот-защиты. Следующие крупные запуски — BAYC, CryptoPunks — учли ошибки и внедрили различные механизмы. Какой из них правильный для вашей коллекции — зависит от размера аудитории и желаемого распределения.
Механизмы защиты и их trade-offs
Merkle whitelist: самая распространённая защита
Merkle tree из адресов whitelist. Каждый адрес из листа может доказать своё членство, предоставив 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 distribution критична и пользователи готовы к двум транзакциям.
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 — один бот создаёт тысячи адресов. Но повышает стоимость атаки: нужно больше кошельков, gas на перемещение 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 эффективен только в сочетании с whitelist, где боты изначально не в листе.
Время ожидания и batch limits
Дополнительная механика против ботов: максимальный 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 limit | Низкая (Sybil) | Отлично | Минимальная |
| Merkle whitelist | Высокая | Хорошо | Средняя |
| Commit-reveal | Высокая | Плохо (2 транзакции) | Высокая |
| Proof-of-work | Средняя | Нормально | Средняя |
| Batch limit по времени | Средняя | Отлично | Низкая |
Рекомендованные комбинации
Маленькая коллекция (<1000), закрытое сообщество: Merkle whitelist + per-address limit 2-3.
Средняя коллекция (1000-10000), открытый mint: Whitelist фаза (Merkle) → public фаза с batch limit по времени + per-address limit.
Крупная коллекция (>10000), высокий спрос: Whitelist фаза + public фаза с proof-of-work или раffle через VRF.
Процесс разработки
Аналитика (1 день). Определяем механику: есть ли whitelist, сколько фаз, лимиты на адрес.
Разработка (1-3 дня). Контракт с выбранными механизмами. Off-chain скрипт генерации Merkle tree. API для выдачи proofs.
Тестирование. Отдельно тестируем каждый механизм: whitelist proof verification, лимиты, timing механики. Foundry fuzz тест: testMintLimit(address,uint256) — любая комбинация не должна превышать лимит.
Ориентиры по срокам
Система с Merkle whitelist + per-address limit — 1-2 дня. Полная многофазная система с proof-of-work и commit-reveal — 3-5 дней.







