Development of NFT Reveal Mechanics
Reveal is the moment when an NFT "opens": a placeholder image is replaced with the final one. Technically, this is updating the baseURI in the contract or switching the tokenURI logic. The problem isn't in implementing reveal itself—the problem is fairness: who knew the final mapping of tokenId → trait before the reveal, and could they use this information to snipe rare tokens?
Why Random Reveals Are a Security Task
The Predictability Problem
If the project team stores metadata files in advance (e.g., 1.json, 2.json, ..., 10000.json), they know before the public reveal which tokenId gets which trait. A simple scenario: the team mints during presale, knowing that token #777 is legendary. Or worse—they sell this information.
The problem exists even if the team is honest: metadata files are often uploaded to IPFS before launch. An attentive researcher can discover the CID, access metadata through a gateway, and snipe rare tokens on the aftermarket immediately after the reveal.
On-chain Randomness Vulnerability
Early projects used blockhash(block.number - 1) or keccak256(abi.encodePacked(block.timestamp, msg.sender)) as randomness sources for reveal. Both implementations are predictable. A miner (on PoW) or validator (on PoS) controls block.timestamp within a few seconds. An attacking contract can check what trait it will get and revert if it's unfavorable—this is called a reroll attack.
Any on-chain randomness source is vulnerable because the result is deterministic and visible to whoever builds the block.
Chainlink VRF as the Standard for Fair Reveals
Chainlink VRF (Verifiable Random Function) v2 is the only production-ready way to get a cryptographically random number on-chain. The scheme:
- The contract requests randomness via
requestRandomWords(keyHash, subId, confirmations, callbackGasLimit, numWords) - Chainlink oracle generates a random number + cryptographic proof
- After 1-3 blocks,
fulfillRandomWords(requestId, randomWords)is called in our contract - The contract saves
revealOffset = randomWords[0] % totalSupply
After reveal: tokenURI(tokenId) returns metadata for (tokenId + revealOffset) % totalSupply. The team doesn't know revealOffset until receiving the VRF response—fairness is guaranteed cryptographically.
Subscription vs Direct Funding
VRF v2 supports two payment models. Subscription — we top up the LINK balance for a subscriptionId; multiple contracts can use one subscription. Direct Funding — each request is paid separately. For reveal, we use Subscription: one request for the entire project, costing 0.1-0.2 LINK (Ethereum) or less on L2.
Important: callbackGasLimit must cover the execution of fulfillRandomWords. If gas is too low, the callback won't execute and randomness will be lost. For simple reveal, 100k gas is sufficient.
Alternative Approaches
Team commit-reveal. The team publishes a hash keccak256(secret) before minting and reveals secret after minting ends. Offset = uint256(keccak256(secret)) % totalSupply. Fair if the team can't change the secret after publishing the hash. Drawback—trust assumption on the team.
Delayed metadata upload. The contract is deployed without baseURI. After minting ends, the team generates the mapping, uploads to IPFS, and sets baseURI. Technically fair but opaque to users—no on-chain guarantee.
Provenance hash. A standard popularized by Bored Ape Yacht Club: before deployment, the hash of the concatenation of all images in final order is published. Users can verify that images haven't changed since the hash was published. Doesn't solve the assignment predictability problem but locks in the content.
Contract Implementation
uint256 public revealOffset;
bool public revealed;
string public unrevealedURI;
function tokenURI(uint256 tokenId) public view override returns (string memory) {
if (!revealed) return unrevealedURI;
uint256 revealedId = (tokenId + revealOffset) % totalSupply();
return string(abi.encodePacked(baseURI, revealedId.toString(), ".json"));
}
// Called only once after minting ends
function requestReveal() external onlyOwner {
require(!revealed, "Already revealed");
// Chainlink VRF v2 request
COORDINATOR.requestRandomWords(keyHash, subId, 3, 100000, 1);
}
function fulfillRandomWords(uint256, uint256[] memory randomWords) internal override {
revealOffset = randomWords[0] % totalSupply();
revealed = true;
emit Revealed(revealOffset);
}
Development Process
VRF Configuration (1 day). Registering a subscription at vrf.chain.link, topping up LINK, adding the consumer contract.
Development and Testing (1-2 days). Contract with VRF integration. Foundry mock for local testing of fulfillRandomWords—use VRFCoordinatorV2Mock from the Chainlink library.
Sepolia Testing (1 day). VRF works on all major testnets. Verify the full flow: mint → requestReveal → await callback → verify tokenURI.
Timeline Estimates
Reveal mechanics as a separate component for an existing contract—2-4 days. As part of a full collection development—included in the main scope.







