From book
Integrates Cartridge VRF into Cairo contracts for provably fair, atomic randomness in Starknet Dojo games. Covers Scarb.toml setup, usage with nonce/salt sources, and JS/TS client tx prefixing.
How this skill is triggered — by the user, by Claude, or both
Slash command
/book:dojo-vrfThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Integrate Cartridge's Verifiable Random Function (VRF) for provably fair, atomic randomness in Dojo games.
Integrate Cartridge's Verifiable Random Function (VRF) for provably fair, atomic randomness in Dojo games.
Cartridge VRF provides cheap, atomic verifiable randomness for fully onchain games. The VRF request and response are processed within the same transaction, enabling synchronous and immediate randomness.
| Network | Contract Address |
|---|---|
| Mainnet | 0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f |
| Sepolia | 0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f |
Add to your Scarb.toml:
[dependencies]
cartridge_vrf = { git = "https://github.com/cartridge-gg/vrf" }
use cartridge_vrf::IVrfProviderDispatcher;
use cartridge_vrf::IVrfProviderDispatcherTrait;
use cartridge_vrf::Source;
// Use the deployed VRF provider address
const VRF_PROVIDER_ADDRESS: felt252 = 0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f;
#[dojo::contract]
mod game_system {
use starknet::{ContractAddress, get_caller_address};
use cartridge_vrf::{IVrfProviderDispatcher, IVrfProviderDispatcherTrait, Source};
const VRF_PROVIDER_ADDRESS: felt252 = 0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f;
#[abi(embed_v0)]
impl GameImpl of IGame<ContractState> {
fn roll_dice(ref self: ContractState) -> u8 {
let vrf_provider = IVrfProviderDispatcher {
contract_address: VRF_PROVIDER_ADDRESS.try_into().unwrap()
};
let player = get_caller_address();
// Consume random value using player's nonce
let random_value: felt252 = vrf_provider.consume_random(Source::Nonce(player));
// Convert to dice roll (1-6)
let random_u256: u256 = random_value.into();
let dice_roll: u8 = (random_u256 % 6 + 1).try_into().unwrap();
dice_roll
}
}
}
Two source types for randomness:
Source::Nonce(ContractAddress)let random = vrf_provider.consume_random(Source::Nonce(player_address));
Source::Salt(felt252)let random = vrf_provider.consume_random(Source::Salt(game_id));
When executing transactions that use VRF, prefix with request_random:
import { Account, CallData } from "starknet";
const VRF_PROVIDER = "0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f";
async function rollDice(account: Account, gameContract: string) {
const call = await account.execute([
// First: request_random
{
contractAddress: VRF_PROVIDER,
entrypoint: "request_random",
calldata: CallData.compile({
caller: gameContract,
source: [0, account.address], // Source::Nonce(address)
}),
},
// Then: your game action
{
contractAddress: gameContract,
entrypoint: "roll_dice",
calldata: [],
},
]);
return call;
}
import { buildVrfCalls } from "@cartridge/vrf";
const calls = await buildVrfCalls({
account,
call: {
contractAddress: GAME_CONTRACT,
entrypoint: "roll_dice",
calldata: [],
},
vrfProviderAddress: VRF_PROVIDER,
});
await account.execute(calls);
request_random(caller, source) as first call in multicallconsume_random(source) internallysubmit_random and assert_consumedassert_consumed ensures consume_random was called and resets storageFor local development/testing, use the mock provider:
#[dojo::contract]
mod vrf_provider_mock {
use cartridge_vrf::PublicKey;
use cartridge_vrf::vrf_provider::vrf_provider_component::VrfProviderComponent;
use openzeppelin::access::ownable::OwnableComponent;
component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
component!(path: VrfProviderComponent, storage: vrf_provider, event: VrfProviderEvent);
#[abi(embed_v0)]
impl VrfProviderImpl = VrfProviderComponent::VrfProviderImpl<ContractState>;
#[storage]
pub struct Storage {
#[substorage(v0)]
ownable: OwnableComponent::Storage,
#[substorage(v0)]
vrf_provider: VrfProviderComponent::Storage,
}
fn dojo_init(ref self: ContractState, pubkey_x: felt252, pubkey_y: felt252) {
self.ownable.initializer(starknet::get_caller_address());
self.vrf_provider.initializer(PublicKey { x: pubkey_x, y: pubkey_y });
}
}
fn random_in_range(random: felt252, min: u32, max: u32) -> u32 {
let random_u256: u256 = random.into();
let range = max - min + 1;
let result: u32 = (random_u256 % range.into()).try_into().unwrap();
result + min
}
fn weighted_random(random: felt252, weights: Span<u32>) -> u32 {
let total_weight: u32 = weights.iter().sum();
let random_u256: u256 = random.into();
let threshold: u32 = (random_u256 % total_weight.into()).try_into().unwrap();
let mut cumulative: u32 = 0;
let mut i: u32 = 0;
loop {
cumulative += *weights.at(i);
if cumulative > threshold {
break i;
}
i += 1;
}
}
fn shuffle<T, impl TCopy: Copy<T>, impl TDrop: Drop<T>>(
ref arr: Array<T>,
vrf_provider: IVrfProviderDispatcher,
player: ContractAddress,
) {
let mut i = arr.len();
loop {
if i <= 1 {
break;
}
i -= 1;
let random = vrf_provider.consume_random(Source::Nonce(player));
let j: u32 = (random.into() % (i + 1).into()).try_into().unwrap();
// Swap arr[i] and arr[j]
let temp = *arr.at(i);
arr.set(i, *arr.at(j));
arr.set(j, temp);
};
}
Source in request_random and consume_randomconsume_random must be called within the same transactionnpx claudepluginhub dojoengine/bookUse when writing Solidity contracts that need randomness — NFT mints, lotteries, games — replacing insecure block variable entropy with Chainlink VRF or commit-reveal to prevent miner manipulation.
Generates and verifies SNIP-36 virtual block proofs on Starknet: runs Cairo logic off-chain, submits stwo-cairo proof on-chain. Use for heavy computation, private attribute proofs, anonymous voting, or provable games.
Generates Cairo Dojo system contracts for implementing game logic, modifying model state, handling player actions, and emitting events in Starknet games.