NFT Allowlist and Presale Mechanics 2 — Questions and Answers
Question 1: What is the key architectural difference between signature-based allowlisting and Merkle tree allowlisting?
- Signature-based stores all addresses on-chain; Merkle-based stores them off-chain
- Signature-based requires a trusted backend signer to issue per-user signatures; Merkle-based uses a precomputed tree with a single on-chain root (Correct answer)
- Signature-based only works with ERC-1155; Merkle-based only works with ERC-721
- Signature-based verification is always cheaper on-chain than Merkle verification
Correct answer: Signature-based requires a trusted backend signer to issue per-user signatures; Merkle-based uses a precomputed tree with a single on-chain root
In signature-based allowlisting a trusted signer generates individual ECDSA signatures for each user, while Merkle trees precompute the full tree off-chain and store only the root, requiring no active signer at mint time.
Question 2: In an ECDSA signature-based allowlist, what does the NFT smart contract verify during a mint call?
- That the caller's address matches the contract owner's address
- That the address recovered from the signature using ecrecover() matches the stored trusted signer address (Correct answer)
- That the signature bytes are exactly 65 bytes long
- That the caller holds at least one existing NFT from the collection
Correct answer: That the address recovered from the signature using ecrecover() matches the stored trusted signer address
The contract hashes the message, calls ecrecover() with the signature, and checks that the recovered address equals the stored trustedSigner variable, confirming the signature was created by an authorized party.
Question 3: What is a signature replay attack in the context of NFT allowlists, and how is it prevented?
- Reusing the same Merkle root across multiple contract deployments; prevented by generating a new root per deployment
- Submitting a valid signature multiple times to mint more NFTs than intended; prevented by tracking used signatures or including per-address nonces (Correct answer)
- Replaying failed transactions after a gas increase; prevented by require() checks
- Using an old signature after the presale ends; prevented by a block.timestamp expiry in the signature
Correct answer: Submitting a valid signature multiple times to mint more NFTs than intended; prevented by tracking used signatures or including per-address nonces
A replay attack reuses a valid signature to call mint() repeatedly; the fix is to record signatures in a mapping after first use or to embed a nonce in the signed message that is incremented and validated on-chain.
Question 4: How should an NFT contract correctly enforce per-address mint limits during a presale?
- Emit a Transfer event and count mints off-chain after each transaction
- Maintain a mapping(address => uint256) on-chain that records how many tokens each address has minted, checked and updated atomically with the mint (Correct answer)
- Check the caller's ERC-721 balanceOf() after each mint to infer how many they hold
- Limit each address to one mint per block using block.number
Correct answer: Maintain a mapping(address => uint256) on-chain that records how many tokens each address has minted, checked and updated atomically with the mint
A mapping(address => uint256) updated inside the mint function atomically records each address's usage, allowing the contract to revert if the caller exceeds their allowed quantity.
Question 5: Which built-in Ethereum function is used in Solidity to recover the signing address from an ECDSA signature?
- recover(bytes32 hash, bytes memory sig)
- ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) (Correct answer)
- abi.decode(sig, (address))
- keccak256(abi.encodePacked(sig))
Correct answer: ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s)
ecrecover() is an Ethereum precompile that accepts a message hash and the three signature components (v, r, s) and returns the Ethereum address whose private key produced that signature.
Question 6: Why should the contract address be included in the signed message payload for signature-based allowlists?
- To reduce gas by hashing fewer parameters together
- To prevent cross-contract replay attacks where a valid signature for one NFT collection is submitted to a different collection's contract (Correct answer)
- To comply with EIP-712 domain separator requirements for gas savings
- To allow the contract to auto-verify the caller's identity without ecrecover()
Correct answer: To prevent cross-contract replay attacks where a valid signature for one NFT collection is submitted to a different collection's contract
Binding the signature to a specific contract address ensures it cannot be replayed against a different contract that uses the same trusted signer, even if deployed by the same team.
Question 7: What is the standard pattern for managing multiple minting phases (closed, allowlist, public sale) in an NFT smart contract?
- Deploy a separate contract for each phase and transfer ownership between them
- Define an enum for sale phases, store the current phase in a state variable, and let the owner advance it with an onlyOwner setter (Correct answer)
- Hardcode block numbers at deployment time to switch phases automatically
- Store phase configuration in IPFS metadata and read it via a Chainlink oracle
Correct answer: Define an enum for sale phases, store the current phase in a state variable, and let the owner advance it with an onlyOwner setter
An enum (e.g., enum Phase { CLOSED, ALLOWLIST, PUBLIC }) paired with a state variable and an onlyOwner transition function is the canonical, auditable on-chain pattern for phase management.
What is the key architectural difference between signature-based allowlisting and Merkle tree allowlisting?