Antibot Mint Protection System Development
Azuki January 2022: 8,700 NFTs in 3 minutes, 30 ETH gas at peak. Bots minted 2/3 of collection before real users. OpenSea week later: same addresses reselling at 10x. Classic situation without antibot protection. Next major launches — BAYC, CryptoPunks — learned from mistakes and implemented various mechanisms. Which one is right for your collection depends on audience size and desired distribution.
Protection Mechanisms and Trade-Offs
Merkle Whitelist: Most Common Protection
Merkle tree of whitelist addresses. Each address from list can prove membership by providing proof of O(log n) hashes. Contract stores only one root (32 bytes), not entire list.
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 generation — off-chain TypeScript script via merkletreejs. Root updated before mint via setMerkleRoot() (onlyOwner). Proofs users get via API or published on IPFS beforehand.
Vulnerability: if frontend compromised, attacker can request proof for any address from list via API. Protection: proofs issued only to wallet requesting them (signature-gated API), or entire list published beforehand (full transparency).
Commit-Reveal: Protection Against Frontrunning on Random Mint
Without commit-reveal: bot analyzes mempool, sees transaction with parameters, makes exact copy with higher gas — frontrunning. With commit-reveal: user first publishes keccak256(secret + address), then reveals secret after N blocks. During N blocks copying is pointless — secret unknown.
Two-stage process inconvenient for users. Use only where random distribution critical and users ready for two transactions.
Per-Address Limits: Necessary Minimum
Most basic protection — per-address limit:
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);
}
Doesn't protect against Sybil — one bot creates thousands of addresses. But raises attack cost: need more wallets, gas for moving ETH between them. Combined with other methods — effective.
Deep Dive: Proof-of-Work on Mint
Rarest applied, but interesting mechanism. Idea: before minting must solve computational task — find nonce such that keccak256(address + nonce) < difficulty. This CPU/GPU work, bot does faster, but creates resource constraint.
uint256 public mintDifficulty = type(uint256).max / 1000; // 0.1% of hashes pass
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 — window of ~100 blocks (~20 minutes). Nonce valid only in this window, can't compute beforehand. Difficulty adjusted via mintDifficulty.
Problem: mobile users spend 10-30 seconds computing. Bots with GPU — 0.1 seconds. Asymmetry not in favor of regular users. Proof-of-work effective only combined with whitelist, where bots not in list initially.
Wait Time and Batch Limits
Additional anti-bot mechanic: maximum mint in first N blocks from start — 1 token. After N blocks — up to MAX_PER_ADDRESS. Bot hitting first second gets only 1 token. Users arriving after minute can take more.
uint256 public publicMintStartBlock;
function maxMintForBlock(uint256 _block) public view returns (uint256) {
if (_block < publicMintStartBlock + 50) return 1; // first ~10 min
return MAX_PER_ADDRESS;
}
Mechanism Comparison
| Mechanism | Attack Cost | UX for Users | Implementation Difficulty |
|---|---|---|---|
| Per-address limit | Low (Sybil) | Excellent | Minimal |
| Merkle whitelist | High | Good | Medium |
| Commit-reveal | High | Poor (2 txs) | High |
| Proof-of-work | Medium | Normal | Medium |
| Batch limit by time | Medium | Excellent | Low |
Recommended Combinations
Small collection (<1000), closed community: Merkle whitelist + per-address limit 2-3.
Medium collection (1000-10000), open mint: Whitelist phase (Merkle) → public phase with batch limit by time + per-address limit.
Large collection (>10000), high demand: Whitelist phase + public phase with proof-of-work or raffle via VRF.
Development Process
Analysis (1 day). Determine mechanics: whitelist or not, how many phases, per-address limits.
Development (1-3 days). Contract with chosen mechanisms. Off-chain script for Merkle tree generation. API for issuing proofs.
Testing. Separately test each mechanism: whitelist proof verification, limits, timing mechanics. Foundry fuzz test: testMintLimit(address,uint256) — any combination shouldn't exceed limit.
Timeline Estimates
System with Merkle whitelist + per-address limit — 1-2 days. Full multi-phase system with proof-of-work and commit-reveal — 3-5 days.







