Whitelist/Allowlist Development for NFT Minting
Typical scenario: a 10,000 NFT collection, whitelist mint for 3,000 addresses 12 hours before public. Storing the whitelist on-chain as mapping(address => bool) costs about 15-20M gas just for SSTORE during contract deployment. At 20 gwei gas price, that's around $4,000. Our team with 7+ years of experience in blockchain development and 50+ completed NFT mint projects offers efficient solutions. We develop turnkey whitelist/allowlist systems using Merkle tree, reducing costs to 50k gas — saving up to $4,000. Evaluate your project in 1 day — get a consultation.
How Merkle proof works and where mistakes happen
A Merkle tree is built off-chain: each address is hashed via keccak256(abi.encodePacked(address)), and the tree is built by pairwise hashing of leaves. The result is a 32-byte merkleRoot. This root is deployed in the contract. During mint, the user provides a proof[] — an array of sibling hashes along the path from their leaf to the root. The contract verifies it via MerkleProof.verify() from OpenZeppelin.
Common mistake — double mint. If the contract lacks a mapping(address => bool) public hasMinted (or mapping(address => uint256) public mintedCount), a whitelisted user can mint unlimited times. The proof remains valid. The contract has no record that the address already minted.
Another serious issue is leaf encoding. OpenZeppelin's MerkleProof expects the leaf to be keccak256(keccak256(data)) (double hash) to protect against preimage attacks in case tree nodes coincide with leaves. If you generate the tree via merkletreejs with single hashing, but the contract uses _leaf = keccak256(abi.encodePacked(account)) without double hashing, a collision attack may be possible under certain configurations. We use keccak256(bytes.concat(keccak256(abi.encode(addr)))) or the standard pairing of @openzeppelin/merkle-tree JS library + MerkleProof.sol.
Which method to choose for your project?
There are three main approaches to implementing a whitelist. Compare their characteristics:
| Criteria | Merkle proof | ECDSA signature | On-chain mapping |
|---|---|---|---|
| Deployment cost (gas) | 50k (fixed) | 50k + base cost | 20k * N addresses |
| Update flexibility | Requires root change | Dynamic (any changes) | Owner-only functions |
| Centralization | None | Depends on signer key | None |
| Security | High (proven) | Medium (key risk) | High |
| Ideal list size | 100 — 1M+ | Any, with unknown changes | < 100 addresses |
Merkle proof is the market standard, 40x cheaper than on-chain mapping for 10,000 addresses. ECDSA is used in gaming mints and dynamic campaigns. On-chain mapping is suitable only for very small fixed lists.
Comparison of schemes for multi-tier and phases
For projects with different access levels (OG, core, community) or time phases (pre-sale, allowlist, public), additional logic is needed. Typical configurations:
| Scheme | Number of roots | Management | Gas per mint |
|---|---|---|---|
| Single root + multi-tier via encoding | 1 | Simple but fixed quotas | ~60k-70k |
| Separate roots per phase | 3-5 | Owner setPhase(), different prices | ~50k + root change |
| Hybrid: root + ECDSA for dynamics | 1 signature | Backend signs permissions | ~70k + signer risk |
The choice depends on requirements: if quotas can change before mint, ECDSA is better; if fixed, Merkle proof with multi-tier encoding is preferable.
How to generate Merkle tree in practice?
- Collect all addresses and quotas into an array.
- Use the
@openzeppelin/merkle-treelibrary:
const { StandardMerkleTree } = require('@openzeppelin/merkle-tree'); const values = [ ['0x...', 1], ['0x...', 3], ]; const tree = StandardMerkleTree.of(values, ['address', 'uint256']); const root = tree.root; - Store the root in the contract.
- For each user, generate a proof via
tree.getProof([address, maxMint])and pass it during mint.
Why Merkle proof is the market standard?
Regardless of list size, deployment costs are the same. The proof is submitted by the user (frontend generates it automatically); the contract only verifies it. We ensure no reentrancy and correct proof verification. Our contracts are audited using Slither and Mythril.
Additional mechanics
Multi-tier whitelist — different quotas for different levels. The Merkle tree contains leaves keccak256(abi.encode(address, maxMintAmount)). The user provides the proof plus their maxMintAmount; the contract verifies both parameters together.
Temporal phases — WL → Allowlist → Public. The contract holds enum SalePhase { PAUSED, WHITELIST, ALLOWLIST, PUBLIC }. Different roots for different phases, different prices. The owner changes the phase via setPhase().
Batch mint with WL — a user can mint N NFTs in one transaction if their quota allows. mintedCount[msg.sender] += amount instead of a bool flag.
What's included in development
- Smart contract with Merkle verify, hasMinted mapping, and phase management
- Comprehensive test suite in Foundry (edge cases: double mint, invalid proof, exhausted quota)
- Frontend integration (proof generation via
@openzeppelin/merkle-tree, wagmiuseMint()hook) - Deployment via Foundry script with automatic Etherscan verification
- Documentation and post-launch support
Time estimates
Whitelist contract with Merkle proof — 2-3 days including tests and frontend integration. Cost is calculated individually. Contact us to discuss your project — we will help you choose the optimal scheme and implement it with security guarantees.







