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)
// 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)
// 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})
)
})
);
}
}