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 initWrite the Dice Game Contract
Create 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:
[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:DiceGameVerify 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 viempnpm add viemyarn add viembun add viemMonitor VRF Events
Create monitor.ts to track dice roll events:
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
- User initiates: Click roll button in the DApp
- Request randomness: DApp calls
rollDice()on the contract - VRF processing: Contract requests random numbers from VRF coordinator
- Instant callback: VRF calls
rawFulfillRandomNumbers()with result - Event emission: Contract emits
DiceRollCompletedevent - Realtime update: WebSocket delivers event to DApp in ~4ms
- 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