Development of Whitelist/Allowlist for NFT Minting
A typical scenario: a collection of 10,000 NFTs with a 3,000-address whitelist for 12 hours before public minting. If we store the whitelist on-chain as a mapping(address => bool)—deploying the contract with 3,000 addresses costs approximately 15-20M gas just for SSTORE operations. At 20 gwei, that's $300-400 in gas just for setup. Merkle proof solves this in one transaction: root hash in the contract, proof for each address off-chain. Gas for verifying one address during minting—around 3-5k gas instead of full SLOAD from the mapping.
Merkle Proof: How It Works and Where People Go Wrong
The Merkle tree is built off-chain: each address is hashed via keccak256(abi.encodePacked(address)), and the tree is built by pairwise hashing of the leaves. The result is a 32-byte merkleRoot. This root is deployed in the contract. When minting, the user passes proof[]—an array of sibling hashes along the path from their leaf to the root. The contract verifies via MerkleProof.verify() from OpenZeppelin.
Common mistake—double minting. If the contract lacks mapping(address => bool) public hasMinted (or mapping(address => uint256) public mintedCount), a user from the whitelist can mint unlimited times. The proof remains valid. The contract doesn't know the address has already minted.
Second mistake—leaf encoding. OpenZeppelin's MerkleProof expects the leaf to be keccak256(keccak256(data)) (double hash) for protection against preimage attacks in case 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 work under certain configurations. Use keccak256(bytes.concat(keccak256(abi.encode(addr)))) or the standard pairing: @openzeppelin/merkle-tree JS library + MerkleProof.sol.
Implementation Options
Merkle Proof (Primary)
Suitable for: lists from 100 to millions of addresses. Deployment cost doesn't depend on list size. Proof is passed by the user during minting (frontend generates it automatically).
Limitation: can't add an address after deployment without rebuilding the tree and updating the root. If the contract is upgradeable or the owner can call setMerkleRoot(bytes32)—this is solved. If immutable—no.
Backend Signature (ECDSA)
The contract owner holds a private key. For each whitelist address, the backend signs keccak256(abi.encodePacked(address, nonce)). The contract verifies the signature via ECDSA.recover() and compares with the signer address.
Advantage: dynamic management—can add addresses without changing the contract, can issue priorities, different quotas. Disadvantage: centralized signer—single point of failure and trust. If the key leaks—anyone can mint.
Use case: gaming mint (backend knows user achievement), dynamic campaigns.
On-chain Mapping
Only for very small lists (<100 addresses) or when the list is known in advance and won't change. addToWhitelist(address[]) with onlyOwner modifier. 20,000 gas per address.
Additional Mechanics
Multi-tier whitelist—different quotas for different levels. Merkle tree contains leaves keccak256(abi.encode(address, maxMintAmount)). The user passes proof + 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 each phase, different prices. owner changes the phase via setPhase().
Batch mint with WL—user can mint N NFTs in one transaction if their quota allows. mintedCount[msg.sender] += amount instead of a bool flag.
Development Process
Implementation (1-2 days). Contract with Merkle verify + hasMinted mapping + phase management. Tests in Foundry with edge cases: repeat mint, invalid proof, exhausted quota.
Frontend Integration (1 day). Proof generation via @openzeppelin/merkle-tree, wagmi hook useMint().
Deployment. Foundry script with automatic Etherscan verification.
Timeline Estimates
Whitelist contract with Merkle proof—2-3 days including tests and frontend integration.
Cost is calculated individually.







