From c8d5e2a7926301c9fade3cee13452180a92d0605 Mon Sep 17 00:00:00 2001 From: Tony Stark Date: Tue, 14 May 2024 11:22:59 -0500 Subject: [PATCH 1/6] feat: dice game vrf application --- Scarb.lock | 13 ++ Scarb.toml | 1 + .../applications/dice_game_vrf/.gitignore | 2 + .../applications/dice_game_vrf/Scarb.toml | 14 ++ .../dice_game_vrf/src/dice_game_vrf.cairo | 214 ++++++++++++++++++ .../applications/dice_game_vrf/src/lib.cairo | 4 + .../dice_game_vrf/src/tests.cairo | 2 + src/ch01/dice_game_vrf.md | 11 + 8 files changed, 261 insertions(+) create mode 100644 listings/applications/dice_game_vrf/.gitignore create mode 100644 listings/applications/dice_game_vrf/Scarb.toml create mode 100644 listings/applications/dice_game_vrf/src/dice_game_vrf.cairo create mode 100644 listings/applications/dice_game_vrf/src/lib.cairo create mode 100644 listings/applications/dice_game_vrf/src/tests.cairo create mode 100644 src/ch01/dice_game_vrf.md diff --git a/Scarb.lock b/Scarb.lock index 60e669e4..91860e70 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -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" @@ -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" diff --git a/Scarb.toml b/Scarb.toml index e5c678ae..157ce6b3 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -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" } diff --git a/listings/applications/dice_game_vrf/.gitignore b/listings/applications/dice_game_vrf/.gitignore new file mode 100644 index 00000000..73aa31e6 --- /dev/null +++ b/listings/applications/dice_game_vrf/.gitignore @@ -0,0 +1,2 @@ +target +.snfoundry_cache/ diff --git a/listings/applications/dice_game_vrf/Scarb.toml b/listings/applications/dice_game_vrf/Scarb.toml new file mode 100644 index 00000000..69f8e1ce --- /dev/null +++ b/listings/applications/dice_game_vrf/Scarb.toml @@ -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]] \ No newline at end of file diff --git a/listings/applications/dice_game_vrf/src/dice_game_vrf.cairo b/listings/applications/dice_game_vrf/src/dice_game_vrf.cairo new file mode 100644 index 00000000..71cdda49 --- /dev/null +++ b/listings/applications/dice_game_vrf/src/dice_game_vrf.cairo @@ -0,0 +1,214 @@ +// 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 { + 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 + ); + fn receive_random_words( + ref self: TContractState, + requester_address: ContractAddress, + request_id: u64, + random_words: Span, + calldata: Array + ); + fn withdraw_extra_fee_fund(ref self: TContractState, receiver: ContractAddress); +} + +#[starknet::interface] +pub trait IDiceGame { + 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; + impl InternalImpl = OwnableComponent::InternalImpl; + + #[storage] + struct Storage { + user_guesses: LegacyMap, + 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 { + 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 { + 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 + ) { + 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, + calldata: Array + ) { + // 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 \ No newline at end of file diff --git a/listings/applications/dice_game_vrf/src/lib.cairo b/listings/applications/dice_game_vrf/src/lib.cairo new file mode 100644 index 00000000..056cbf8a --- /dev/null +++ b/listings/applications/dice_game_vrf/src/lib.cairo @@ -0,0 +1,4 @@ +mod dice_game_vrf; + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/listings/applications/dice_game_vrf/src/tests.cairo b/listings/applications/dice_game_vrf/src/tests.cairo new file mode 100644 index 00000000..361dba07 --- /dev/null +++ b/listings/applications/dice_game_vrf/src/tests.cairo @@ -0,0 +1,2 @@ +mod tests { // TODO +} diff --git a/src/ch01/dice_game_vrf.md b/src/ch01/dice_game_vrf.md new file mode 100644 index 00000000..fe8125a1 --- /dev/null +++ b/src/ch01/dice_game_vrf.md @@ -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) Oracle to generate random numbers. + +```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}} +``` From d39554a48ba6a684e2f25f7c9b22f49521138388 Mon Sep 17 00:00:00 2001 From: Tony Stark Date: Tue, 14 May 2024 11:35:35 -0500 Subject: [PATCH 2/6] feat: add summary nav --- src/SUMMARY.md | 1 + src/ch01/dice_game_vrf.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SUMMARY.md b/src/SUMMARY.md index c237c3d7..81d417c6 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -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) diff --git a/src/ch01/dice_game_vrf.md b/src/ch01/dice_game_vrf.md index fe8125a1..f1d64507 100644 --- a/src/ch01/dice_game_vrf.md +++ b/src/ch01/dice_game_vrf.md @@ -1,6 +1,6 @@ # Dice Game using Pragma VRF -This code provides an implementation of a Dice Game contract that utilizes a Pragma Verifiable Random Function (VRF) Oracle to generate random numbers. +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}} From a92adc53f4cbd1f3e8cdfb4ca4c34504295f9df3 Mon Sep 17 00:00:00 2001 From: Tony Stark Date: Tue, 14 May 2024 11:41:15 -0500 Subject: [PATCH 3/6] fix: ran scarb fmt --- .../dice_game_vrf/src/dice_game_vrf.cairo | 44 +++++++++++++------ .../applications/dice_game_vrf/src/lib.cairo | 2 +- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/listings/applications/dice_game_vrf/src/dice_game_vrf.cairo b/listings/applications/dice_game_vrf/src/dice_game_vrf.cairo index 71cdda49..6453bf48 100644 --- a/listings/applications/dice_game_vrf/src/dice_game_vrf.cairo +++ b/listings/applications/dice_game_vrf/src/dice_game_vrf.cairo @@ -38,7 +38,8 @@ pub trait IDiceGame { #[starknet::contract] mod DiceGame { use starknet::{ - ContractAddress, contract_address_const, get_block_number, get_caller_address, get_contract_address + 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}; @@ -78,7 +79,11 @@ mod DiceGame { } #[constructor] - fn constructor(ref self: ContractState, pragma_vrf_contract_address: ContractAddress, owner: ContractAddress) { + 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); @@ -88,7 +93,7 @@ mod DiceGame { impl DiceGame of super::IDiceGame { fn guess(ref self: ContractState, guess: u8) { assert(self.game_window.read(), 'GAME_INACTIVE'); - assert(guess >= 1 && guess <=6, 'INVALID_GUESS'); + assert(guess >= 1 && guess <= 6, 'INVALID_GUESS'); let caller = get_caller_address(); self.user_guesses.write(caller, guess); @@ -115,17 +120,27 @@ mod DiceGame { 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 - })); + 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 - })); + self + .emit( + Event::GameLost( + ResultAnnouncement { + caller: caller, + guess: user_guess, + random_number: reduced_random_number + } + ) + ); } } } @@ -211,4 +226,5 @@ mod DiceGame { } } } -// ANCHOR_END: DiceGameContract \ No newline at end of file +// ANCHOR_END: DiceGameContract + diff --git a/listings/applications/dice_game_vrf/src/lib.cairo b/listings/applications/dice_game_vrf/src/lib.cairo index 056cbf8a..21e7daa8 100644 --- a/listings/applications/dice_game_vrf/src/lib.cairo +++ b/listings/applications/dice_game_vrf/src/lib.cairo @@ -1,4 +1,4 @@ mod dice_game_vrf; #[cfg(test)] -mod tests; \ No newline at end of file +mod tests; From 59eedbab077b0774677694aad69faec5ff54e999 Mon Sep 17 00:00:00 2001 From: Tony Stark Date: Tue, 14 May 2024 11:43:13 -0500 Subject: [PATCH 4/6] fix: ran scarb fmt --- listings/applications/dice_game_vrf/src/dice_game_vrf.cairo | 1 + 1 file changed, 1 insertion(+) diff --git a/listings/applications/dice_game_vrf/src/dice_game_vrf.cairo b/listings/applications/dice_game_vrf/src/dice_game_vrf.cairo index 6453bf48..0e7cbfe6 100644 --- a/listings/applications/dice_game_vrf/src/dice_game_vrf.cairo +++ b/listings/applications/dice_game_vrf/src/dice_game_vrf.cairo @@ -228,3 +228,4 @@ mod DiceGame { } // ANCHOR_END: DiceGameContract + From 5a7f68adf443bec47046af8fc639feb3044e1e17 Mon Sep 17 00:00:00 2001 From: Tony Stark Date: Mon, 22 Jul 2024 08:32:19 -0500 Subject: [PATCH 5/6] merge match --- Scarb.lock | 7 +++++++ Scarb.toml | 10 ---------- listings/applications/dice_game_vrf/Scarb.toml | 4 ++-- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Scarb.lock b/Scarb.lock index 1b9149f8..82894273 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -144,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" diff --git a/Scarb.toml b/Scarb.toml index b0161d18..8df8fcb2 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -12,22 +12,12 @@ test = "$(git rev-parse --show-toplevel)/scripts/test_resolver.sh" [workspace.tool.snforge] [workspace.dependencies] -<<<<<<< HEAD -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" } -======= starknet = ">=2.6.4" openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag="v0.14.0" } components = { path = "listings/applications/components" } snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.25.0" } # The latest Alexandria release supports only Cairo v2.6.0, so using explicit rev that supports Cairo v2.6.3 alexandria_storage = { git = "https://github.com/keep-starknet-strange/alexandria.git", rev="800f5ad" } ->>>>>>> main [workspace.package] description = "Collection of examples of how to use the Cairo programming language to create smart contracts on Starknet." diff --git a/listings/applications/dice_game_vrf/Scarb.toml b/listings/applications/dice_game_vrf/Scarb.toml index 69f8e1ce..fb2509c4 100644 --- a/listings/applications/dice_game_vrf/Scarb.toml +++ b/listings/applications/dice_game_vrf/Scarb.toml @@ -6,9 +6,9 @@ edition = "2023_11" [dependencies] starknet.workspace = true openzeppelin.workspace = true -pragma_lib.workspace = true +pragma_lib = { git = "https://github.com/astraly-labs/pragma-lib" } [scripts] test.workspace = true -[[target.starknet-contract]] \ No newline at end of file +[[target.starknet-contract]] From e7a1f3110e5e221ea807405e4a5f4820f4498f73 Mon Sep 17 00:00:00 2001 From: Tony Stark Date: Mon, 22 Jul 2024 21:57:05 -0500 Subject: [PATCH 6/6] wip: extra randomness context --- src/SUMMARY.md | 2 +- src/applications/dice_game_vrf.md | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 78d356da..45f2004d 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -62,7 +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](./ch01/dice_game_vrf.md) +- [Dice Game VRF](./applications/dice_game_vrf.md) diff --git a/src/applications/dice_game_vrf.md b/src/applications/dice_game_vrf.md index f1d64507..eef0f0ab 100644 --- a/src/applications/dice_game_vrf.md +++ b/src/applications/dice_game_vrf.md @@ -1,4 +1,10 @@ -# Dice Game using Pragma VRF +# 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.