Back to How-To Guides
HOW-TO9 min read2025-06-23

How to Integrate Chainlink VRF — Verifiable Random Number for NFT Reveals and Gaming

Chainlink VRF (Verifiable Random Function) provides cryptographically provable randomness on-chain. Essential for: NFT trait reveals, lottery selection, game outcomes, any application requiring tamper-proof randomness.

ClickMasters Team
Step-by-step implementation
Expert Assistance

Ready to Integrate Chainlink VRF?

Get expert guidance on implementing verifiable randomness in your project.

Complete Guide

Quick Answer

Chainlink VRF provides verifiable randomness that cannot be manipulated by validators. VRF V2.5 with direct funding costs 0.25–2.5 LINK per request (~$2.50–$25 at $10/LINK). For 10,000-item NFT collections: use one VRF request and derive all traits from a single seed to reduce cost to ~$25 total. Alternative: use the subscription model for pre-funded, cost-efficient requests.

Why Not Use block.prevrandao or block.timestamp?

block.prevrandao: validators can slightly influence this value. For high-value randomness: compromised.

block.timestamp: validators can adjust by ±15 seconds. Exploitable for timestamp-dependent randomness.

Chainlink VRF: Generates randomness off-chain with a cryptographic proof. The on-chain verifier confirms the proof before accepting the random value. Cannot be manipulated by validators or anyone else.

VRF V2.5 Direct Funding (No Subscription)

Example
// VRF V2.5: Direct Funding model (pay per request in LINK)
import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";

contract NFTReveal is VRFConsumerBaseV2Plus, ERC721 {
    
    // Chainlink VRF configuration
    address constant VRF_COORDINATOR = 0x9DdfaCa8183c41ad55329BdeeD9F6A8d53168B1B; // Mainnet
    bytes32 constant KEY_HASH = 0x8af398995b04c28e9951adb9721ef74c74f93e6a478f39e7e0777be13527e7ef;
    uint32 constant CALLBACK_GAS_LIMIT = 100_000;
    uint16 constant REQUEST_CONFIRMATIONS = 3;
    
    mapping(uint256 => uint256) public requestIdToTokenId;
    mapping(uint256 => uint256) public tokenIdToRevealSeed;
    
    constructor() VRFConsumerBaseV2Plus(VRF_COORDINATOR) ERC721("MyNFT", "MNFT") {}
    
    // After mint: request VRF for this token's trait assignment
    function requestReveal(uint256 tokenId) external {
        require(ownerOf(tokenId) == msg.sender, "Not owner");
        require(tokenIdToRevealSeed[tokenId] == 0, "Already revealed");
        
        uint256 requestId = s_vrfCoordinator.requestRandomWords(
            VRFV2PlusClient.RandomWordsRequest({
                keyHash: KEY_HASH,
                subId: 0,
                requestConfirmations: REQUEST_CONFIRMATIONS,
                callbackGasLimit: CALLBACK_GAS_LIMIT,
                numWords: 1,         // One random number per token
                extraArgs: VRFV2PlusClient._argsToBytes(
                    VRFV2PlusClient.ExtraArgsV1({nativePayment: false}) // Pay in LINK
                )
            })
        );
        
        requestIdToTokenId[requestId] = tokenId;
        emit RevealRequested(tokenId, requestId);
    }
    
    // Chainlink calls this with the random number
    function fulfillRandomWords(
        uint256 requestId,
        uint256[] calldata randomWords
    ) internal override {
        uint256 tokenId = requestIdToTokenId[requestId];
        tokenIdToRevealSeed[tokenId] = randomWords[0];
        
        emit TokenRevealed(tokenId, randomWords[0]);
    }
    
    // Derive traits from the random seed
    function getTraits(uint256 tokenId) public view returns (
        string memory background,
        string memory body,
        string memory accessory
    ) {
        uint256 seed = tokenIdToRevealSeed[tokenId];
        require(seed != 0, "Not revealed");
        
        // Use different slices of the seed for different traits
        uint256 backgroundSeed = uint256(keccak256(abi.encode(seed, "background"))) % 100;
        uint256 bodySeed = uint256(keccak256(abi.encode(seed, "body"))) % 100;
        uint256 accessorySeed = uint256(keccak256(abi.encode(seed, "accessory"))) % 100;
        
        background = _getBackground(backgroundSeed);
        body = _getBody(bodySeed);
        accessory = _getAccessory(accessorySeed);
    }
    
    function _getBackground(uint256 seed) internal pure returns (string memory) {
        if (seed < 5) return "Gold";        // 5% chance
        if (seed < 20) return "Purple";     // 15% chance
        if (seed < 50) return "Blue";       // 30% chance
        return "White";                      // 50% chance
    }
    
    event RevealRequested(uint256 tokenId, uint256 requestId);
    event TokenRevealed(uint256 tokenId, uint256 seed);
}

VRF Subscription Model (Cost Efficient for Many Requests)

Example
// For projects with many VRF requests: use subscription to pre-fund
// Create subscription at vrf.chain.link → get subscriptionId
// Fund subscription with LINK → requests draw from the balance

contract SubscriptionVRF is VRFConsumerBaseV2Plus {
    
    uint256 immutable subscriptionId;
    
    constructor(uint256 _subscriptionId) VRFConsumerBaseV2Plus(VRF_COORDINATOR) {
        subscriptionId = _subscriptionId;
    }
    
    function requestRandom() internal returns (uint256 requestId) {
        return s_vrfCoordinator.requestRandomWords(
            VRFV2PlusClient.RandomWordsRequest({
                keyHash: KEY_HASH,
                subId: subscriptionId,    // Use subscription
                requestConfirmations: 3,
                callbackGasLimit: 100_000,
                numWords: 1,
                extraArgs: VRFV2PlusClient._argsToBytes(
                    VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
                )
            })
        );
    }
}

Frequently Asked Questions

Common questions before following this guide

Clear answers to the most common practical, technical, and implementation questions.

1

Answers

How much does Chainlink VRF cost per request?

VRF V2.5 cost: approximately 0.25–2.5 LINK per request depending on: network (mainnet vs L2), gas limit, and key hash. At $10/LINK: $2.50–$25 per random number. For a 10,000-item NFT collection with per-token reveals: $25,000–$250,000 in VRF costs at mainnet. Cost optimization: use one VRF request for all tokens (request 1 random number, then derive all token traits from that single seed using different hash slices) — reduces to $25 total regardless of collection size.

Expert Assistance

Ready to Integrate Chainlink VRF?

Get expert guidance on implementing verifiable randomness in your project.