Randomness
Since all blockchains are fundamentally deterministic and most are public ledgers, generating truly unpredictatable randomness on-chain presents a challenge. This randomness is crucial for fair outcomes in gaming, lotteries, and unique generation of NFTs. To address this, verifiable random functions (VRFs) provided by oracles offer a solution. VRFs guarantee that the randomness can't be predicted or tampered with, ensuring trust and transparency in these applications.
Overview on VRFs
VRFs use a secret key and a nonce (a unique input) to generate an output that appears random. While technically 'pseudo-random', it's practically impossible for another party to predict the outcome without knowing the secret key.
VRFs produce not only the random number but also a proof that anyone can use to independently verify that the result was generated correctly according to the function's parameters.
Generating Randomness with Pragma
Pragma, an oracle on Starknet provides a solution for generating random numbers using VRFs. Let's dive into how to use Pragma VRF to generate a random number in a simple dice game contract.
Add Pragma as a Dependency
Edit your cairo project's Scarb.toml
file to include the path to use Pragma.
[dependencies]
pragma_lib = { git = "https://github.com/astraly-labs/pragma-lib" }
Define the Contract Interface
use core::starknet::ContractAddress;
#[starknet::interface]
pub trait IPragmaVRF<TContractState> {
fn get_last_random_number(self: @TContractState) -> felt252;
fn request_randomness_from_pragma(
ref self: TContractState,
seed: u64,
callback_address: ContractAddress,
callback_fee_limit: u128,
publish_delay: u64,
num_words: u64,
calldata: Array<felt252>
);
fn receive_random_words(
ref self: TContractState,
requester_address: ContractAddress,
request_id: u64,
random_words: Span<felt252>,
calldata: Array<felt252>
);
fn withdraw_extra_fee_fund(ref self: TContractState, receiver: ContractAddress);
}
#[starknet::interface]
pub trait IDiceGame<TContractState> {
fn guess(ref self: TContractState, guess: u8);
fn toggle_play_window(ref self: TContractState);
fn get_game_window(self: @TContractState) -> bool;
fn process_game_winners(ref self: TContractState);
}
Description of Key IPragmaVRF Entrypoints and Their Inputs
The function request_randomness_from_pragma
initiates a request for verifiable randomness from the Pragma oracle. It does this by emitting an event that triggers the following actions off-chain:
- Randomness generation: The oracle generates random values and a corresponding proof.
- On-chain submission: The oracle submits the generated randomness and proof back to the blockchain via the
receive_random_words
callback function.
request_randomness_from_pragma
Inputs
seed
: A value used to initialize the randomness generation process. This should be unique to ensure unpredictable results.callback_address
: The contract address where thereceive_random_words
function will be called to deliver the generated randomness. It is typically the address of your deployed contract implementing Pragma VRF.callback_fee_limit
: The maximum amount of gas you're willing to spend on executing thereceive_random_words
callback function.publish_delay
: The minimum delay (in blocks) between requesting randomness and the oracle fulfilling the request.num_words
: The number of random values (each represented as afelt252
) you want to receive in a single callback.calldata
: Additional data you want to pass to thereceive_random_words
callback function.
receive_randomn_words
Inputs
requester_address
: The contract address that initiated the randomness request.request_id
: A unique identifier assigned to the randomness request.random_words
: An array (span) of the generated random values (represented asfelt252
).calldata
: Additional data passed along with the initial randomness request.
Dice Game Contract
This dice game contract allows players to guess a number between 1 & 6 during an active game window. The contract owner then has the ability to toggle the game window to disable new guesses from players. To determine the winning number, the contract owner calls the request_randomness_from_pragma
function to request a random number from the Pragma VRF oracle. Once the random number is received through the receive_random_words
callback function, it is stored in the last_random_number
storage variable. Each player has to call process_game_winners
function to determine if they have won or lost. The last_random_number
generated is then reduced to a number between 1 & 6, and compared to the guesses of the players stored in the user_guesses
mapping, which leads to the emission of an event GameWinner
or GameLost
.
#[starknet::contract]
mod DiceGame {
use core::starknet::storage::{
Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess
};
use core::starknet::{
ContractAddress, contract_address_const, get_block_number, get_caller_address,
get_contract_address
};
use pragma_lib::abi::{IRandomnessDispatcher, IRandomnessDispatcherTrait};
use openzeppelin::token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait};
use openzeppelin::access::ownable::OwnableComponent;
component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
#[abi(embed_v0)]
impl OwnableImpl = OwnableComponent::OwnableImpl<ContractState>;
impl InternalImpl = OwnableComponent::InternalImpl<ContractState>;
#[storage]
struct Storage {
user_guesses: Map<ContractAddress, u8>,
pragma_vrf_contract_address: ContractAddress,
game_window: bool,
min_block_number_storage: u64,
last_random_number: felt252,
#[substorage(v0)]
ownable: OwnableComponent::Storage
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
GameWinner: ResultAnnouncement,
GameLost: ResultAnnouncement,
#[flat]
OwnableEvent: OwnableComponent::Event
}
#[derive(Drop, starknet::Event)]
struct ResultAnnouncement {
caller: ContractAddress,
guess: u8,
random_number: u256
}
#[constructor]
fn constructor(
ref self: ContractState,
pragma_vrf_contract_address: ContractAddress,
owner: ContractAddress
) {
self.ownable.initializer(owner);
self.pragma_vrf_contract_address.write(pragma_vrf_contract_address);
self.game_window.write(true);
}
#[abi(embed_v0)]
impl DiceGame of super::IDiceGame<ContractState> {
fn guess(ref self: ContractState, guess: u8) {
assert(self.game_window.read(), 'GAME_INACTIVE');
assert(guess >= 1 && guess <= 6, 'INVALID_GUESS');
let caller = get_caller_address();
self.user_guesses.entry(caller).write(guess);
}
fn toggle_play_window(ref self: ContractState) {
self.ownable.assert_only_owner();
let current: bool = self.game_window.read();
self.game_window.write(!current);
}
fn get_game_window(self: @ContractState) -> bool {
self.game_window.read()
}
fn process_game_winners(ref self: ContractState) {
assert(!self.game_window.read(), 'GAME_ACTIVE');
assert(self.last_random_number.read() != 0, 'NO_RANDOM_NUMBER_YET');
let caller = get_caller_address();
let user_guess: u8 = self.user_guesses.entry(caller).read();
let reduced_random_number: u256 = self.last_random_number.read().into() % 6 + 1;
if user_guess == reduced_random_number.try_into().unwrap() {
self
.emit(
Event::GameWinner(
ResultAnnouncement {
caller: caller,
guess: user_guess,
random_number: reduced_random_number
}
)
);
} else {
self
.emit(
Event::GameLost(
ResultAnnouncement {
caller: caller,
guess: user_guess,
random_number: reduced_random_number
}
)
);
}
}
}
#[abi(embed_v0)]
impl PragmaVRFOracle of super::IPragmaVRF<ContractState> {
fn get_last_random_number(self: @ContractState) -> felt252 {
let last_random = self.last_random_number.read();
last_random
}
fn request_randomness_from_pragma(
ref self: ContractState,
seed: u64,
callback_address: ContractAddress,
callback_fee_limit: u128,
publish_delay: u64,
num_words: u64,
calldata: Array<felt252>
) {
self.ownable.assert_only_owner();
let randomness_contract_address = self.pragma_vrf_contract_address.read();
let randomness_dispatcher = IRandomnessDispatcher {
contract_address: randomness_contract_address
};
// Approve the randomness contract to transfer the callback fee
// You would need to send some ETH to this contract first to cover the fees
let eth_dispatcher = ERC20ABIDispatcher {
contract_address: contract_address_const::<
0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7
>() // ETH Contract Address
};
eth_dispatcher
.approve(
randomness_contract_address,
(callback_fee_limit + callback_fee_limit / 5).into()
);
// Request the randomness
randomness_dispatcher
.request_random(
seed, callback_address, callback_fee_limit, publish_delay, num_words, calldata
);
let current_block_number = get_block_number();
self.min_block_number_storage.write(current_block_number + publish_delay);
}
fn receive_random_words(
ref self: ContractState,
requester_address: ContractAddress,
request_id: u64,
random_words: Span<felt252>,
calldata: Array<felt252>
) {
// Have to make sure that the caller is the Pragma Randomness Oracle contract
let caller_address = get_caller_address();
assert(
caller_address == self.pragma_vrf_contract_address.read(),
'caller not randomness contract'
);
// and that the current block is within publish_delay of the request block
let current_block_number = get_block_number();
let min_block_number = self.min_block_number_storage.read();
assert(min_block_number <= current_block_number, 'block number issue');
let random_word = *random_words.at(0);
self.last_random_number.write(random_word);
}
fn withdraw_extra_fee_fund(ref self: ContractState, receiver: ContractAddress) {
self.ownable.assert_only_owner();
let eth_dispatcher = ERC20ABIDispatcher {
contract_address: contract_address_const::<
0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7
>() // ETH Contract Address
};
let balance = eth_dispatcher.balance_of(get_contract_address());
eth_dispatcher.transfer(receiver, balance);
}
}
}
NB: Fund Your Contract After Deployment to Utilize Pragma VRF
After deploying your contract that includes Pragma VRF functionalities, ensure it holds sufficient ETH to cover the expenses related to requesting random values. Pragma VRF requires payment for both generating the random numbers and executing the callback function defined in your contract.
For more information, please refer to the Pragma docs.