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 4 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
13 changes: 13 additions & 0 deletions Scarb.lock
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ version = "0.1.0"
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 @@ -89,6 +97,11 @@ name = "openzeppelin"
version = "0.11.0"
source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.11.0#a83f36b23f1af6e160288962be4a2701c3ecbcda"

[[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 Down
1 change: 1 addition & 0 deletions Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ test = "$(git rev-parse --show-toplevel)/scripts/test_resolver.sh"
starknet = ">=2.6.3"
# snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.11.0" }
openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag="v0.11.0" }
pragma_lib = { git = "https://github.com/astraly-labs/pragma-lib" }

# [workspace.dev-dependencies]
# openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag="v0.11.0" }
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.workspace = true

[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 @@ -55,6 +55,7 @@ Summary
- [Defi Vault](./ch01/simple_vault.md)
- [ERC20 Token](./ch01/erc20.md)
- [Constant Product AMM](./ch01/constant-product-amm.md)
- [Dice Game VRF](./ch01/dice_game_vrf.md)

<!-- ch02 -->

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

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about explaining a bit more about randomness in the context of blockchain before jumping directly in the implementation? Especially to outline where the entropy comes from and potential risks with it. You can then just link to Pragma - Randomness

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Entropy sources:

  • Blockchain: block hashes, timestamps, ...
  • External Sources: Oracles

Risks: Predictability/Manipulation/Centralization-trust

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright. I'll make an update to this week

```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}}
```