RISE Logo-Light

Quickstart

Build a dice game with verifiable randomness in 15 minutes

VRF Quickstart

Build a dice game with verifiable randomness and realtime updates in under 15 minutes. This tutorial demonstrates VRF integration with instant result notifications.

Testnet Only

This quickstart is designed for testnet development and testing. For mainnet production deployment, additional security measures including cryptographic proof verification and spam prevention are required. See the Smart Contracts page for details.

What We'll Build

A dice game that:

  • Requests verifiable random numbers
  • Tracks player streaks for rolling sixes
  • Updates UI in realtime via WebSocket
  • Handles VRF callbacks securely

Prerequisites

  • Node.js 16+ installed
  • Basic Solidity knowledge
  • MetaMask or similar wallet
  • RISE testnet tokens

Smart Contract Setup

Create the Project

mkdir vrf-dice-game
cd vrf-dice-game
forge init

Write the Dice Game Contract

Create src/DiceGame.sol:

src/DiceGame.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IVRFCoordinator {
    function requestRandomNumbers(uint32 numNumbers, uint256 seed) external returns (uint256);
}

interface IVRFConsumer {
    function rawFulfillRandomNumbers(
        uint256 requestId,
        uint256[] memory randomNumbers
    ) external;
}

contract DiceGame is IVRFConsumer {
    IVRFCoordinator public coordinator;

    mapping(address => bool) public hasPendingRoll;
    mapping(address => uint256) public currentStreak;
    mapping(address => uint256) public topStreak;
    mapping(uint256 => address) public requestOwners;

    uint256 public requestCount = 0;
    uint8 private constant DICE_SIDES = 6;

    event DiceRollRequested(address indexed player, uint256 indexed requestId);
    event DiceRollCompleted(
        address indexed player,
        uint256 indexed requestId,
        uint256 result,
        uint256 currentStreak,
        uint256 topStreak
    );
    event NewTopScore(address indexed player, uint256 newTopStreak);

    constructor(address _coordinator) {
        coordinator = IVRFCoordinator(_coordinator);
    }

    function rollDice() external returns (uint256 requestId) {
        require(!hasPendingRoll[msg.sender], "Already has a pending roll");

        hasPendingRoll[msg.sender] = true;
        uint256 seed = requestCount++;
        requestId = coordinator.requestRandomNumbers(1, seed);
        requestOwners[requestId] = msg.sender;

        emit DiceRollRequested(msg.sender, requestId);
        return requestId;
    }

    function rawFulfillRandomNumbers(
        uint256 requestId,
        uint256[] memory randomNumbers
    ) external {
        require(msg.sender == address(coordinator), "Only coordinator can fulfill");
        require(randomNumbers.length > 0, "No random numbers provided");

        address player = requestOwners[requestId];
        require(player != address(0), "Unknown request ID");
        require(hasPendingRoll[player], "No pending roll");

        uint256 diceRoll = (randomNumbers[0] % DICE_SIDES) + 1;

        if (diceRoll == 6) {
            currentStreak[player] += 1;
            if (currentStreak[player] > topStreak[player]) {
                topStreak[player] = currentStreak[player];
                emit NewTopScore(player, topStreak[player]);
            }
        } else {
            currentStreak[player] = 0;
        }

        hasPendingRoll[player] = false;

        emit DiceRollCompleted(
            player,
            requestId,
            diceRoll,
            currentStreak[player],
            topStreak[player]
        );

        delete requestOwners[requestId];
    }
}

Deploy to RISE Testnet

Configure foundry.toml:

foundry.toml
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc_version = "0.8.19"

[rpc_endpoints]
rise_testnet = "https://testnet.riselabs.xyz"

[etherscan]
rise_testnet = { key = "", url = "https://explorer.testnet.riselabs.xyz/api" }

Deploy:

forge create --rpc-url rise_testnet \
  --private-key $PRIVATE_KEY \
  --constructor-args 0x9d57aB4517ba97349551C876a01a7580B1338909 \
  src/DiceGame.sol:DiceGame

Verify on Blockscout:

forge verify-contract <DEPLOYED_ADDRESS> \
  src/DiceGame.sol:DiceGame \
  --chain 11155931 \
  --verifier blockscout \
  --verifier-url https://explorer.testnet.riselabs.xyz/api \
  --constructor-args $(cast abi-encode "constructor(address)" 0x9d57aB4517ba97349551C876a01a7580B1338909)

Frontend Integration

Setup Frontend

npm install viem
pnpm add viem
yarn add viem
bun add viem

Monitor VRF Events

Create monitor.ts to track dice roll events:

monitor.ts
import { createPublicClient, webSocket } from 'viem';
import { riseTestnet } from 'viem/chains';

const wsUrl = 'wss://testnet.riselabs.xyz/ws';
const diceGameAddress = '0x...'; // Your deployed contract

const vrfAbi = [
  {
    type: 'event',
    name: 'DiceRollCompleted',
    inputs: [
      { name: 'player', type: 'address', indexed: true },
      { name: 'requestId', type: 'uint256', indexed: true },
      { name: 'result', type: 'uint256' },
      { name: 'currentStreak', type: 'uint256' },
      { name: 'topStreak', type: 'uint256' }
    ]
  }
] as const;

const client = createPublicClient({
  chain: riseTestnet,
  transport: webSocket(wsUrl)
});

const unwatch = client.watchContractEvent({
  address: diceGameAddress,
  abi: vrfAbi,
  eventName: 'DiceRollCompleted',
  onLogs: (logs) => {
    logs.forEach((log) => {
      console.log(`Player rolled: ${log.args.result}`);
      console.log(`Streak: ${log.args.currentStreak}`);
    });
  }
});

How It Works

  1. User initiates: Click roll button in the DApp
  2. Request randomness: DApp calls rollDice() on the contract
  3. VRF processing: Contract requests random numbers from VRF coordinator
  4. Instant callback: VRF calls rawFulfillRandomNumbers() with result
  5. Event emission: Contract emits DiceRollCompleted event
  6. Realtime update: WebSocket delivers event to DApp in ~4ms
  7. UI update: DApp shows the dice result immediately

Key Takeaways

  • Instant Results: VRF responds in milliseconds via shreds
  • Realtime Updates: WebSocket delivers events instantly
  • Secure Randomness: Cryptographically verifiable numbers
  • Simple Integration: Standard Solidity interfaces

Next Steps

  • Add betting mechanics with token stakes
  • Create multiplayer dice battles
  • Implement leaderboards
  • Add different game modes