Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Randomness #183

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Scarb.lock
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ dependencies = [
name = "custom_type_serde"
version = "0.1.0"

[[package]]
name = "dice_game_vrf"
version = "0.1.0"
dependencies = [
"openzeppelin",
"pragma_lib",
]

[[package]]
name = "ecdsa_verification"
version = "0.1.0"
Expand Down Expand Up @@ -120,6 +128,11 @@ name = "openzeppelin"
version = "0.14.0"
source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.14.0#f091c4f51ddeb10297db984acae965328c5a4e5b"

[[package]]
name = "pragma_lib"
version = "1.0.0"
source = "git+https://github.com/astraly-labs/pragma-lib#2eca9b70dc505788423da1eedbf2d65563cc3102"

[[package]]
name = "scarb"
version = "0.1.0"
Expand All @@ -131,9 +144,16 @@ dependencies = [
"openzeppelin",
]

[[package]]
name = "simple_storage"
version = "0.1.0"

[[package]]
name = "simple_vault"
version = "0.1.0"
dependencies = [
"erc20",
]

[[package]]
name = "snforge_std"
Expand Down
2 changes: 2 additions & 0 deletions listings/applications/dice_game_vrf/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target
.snfoundry_cache/
14 changes: 14 additions & 0 deletions listings/applications/dice_game_vrf/Scarb.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "dice_game_vrf"
version = "0.1.0"
edition = "2023_11"

[dependencies]
starknet.workspace = true
openzeppelin.workspace = true
pragma_lib = { git = "https://github.com/astraly-labs/pragma-lib" }

[scripts]
test.workspace = true

[[target.starknet-contract]]
231 changes: 231 additions & 0 deletions listings/applications/dice_game_vrf/src/dice_game_vrf.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// ANCHOR: DiceGameInterfaces
use starknet::ContractAddress;

// In order to generate a verifiable random number on chain we need to use a VRF (Verifiable Random Function) Oracle.
// We are using the Pragma Oracle VRF in this example.
#[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);
}
// ANCHOR_END: DiceGameInterfaces

// ANCHOR: DiceGameContract
#[starknet::contract]
mod DiceGame {
use 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: LegacyMap<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.write(caller, 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.read(caller).into();

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)]
// ANCHOR: PragmaVRFOracle
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);
}
}
}
// ANCHOR_END: DiceGameContract


4 changes: 4 additions & 0 deletions listings/applications/dice_game_vrf/src/lib.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
mod dice_game_vrf;

#[cfg(test)]
mod tests;
2 changes: 2 additions & 0 deletions listings/applications/dice_game_vrf/src/tests.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mod tests { // TODO
}
1 change: 1 addition & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Summary
- [Simple Storage with Starknet-js](./applications/simple_storage_starknetjs.md)
- [Crowdfunding Campaign](./applications/crowdfunding.md)
- [AdvancedFactory: Crowdfunding](./applications/advanced_factory.md)
- [Dice Game VRF](./applications/dice_game_vrf.md)

<!-- advanced-concepts -->

Expand Down
17 changes: 17 additions & 0 deletions src/applications/dice_game_vrf.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Randomness: Dice Game using Pragma VRF

## Understanding Randomness on the Blockchain

Randomness in blockchain applications is a challenging problem due to the deterministic nature of blockchains. In a blockchain, the same input always produces the same output, which conflicts with the need for unpredictable, random numbers in many applications. This determinism is crucial for consensus and verification but presents obstacles for use cases requiring randomness, such as fair selection in consensus mechanisms, lottery systems, or gaming applications.

One approach to generate randomness on the blockchain is the use of Verifiable Random Functions (VRFs). A VRF is a cryptographic function that generates a random number along with a proof of its correct generation. This proof can be verified by anyone, ensuring transparency and fairness.

This code provides an implementation of a Dice Game contract that utilizes a [Pragma Verifiable Random Function (VRF)](https://docs.pragma.build/Resources/Cairo%201/randomness/randomness) to generate random numbers on-chain.

```rust
{{#include ../../listings/applications/dice_game_vrf/src/dice_game_vrf.cairo:DiceGameInterfaces}}
```

```rust
{{#include ../../listings/applications/dice_game_vrf/src/dice_game_vrf.cairo:DiceGameContract}}
```