Chainlink VRF integration for casino random number generation

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Chainlink VRF integration for casino random number generation
Simple
from 1 business day to 3 business days
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1218
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    853
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1047
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Chainlink VRF Integration for Casino Random Number Generation

A blockchain casino with fair randomness — this is not a marketing claim, it's a technical architecture. Chainlink VRF (Verifiable Random Function) generates a random number with a cryptographic proof of its correctness. On-chain verification of the proof happens before the number is used in game logic — manipulation is excluded mathematically, not organizationally.

VRF v2.5: Subscription vs Direct Funding

The current version is VRF v2.5. Two payment models:

Subscription model. A subscription is created at vrf.chain.link, replenished with LINK. Multiple casino contracts use one balance. Suitable for products with regular requests — roulette, slots, card games.

Direct Funding (VRFV2PlusWrapper). The contract pays for each request itself in LINK or native token (ETH, MATIC). Simpler to launch, no need to manage a separate subscription. But each request is slightly more expensive.

For high-load casinos — subscription. For NFT minting with random attributes or one-off tournaments — Direct Funding is operationally simpler.

Integration into a Casino Contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

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 RouletteGame is VRFConsumerBaseV2Plus {
    uint256 public immutable subscriptionId;
    bytes32 public immutable keyHash;

    struct Bet {
        address player;
        uint256 amount;
        uint8 betType;   // 0=red, 1=black, 2=number
        uint8 number;
    }

    mapping(uint256 requestId => Bet) public pendingBets;

    event BetPlaced(uint256 indexed requestId, address indexed player);
    event GameResult(uint256 indexed requestId, uint8 result, bool won);

    function placeBet(uint8 betType, uint8 number) external payable {
        require(msg.value >= MIN_BET, "Below minimum");
        require(msg.value <= MAX_BET, "Above maximum");

        uint256 requestId = s_vrfCoordinator.requestRandomWords(
            VRFV2PlusClient.RandomWordsRequest({
                keyHash: keyHash,
                subId: subscriptionId,
                requestConfirmations: 3,
                callbackGasLimit: 150_000,
                numWords: 1,
                extraArgs: VRFV2PlusClient._argsToBytes(
                    VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
                )
            })
        );

        pendingBets[requestId] = Bet({
            player: msg.sender,
            amount: msg.value,
            betType: betType,
            number: number
        });

        emit BetPlaced(requestId, msg.sender);
    }

    function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords)
        internal override {
        Bet memory bet = pendingBets[requestId];
        delete pendingBets[requestId];

        uint8 result = uint8(randomWords[0] % 37); // 0-36
        bool won = _checkWin(bet, result);

        if (won) {
            uint256 payout = _calculatePayout(bet);
            payable(bet.player).transfer(payout);
        }

        emit GameResult(requestId, result, won);
    }
}

Critical Details

callbackGasLimit must have a buffer. If there's not enough gas in fulfillRandomWords — Chainlink won't retry automatically. The request will be lost, the bet will hang. Calculate real gas: forge test --gas-report. For roulette with payout and events — 150K gas is enough. For complex logic with multiple bets — increase.

requestConfirmations: 3 — minimum. On Ethereum with a 2-block reorg, Chainlink might get a different seed for random. 3 confirmations — a reasonable compromise between speed and security. For jackpot bets, use 5-10.

Don't store bets in an array. Mapping requestId => Bet is the right approach. An array of bets with requestId search — O(n) in the callback, gas griefing with a large number of pending bets.

keyHash — Lane Selection

Chainlink provides several keyHash-s for one network — they differ in maximum gas price Chainlink is willing to spend on delivering random. On Ethereum mainnet:

  • 0x8af... — 200 gwei lane: Chainlink delays delivery if gas price is higher
  • 0x9fe... — 500 gwei lane: delivery even during network congestion (more expensive)

For a casino with instant games, use the 500 gwei lane — a player shouldn't wait hours during peak load.

Protection Against Abuse

A bet is placed, random is in transit. What if a player places a bet and waits for the result on another device, then cancels if they don't like the result? In the current architecture, cancellation is impossible — funds are locked in the contract until fulfillRandomWords. This is correct.

But what if random doesn't arrive within 24 hours (Chainlink is unavailable, subscription balance exhausted)? We need a bet refund function with timeout check:

function refundExpiredBet(uint256 requestId) external {
    Bet memory bet = pendingBets[requestId];
    require(bet.player == msg.sender, "Not your bet");
    require(block.timestamp > betTimestamps[requestId] + 24 hours, "Not expired");
    delete pendingBets[requestId];
    payable(msg.sender).transfer(bet.amount);
}

Testing

Chainlink provides VRFCoordinatorV2_5Mock for Foundry tests — it imitates the Coordinator and lets you manually call fulfillRandomWords with a given random:

vrfCoordinator.fulfillRandomWords(requestId, address(game));

Fuzz test: check payout correctness for all randomWords[0] values from 0 to 2^256-1. Edge case: randomWords[0] % 37 == 0 — zero on the roulette, a rare edge case.

Timelines

Basic VRF integration into an existing game contract: 1-2 days. New casino contract with VRF, payouts, and timeout protection: 2-3 days. Testing on Sepolia with real VRF — included.

Cost is calculated after clarifying the game type and betting mechanics.