diff --git a/Scarb.lock b/Scarb.lock index 96d58087..82894273 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -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" @@ -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" @@ -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" 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..fb2509c4 --- /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 = { git = "https://github.com/astraly-labs/pragma-lib" } + +[scripts] +test.workspace = true + +[[target.starknet-contract]] 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..0e7cbfe6 --- /dev/null +++ b/listings/applications/dice_game_vrf/src/dice_game_vrf.cairo @@ -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 { + 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 + + 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..21e7daa8 --- /dev/null +++ b/listings/applications/dice_game_vrf/src/lib.cairo @@ -0,0 +1,4 @@ +mod dice_game_vrf; + +#[cfg(test)] +mod tests; 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/SUMMARY.md b/src/SUMMARY.md index ae79c8d8..45f2004d 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -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) diff --git a/src/applications/dice_game_vrf.md b/src/applications/dice_game_vrf.md new file mode 100644 index 00000000..eef0f0ab --- /dev/null +++ b/src/applications/dice_game_vrf.md @@ -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}} +```