From 33085ea6fe34efbbb5dc76af0873c1976ea37375 Mon Sep 17 00:00:00 2001 From: Tony Stark Date: Tue, 14 May 2024 11:22:59 -0500 Subject: [PATCH 01/48] 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/applications/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/applications/dice_game_vrf.md diff --git a/Scarb.lock b/Scarb.lock index 96d58087..1b9149f8 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" diff --git a/Scarb.toml b/Scarb.toml index 8df8fcb2..395921fa 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -18,6 +18,7 @@ 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" } +pragma_lib = { git = "https://github.com/astraly-labs/pragma-lib" } [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/.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/applications/dice_game_vrf.md b/src/applications/dice_game_vrf.md new file mode 100644 index 00000000..fe8125a1 --- /dev/null +++ b/src/applications/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 a6aa09774f8d94cbfe365a56fc7bbe6876895ab5 Mon Sep 17 00:00:00 2001 From: Tony Stark Date: Tue, 14 May 2024 11:35:35 -0500 Subject: [PATCH 02/48] feat: add summary nav --- src/SUMMARY.md | 1 + src/applications/dice_game_vrf.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) 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 index fe8125a1..f1d64507 100644 --- a/src/applications/dice_game_vrf.md +++ b/src/applications/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 ef37fa613f9b5971820201a0356c5fd28b5df347 Mon Sep 17 00:00:00 2001 From: Tony Stark Date: Tue, 14 May 2024 11:41:15 -0500 Subject: [PATCH 03/48] 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 c2e1553ce047be885bcc1ef6554c1551d2cb562f Mon Sep 17 00:00:00 2001 From: Tony Stark Date: Tue, 14 May 2024 11:43:13 -0500 Subject: [PATCH 04/48] 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 f594f76704cfbf585f845bf5737c1aec2260d1e0 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 16 Jul 2024 12:24:28 +0200 Subject: [PATCH 05/48] Fix new lines --- Scarb.lock | 4 ++++ listings/applications/dice_game_vrf/Scarb.toml | 2 +- listings/applications/dice_game_vrf/src/dice_game_vrf.cairo | 1 - 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Scarb.lock b/Scarb.lock index 1b9149f8..178ed7df 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -144,6 +144,10 @@ dependencies = [ "openzeppelin", ] +[[package]] +name = "simple_storage" +version = "0.1.0" + [[package]] name = "simple_vault" version = "0.1.0" diff --git a/listings/applications/dice_game_vrf/Scarb.toml b/listings/applications/dice_game_vrf/Scarb.toml index 69f8e1ce..a1e1a2b8 100644 --- a/listings/applications/dice_game_vrf/Scarb.toml +++ b/listings/applications/dice_game_vrf/Scarb.toml @@ -11,4 +11,4 @@ pragma_lib.workspace = true [scripts] test.workspace = true -[[target.starknet-contract]] \ No newline at end of file +[[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 index 0e7cbfe6..6453bf48 100644 --- a/listings/applications/dice_game_vrf/src/dice_game_vrf.cairo +++ b/listings/applications/dice_game_vrf/src/dice_game_vrf.cairo @@ -228,4 +228,3 @@ mod DiceGame { } // ANCHOR_END: DiceGameContract - From 3db10380432b9e44e1121bb8ec14cc49565e7667 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 16 Jul 2024 12:46:28 +0200 Subject: [PATCH 06/48] Add more info on randomness sources --- src/applications/dice_game_vrf.md | 63 ++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/src/applications/dice_game_vrf.md b/src/applications/dice_game_vrf.md index f1d64507..742fa2ec 100644 --- a/src/applications/dice_game_vrf.md +++ b/src/applications/dice_game_vrf.md @@ -1,11 +1,64 @@ # 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. +Randomness plays a crucial role in blockchain and smart contract development. In the context of blockchain, randomness is about generating unpredictable values using some source of entropy that is fair and resistant to manipulation. -```rust -{{#include ../../listings/applications/dice_game_vrf/src/dice_game_vrf.cairo:DiceGameInterfaces}} -``` +In blockchain and smart contracts, randomness is needed for: + +- **Gaming:** Ensuring fair outcomes in games of chance. +- **Lotteries:** Selecting winners in a verifiable and unbiased manner. +- **Security:** Generating cryptographic keys and nonces that are hard to predict. +- **Consensus Protocols:** Selecting validators or block producers in some proof-of-stake systems. + + +However, achieving true randomness on a decentralized platform poses significant challenges. There are numerous sources of entropy, each with its stregths and weaknesses. + +### Sources of Entropy + +#### 1. Block Properties (e.g., Block Hash) + +- **Description:** Using properties of the blockchain itself, like the hash of a block, as a source of randomness. +- **Example:** A common approach is to use the hash of a recent block as a seed for random number generation. +- **Risks:** + - **Predictability:** Miners can influence future block hashes by controlling the nonce they use during mining. + - **Manipulation:** Miners or validators with significant influence can manipulate the block hash to their advantage, especially if they stand to gain from a specific random outcome. + +#### 2. User-Provided Inputs + +- **Description:** Allowing users to provide entropy directly, often combined with other sources to generate a random number. +- **Example:** Users submitting their own random values which are then hashed together with other inputs. +- **Risks:** + - **Collusion:** Users may collude to provide inputs that skew the randomness in their favor. + - **Front-Running:** Other participants might observe a user's input and act on it before it gets included in the block, affecting the outcome. + +#### 3. External Oracles + +- **Description:** Using a trusted third-party service to supply randomness. Oracles are off-chain services that provide data to smart contracts. +- **Example:** Chainlink VRF (Verifiable Random Function) is a service that provides cryptographically secure randomness. +- **Risks:** + - **Trust:** Reliance on a third party undermines the trustless nature of blockchain. + - **Centralization:** If the oracle service is compromised, so is the randomness it provides. + - **Cost:** Using an oracle often involves additional transaction fees. + +#### 4. Commit-Reveal Schemes + +- **Description:** A multi-phase protocol where participants commit to a value in the first phase and reveal it in the second. +- **Example:** Participants submit a hash of their random value (commitment) first and reveal the actual value later. The final random number is derived from all revealed values. +- **Risks:** + - **Dishonest Behavior:** Participants may choose not to reveal their values if the outcome is unfavorable. + - **Coordination:** Requires honest participation from multiple parties, which can be hard to guarantee. + +#### 5. On-Chain Randomness Beacons + +- **Description:** Dedicated smart contracts or mechanisms designed to generate and provide randomness on-chain. +- **Example:** Randomness beacons like RANDAO, which aggregate randomness from multiple participants. +- **Risks:** + - **Collusion:** If participants collude, they can influence the randomness outcome. + - **Disruption:** If participants do not follow through (e.g., not revealing their values), the beacon may fail to produce a valid random number. + + + +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:DiceGameContract}} +{{#include ../../listings/applications/dice_game_vrf/src/dice_game_vrf.cairo}} ``` From 24fc67f04504135c8eacd2b49901e15fdddfccba Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 16 Jul 2024 12:54:07 +0200 Subject: [PATCH 07/48] Rename dice_game_vrf.md->random_number_generator.md and update titles --- src/SUMMARY.md | 2 +- .../{dice_game_vrf.md => random_number_generator.md} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/applications/{dice_game_vrf.md => random_number_generator.md} (98%) diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 45f2004d..c9935f20 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](./applications/dice_game_vrf.md) +- [Dice Game VRF](./applications/random_number_generator.md) diff --git a/src/applications/dice_game_vrf.md b/src/applications/random_number_generator.md similarity index 98% rename from src/applications/dice_game_vrf.md rename to src/applications/random_number_generator.md index 742fa2ec..244b2418 100644 --- a/src/applications/dice_game_vrf.md +++ b/src/applications/random_number_generator.md @@ -1,4 +1,4 @@ -# Dice Game using Pragma VRF +# Random Number Generator Randomness plays a crucial role in blockchain and smart contract development. In the context of blockchain, randomness is about generating unpredictable values using some source of entropy that is fair and resistant to manipulation. @@ -55,7 +55,7 @@ However, achieving true randomness on a decentralized platform poses significant - **Collusion:** If participants collude, they can influence the randomness outcome. - **Disruption:** If participants do not follow through (e.g., not revealing their values), the beacon may fail to produce a valid random number. - +## 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. From 3dc1a267676957d3914bec3e8e1fa1dcb4bd137b Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 16 Jul 2024 13:00:04 +0200 Subject: [PATCH 08/48] minor rewording of 1 entropy source --- src/applications/random_number_generator.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/applications/random_number_generator.md b/src/applications/random_number_generator.md index 244b2418..715f56aa 100644 --- a/src/applications/random_number_generator.md +++ b/src/applications/random_number_generator.md @@ -14,9 +14,9 @@ However, achieving true randomness on a decentralized platform poses significant ### Sources of Entropy -#### 1. Block Properties (e.g., Block Hash) +#### 1. Block Properties -- **Description:** Using properties of the blockchain itself, like the hash of a block, as a source of randomness. +- **Description:** Using properties of the blockchain itself, like the hash of a block, or a block timestamp, as a source of randomness. - **Example:** A common approach is to use the hash of a recent block as a seed for random number generation. - **Risks:** - **Predictability:** Miners can influence future block hashes by controlling the nonce they use during mining. @@ -57,7 +57,7 @@ However, achieving true randomness on a decentralized platform poses significant ## 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. +Below is 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}} From 272c1f8fea2724c50d6509c9ca844c23d01de853 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 16 Jul 2024 13:01:11 +0200 Subject: [PATCH 09/48] remove anchors --- listings/applications/dice_game_vrf/src/dice_game_vrf.cairo | 4 ---- 1 file changed, 4 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 6453bf48..0fa53fe0 100644 --- a/listings/applications/dice_game_vrf/src/dice_game_vrf.cairo +++ b/listings/applications/dice_game_vrf/src/dice_game_vrf.cairo @@ -1,4 +1,3 @@ -// 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. @@ -32,9 +31,7 @@ pub trait IDiceGame { 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::{ @@ -226,5 +223,4 @@ mod DiceGame { } } } -// ANCHOR_END: DiceGameContract From 60ecfbdc1e7354160d4e7fdeaacac1a43bd1ea90 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 16 Jul 2024 13:43:40 +0200 Subject: [PATCH 10/48] Minor changes to fn names --- .../dice_game_vrf/src/dice_game_vrf.cairo | 11 ++++++----- 1 file changed, 6 insertions(+), 5 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 0fa53fe0..7057bbbb 100644 --- a/listings/applications/dice_game_vrf/src/dice_game_vrf.cairo +++ b/listings/applications/dice_game_vrf/src/dice_game_vrf.cairo @@ -1,11 +1,9 @@ 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( + fn request_randomness( ref self: TContractState, seed: u64, callback_address: ContractAddress, @@ -75,6 +73,10 @@ mod DiceGame { random_number: u256 } + mod Errors { + pub const GAME_INACTIVE: felt252 = 'Game inactive'; + } + #[constructor] fn constructor( ref self: ContractState, @@ -143,14 +145,13 @@ mod DiceGame { } #[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( + fn request_randomness( ref self: ContractState, seed: u64, callback_address: ContractAddress, From 416fefbf80fd3b1e310b9f2f10ff73f66adc78d1 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 25 Jul 2024 11:35:44 +0200 Subject: [PATCH 11/48] Implement dice game scaffold --- .../applications/dice_game_vrf/src/dice.cairo | 112 ++++++++++++++++++ .../applications/dice_game_vrf/src/lib.cairo | 1 + 2 files changed, 113 insertions(+) create mode 100644 listings/applications/dice_game_vrf/src/dice.cairo diff --git a/listings/applications/dice_game_vrf/src/dice.cairo b/listings/applications/dice_game_vrf/src/dice.cairo new file mode 100644 index 00000000..3f7acc4b --- /dev/null +++ b/listings/applications/dice_game_vrf/src/dice.cairo @@ -0,0 +1,112 @@ +#[starknet::interface] +pub trait IDiceGame { + fn roll_the_dice(ref self: TContractState, wager: u256); +} + +#[starknet::contract] +mod DiceGame { + use core::num::traits::zero::Zero; + use starknet::{ContractAddress, get_caller_address, get_contract_address}; + use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + + #[storage] + struct Storage { + min_wager: u256, + pragma_vrf_contract_address: ContractAddress, + prize: u256, + token: IERC20Dispatcher, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + Roll: Roll, + Winner: Winner, + } + + #[derive(Drop, starknet::Event)] + struct Roll { + player: ContractAddress, + wager: u256, + roll: u8 + } + + #[derive(Drop, starknet::Event)] + struct Winner { + winner: ContractAddress, + amount: u256, + } + + mod Errors { + pub const INVALID_ADDRESS: felt252 = 'Invalid address'; + pub const INVALID_BALANCE: felt252 = 'Invalid balance'; + pub const INVALID_MIN_WAGER: felt252 = 'Invalid minimum wager'; + pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; + pub const WAGER_TOO_LOW: felt252 = 'Wager too low'; + } + + #[constructor] + fn constructor( + ref self: ContractState, + token_address: ContractAddress, + init_balance: u256, + min_wager: u256, + pragma_vrf_contract_address: ContractAddress, + ) { + assert(token_address.is_non_zero(), Errors::INVALID_ADDRESS); + assert(init_balance > 0, Errors::INVALID_BALANCE); + assert(min_wager > 0, Errors::INVALID_MIN_WAGER); + assert(pragma_vrf_contract_address.is_non_zero(), Errors::INVALID_ADDRESS); + + self.pragma_vrf_contract_address.write(pragma_vrf_contract_address); + self.token.write(IERC20Dispatcher { contract_address: token_address }); + + let caller = get_caller_address(); + let this = get_contract_address(); + let success = self.token.read().transfer_from(caller, this, init_balance); + assert(success, Errors::TRANSFER_FAILED); + + self._reset_prize(); + } + + #[abi(embed_v0)] + impl DiceGame of super::IDiceGame { + fn roll_the_dice(ref self: ContractState, wager: u256) { + assert(wager >= self.min_wager.read(), Errors::WAGER_TOO_LOW); + + let player = get_caller_address(); + let this = get_contract_address(); + let token = self.token.read(); + + let success = token.transfer_from(player, this, wager); + assert(success, Errors::TRANSFER_FAILED); + + let new_prize = self.prize.read() + (wager * 40) / 100; + + let roll: u8 = 3; // implement random roll with pragma + self.emit(Event::Roll(Roll { player, wager, roll })); + + if (roll > 2) { + self.prize.write(new_prize); + return; + } + + self._reset_prize(); + + let amount = new_prize; + let success = token.transfer_from(this, player, amount); + assert(success, Errors::TRANSFER_FAILED); + + self.emit(Event::Winner(Winner { winner: player, amount })); + } + } + + #[generate_trait] + impl Private of PrivateTrait { + fn _reset_prize(ref self: ContractState) { + let this = get_contract_address(); + let balance = self.token.read().balance_of(this); + self.prize.write((balance * 10) / 100); + } + } +} diff --git a/listings/applications/dice_game_vrf/src/lib.cairo b/listings/applications/dice_game_vrf/src/lib.cairo index 21e7daa8..d88fba49 100644 --- a/listings/applications/dice_game_vrf/src/lib.cairo +++ b/listings/applications/dice_game_vrf/src/lib.cairo @@ -1,4 +1,5 @@ mod dice_game_vrf; +mod dice; #[cfg(test)] mod tests; From a19d4dcb04b435ca01248ec3610331507682b75f Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 25 Jul 2024 13:27:30 +0200 Subject: [PATCH 12/48] Implement Pragma randomness --- .../applications/dice_game_vrf/src/dice.cairo | 147 ++++++++++++++---- 1 file changed, 120 insertions(+), 27 deletions(-) diff --git a/listings/applications/dice_game_vrf/src/dice.cairo b/listings/applications/dice_game_vrf/src/dice.cairo index 3f7acc4b..3c7a4bc6 100644 --- a/listings/applications/dice_game_vrf/src/dice.cairo +++ b/listings/applications/dice_game_vrf/src/dice.cairo @@ -1,20 +1,38 @@ +use starknet::ContractAddress; + #[starknet::interface] pub trait IDiceGame { fn roll_the_dice(ref self: TContractState, wager: u256); } +#[starknet::interface] +pub trait IPragmaVRF { + fn receive_random_words( + ref self: TContractState, + requestor_address: ContractAddress, + request_id: u64, + random_words: Span, + calldata: Array + ); +} + #[starknet::contract] mod DiceGame { use core::num::traits::zero::Zero; - use starknet::{ContractAddress, get_caller_address, get_contract_address}; + use starknet::{ + ContractAddress, contract_address_const, get_caller_address, get_contract_address, + get_block_number + }; + use pragma_lib::abi::{IRandomnessDispatcher, IRandomnessDispatcherTrait}; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; #[storage] struct Storage { - min_wager: u256, - pragma_vrf_contract_address: ContractAddress, + randomness_contract_address: ContractAddress, prize: u256, - token: IERC20Dispatcher, + eth_dispatcher: IERC20Dispatcher, + nonce: u64, + roll_requests: LegacyMap } #[event] @@ -40,30 +58,38 @@ mod DiceGame { mod Errors { pub const INVALID_ADDRESS: felt252 = 'Invalid address'; pub const INVALID_BALANCE: felt252 = 'Invalid balance'; - pub const INVALID_MIN_WAGER: felt252 = 'Invalid minimum wager'; pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; pub const WAGER_TOO_LOW: felt252 = 'Wager too low'; + pub const CALLER_NOT_RANDOMNESS: felt252 = 'Caller not randomness contract'; + pub const REQUESTOR_NOT_SELF: felt252 = 'Requestor is not self'; + pub const INVALID_REQUEST_ID: felt252 = 'No wager with given request ID'; } + const PUBLISH_DELAY: u64 = 0; + const NUM_OF_WORDS: u64 = 1; + const CALLBACK_FEE_LIMIT: u128 = 100; + #[constructor] fn constructor( - ref self: ContractState, - token_address: ContractAddress, - init_balance: u256, - min_wager: u256, - pragma_vrf_contract_address: ContractAddress, + ref self: ContractState, init_balance: u256, randomness_contract_address: ContractAddress, ) { - assert(token_address.is_non_zero(), Errors::INVALID_ADDRESS); assert(init_balance > 0, Errors::INVALID_BALANCE); - assert(min_wager > 0, Errors::INVALID_MIN_WAGER); - assert(pragma_vrf_contract_address.is_non_zero(), Errors::INVALID_ADDRESS); - - self.pragma_vrf_contract_address.write(pragma_vrf_contract_address); - self.token.write(IERC20Dispatcher { contract_address: token_address }); + assert(randomness_contract_address.is_non_zero(), Errors::INVALID_ADDRESS); + + self.randomness_contract_address.write(randomness_contract_address); + self + .eth_dispatcher + .write( + IERC20Dispatcher { + contract_address: contract_address_const::< + 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 + >() // ETH Contract Address + } + ); let caller = get_caller_address(); let this = get_contract_address(); - let success = self.token.read().transfer_from(caller, this, init_balance); + let success = self.eth_dispatcher.read().transfer_from(caller, this, init_balance); assert(success, Errors::TRANSFER_FAILED); self._reset_prize(); @@ -72,18 +98,86 @@ mod DiceGame { #[abi(embed_v0)] impl DiceGame of super::IDiceGame { fn roll_the_dice(ref self: ContractState, wager: u256) { - assert(wager >= self.min_wager.read(), Errors::WAGER_TOO_LOW); + assert(wager >= 2000, Errors::WAGER_TOO_LOW); let player = get_caller_address(); let this = get_contract_address(); - let token = self.token.read(); - let success = token.transfer_from(player, this, wager); + let success = self.eth_dispatcher.read().transfer_from(player, this, wager); assert(success, Errors::TRANSFER_FAILED); + self._request_dice_roll(player, wager); + } + } + + #[abi(embed_v0)] + impl PragmaVRF of super::IPragmaVRF { + fn receive_random_words( + ref self: ContractState, + requestor_address: ContractAddress, + request_id: u64, + random_words: Span, + calldata: Array + ) { + let caller = get_caller_address(); + assert( + caller == self.randomness_contract_address.read(), Errors::CALLER_NOT_RANDOMNESS + ); + + let this = get_contract_address(); + assert(requestor_address == this, Errors::REQUESTOR_NOT_SELF); + + self._process_dice_roll(request_id, random_words.at(0)); + } + } + + #[generate_trait] + impl Private of PrivateTrait { + fn _request_dice_roll(ref self: ContractState, player: ContractAddress, wager: u256) { + let randomness_contract_address = self.randomness_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 = self.eth_dispatcher.read(); + eth_dispatcher + .approve( + randomness_contract_address, + (CALLBACK_FEE_LIMIT + CALLBACK_FEE_LIMIT / 5).into() + ); + + let nonce = self.nonce.read(); + + // Request the randomness + let request_id = randomness_dispatcher + .request_random( + nonce, + get_contract_address(), + CALLBACK_FEE_LIMIT, + PUBLISH_DELAY, + NUM_OF_WORDS, + array![] + ); + + // store the wager with the request ID + self.roll_requests.write(request_id, (player, wager)); + self.nonce.write(nonce + 1); + } + + fn _process_dice_roll(ref self: ContractState, request_id: u64, random_word: @felt252) { + let (player, wager) = self.roll_requests.read(request_id); + assert(player.is_non_zero() && wager > 0, Errors::INVALID_REQUEST_ID); + + // "consume" the stored wager + self.roll_requests.write(request_id, (Zero::zero(), 0)); + let new_prize = self.prize.read() + (wager * 40) / 100; - let roll: u8 = 3; // implement random roll with pragma + let random_word: u256 = (*random_word).into(); + let roll: u8 = (random_word % 6 + 1).try_into().unwrap(); + self.emit(Event::Roll(Roll { player, wager, roll })); if (roll > 2) { @@ -91,21 +185,20 @@ mod DiceGame { return; } + let this = get_contract_address(); + let amount = new_prize; + self._reset_prize(); - let amount = new_prize; - let success = token.transfer_from(this, player, amount); + let success = self.eth_dispatcher.read().transfer_from(this, player, amount); assert(success, Errors::TRANSFER_FAILED); self.emit(Event::Winner(Winner { winner: player, amount })); } - } - #[generate_trait] - impl Private of PrivateTrait { fn _reset_prize(ref self: ContractState) { let this = get_contract_address(); - let balance = self.token.read().balance_of(this); + let balance = self.eth_dispatcher.read().balance_of(this); self.prize.write((balance * 10) / 100); } } From 5b78f83cb29d44bbec43ec76b082dacd75c6aa34 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 26 Jul 2024 10:05:54 +0200 Subject: [PATCH 13/48] minor refactor in randomness request --- .../applications/dice_game_vrf/src/dice.cairo | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/listings/applications/dice_game_vrf/src/dice.cairo b/listings/applications/dice_game_vrf/src/dice.cairo index 3c7a4bc6..1df77f23 100644 --- a/listings/applications/dice_game_vrf/src/dice.cairo +++ b/listings/applications/dice_game_vrf/src/dice.cairo @@ -106,7 +106,12 @@ mod DiceGame { let success = self.eth_dispatcher.read().transfer_from(player, this, wager); assert(success, Errors::TRANSFER_FAILED); - self._request_dice_roll(player, wager); + let nonce = self.nonce.read(); + let request_id = self._request_randomness(player, wager, nonce); + + // store the wager with the request ID + self.roll_requests.write(request_id, (player, wager)); + self.nonce.write(nonce + 1); } } @@ -133,7 +138,7 @@ mod DiceGame { #[generate_trait] impl Private of PrivateTrait { - fn _request_dice_roll(ref self: ContractState, player: ContractAddress, wager: u256) { + fn _request_randomness(ref self: ContractState, player: ContractAddress, wager: u256, seed: u64) -> u64 { let randomness_contract_address = self.randomness_contract_address.read(); let randomness_dispatcher = IRandomnessDispatcher { contract_address: randomness_contract_address @@ -148,22 +153,16 @@ mod DiceGame { (CALLBACK_FEE_LIMIT + CALLBACK_FEE_LIMIT / 5).into() ); - let nonce = self.nonce.read(); - // Request the randomness - let request_id = randomness_dispatcher + return randomness_dispatcher .request_random( - nonce, + seed, get_contract_address(), CALLBACK_FEE_LIMIT, PUBLISH_DELAY, NUM_OF_WORDS, array![] ); - - // store the wager with the request ID - self.roll_requests.write(request_id, (player, wager)); - self.nonce.write(nonce + 1); } fn _process_dice_roll(ref self: ContractState, request_id: u64, random_word: @felt252) { From fb754730b2a69bb64c322c3e55573bd6cf84a9e4 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 26 Jul 2024 13:15:59 +0200 Subject: [PATCH 14/48] Implement powerball scaffold --- Scarb.lock | 42 +++ Scarb.toml | 1 + .../applications/dice_game_vrf/Scarb.toml | 1 + .../src/{dice.cairo => dice_old.cairo} | 41 +-- .../applications/dice_game_vrf/src/lib.cairo | 2 +- .../dice_game_vrf/src/powerball_lottery.cairo | 248 ++++++++++++++++++ 6 files changed, 317 insertions(+), 18 deletions(-) rename listings/applications/dice_game_vrf/src/{dice.cairo => dice_old.cairo} (84%) create mode 100644 listings/applications/dice_game_vrf/src/powerball_lottery.cairo diff --git a/Scarb.lock b/Scarb.lock index 178ed7df..7d196605 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -11,6 +11,47 @@ dependencies = [ "snforge_std", ] +[[package]] +name = "alexandria_data_structures" +version = "0.2.0" +source = "git+https://github.com/keep-starknet-strange/alexandria.git?rev=800f5ad#800f5ad217847b5ded63c0302a444161766ee9d6" +dependencies = [ + "alexandria_encoding", +] + +[[package]] +name = "alexandria_encoding" +version = "0.1.0" +source = "git+https://github.com/keep-starknet-strange/alexandria.git?rev=800f5ad#800f5ad217847b5ded63c0302a444161766ee9d6" +dependencies = [ + "alexandria_math", + "alexandria_numeric", +] + +[[package]] +name = "alexandria_math" +version = "0.2.0" +source = "git+https://github.com/keep-starknet-strange/alexandria.git?rev=800f5ad#800f5ad217847b5ded63c0302a444161766ee9d6" +dependencies = [ + "alexandria_data_structures", +] + +[[package]] +name = "alexandria_numeric" +version = "0.1.0" +source = "git+https://github.com/keep-starknet-strange/alexandria.git?rev=800f5ad#800f5ad217847b5ded63c0302a444161766ee9d6" +dependencies = [ + "alexandria_math", +] + +[[package]] +name = "alexandria_sorting" +version = "0.1.0" +source = "git+https://github.com/keep-starknet-strange/alexandria.git?rev=800f5ad#800f5ad217847b5ded63c0302a444161766ee9d6" +dependencies = [ + "alexandria_data_structures", +] + [[package]] name = "alexandria_storage" version = "0.3.0" @@ -71,6 +112,7 @@ version = "0.1.0" name = "dice_game_vrf" version = "0.1.0" dependencies = [ + "alexandria_sorting", "openzeppelin", "pragma_lib", ] diff --git a/Scarb.toml b/Scarb.toml index 395921fa..0397a362 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -18,6 +18,7 @@ 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" } +alexandria_sorting = { git = "https://github.com/keep-starknet-strange/alexandria.git", rev="800f5ad" } pragma_lib = { git = "https://github.com/astraly-labs/pragma-lib" } [workspace.package] diff --git a/listings/applications/dice_game_vrf/Scarb.toml b/listings/applications/dice_game_vrf/Scarb.toml index a1e1a2b8..94af7410 100644 --- a/listings/applications/dice_game_vrf/Scarb.toml +++ b/listings/applications/dice_game_vrf/Scarb.toml @@ -7,6 +7,7 @@ edition = "2023_11" starknet.workspace = true openzeppelin.workspace = true pragma_lib.workspace = true +alexandria_sorting.workspace = true [scripts] test.workspace = true diff --git a/listings/applications/dice_game_vrf/src/dice.cairo b/listings/applications/dice_game_vrf/src/dice_old.cairo similarity index 84% rename from listings/applications/dice_game_vrf/src/dice.cairo rename to listings/applications/dice_game_vrf/src/dice_old.cairo index 1df77f23..40a05fc5 100644 --- a/listings/applications/dice_game_vrf/src/dice.cairo +++ b/listings/applications/dice_game_vrf/src/dice_old.cairo @@ -1,7 +1,7 @@ use starknet::ContractAddress; #[starknet::interface] -pub trait IDiceGame { +pub trait IPowerball { fn roll_the_dice(ref self: TContractState, wager: u256); } @@ -17,7 +17,7 @@ pub trait IPragmaVRF { } #[starknet::contract] -mod DiceGame { +mod Powerball { use core::num::traits::zero::Zero; use starknet::{ ContractAddress, contract_address_const, get_caller_address, get_contract_address, @@ -28,6 +28,7 @@ mod DiceGame { #[storage] struct Storage { + duration_in_blocks: u32, randomness_contract_address: ContractAddress, prize: u256, eth_dispatcher: IERC20Dispatcher, @@ -58,6 +59,7 @@ mod DiceGame { mod Errors { pub const INVALID_ADDRESS: felt252 = 'Invalid address'; pub const INVALID_BALANCE: felt252 = 'Invalid balance'; + pub const INVALID_DURATION: felt252 = 'Invalid duration'; pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; pub const WAGER_TOO_LOW: felt252 = 'Wager too low'; pub const CALLER_NOT_RANDOMNESS: felt252 = 'Caller not randomness contract'; @@ -65,38 +67,41 @@ mod DiceGame { pub const INVALID_REQUEST_ID: felt252 = 'No wager with given request ID'; } - const PUBLISH_DELAY: u64 = 0; + const TICKET_PRICE: u32 = 300_000_000_000_000; // 0.0003 ETH const NUM_OF_WORDS: u64 = 1; const CALLBACK_FEE_LIMIT: u128 = 100; #[constructor] fn constructor( - ref self: ContractState, init_balance: u256, randomness_contract_address: ContractAddress, + ref self: ContractState, + init_balance: u256, + randomness_contract_address: ContractAddress, + duration_in_blocks: u32 ) { assert(init_balance > 0, Errors::INVALID_BALANCE); assert(randomness_contract_address.is_non_zero(), Errors::INVALID_ADDRESS); + assert(duration_in_blocks > 0, Errors::INVALID_DURATION); self.randomness_contract_address.write(randomness_contract_address); - self - .eth_dispatcher - .write( - IERC20Dispatcher { - contract_address: contract_address_const::< - 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - >() // ETH Contract Address - } - ); + self.duration_in_blocks.write(duration_in_blocks); + let eth_dispatcher = IERC20Dispatcher { + contract_address: contract_address_const::< + 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 + >() // ETH Contract Address + }; let caller = get_caller_address(); let this = get_contract_address(); - let success = self.eth_dispatcher.read().transfer_from(caller, this, init_balance); + let success = eth_dispatcher.transfer_from(caller, this, init_balance); assert(success, Errors::TRANSFER_FAILED); + self.eth_dispatcher.write(eth_dispatcher); + self._reset_prize(); } #[abi(embed_v0)] - impl DiceGame of super::IDiceGame { + impl Powerball of super::IPowerball { fn roll_the_dice(ref self: ContractState, wager: u256) { assert(wager >= 2000, Errors::WAGER_TOO_LOW); @@ -138,7 +143,9 @@ mod DiceGame { #[generate_trait] impl Private of PrivateTrait { - fn _request_randomness(ref self: ContractState, player: ContractAddress, wager: u256, seed: u64) -> u64 { + fn _request_randomness( + ref self: ContractState, player: ContractAddress, wager: u256, seed: u64 + ) -> u64 { let randomness_contract_address = self.randomness_contract_address.read(); let randomness_dispatcher = IRandomnessDispatcher { contract_address: randomness_contract_address @@ -159,7 +166,7 @@ mod DiceGame { seed, get_contract_address(), CALLBACK_FEE_LIMIT, - PUBLISH_DELAY, + self.duration_in_blocks.read(), NUM_OF_WORDS, array![] ); diff --git a/listings/applications/dice_game_vrf/src/lib.cairo b/listings/applications/dice_game_vrf/src/lib.cairo index d88fba49..7f82c3eb 100644 --- a/listings/applications/dice_game_vrf/src/lib.cairo +++ b/listings/applications/dice_game_vrf/src/lib.cairo @@ -1,5 +1,5 @@ mod dice_game_vrf; -mod dice; +mod powerball_lottery; #[cfg(test)] mod tests; diff --git a/listings/applications/dice_game_vrf/src/powerball_lottery.cairo b/listings/applications/dice_game_vrf/src/powerball_lottery.cairo new file mode 100644 index 00000000..167eafd3 --- /dev/null +++ b/listings/applications/dice_game_vrf/src/powerball_lottery.cairo @@ -0,0 +1,248 @@ +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IPowerball { + fn buy_ticket(ref self: TContractState, white_balls: Array, red_ball: u8); +} + +#[starknet::interface] +pub trait IPragmaVRF { + fn receive_random_words( + ref self: TContractState, + requestor_address: ContractAddress, + request_id: u64, + random_words: Span, + calldata: Array + ); +} + +#[starknet::contract] +mod Powerball { + use core::num::traits::zero::Zero; + use starknet::{ + ContractAddress, contract_address_const, get_caller_address, get_contract_address, + get_block_number + }; + use alexandria_sorting::merge_sort::merge as sort; + use pragma_lib::abi::{IRandomnessDispatcher, IRandomnessDispatcherTrait}; + use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + use openzeppelin::access::ownable::OwnableComponent; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl InternalImpl = OwnableComponent::InternalImpl; + + #[derive(Drop, Serde, starknet::Store)] + struct TicketData { + player: ContractAddress, + white_balls: Span, + red_ball: u8, + } + + #[storage] + struct Storage { + duration_in_blocks: u32, + randomness_contract_address: ContractAddress, + prize: u256, + eth_dispatcher: IERC20Dispatcher, + nonce: u64, + tickets: LegacyMap, + next_ticket_id: u64, + #[substorage(v0)] + ownable: OwnableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + TicketBought: TicketBought, + OwnableEvent: OwnableComponent::Event + } + + #[derive(Drop, starknet::Event)] + struct TicketBought { + player: ContractAddress, + } + + mod Errors { + pub const CALLER_NOT_RANDOMNESS: felt252 = 'Caller not randomness contract'; + pub const INVALID_ADDRESS: felt252 = 'Invalid address'; + pub const INVALID_PRIZE: felt252 = 'Invalid prize'; + pub const INVALID_DURATION: felt252 = 'Invalid duration'; + pub const INVALID_RED_BALL: felt252 = 'Invalid Red Ball'; + pub const INVALID_REQUEST_ID: felt252 = 'No wager with given request ID'; + pub const INVALID_WHITE_BALLS_LENGTH: felt252 = 'White balls length must be 5'; + pub const NON_UNIQUE_WHITE_BALL: felt252 = 'Must submit unique white balls'; + pub const REQUESTOR_NOT_SELF: felt252 = 'Requestor is not self'; + pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; + } + + const MIN_NUMBER: u8 = 1; + const MAX_RED_BALL_NUMBER: u8 = 26; + const MAX_WHITE_BALL_NUMBER: u8 = 69; + const WHITE_BALLS_LENGTH: u32 = 5; + const TICKET_PRICE: u64 = 300_000_000_000_000; // 0.0003 ETH + const NUM_OF_WORDS: u64 = 6; // 5 white balls + 1 red ball + const CALLBACK_FEE_LIMIT: u128 = 100_000_000_000_000; // 0.0001 ETH + + #[constructor] + fn constructor( + ref self: ContractState, + init_prize: u256, + randomness_contract_address: ContractAddress, + duration_in_blocks: u32 + ) { + assert(init_prize > 0, Errors::INVALID_PRIZE); + assert(randomness_contract_address.is_non_zero(), Errors::INVALID_ADDRESS); + assert(duration_in_blocks > 0, Errors::INVALID_DURATION); + + let owner = get_caller_address(); + self.ownable.initializer(owner); + + let eth_dispatcher = IERC20Dispatcher { + contract_address: contract_address_const::< + 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 + >() // ETH Contract Address + }; + let this = get_contract_address(); + let success = eth_dispatcher.transfer_from(owner, this, init_prize); + assert(success, Errors::TRANSFER_FAILED); + + self.randomness_contract_address.write(randomness_contract_address); + self.duration_in_blocks.write(duration_in_blocks); + self.eth_dispatcher.write(eth_dispatcher); + self.prize.write(init_prize); + + self._start_lottery(); + } + + #[abi(embed_v0)] + impl Powerball of super::IPowerball { + fn buy_ticket(ref self: ContractState, white_balls: Array, red_ball: u8) { + assert( + MIN_NUMBER <= red_ball && red_ball <= MAX_RED_BALL_NUMBER, Errors::INVALID_RED_BALL + ); + self._validate_white_balls(white_balls.clone()); + + let player = get_caller_address(); + let this = get_contract_address(); + + let success = self + .eth_dispatcher + .read() + .transfer_from(player, this, TICKET_PRICE.into()); + assert(success, Errors::TRANSFER_FAILED); + + let white_balls_sorted = sort(white_balls); + + let ticket_id = self.next_ticket_id.read(); + + self.next_ticket_id.write(ticket_id + 1); + } + } + + #[abi(embed_v0)] + impl PragmaVRF of super::IPragmaVRF { + fn receive_random_words( + ref self: ContractState, + requestor_address: ContractAddress, + request_id: u64, + random_words: Span, + calldata: Array + ) { + let caller = get_caller_address(); + assert( + caller == self.randomness_contract_address.read(), Errors::CALLER_NOT_RANDOMNESS + ); + + let this = get_contract_address(); + assert(requestor_address == this, Errors::REQUESTOR_NOT_SELF); + + self._process_dice_roll(request_id, random_words.at(0)); + } + } + + #[generate_trait] + impl Private of PrivateTrait { + fn _validate_white_balls(self: @ContractState, white_balls: Array) { + assert(white_balls.len() == WHITE_BALLS_LENGTH, Errors::INVALID_WHITE_BALLS_LENGTH); + + let mut found: Felt252Dict = Default::default(); + let mut i = 0; + while i < WHITE_BALLS_LENGTH { + let white_ball = *white_balls.at(i); + assert!( + MIN_NUMBER <= white_ball && white_ball <= MAX_WHITE_BALL_NUMBER, + "Invalid white ball: {}", + white_ball + ); + assert(!found.get(white_ball.into()), Errors::NON_UNIQUE_WHITE_BALL); + found.insert(white_ball.into(), true); + i += 1; + }; + } + + fn _start_lottery(ref self: ContractState) { + let randomness_contract_address = self.randomness_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 = self.eth_dispatcher.read(); + eth_dispatcher + .approve( + randomness_contract_address, + (CALLBACK_FEE_LIMIT + CALLBACK_FEE_LIMIT / 5).into() + ); + + let nonce = self.nonce.read(); + // Request the randomness to be used to construct the winning combination + randomness_dispatcher + .request_random( + nonce, + get_contract_address(), + CALLBACK_FEE_LIMIT, + self.duration_in_blocks.read().into(), + NUM_OF_WORDS, + array![] + ); + + self.nonce.write(nonce + 1); + } + + fn _process_dice_roll( + ref self: ContractState, request_id: u64, random_word: @felt252 + ) { // let (player, wager) = self.roll_requests.read(request_id); + // assert(player.is_non_zero() && wager > 0, Errors::INVALID_REQUEST_ID); + + // // "consume" the stored wager + // self.roll_requests.write(request_id, (Zero::zero(), 0)); + + // let new_prize = self.prize.read() + (wager * 40) / 100; + + // let random_word: u256 = (*random_word).into(); + // let roll: u8 = (random_word % 6 + 1).try_into().unwrap(); + + // self.emit(Event::Roll(Roll { player, wager, roll })); + + // if (roll > 2) { + // self.prize.write(new_prize); + // return; + // } + + // let this = get_contract_address(); + // let amount = new_prize; + + // self._reset_prize(); + + // let success = self.eth_dispatcher.read().transfer_from(this, player, amount); + // assert(success, Errors::TRANSFER_FAILED); + + // self.emit(Event::Winner(Winner { winner: player, amount })); + } + } +} From 7e47ed39926dd7e5ace854754cb29a2a3fa248b9 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 26 Jul 2024 15:29:06 +0200 Subject: [PATCH 15/48] Turn Dice Game into CoinFlip --- Scarb.lock | 58 +--- Scarb.toml | 1 - .../{dice_game_vrf => coin_flip}/.gitignore | 0 .../{dice_game_vrf => coin_flip}/Scarb.toml | 3 +- .../coin_flip/src/coin_flip.cairo | 182 +++++++++++++ listings/applications/coin_flip/src/lib.cairo | 4 + .../src/tests.cairo | 0 .../dice_game_vrf/src/dice_game_vrf.cairo | 227 ---------------- .../dice_game_vrf/src/dice_old.cairo | 211 --------------- .../applications/dice_game_vrf/src/lib.cairo | 5 - .../dice_game_vrf/src/powerball_lottery.cairo | 248 ------------------ src/applications/random_number_generator.md | 4 +- 12 files changed, 197 insertions(+), 746 deletions(-) rename listings/applications/{dice_game_vrf => coin_flip}/.gitignore (100%) rename listings/applications/{dice_game_vrf => coin_flip}/Scarb.toml (78%) create mode 100644 listings/applications/coin_flip/src/coin_flip.cairo create mode 100644 listings/applications/coin_flip/src/lib.cairo rename listings/applications/{dice_game_vrf => coin_flip}/src/tests.cairo (100%) delete mode 100644 listings/applications/dice_game_vrf/src/dice_game_vrf.cairo delete mode 100644 listings/applications/dice_game_vrf/src/dice_old.cairo delete mode 100644 listings/applications/dice_game_vrf/src/lib.cairo delete mode 100644 listings/applications/dice_game_vrf/src/powerball_lottery.cairo diff --git a/Scarb.lock b/Scarb.lock index 7d196605..6b0a6bc4 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -11,47 +11,6 @@ dependencies = [ "snforge_std", ] -[[package]] -name = "alexandria_data_structures" -version = "0.2.0" -source = "git+https://github.com/keep-starknet-strange/alexandria.git?rev=800f5ad#800f5ad217847b5ded63c0302a444161766ee9d6" -dependencies = [ - "alexandria_encoding", -] - -[[package]] -name = "alexandria_encoding" -version = "0.1.0" -source = "git+https://github.com/keep-starknet-strange/alexandria.git?rev=800f5ad#800f5ad217847b5ded63c0302a444161766ee9d6" -dependencies = [ - "alexandria_math", - "alexandria_numeric", -] - -[[package]] -name = "alexandria_math" -version = "0.2.0" -source = "git+https://github.com/keep-starknet-strange/alexandria.git?rev=800f5ad#800f5ad217847b5ded63c0302a444161766ee9d6" -dependencies = [ - "alexandria_data_structures", -] - -[[package]] -name = "alexandria_numeric" -version = "0.1.0" -source = "git+https://github.com/keep-starknet-strange/alexandria.git?rev=800f5ad#800f5ad217847b5ded63c0302a444161766ee9d6" -dependencies = [ - "alexandria_math", -] - -[[package]] -name = "alexandria_sorting" -version = "0.1.0" -source = "git+https://github.com/keep-starknet-strange/alexandria.git?rev=800f5ad#800f5ad217847b5ded63c0302a444161766ee9d6" -dependencies = [ - "alexandria_data_structures", -] - [[package]] name = "alexandria_storage" version = "0.3.0" @@ -69,6 +28,14 @@ version = "0.1.0" name = "calling_other_contracts" version = "0.1.0" +[[package]] +name = "coin_flip" +version = "0.1.0" +dependencies = [ + "openzeppelin", + "pragma_lib", +] + [[package]] name = "components" version = "0.1.0" @@ -108,15 +75,6 @@ dependencies = [ name = "custom_type_serde" version = "0.1.0" -[[package]] -name = "dice_game_vrf" -version = "0.1.0" -dependencies = [ - "alexandria_sorting", - "openzeppelin", - "pragma_lib", -] - [[package]] name = "ecdsa_verification" version = "0.1.0" diff --git a/Scarb.toml b/Scarb.toml index 0397a362..395921fa 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -18,7 +18,6 @@ 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" } -alexandria_sorting = { git = "https://github.com/keep-starknet-strange/alexandria.git", rev="800f5ad" } pragma_lib = { git = "https://github.com/astraly-labs/pragma-lib" } [workspace.package] diff --git a/listings/applications/dice_game_vrf/.gitignore b/listings/applications/coin_flip/.gitignore similarity index 100% rename from listings/applications/dice_game_vrf/.gitignore rename to listings/applications/coin_flip/.gitignore diff --git a/listings/applications/dice_game_vrf/Scarb.toml b/listings/applications/coin_flip/Scarb.toml similarity index 78% rename from listings/applications/dice_game_vrf/Scarb.toml rename to listings/applications/coin_flip/Scarb.toml index 94af7410..c1587576 100644 --- a/listings/applications/dice_game_vrf/Scarb.toml +++ b/listings/applications/coin_flip/Scarb.toml @@ -1,5 +1,5 @@ [package] -name = "dice_game_vrf" +name = "coin_flip" version = "0.1.0" edition = "2023_11" @@ -7,7 +7,6 @@ edition = "2023_11" starknet.workspace = true openzeppelin.workspace = true pragma_lib.workspace = true -alexandria_sorting.workspace = true [scripts] test.workspace = true diff --git a/listings/applications/coin_flip/src/coin_flip.cairo b/listings/applications/coin_flip/src/coin_flip.cairo new file mode 100644 index 00000000..6525ed6d --- /dev/null +++ b/listings/applications/coin_flip/src/coin_flip.cairo @@ -0,0 +1,182 @@ +use starknet::ContractAddress; + +#[starknet::interface] +pub trait ICoinFlip { + fn flip(ref self: TContractState); +} + +#[starknet::interface] +pub trait IPragmaVRF { + fn receive_random_words( + ref self: TContractState, + requestor_address: ContractAddress, + request_id: u64, + random_words: Span, + calldata: Array + ); +} + +#[starknet::contract] +mod CoinFlip { + use core::num::traits::zero::Zero; + use starknet::{ + ContractAddress, contract_address_const, get_caller_address, get_contract_address, + get_block_number + }; + use pragma_lib::abi::{IRandomnessDispatcher, IRandomnessDispatcherTrait}; + use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + + #[storage] + struct Storage { + eth_dispatcher: IERC20Dispatcher, + flips: LegacyMap, + nonce: u64, + randomness_contract_address: ContractAddress, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + Flipped: Flipped, + Landed: Landed + } + + #[derive(Drop, starknet::Event)] + struct Flipped { + flip_id: u64, + flipper: ContractAddress, + } + + #[derive(Drop, starknet::Event)] + struct Landed { + flip_id: u64, + flipper: ContractAddress, + side: Side + } + + #[derive(Drop, Serde)] + enum Side { + Heads, + Tails, + Sideways + } + + mod Errors { + pub const CALLER_NOT_RANDOMNESS: felt252 = 'Caller not randomness contract'; + pub const INVALID_ADDRESS: felt252 = 'Invalid address'; + pub const INVALID_FLIP_ID: felt252 = 'No flip with the given ID'; + pub const REQUESTOR_NOT_SELF: felt252 = 'Requestor is not self'; + pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; + } + + const PUBLISH_DELAY: u64 = 0; // return the random value asap + const NUM_OF_WORDS: u64 = 1; // one random value is sufficient + const CALLBACK_FEE_LIMIT: u128 = 10_000_000_000_000; // 0.00001 ETH + + #[constructor] + fn constructor(ref self: ContractState, randomness_contract_address: ContractAddress,) { + assert(randomness_contract_address.is_non_zero(), Errors::INVALID_ADDRESS); + self.randomness_contract_address.write(randomness_contract_address); + self + .eth_dispatcher + .write( + IERC20Dispatcher { + contract_address: contract_address_const::< + 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 + >() // ETH Contract Address + } + ); + } + + #[abi(embed_v0)] + impl CoinFlip of super::ICoinFlip { + fn flip(ref self: ContractState) { + let flipper = get_caller_address(); + let this = get_contract_address(); + + // we pass the PragmaVRF fee to the caller + let eth_dispatcher = self.eth_dispatcher.read(); + let success = eth_dispatcher.transfer_from(flipper, this, CALLBACK_FEE_LIMIT.into()); + assert(success, Errors::TRANSFER_FAILED); + + let flip_id = self._request_my_randomness(); + + self.flips.write(flip_id, flipper); + + self.emit(Event::Flipped(Flipped { flip_id, flipper })); + } + } + + #[abi(embed_v0)] + impl PragmaVRF of super::IPragmaVRF { + fn receive_random_words( + ref self: ContractState, + requestor_address: ContractAddress, + request_id: u64, + random_words: Span, + calldata: Array + ) { + let caller = get_caller_address(); + assert( + caller == self.randomness_contract_address.read(), Errors::CALLER_NOT_RANDOMNESS + ); + + let this = get_contract_address(); + assert(requestor_address == this, Errors::REQUESTOR_NOT_SELF); + + self._process_coin_flip(request_id, random_words.at(0)); + } + } + + #[generate_trait] + impl Private of PrivateTrait { + fn _request_my_randomness(ref self: ContractState) -> u64 { + let randomness_contract_address = self.randomness_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 = self.eth_dispatcher.read(); + eth_dispatcher.approve(randomness_contract_address, CALLBACK_FEE_LIMIT.into()); + + let nonce = self.nonce.read(); + + // Request the randomness to be used to construct the winning combination + let request_id = randomness_dispatcher + .request_random( + nonce, + get_contract_address(), + CALLBACK_FEE_LIMIT, + PUBLISH_DELAY, + NUM_OF_WORDS, + array![] + ); + + self.nonce.write(nonce + 1); + + request_id + } + + fn _process_coin_flip(ref self: ContractState, flip_id: u64, random_value: @felt252) { + let flipper = self.flips.read(flip_id); + assert(flipper.is_non_zero(), Errors::INVALID_FLIP_ID); + + // The chance of a flipped coin landing sideways is approximately 1 in 6000. + // https://journals.aps.org/pre/abstract/10.1103/PhysRevE.48.2547 + // + // Since splitting the remainder (5999) equally is impossible, we double the values. + let random_value: u256 = (*random_value).into() % 12000; + let side = if random_value < 5999 { + Side::Heads + } else if random_value > 6000 { + Side::Tails + } else { + Side::Sideways + }; + + self.emit(Event::Landed(Landed { flip_id, flipper, side })); + } + } +} diff --git a/listings/applications/coin_flip/src/lib.cairo b/listings/applications/coin_flip/src/lib.cairo new file mode 100644 index 00000000..9b09518b --- /dev/null +++ b/listings/applications/coin_flip/src/lib.cairo @@ -0,0 +1,4 @@ +mod coin_flip; + +#[cfg(test)] +mod tests; diff --git a/listings/applications/dice_game_vrf/src/tests.cairo b/listings/applications/coin_flip/src/tests.cairo similarity index 100% rename from listings/applications/dice_game_vrf/src/tests.cairo rename to listings/applications/coin_flip/src/tests.cairo diff --git a/listings/applications/dice_game_vrf/src/dice_game_vrf.cairo b/listings/applications/dice_game_vrf/src/dice_game_vrf.cairo deleted file mode 100644 index 7057bbbb..00000000 --- a/listings/applications/dice_game_vrf/src/dice_game_vrf.cairo +++ /dev/null @@ -1,227 +0,0 @@ -use starknet::ContractAddress; - -#[starknet::interface] -pub trait IPragmaVRF { - fn get_last_random_number(self: @TContractState) -> felt252; - fn request_randomness( - 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); -} - -#[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 - } - - mod Errors { - pub const GAME_INACTIVE: felt252 = 'Game inactive'; - } - - #[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)] - 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( - 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); - } - } -} - diff --git a/listings/applications/dice_game_vrf/src/dice_old.cairo b/listings/applications/dice_game_vrf/src/dice_old.cairo deleted file mode 100644 index 40a05fc5..00000000 --- a/listings/applications/dice_game_vrf/src/dice_old.cairo +++ /dev/null @@ -1,211 +0,0 @@ -use starknet::ContractAddress; - -#[starknet::interface] -pub trait IPowerball { - fn roll_the_dice(ref self: TContractState, wager: u256); -} - -#[starknet::interface] -pub trait IPragmaVRF { - fn receive_random_words( - ref self: TContractState, - requestor_address: ContractAddress, - request_id: u64, - random_words: Span, - calldata: Array - ); -} - -#[starknet::contract] -mod Powerball { - use core::num::traits::zero::Zero; - use starknet::{ - ContractAddress, contract_address_const, get_caller_address, get_contract_address, - get_block_number - }; - use pragma_lib::abi::{IRandomnessDispatcher, IRandomnessDispatcherTrait}; - use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; - - #[storage] - struct Storage { - duration_in_blocks: u32, - randomness_contract_address: ContractAddress, - prize: u256, - eth_dispatcher: IERC20Dispatcher, - nonce: u64, - roll_requests: LegacyMap - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - Roll: Roll, - Winner: Winner, - } - - #[derive(Drop, starknet::Event)] - struct Roll { - player: ContractAddress, - wager: u256, - roll: u8 - } - - #[derive(Drop, starknet::Event)] - struct Winner { - winner: ContractAddress, - amount: u256, - } - - mod Errors { - pub const INVALID_ADDRESS: felt252 = 'Invalid address'; - pub const INVALID_BALANCE: felt252 = 'Invalid balance'; - pub const INVALID_DURATION: felt252 = 'Invalid duration'; - pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; - pub const WAGER_TOO_LOW: felt252 = 'Wager too low'; - pub const CALLER_NOT_RANDOMNESS: felt252 = 'Caller not randomness contract'; - pub const REQUESTOR_NOT_SELF: felt252 = 'Requestor is not self'; - pub const INVALID_REQUEST_ID: felt252 = 'No wager with given request ID'; - } - - const TICKET_PRICE: u32 = 300_000_000_000_000; // 0.0003 ETH - const NUM_OF_WORDS: u64 = 1; - const CALLBACK_FEE_LIMIT: u128 = 100; - - #[constructor] - fn constructor( - ref self: ContractState, - init_balance: u256, - randomness_contract_address: ContractAddress, - duration_in_blocks: u32 - ) { - assert(init_balance > 0, Errors::INVALID_BALANCE); - assert(randomness_contract_address.is_non_zero(), Errors::INVALID_ADDRESS); - assert(duration_in_blocks > 0, Errors::INVALID_DURATION); - - self.randomness_contract_address.write(randomness_contract_address); - self.duration_in_blocks.write(duration_in_blocks); - - let eth_dispatcher = IERC20Dispatcher { - contract_address: contract_address_const::< - 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - >() // ETH Contract Address - }; - let caller = get_caller_address(); - let this = get_contract_address(); - let success = eth_dispatcher.transfer_from(caller, this, init_balance); - assert(success, Errors::TRANSFER_FAILED); - - self.eth_dispatcher.write(eth_dispatcher); - - self._reset_prize(); - } - - #[abi(embed_v0)] - impl Powerball of super::IPowerball { - fn roll_the_dice(ref self: ContractState, wager: u256) { - assert(wager >= 2000, Errors::WAGER_TOO_LOW); - - let player = get_caller_address(); - let this = get_contract_address(); - - let success = self.eth_dispatcher.read().transfer_from(player, this, wager); - assert(success, Errors::TRANSFER_FAILED); - - let nonce = self.nonce.read(); - let request_id = self._request_randomness(player, wager, nonce); - - // store the wager with the request ID - self.roll_requests.write(request_id, (player, wager)); - self.nonce.write(nonce + 1); - } - } - - #[abi(embed_v0)] - impl PragmaVRF of super::IPragmaVRF { - fn receive_random_words( - ref self: ContractState, - requestor_address: ContractAddress, - request_id: u64, - random_words: Span, - calldata: Array - ) { - let caller = get_caller_address(); - assert( - caller == self.randomness_contract_address.read(), Errors::CALLER_NOT_RANDOMNESS - ); - - let this = get_contract_address(); - assert(requestor_address == this, Errors::REQUESTOR_NOT_SELF); - - self._process_dice_roll(request_id, random_words.at(0)); - } - } - - #[generate_trait] - impl Private of PrivateTrait { - fn _request_randomness( - ref self: ContractState, player: ContractAddress, wager: u256, seed: u64 - ) -> u64 { - let randomness_contract_address = self.randomness_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 = self.eth_dispatcher.read(); - eth_dispatcher - .approve( - randomness_contract_address, - (CALLBACK_FEE_LIMIT + CALLBACK_FEE_LIMIT / 5).into() - ); - - // Request the randomness - return randomness_dispatcher - .request_random( - seed, - get_contract_address(), - CALLBACK_FEE_LIMIT, - self.duration_in_blocks.read(), - NUM_OF_WORDS, - array![] - ); - } - - fn _process_dice_roll(ref self: ContractState, request_id: u64, random_word: @felt252) { - let (player, wager) = self.roll_requests.read(request_id); - assert(player.is_non_zero() && wager > 0, Errors::INVALID_REQUEST_ID); - - // "consume" the stored wager - self.roll_requests.write(request_id, (Zero::zero(), 0)); - - let new_prize = self.prize.read() + (wager * 40) / 100; - - let random_word: u256 = (*random_word).into(); - let roll: u8 = (random_word % 6 + 1).try_into().unwrap(); - - self.emit(Event::Roll(Roll { player, wager, roll })); - - if (roll > 2) { - self.prize.write(new_prize); - return; - } - - let this = get_contract_address(); - let amount = new_prize; - - self._reset_prize(); - - let success = self.eth_dispatcher.read().transfer_from(this, player, amount); - assert(success, Errors::TRANSFER_FAILED); - - self.emit(Event::Winner(Winner { winner: player, amount })); - } - - fn _reset_prize(ref self: ContractState) { - let this = get_contract_address(); - let balance = self.eth_dispatcher.read().balance_of(this); - self.prize.write((balance * 10) / 100); - } - } -} diff --git a/listings/applications/dice_game_vrf/src/lib.cairo b/listings/applications/dice_game_vrf/src/lib.cairo deleted file mode 100644 index 7f82c3eb..00000000 --- a/listings/applications/dice_game_vrf/src/lib.cairo +++ /dev/null @@ -1,5 +0,0 @@ -mod dice_game_vrf; -mod powerball_lottery; - -#[cfg(test)] -mod tests; diff --git a/listings/applications/dice_game_vrf/src/powerball_lottery.cairo b/listings/applications/dice_game_vrf/src/powerball_lottery.cairo deleted file mode 100644 index 167eafd3..00000000 --- a/listings/applications/dice_game_vrf/src/powerball_lottery.cairo +++ /dev/null @@ -1,248 +0,0 @@ -use starknet::ContractAddress; - -#[starknet::interface] -pub trait IPowerball { - fn buy_ticket(ref self: TContractState, white_balls: Array, red_ball: u8); -} - -#[starknet::interface] -pub trait IPragmaVRF { - fn receive_random_words( - ref self: TContractState, - requestor_address: ContractAddress, - request_id: u64, - random_words: Span, - calldata: Array - ); -} - -#[starknet::contract] -mod Powerball { - use core::num::traits::zero::Zero; - use starknet::{ - ContractAddress, contract_address_const, get_caller_address, get_contract_address, - get_block_number - }; - use alexandria_sorting::merge_sort::merge as sort; - use pragma_lib::abi::{IRandomnessDispatcher, IRandomnessDispatcherTrait}; - use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; - use openzeppelin::access::ownable::OwnableComponent; - - component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); - - #[abi(embed_v0)] - impl OwnableImpl = OwnableComponent::OwnableImpl; - impl InternalImpl = OwnableComponent::InternalImpl; - - #[derive(Drop, Serde, starknet::Store)] - struct TicketData { - player: ContractAddress, - white_balls: Span, - red_ball: u8, - } - - #[storage] - struct Storage { - duration_in_blocks: u32, - randomness_contract_address: ContractAddress, - prize: u256, - eth_dispatcher: IERC20Dispatcher, - nonce: u64, - tickets: LegacyMap, - next_ticket_id: u64, - #[substorage(v0)] - ownable: OwnableComponent::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - TicketBought: TicketBought, - OwnableEvent: OwnableComponent::Event - } - - #[derive(Drop, starknet::Event)] - struct TicketBought { - player: ContractAddress, - } - - mod Errors { - pub const CALLER_NOT_RANDOMNESS: felt252 = 'Caller not randomness contract'; - pub const INVALID_ADDRESS: felt252 = 'Invalid address'; - pub const INVALID_PRIZE: felt252 = 'Invalid prize'; - pub const INVALID_DURATION: felt252 = 'Invalid duration'; - pub const INVALID_RED_BALL: felt252 = 'Invalid Red Ball'; - pub const INVALID_REQUEST_ID: felt252 = 'No wager with given request ID'; - pub const INVALID_WHITE_BALLS_LENGTH: felt252 = 'White balls length must be 5'; - pub const NON_UNIQUE_WHITE_BALL: felt252 = 'Must submit unique white balls'; - pub const REQUESTOR_NOT_SELF: felt252 = 'Requestor is not self'; - pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; - } - - const MIN_NUMBER: u8 = 1; - const MAX_RED_BALL_NUMBER: u8 = 26; - const MAX_WHITE_BALL_NUMBER: u8 = 69; - const WHITE_BALLS_LENGTH: u32 = 5; - const TICKET_PRICE: u64 = 300_000_000_000_000; // 0.0003 ETH - const NUM_OF_WORDS: u64 = 6; // 5 white balls + 1 red ball - const CALLBACK_FEE_LIMIT: u128 = 100_000_000_000_000; // 0.0001 ETH - - #[constructor] - fn constructor( - ref self: ContractState, - init_prize: u256, - randomness_contract_address: ContractAddress, - duration_in_blocks: u32 - ) { - assert(init_prize > 0, Errors::INVALID_PRIZE); - assert(randomness_contract_address.is_non_zero(), Errors::INVALID_ADDRESS); - assert(duration_in_blocks > 0, Errors::INVALID_DURATION); - - let owner = get_caller_address(); - self.ownable.initializer(owner); - - let eth_dispatcher = IERC20Dispatcher { - contract_address: contract_address_const::< - 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - >() // ETH Contract Address - }; - let this = get_contract_address(); - let success = eth_dispatcher.transfer_from(owner, this, init_prize); - assert(success, Errors::TRANSFER_FAILED); - - self.randomness_contract_address.write(randomness_contract_address); - self.duration_in_blocks.write(duration_in_blocks); - self.eth_dispatcher.write(eth_dispatcher); - self.prize.write(init_prize); - - self._start_lottery(); - } - - #[abi(embed_v0)] - impl Powerball of super::IPowerball { - fn buy_ticket(ref self: ContractState, white_balls: Array, red_ball: u8) { - assert( - MIN_NUMBER <= red_ball && red_ball <= MAX_RED_BALL_NUMBER, Errors::INVALID_RED_BALL - ); - self._validate_white_balls(white_balls.clone()); - - let player = get_caller_address(); - let this = get_contract_address(); - - let success = self - .eth_dispatcher - .read() - .transfer_from(player, this, TICKET_PRICE.into()); - assert(success, Errors::TRANSFER_FAILED); - - let white_balls_sorted = sort(white_balls); - - let ticket_id = self.next_ticket_id.read(); - - self.next_ticket_id.write(ticket_id + 1); - } - } - - #[abi(embed_v0)] - impl PragmaVRF of super::IPragmaVRF { - fn receive_random_words( - ref self: ContractState, - requestor_address: ContractAddress, - request_id: u64, - random_words: Span, - calldata: Array - ) { - let caller = get_caller_address(); - assert( - caller == self.randomness_contract_address.read(), Errors::CALLER_NOT_RANDOMNESS - ); - - let this = get_contract_address(); - assert(requestor_address == this, Errors::REQUESTOR_NOT_SELF); - - self._process_dice_roll(request_id, random_words.at(0)); - } - } - - #[generate_trait] - impl Private of PrivateTrait { - fn _validate_white_balls(self: @ContractState, white_balls: Array) { - assert(white_balls.len() == WHITE_BALLS_LENGTH, Errors::INVALID_WHITE_BALLS_LENGTH); - - let mut found: Felt252Dict = Default::default(); - let mut i = 0; - while i < WHITE_BALLS_LENGTH { - let white_ball = *white_balls.at(i); - assert!( - MIN_NUMBER <= white_ball && white_ball <= MAX_WHITE_BALL_NUMBER, - "Invalid white ball: {}", - white_ball - ); - assert(!found.get(white_ball.into()), Errors::NON_UNIQUE_WHITE_BALL); - found.insert(white_ball.into(), true); - i += 1; - }; - } - - fn _start_lottery(ref self: ContractState) { - let randomness_contract_address = self.randomness_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 = self.eth_dispatcher.read(); - eth_dispatcher - .approve( - randomness_contract_address, - (CALLBACK_FEE_LIMIT + CALLBACK_FEE_LIMIT / 5).into() - ); - - let nonce = self.nonce.read(); - // Request the randomness to be used to construct the winning combination - randomness_dispatcher - .request_random( - nonce, - get_contract_address(), - CALLBACK_FEE_LIMIT, - self.duration_in_blocks.read().into(), - NUM_OF_WORDS, - array![] - ); - - self.nonce.write(nonce + 1); - } - - fn _process_dice_roll( - ref self: ContractState, request_id: u64, random_word: @felt252 - ) { // let (player, wager) = self.roll_requests.read(request_id); - // assert(player.is_non_zero() && wager > 0, Errors::INVALID_REQUEST_ID); - - // // "consume" the stored wager - // self.roll_requests.write(request_id, (Zero::zero(), 0)); - - // let new_prize = self.prize.read() + (wager * 40) / 100; - - // let random_word: u256 = (*random_word).into(); - // let roll: u8 = (random_word % 6 + 1).try_into().unwrap(); - - // self.emit(Event::Roll(Roll { player, wager, roll })); - - // if (roll > 2) { - // self.prize.write(new_prize); - // return; - // } - - // let this = get_contract_address(); - // let amount = new_prize; - - // self._reset_prize(); - - // let success = self.eth_dispatcher.read().transfer_from(this, player, amount); - // assert(success, Errors::TRANSFER_FAILED); - - // self.emit(Event::Winner(Winner { winner: player, amount })); - } - } -} diff --git a/src/applications/random_number_generator.md b/src/applications/random_number_generator.md index 715f56aa..149ce87d 100644 --- a/src/applications/random_number_generator.md +++ b/src/applications/random_number_generator.md @@ -57,8 +57,8 @@ However, achieving true randomness on a decentralized platform poses significant ## Dice Game using Pragma VRF -Below is 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. +Below is an implementation of a `CoinFlip` 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}} +{{#include ../../listings/applications/coin_flip/src/coin_flip.cairo}} ``` From 104bd222a31a561e48b80a9eafaddb0056536413 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 26 Jul 2024 20:15:34 +0200 Subject: [PATCH 16/48] Implement coin_flip test --- Scarb.lock | 1 + .../advanced_factory/src/tests.cairo | 3 - listings/applications/coin_flip/Scarb.toml | 3 + .../src/{coin_flip.cairo => contract.cairo} | 45 +++-- listings/applications/coin_flip/src/lib.cairo | 3 +- .../coin_flip/src/mock_randomness.cairo | 156 ++++++++++++++++++ .../applications/coin_flip/src/tests.cairo | 72 +++++++- .../applications/crowdfunding/src/tests.cairo | 3 - 8 files changed, 254 insertions(+), 32 deletions(-) rename listings/applications/coin_flip/src/{coin_flip.cairo => contract.cairo} (85%) create mode 100644 listings/applications/coin_flip/src/mock_randomness.cairo diff --git a/Scarb.lock b/Scarb.lock index 6b0a6bc4..26bec0f3 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -34,6 +34,7 @@ version = "0.1.0" dependencies = [ "openzeppelin", "pragma_lib", + "snforge_std", ] [[package]] diff --git a/listings/applications/advanced_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo index eb0cc1b5..0d7def98 100644 --- a/listings/applications/advanced_factory/src/tests.cairo +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -1,6 +1,3 @@ -use core::traits::TryInto; -use core::clone::Clone; -use core::result::ResultTrait; use advanced_factory::contract::{ CampaignFactory, ICampaignFactoryDispatcher, ICampaignFactoryDispatcherTrait }; diff --git a/listings/applications/coin_flip/Scarb.toml b/listings/applications/coin_flip/Scarb.toml index c1587576..22217d18 100644 --- a/listings/applications/coin_flip/Scarb.toml +++ b/listings/applications/coin_flip/Scarb.toml @@ -7,8 +7,11 @@ edition = "2023_11" starknet.workspace = true openzeppelin.workspace = true pragma_lib.workspace = true +snforge_std.workspace = true [scripts] test.workspace = true [[target.starknet-contract]] +casm = true +build-external-contracts = ["openzeppelin::presets::erc20::ERC20Upgradeable"] diff --git a/listings/applications/coin_flip/src/coin_flip.cairo b/listings/applications/coin_flip/src/contract.cairo similarity index 85% rename from listings/applications/coin_flip/src/coin_flip.cairo rename to listings/applications/coin_flip/src/contract.cairo index 6525ed6d..2371a7b0 100644 --- a/listings/applications/coin_flip/src/coin_flip.cairo +++ b/listings/applications/coin_flip/src/contract.cairo @@ -17,7 +17,7 @@ pub trait IPragmaVRF { } #[starknet::contract] -mod CoinFlip { +pub mod CoinFlip { use core::num::traits::zero::Zero; use starknet::{ ContractAddress, contract_address_const, get_caller_address, get_contract_address, @@ -36,32 +36,32 @@ mod CoinFlip { #[event] #[derive(Drop, starknet::Event)] - enum Event { + pub enum Event { Flipped: Flipped, Landed: Landed } #[derive(Drop, starknet::Event)] - struct Flipped { - flip_id: u64, - flipper: ContractAddress, + pub struct Flipped { + pub flip_id: u64, + pub flipper: ContractAddress, } #[derive(Drop, starknet::Event)] - struct Landed { - flip_id: u64, - flipper: ContractAddress, - side: Side + pub struct Landed { + pub flip_id: u64, + pub flipper: ContractAddress, + pub side: Side } #[derive(Drop, Serde)] - enum Side { + pub enum Side { Heads, Tails, Sideways } - mod Errors { + pub mod Errors { pub const CALLER_NOT_RANDOMNESS: felt252 = 'Caller not randomness contract'; pub const INVALID_ADDRESS: felt252 = 'Invalid address'; pub const INVALID_FLIP_ID: felt252 = 'No flip with the given ID'; @@ -69,23 +69,20 @@ mod CoinFlip { pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; } - const PUBLISH_DELAY: u64 = 0; // return the random value asap - const NUM_OF_WORDS: u64 = 1; // one random value is sufficient - const CALLBACK_FEE_LIMIT: u128 = 10_000_000_000_000; // 0.00001 ETH + pub const PUBLISH_DELAY: u64 = 0; // return the random value asap + pub const NUM_OF_WORDS: u64 = 1; // one random value is sufficient + pub const CALLBACK_FEE_LIMIT: u128 = 10_000_000_000_000; // 0.00001 ETH #[constructor] - fn constructor(ref self: ContractState, randomness_contract_address: ContractAddress,) { + fn constructor( + ref self: ContractState, + randomness_contract_address: ContractAddress, + eth_address: ContractAddress + ) { assert(randomness_contract_address.is_non_zero(), Errors::INVALID_ADDRESS); + assert(eth_address.is_non_zero(), Errors::INVALID_ADDRESS); self.randomness_contract_address.write(randomness_contract_address); - self - .eth_dispatcher - .write( - IERC20Dispatcher { - contract_address: contract_address_const::< - 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - >() // ETH Contract Address - } - ); + self.eth_dispatcher.write(IERC20Dispatcher { contract_address: eth_address }); } #[abi(embed_v0)] diff --git a/listings/applications/coin_flip/src/lib.cairo b/listings/applications/coin_flip/src/lib.cairo index 9b09518b..a4238d37 100644 --- a/listings/applications/coin_flip/src/lib.cairo +++ b/listings/applications/coin_flip/src/lib.cairo @@ -1,4 +1,5 @@ -mod coin_flip; +mod contract; +mod mock_randomness; #[cfg(test)] mod tests; diff --git a/listings/applications/coin_flip/src/mock_randomness.cairo b/listings/applications/coin_flip/src/mock_randomness.cairo new file mode 100644 index 00000000..198e757d --- /dev/null +++ b/listings/applications/coin_flip/src/mock_randomness.cairo @@ -0,0 +1,156 @@ +#[starknet::contract] +pub mod MockRandomness { + use pragma_lib::abi::IRandomness; + use pragma_lib::types::RequestStatus; + use starknet::{ContractAddress, ClassHash, get_caller_address, get_contract_address}; + use core::num::traits::zero::Zero; + use core::poseidon::PoseidonTrait; + use core::hash::{HashStateTrait, HashStateExTrait}; + use coin_flip::contract::{IPragmaVRFDispatcher, IPragmaVRFDispatcherTrait}; + use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + + #[storage] + struct Storage { + eth_dispatcher: IERC20Dispatcher, + next_request_id: u64 + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event {} + + pub mod Errors { + pub const INVALID_ADDRESS: felt252 = 'Invalid address'; + pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; + } + + #[constructor] + fn constructor(ref self: ContractState, eth_address: ContractAddress) { + assert(eth_address.is_non_zero(), Errors::INVALID_ADDRESS); + self.eth_dispatcher.write(IERC20Dispatcher { contract_address: eth_address }); + } + + #[abi(embed_v0)] + impl MockRandomness of IRandomness { + fn request_random( + ref self: ContractState, + seed: u64, + callback_address: ContractAddress, + callback_fee_limit: u128, + publish_delay: u64, + num_words: u64, + calldata: Array + ) -> u64 { + let caller = get_caller_address(); + let this = get_contract_address(); + let eth_dispatcher = self.eth_dispatcher.read(); + let success = eth_dispatcher.transfer_from(caller, this, callback_fee_limit.into()); + assert(success, Errors::TRANSFER_FAILED); + + let request_id = self.next_request_id.read(); + self.next_request_id.write(request_id + 1); + request_id + } + + fn submit_random( + ref self: ContractState, + request_id: u64, + requestor_address: ContractAddress, + seed: u64, + minimum_block_number: u64, + callback_address: ContractAddress, + callback_fee_limit: u128, + callback_fee: u128, + random_words: Span, + proof: Span, + calldata: Array + ) { + let requestor = IPragmaVRFDispatcher { contract_address: callback_address }; + let hash = PoseidonTrait::new().update_with((seed, request_id)).finalize(); + requestor + .receive_random_words(requestor_address, request_id, array![hash].span(), calldata); + } + + + fn update_status( + ref self: ContractState, + requestor_address: ContractAddress, + request_id: u64, + new_status: RequestStatus + ) { + panic!("unimplemented") + } + + fn cancel_random_request( + ref self: ContractState, + request_id: u64, + requestor_address: ContractAddress, + seed: u64, + minimum_block_number: u64, + callback_address: ContractAddress, + callback_fee_limit: u128, + num_words: u64 + ) { + panic!("unimplemented") + } + + fn get_pending_requests( + self: @ContractState, requestor_address: ContractAddress, offset: u64, max_len: u64 + ) -> Span { + panic!("unimplemented") + } + + fn get_request_status( + self: @ContractState, requestor_address: ContractAddress, request_id: u64 + ) -> RequestStatus { + panic!("unimplemented") + } + fn requestor_current_index( + self: @ContractState, requestor_address: ContractAddress + ) -> u64 { + panic!("unimplemented") + } + fn get_public_key(self: @ContractState, requestor_address: ContractAddress) -> felt252 { + panic!("unimplemented") + } + fn get_payment_token(self: @ContractState) -> ContractAddress { + panic!("unimplemented") + } + fn set_payment_token(ref self: ContractState, token_contract: ContractAddress) { + panic!("unimplemented") + } + fn upgrade(ref self: ContractState, impl_hash: ClassHash) { + panic!("unimplemented") + } + fn refund_operation( + ref self: ContractState, caller_address: ContractAddress, request_id: u64 + ) { + panic!("unimplemented") + } + fn get_total_fees( + self: @ContractState, caller_address: ContractAddress, request_id: u64 + ) -> u256 { + panic!("unimplemented") + } + fn get_out_of_gas_requests( + self: @ContractState, requestor_address: ContractAddress, + ) -> Span { + panic!("unimplemented") + } + fn withdraw_funds(ref self: ContractState, receiver_address: ContractAddress) { + panic!("unimplemented") + } + fn get_contract_balance(self: @ContractState) -> u256 { + panic!("unimplemented") + } + fn compute_premium_fee(self: @ContractState, caller_address: ContractAddress) -> u128 { + panic!("unimplemented") + } + fn get_admin_address(self: @ContractState,) -> ContractAddress { + panic!("unimplemented") + } + fn set_admin_address(ref self: ContractState, new_admin_address: ContractAddress) { + panic!("unimplemented") + } + } +} diff --git a/listings/applications/coin_flip/src/tests.cairo b/listings/applications/coin_flip/src/tests.cairo index 361dba07..c2695760 100644 --- a/listings/applications/coin_flip/src/tests.cairo +++ b/listings/applications/coin_flip/src/tests.cairo @@ -1,2 +1,72 @@ -mod tests { // TODO +use coin_flip::contract::{ + CoinFlip, ICoinFlipDispatcher, ICoinFlipDispatcherTrait, IPragmaVRFDispatcher, + IPragmaVRFDispatcherTrait +}; +use coin_flip::mock_randomness::MockRandomness; +use starknet::{ + ContractAddress, ClassHash, get_block_timestamp, contract_address_const, get_caller_address +}; +use snforge_std::{ + declare, ContractClass, ContractClassTrait, start_cheat_caller_address, + stop_cheat_caller_address, spy_events, SpyOn, EventSpy, EventAssertions, get_class_hash +}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use pragma_lib::abi::{IRandomnessDispatcher, IRandomnessDispatcherTrait}; + +fn deploy() -> (ICoinFlipDispatcher, IRandomnessDispatcher, ContractAddress) { + // deploy mock ETH token + let eth_contract = declare("ERC20Upgradeable").unwrap(); + let eth_name: ByteArray = "Ethereum"; + let eth_symbol: ByteArray = "ETH"; + let eth_supply: u256 = CoinFlip::CALLBACK_FEE_LIMIT.into() * 10; + let mut eth_ctor_calldata = array![]; + let deployer = contract_address_const::<'deployer'>(); + ((eth_name, eth_symbol, eth_supply, deployer), deployer).serialize(ref eth_ctor_calldata); + let eth_address = eth_contract.precalculate_address(@eth_ctor_calldata); + start_cheat_caller_address(eth_address, deployer); + eth_contract.deploy(@eth_ctor_calldata).unwrap(); + stop_cheat_caller_address(eth_address); + + // deploy MockRandomness + let mock_randomness = declare("MockRandomness").unwrap(); + let mut randomness_calldata: Array = array![]; + (eth_address).serialize(ref randomness_calldata); + let (randomness_address, _) = mock_randomness.deploy(@randomness_calldata).unwrap(); + + // deploy the actual CoinFlip contract + let coin_flip_contract = declare("CoinFlip").unwrap(); + let mut coin_flip_ctor_calldata: Array = array![]; + (randomness_address, eth_address).serialize(ref coin_flip_ctor_calldata); + let (coin_flip_address, _) = coin_flip_contract.deploy(@coin_flip_ctor_calldata).unwrap(); + + // approve the CoinFlip contract to spend the callback fee + let eth_dispatcher = IERC20Dispatcher { contract_address: eth_address }; + start_cheat_caller_address(eth_address, deployer); + eth_dispatcher.approve(coin_flip_address, CoinFlip::CALLBACK_FEE_LIMIT.into() * 2); + stop_cheat_caller_address(eth_address); + + let randomness_dispatcher = IRandomnessDispatcher { contract_address: randomness_address }; + let coin_flip_dispathcer = ICoinFlipDispatcher { contract_address: coin_flip_address }; + + (coin_flip_dispathcer, randomness_dispatcher, deployer) +} + +#[test] +fn test_flip() { + let (coin_flip, _randomness, deployer) = deploy(); + + let mut spy = spy_events(SpyOn::One(coin_flip.contract_address)); + + start_cheat_caller_address(coin_flip.contract_address, deployer); + coin_flip.flip(); + + spy + .assert_emitted( + @array![ + ( + coin_flip.contract_address, + CoinFlip::Event::Flipped(CoinFlip::Flipped { flip_id: 0, flipper: deployer }) + ) + ] + ); } diff --git a/listings/applications/crowdfunding/src/tests.cairo b/listings/applications/crowdfunding/src/tests.cairo index fde363c2..41453d16 100644 --- a/listings/applications/crowdfunding/src/tests.cairo +++ b/listings/applications/crowdfunding/src/tests.cairo @@ -1,6 +1,3 @@ -use core::traits::TryInto; -use core::clone::Clone; -use core::result::ResultTrait; use starknet::{ ContractAddress, ClassHash, get_block_timestamp, contract_address_const, get_caller_address, }; From c16a65c230082b56a464d4780d5e9b1184d386a3 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 26 Jul 2024 21:22:18 +0200 Subject: [PATCH 17/48] Add more tests --- .../coin_flip/src/mock_randomness.cairo | 4 +- .../applications/coin_flip/src/tests.cairo | 140 +++++++++++++++++- 2 files changed, 136 insertions(+), 8 deletions(-) diff --git a/listings/applications/coin_flip/src/mock_randomness.cairo b/listings/applications/coin_flip/src/mock_randomness.cairo index 198e757d..12d9b248 100644 --- a/listings/applications/coin_flip/src/mock_randomness.cairo +++ b/listings/applications/coin_flip/src/mock_randomness.cairo @@ -66,9 +66,7 @@ pub mod MockRandomness { calldata: Array ) { let requestor = IPragmaVRFDispatcher { contract_address: callback_address }; - let hash = PoseidonTrait::new().update_with((seed, request_id)).finalize(); - requestor - .receive_random_words(requestor_address, request_id, array![hash].span(), calldata); + requestor.receive_random_words(requestor_address, request_id, random_words, calldata); } diff --git a/listings/applications/coin_flip/src/tests.cairo b/listings/applications/coin_flip/src/tests.cairo index c2695760..63fda77b 100644 --- a/listings/applications/coin_flip/src/tests.cairo +++ b/listings/applications/coin_flip/src/tests.cairo @@ -13,7 +13,7 @@ use snforge_std::{ use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use pragma_lib::abi::{IRandomnessDispatcher, IRandomnessDispatcherTrait}; -fn deploy() -> (ICoinFlipDispatcher, IRandomnessDispatcher, ContractAddress) { +fn deploy() -> (ICoinFlipDispatcher, IRandomnessDispatcher, IERC20Dispatcher, ContractAddress) { // deploy mock ETH token let eth_contract = declare("ERC20Upgradeable").unwrap(); let eth_name: ByteArray = "Ethereum"; @@ -48,25 +48,155 @@ fn deploy() -> (ICoinFlipDispatcher, IRandomnessDispatcher, ContractAddress) { let randomness_dispatcher = IRandomnessDispatcher { contract_address: randomness_address }; let coin_flip_dispathcer = ICoinFlipDispatcher { contract_address: coin_flip_address }; - (coin_flip_dispathcer, randomness_dispatcher, deployer) + (coin_flip_dispathcer, randomness_dispatcher, eth_dispatcher, deployer) } #[test] -fn test_flip() { - let (coin_flip, _randomness, deployer) = deploy(); +#[fuzzer(runs: 10, seed: 22)] +fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { + let (coin_flip, randomness, _, deployer) = deploy(); let mut spy = spy_events(SpyOn::One(coin_flip.contract_address)); start_cheat_caller_address(coin_flip.contract_address, deployer); coin_flip.flip(); + stop_cheat_caller_address(coin_flip.contract_address); + + let expected_request_id = 0; + + spy + .assert_emitted( + @array![ + ( + coin_flip.contract_address, + CoinFlip::Event::Flipped( + CoinFlip::Flipped { flip_id: expected_request_id, flipper: deployer } + ) + ) + ] + ); + + randomness + .submit_random( + expected_request_id, + coin_flip.contract_address, + 0, + 0, + coin_flip.contract_address, + CoinFlip::CALLBACK_FEE_LIMIT, + CoinFlip::CALLBACK_FEE_LIMIT, + array![random_word_1].span(), + array![].span(), + array![] + ); + + let random_value: u256 = random_word_1.into() % 12000; + let expected_side = if random_value < 5999 { + CoinFlip::Side::Heads + } else if random_value > 6000 { + CoinFlip::Side::Tails + } else { + CoinFlip::Side::Sideways + }; + + spy + .assert_emitted( + @array![ + ( + coin_flip.contract_address, + CoinFlip::Event::Landed( + CoinFlip::Landed { + flip_id: expected_request_id, flipper: deployer, side: expected_side + } + ) + ) + ] + ); + + start_cheat_caller_address(coin_flip.contract_address, deployer); + coin_flip.flip(); + stop_cheat_caller_address(coin_flip.contract_address); + + let expected_request_id = 1; spy .assert_emitted( @array![ ( coin_flip.contract_address, - CoinFlip::Event::Flipped(CoinFlip::Flipped { flip_id: 0, flipper: deployer }) + CoinFlip::Event::Flipped( + CoinFlip::Flipped { flip_id: expected_request_id, flipper: deployer } + ) ) ] ); + + randomness + .submit_random( + expected_request_id, + coin_flip.contract_address, + 1, + 0, + coin_flip.contract_address, + CoinFlip::CALLBACK_FEE_LIMIT, + CoinFlip::CALLBACK_FEE_LIMIT, + array![random_word_2].span(), + array![].span(), + array![] + ); + + let random_value: u256 = random_word_2.into() % 12000; + let expected_side = if random_value < 5999 { + CoinFlip::Side::Heads + } else if random_value > 6000 { + CoinFlip::Side::Tails + } else { + CoinFlip::Side::Sideways + }; + + spy + .assert_emitted( + @array![ + ( + coin_flip.contract_address, + CoinFlip::Event::Landed( + CoinFlip::Landed { + flip_id: expected_request_id, flipper: deployer, side: expected_side + } + ) + ) + ] + ); +} + +#[test] +#[should_panic(expected: 'ERC20: insufficient allowance')] +fn test_flip_no_allowance() { + let (coin_flip, _, eth, deployer) = deploy(); + + let new_flipper = contract_address_const::<'new_flipper'>(); + + // ensure new flipper has funds, just that they haven't approved the + // CoinFlip contract to spend them to cover the fee + start_cheat_caller_address(eth.contract_address, deployer); + eth.transfer(new_flipper, (CoinFlip::CALLBACK_FEE_LIMIT).into() * 2); + stop_cheat_caller_address(eth.contract_address); + + start_cheat_caller_address(coin_flip.contract_address, new_flipper); + coin_flip.flip(); +} + +#[test] +#[should_panic(expected: 'ERC20: insufficient balance')] +fn test_flip_without_enough_for_fees() { + let (coin_flip, _, eth, _) = deploy(); + + // approve the CoinFlip contract, but leave the flipper with no balance + let flipper_with_no_funds = contract_address_const::<'flipper_with_no_funds'>(); + start_cheat_caller_address(eth.contract_address, flipper_with_no_funds); + eth.approve(coin_flip.contract_address, (CoinFlip::CALLBACK_FEE_LIMIT).into() * 2); + stop_cheat_caller_address(eth.contract_address); + + start_cheat_caller_address(coin_flip.contract_address, flipper_with_no_funds); + coin_flip.flip(); } From f080d475089ae1ceec3ef62f779319d00a51fc1e Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 26 Jul 2024 21:33:49 +0200 Subject: [PATCH 18/48] Update titles --- src/SUMMARY.md | 2 +- src/applications/random_number_generator.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SUMMARY.md b/src/SUMMARY.md index c9935f20..9081fb38 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](./applications/random_number_generator.md) +- [Random Number Generator](./applications/random_number_generator.md) diff --git a/src/applications/random_number_generator.md b/src/applications/random_number_generator.md index 149ce87d..ca7e2eac 100644 --- a/src/applications/random_number_generator.md +++ b/src/applications/random_number_generator.md @@ -55,10 +55,10 @@ However, achieving true randomness on a decentralized platform poses significant - **Collusion:** If participants collude, they can influence the randomness outcome. - **Disruption:** If participants do not follow through (e.g., not revealing their values), the beacon may fail to produce a valid random number. -## Dice Game using Pragma VRF +## CoinFlip using Pragma VRF Below is an implementation of a `CoinFlip` 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/coin_flip/src/coin_flip.cairo}} +{{#include ../../listings/applications/coin_flip/src/contract.cairo}} ``` From c0e0ff6557d02393c214c02af4db97b56a194ba1 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 26 Jul 2024 21:55:10 +0200 Subject: [PATCH 19/48] Remove redundant blank line --- src/applications/random_number_generator.md | 1 - 1 file changed, 1 deletion(-) diff --git a/src/applications/random_number_generator.md b/src/applications/random_number_generator.md index ca7e2eac..fb977f55 100644 --- a/src/applications/random_number_generator.md +++ b/src/applications/random_number_generator.md @@ -9,7 +9,6 @@ In blockchain and smart contracts, randomness is needed for: - **Security:** Generating cryptographic keys and nonces that are hard to predict. - **Consensus Protocols:** Selecting validators or block producers in some proof-of-stake systems. - However, achieving true randomness on a decentralized platform poses significant challenges. There are numerous sources of entropy, each with its stregths and weaknesses. ### Sources of Entropy From 2f6000da645f2faacc4773959bc0ef47bf883d30 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 30 Jul 2024 12:44:48 +0200 Subject: [PATCH 20/48] Add premium fee calculation into tests --- listings/applications/coin_flip/Scarb.toml | 2 +- .../applications/coin_flip/src/contract.cairo | 20 ++++++++++++++++--- .../coin_flip/src/mock_randomness.cairo | 14 +++++++++---- .../applications/coin_flip/src/tests.cairo | 2 +- .../coin_flip/starkli-wallet/account.json | 14 +++++++++++++ .../coin_flip/starkli-wallet/keystore.json | 1 + 6 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 listings/applications/coin_flip/starkli-wallet/account.json create mode 100644 listings/applications/coin_flip/starkli-wallet/keystore.json diff --git a/listings/applications/coin_flip/Scarb.toml b/listings/applications/coin_flip/Scarb.toml index 22217d18..82f0da9b 100644 --- a/listings/applications/coin_flip/Scarb.toml +++ b/listings/applications/coin_flip/Scarb.toml @@ -14,4 +14,4 @@ test.workspace = true [[target.starknet-contract]] casm = true -build-external-contracts = ["openzeppelin::presets::erc20::ERC20Upgradeable"] +build-external-contracts = ["openzeppelin::presets::erc20::ERC20Upgradeable"] \ No newline at end of file diff --git a/listings/applications/coin_flip/src/contract.cairo b/listings/applications/coin_flip/src/contract.cairo index 2371a7b0..38ca2045 100644 --- a/listings/applications/coin_flip/src/contract.cairo +++ b/listings/applications/coin_flip/src/contract.cairo @@ -91,14 +91,21 @@ pub mod CoinFlip { let flipper = get_caller_address(); let this = get_contract_address(); - // we pass the PragmaVRF fee to the caller + // we pass the PragmaVRF fee to the flipper + // we take twice the callback fee amount just to make sure we + // can cover it let eth_dispatcher = self.eth_dispatcher.read(); - let success = eth_dispatcher.transfer_from(flipper, this, CALLBACK_FEE_LIMIT.into()); + let success = eth_dispatcher.transfer_from(flipper, this, CALLBACK_FEE_LIMIT.into() * 2); assert(success, Errors::TRANSFER_FAILED); let flip_id = self._request_my_randomness(); self.flips.write(flip_id, flipper); + + // we return to the flipper whatever is left over after the fee is paid + let leftover_balance = eth_dispatcher.balance_of(this); + let success = eth_dispatcher.transfer(flipper, leftover_balance); + assert(success, Errors::TRANSFER_FAILED); self.emit(Event::Flipped(Flipped { flip_id, flipper })); } @@ -133,10 +140,17 @@ pub mod CoinFlip { contract_address: randomness_contract_address }; + let caller = get_caller_address(); + let premium_fee = randomness_dispatcher.compute_premium_fee(caller); + // 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 = self.eth_dispatcher.read(); - eth_dispatcher.approve(randomness_contract_address, CALLBACK_FEE_LIMIT.into()); + eth_dispatcher + .approve( + randomness_contract_address, + (CALLBACK_FEE_LIMIT + premium_fee + CALLBACK_FEE_LIMIT / 5).into() + ); let nonce = self.nonce.read(); diff --git a/listings/applications/coin_flip/src/mock_randomness.cairo b/listings/applications/coin_flip/src/mock_randomness.cairo index 12d9b248..7e699164 100644 --- a/listings/applications/coin_flip/src/mock_randomness.cairo +++ b/listings/applications/coin_flip/src/mock_randomness.cairo @@ -44,7 +44,12 @@ pub mod MockRandomness { let caller = get_caller_address(); let this = get_contract_address(); let eth_dispatcher = self.eth_dispatcher.read(); - let success = eth_dispatcher.transfer_from(caller, this, callback_fee_limit.into()); + let success = eth_dispatcher + .transfer_from( + caller, + this, + (callback_fee_limit + self.compute_premium_fee(callback_address)).into() + ); assert(success, Errors::TRANSFER_FAILED); let request_id = self.next_request_id.read(); @@ -69,6 +74,10 @@ pub mod MockRandomness { requestor.receive_random_words(requestor_address, request_id, random_words, calldata); } + fn compute_premium_fee(self: @ContractState, caller_address: ContractAddress) -> u128 { + 100_000_000 + } + fn update_status( ref self: ContractState, @@ -141,9 +150,6 @@ pub mod MockRandomness { fn get_contract_balance(self: @ContractState) -> u256 { panic!("unimplemented") } - fn compute_premium_fee(self: @ContractState, caller_address: ContractAddress) -> u128 { - panic!("unimplemented") - } fn get_admin_address(self: @ContractState,) -> ContractAddress { panic!("unimplemented") } diff --git a/listings/applications/coin_flip/src/tests.cairo b/listings/applications/coin_flip/src/tests.cairo index 63fda77b..c866ae68 100644 --- a/listings/applications/coin_flip/src/tests.cairo +++ b/listings/applications/coin_flip/src/tests.cairo @@ -42,7 +42,7 @@ fn deploy() -> (ICoinFlipDispatcher, IRandomnessDispatcher, IERC20Dispatcher, Co // approve the CoinFlip contract to spend the callback fee let eth_dispatcher = IERC20Dispatcher { contract_address: eth_address }; start_cheat_caller_address(eth_address, deployer); - eth_dispatcher.approve(coin_flip_address, CoinFlip::CALLBACK_FEE_LIMIT.into() * 2); + eth_dispatcher.approve(coin_flip_address, CoinFlip::CALLBACK_FEE_LIMIT.into() * 4); stop_cheat_caller_address(eth_address); let randomness_dispatcher = IRandomnessDispatcher { contract_address: randomness_address }; diff --git a/listings/applications/coin_flip/starkli-wallet/account.json b/listings/applications/coin_flip/starkli-wallet/account.json new file mode 100644 index 00000000..32132ff9 --- /dev/null +++ b/listings/applications/coin_flip/starkli-wallet/account.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "variant": { + "type": "open_zeppelin", + "version": 1, + "public_key": "0x50fa8885bddcceb6943bdf2ac8567ab1cf31401a0271963c8cb5a7fd66257db", + "legacy": false + }, + "deployment": { + "status": "deployed", + "class_hash": "0x1e60c8722677cfb7dd8dbea5be86c09265db02cdfe77113e77da7d44c017388", + "address": "0x3b43b843034677e6bc57235d390884758b414b7df8fd777c83ce1011f7aa3b3" + } +} diff --git a/listings/applications/coin_flip/starkli-wallet/keystore.json b/listings/applications/coin_flip/starkli-wallet/keystore.json new file mode 100644 index 00000000..11c65c11 --- /dev/null +++ b/listings/applications/coin_flip/starkli-wallet/keystore.json @@ -0,0 +1 @@ +{"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"94f17a7492e0e66a7c0baefda3a2dcaf"},"ciphertext":"dc2fb67bed40fa7deb3e30e9d6fb17b5d81da293621fe0d36791dcc2e2dd4edc","kdf":"scrypt","kdfparams":{"dklen":32,"n":8192,"p":1,"r":8,"salt":"a6550c0fc7c3457b9afc19652270bab741c194a3095d7941ada30617d9d22723"},"mac":"d7b9d49ae862737af301f1ac5fc1260af53d17ae7d63c6f2afb75f549c6f56f9"},"id":"fa0e5704-32c6-46fd-a368-362007c923f2","version":3} \ No newline at end of file From dca7512028aa44644f097ed4a17d10c56efe1c42 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 30 Jul 2024 12:54:33 +0200 Subject: [PATCH 21/48] Assert leftover balance --- listings/applications/coin_flip/src/mock_randomness.cairo | 4 +++- listings/applications/coin_flip/src/tests.cairo | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/listings/applications/coin_flip/src/mock_randomness.cairo b/listings/applications/coin_flip/src/mock_randomness.cairo index 7e699164..71a67d6c 100644 --- a/listings/applications/coin_flip/src/mock_randomness.cairo +++ b/listings/applications/coin_flip/src/mock_randomness.cairo @@ -23,6 +23,8 @@ pub mod MockRandomness { pub const INVALID_ADDRESS: felt252 = 'Invalid address'; pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; } + + pub const PREMIUM_FEE: u128 = 100_000_000; #[constructor] fn constructor(ref self: ContractState, eth_address: ContractAddress) { @@ -75,7 +77,7 @@ pub mod MockRandomness { } fn compute_premium_fee(self: @ContractState, caller_address: ContractAddress) -> u128 { - 100_000_000 + PREMIUM_FEE } diff --git a/listings/applications/coin_flip/src/tests.cairo b/listings/applications/coin_flip/src/tests.cairo index c866ae68..a657e4a1 100644 --- a/listings/applications/coin_flip/src/tests.cairo +++ b/listings/applications/coin_flip/src/tests.cairo @@ -54,10 +54,11 @@ fn deploy() -> (ICoinFlipDispatcher, IRandomnessDispatcher, IERC20Dispatcher, Co #[test] #[fuzzer(runs: 10, seed: 22)] fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { - let (coin_flip, randomness, _, deployer) = deploy(); + let (coin_flip, randomness, eth, deployer) = deploy(); let mut spy = spy_events(SpyOn::One(coin_flip.contract_address)); + let previous_balance = eth.balance_of(deployer); start_cheat_caller_address(coin_flip.contract_address, deployer); coin_flip.flip(); stop_cheat_caller_address(coin_flip.contract_address); @@ -75,6 +76,7 @@ fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { ) ] ); + assert_eq!(eth.balance_of(deployer), previous_balance - CoinFlip::CALLBACK_FEE_LIMIT.into() - MockRandomness::PREMIUM_FEE.into()); randomness .submit_random( @@ -113,6 +115,8 @@ fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { ] ); + let previous_balance = eth.balance_of(deployer); + start_cheat_caller_address(coin_flip.contract_address, deployer); coin_flip.flip(); stop_cheat_caller_address(coin_flip.contract_address); @@ -130,6 +134,7 @@ fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { ) ] ); + assert_eq!(eth.balance_of(deployer), previous_balance - CoinFlip::CALLBACK_FEE_LIMIT.into() - MockRandomness::PREMIUM_FEE.into()); randomness .submit_random( From 06f9dc3a07fdd5547015beadc754a5651ae6a9a2 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 30 Jul 2024 12:57:54 +0200 Subject: [PATCH 22/48] Remove comment about fees --- listings/applications/coin_flip/src/contract.cairo | 1 - 1 file changed, 1 deletion(-) diff --git a/listings/applications/coin_flip/src/contract.cairo b/listings/applications/coin_flip/src/contract.cairo index 38ca2045..1c11c23b 100644 --- a/listings/applications/coin_flip/src/contract.cairo +++ b/listings/applications/coin_flip/src/contract.cairo @@ -144,7 +144,6 @@ pub mod CoinFlip { let premium_fee = randomness_dispatcher.compute_premium_fee(caller); // 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 = self.eth_dispatcher.read(); eth_dispatcher .approve( From f6f7dca74c26c540dff6f6aa9f8c9de47dfa98ab Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 30 Jul 2024 13:56:58 +0200 Subject: [PATCH 23/48] Increase the expected callback fee, update mock to expose fee calc fn --- .../applications/coin_flip/src/contract.cairo | 15 ++++++++---- .../coin_flip/src/mock_randomness.cairo | 23 +++++++++++++++++-- .../applications/coin_flip/src/tests.cairo | 16 +++++++++---- 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/listings/applications/coin_flip/src/contract.cairo b/listings/applications/coin_flip/src/contract.cairo index 1c11c23b..75fa4973 100644 --- a/listings/applications/coin_flip/src/contract.cairo +++ b/listings/applications/coin_flip/src/contract.cairo @@ -71,7 +71,7 @@ pub mod CoinFlip { pub const PUBLISH_DELAY: u64 = 0; // return the random value asap pub const NUM_OF_WORDS: u64 = 1; // one random value is sufficient - pub const CALLBACK_FEE_LIMIT: u128 = 10_000_000_000_000; // 0.00001 ETH + pub const CALLBACK_FEE_LIMIT: u128 = 1_000_000_000_000_000; // 0.001 ETH #[constructor] fn constructor( @@ -95,13 +95,13 @@ pub mod CoinFlip { // we take twice the callback fee amount just to make sure we // can cover it let eth_dispatcher = self.eth_dispatcher.read(); - let success = eth_dispatcher.transfer_from(flipper, this, CALLBACK_FEE_LIMIT.into() * 2); + let success = eth_dispatcher.transfer_from(flipper, this, CALLBACK_FEE_LIMIT.into()); assert(success, Errors::TRANSFER_FAILED); let flip_id = self._request_my_randomness(); self.flips.write(flip_id, flipper); - + // we return to the flipper whatever is left over after the fee is paid let leftover_balance = eth_dispatcher.balance_of(this); let success = eth_dispatcher.transfer(flipper, leftover_balance); @@ -140,6 +140,8 @@ pub mod CoinFlip { contract_address: randomness_contract_address }; + let callback_address = get_contract_address(); + let caller = get_caller_address(); let premium_fee = randomness_dispatcher.compute_premium_fee(caller); @@ -148,7 +150,7 @@ pub mod CoinFlip { eth_dispatcher .approve( randomness_contract_address, - (CALLBACK_FEE_LIMIT + premium_fee + CALLBACK_FEE_LIMIT / 5).into() + CALLBACK_FEE_LIMIT.into() + premium_fee.into() + CALLBACK_FEE_LIMIT.into() / 5 ); let nonce = self.nonce.read(); @@ -157,13 +159,16 @@ pub mod CoinFlip { let request_id = randomness_dispatcher .request_random( nonce, - get_contract_address(), + callback_address, CALLBACK_FEE_LIMIT, PUBLISH_DELAY, NUM_OF_WORDS, array![] ); + // remove approval once the randomness is paid for + eth_dispatcher.approve(randomness_contract_address, 0); + self.nonce.write(nonce + 1); request_id diff --git a/listings/applications/coin_flip/src/mock_randomness.cairo b/listings/applications/coin_flip/src/mock_randomness.cairo index 71a67d6c..b5480427 100644 --- a/listings/applications/coin_flip/src/mock_randomness.cairo +++ b/listings/applications/coin_flip/src/mock_randomness.cairo @@ -1,3 +1,12 @@ +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IMockRandomnessFee { + fn calculate_total_fee( + self: @TContractState, callback_fee_limit: u128, callback_address: ContractAddress + ) -> u128; +} + #[starknet::contract] pub mod MockRandomness { use pragma_lib::abi::IRandomness; @@ -23,7 +32,7 @@ pub mod MockRandomness { pub const INVALID_ADDRESS: felt252 = 'Invalid address'; pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; } - + pub const PREMIUM_FEE: u128 = 100_000_000; #[constructor] @@ -32,6 +41,15 @@ pub mod MockRandomness { self.eth_dispatcher.write(IERC20Dispatcher { contract_address: eth_address }); } + #[abi(embed_v0)] + impl MockRandomnessFee of super::IMockRandomnessFee { + fn calculate_total_fee( + self: @ContractState, callback_fee_limit: u128, callback_address: ContractAddress + ) -> u128 { + callback_fee_limit / 2 + self.compute_premium_fee(callback_address) + } + } + #[abi(embed_v0)] impl MockRandomness of IRandomness { fn request_random( @@ -45,12 +63,13 @@ pub mod MockRandomness { ) -> u64 { let caller = get_caller_address(); let this = get_contract_address(); + let eth_dispatcher = self.eth_dispatcher.read(); let success = eth_dispatcher .transfer_from( caller, this, - (callback_fee_limit + self.compute_premium_fee(callback_address)).into() + self.calculate_total_fee(callback_fee_limit, callback_address).into() ); assert(success, Errors::TRANSFER_FAILED); diff --git a/listings/applications/coin_flip/src/tests.cairo b/listings/applications/coin_flip/src/tests.cairo index a657e4a1..e3e45d68 100644 --- a/listings/applications/coin_flip/src/tests.cairo +++ b/listings/applications/coin_flip/src/tests.cairo @@ -2,7 +2,9 @@ use coin_flip::contract::{ CoinFlip, ICoinFlipDispatcher, ICoinFlipDispatcherTrait, IPragmaVRFDispatcher, IPragmaVRFDispatcherTrait }; -use coin_flip::mock_randomness::MockRandomness; +use coin_flip::mock_randomness::{ + MockRandomness, IMockRandomnessFeeDispatcher, IMockRandomnessFeeDispatcherTrait +}; use starknet::{ ContractAddress, ClassHash, get_block_timestamp, contract_address_const, get_caller_address }; @@ -42,7 +44,7 @@ fn deploy() -> (ICoinFlipDispatcher, IRandomnessDispatcher, IERC20Dispatcher, Co // approve the CoinFlip contract to spend the callback fee let eth_dispatcher = IERC20Dispatcher { contract_address: eth_address }; start_cheat_caller_address(eth_address, deployer); - eth_dispatcher.approve(coin_flip_address, CoinFlip::CALLBACK_FEE_LIMIT.into() * 4); + eth_dispatcher.approve(coin_flip_address, CoinFlip::CALLBACK_FEE_LIMIT.into() * 2); stop_cheat_caller_address(eth_address); let randomness_dispatcher = IRandomnessDispatcher { contract_address: randomness_address }; @@ -56,6 +58,11 @@ fn deploy() -> (ICoinFlipDispatcher, IRandomnessDispatcher, IERC20Dispatcher, Co fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { let (coin_flip, randomness, eth, deployer) = deploy(); + let expected_fee = IMockRandomnessFeeDispatcher { + contract_address: randomness.contract_address + } + .calculate_total_fee(CoinFlip::CALLBACK_FEE_LIMIT, coin_flip.contract_address); + let mut spy = spy_events(SpyOn::One(coin_flip.contract_address)); let previous_balance = eth.balance_of(deployer); @@ -76,7 +83,8 @@ fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { ) ] ); - assert_eq!(eth.balance_of(deployer), previous_balance - CoinFlip::CALLBACK_FEE_LIMIT.into() - MockRandomness::PREMIUM_FEE.into()); + + assert_eq!(eth.balance_of(deployer), previous_balance - expected_fee.into()); randomness .submit_random( @@ -134,7 +142,7 @@ fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { ) ] ); - assert_eq!(eth.balance_of(deployer), previous_balance - CoinFlip::CALLBACK_FEE_LIMIT.into() - MockRandomness::PREMIUM_FEE.into()); + assert_eq!(eth.balance_of(deployer), previous_balance - expected_fee.into()); randomness .submit_random( From fe21adcf57a15b668c83e9bb8f8fe5a0b5890853 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 30 Jul 2024 14:31:41 +0200 Subject: [PATCH 24/48] Unfinished: refunded --- .../applications/coin_flip/src/contract.cairo | 68 ++++++++++++++----- .../applications/coin_flip/src/tests.cairo | 2 +- 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/listings/applications/coin_flip/src/contract.cairo b/listings/applications/coin_flip/src/contract.cairo index 75fa4973..b45701ac 100644 --- a/listings/applications/coin_flip/src/contract.cairo +++ b/listings/applications/coin_flip/src/contract.cairo @@ -3,6 +3,7 @@ use starknet::ContractAddress; #[starknet::interface] pub trait ICoinFlip { fn flip(ref self: TContractState); + fn refund(ref self: TContractState); } #[starknet::interface] @@ -29,7 +30,7 @@ pub mod CoinFlip { #[storage] struct Storage { eth_dispatcher: IERC20Dispatcher, - flips: LegacyMap, + flips: LegacyMap, nonce: u64, randomness_contract_address: ContractAddress, } @@ -38,7 +39,8 @@ pub mod CoinFlip { #[derive(Drop, starknet::Event)] pub enum Event { Flipped: Flipped, - Landed: Landed + Landed: Landed, + Refunded: Refunded } #[derive(Drop, starknet::Event)] @@ -54,6 +56,13 @@ pub mod CoinFlip { pub side: Side } + #[derive(Drop, starknet::Event)] + pub struct Refunded { + pub flip_id: u64, + pub flipper: ContractAddress, + pub amount: u256 + } + #[derive(Drop, Serde)] pub enum Side { Heads, @@ -65,6 +74,7 @@ pub mod CoinFlip { pub const CALLER_NOT_RANDOMNESS: felt252 = 'Caller not randomness contract'; pub const INVALID_ADDRESS: felt252 = 'Invalid address'; pub const INVALID_FLIP_ID: felt252 = 'No flip with the given ID'; + pub const ONLY_FLIPPER_CAN_REFUND: felt252 = 'Only the flipper can refund'; pub const REQUESTOR_NOT_SELF: felt252 = 'Requestor is not self'; pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; } @@ -93,22 +103,40 @@ pub mod CoinFlip { // we pass the PragmaVRF fee to the flipper // we take twice the callback fee amount just to make sure we - // can cover it + // can cover the fee + the premium + let total_fee = self._calculate_total_fee_limit(this); let eth_dispatcher = self.eth_dispatcher.read(); - let success = eth_dispatcher.transfer_from(flipper, this, CALLBACK_FEE_LIMIT.into()); + let success = eth_dispatcher + .transfer_from(flipper, this, total_fee.into()); assert(success, Errors::TRANSFER_FAILED); let flip_id = self._request_my_randomness(); - self.flips.write(flip_id, flipper); - - // we return to the flipper whatever is left over after the fee is paid - let leftover_balance = eth_dispatcher.balance_of(this); - let success = eth_dispatcher.transfer(flipper, leftover_balance); - assert(success, Errors::TRANSFER_FAILED); + self.flips.write(flip_id, (flipper, total_fee)); self.emit(Event::Flipped(Flipped { flip_id, flipper })); } + + fn refund(ref self: ContractState, flip_id: u64) { + let caller = get_caller_address(); + let (flipper, total_fee) = self.flips.read(flip_id); + assert(flipper.is_non_zero(), Errors::INVALID_FLIP_ID); + assert(flipper == caller, Errors::ONLY_FLIPPER_CAN_REFUND); + + let randomness_dispatcher = IRandomnessDispatcher { + contract_address: self.randomness_contract_address.read() + }; + + let total_paid = randomness_dispatcher.get_total_fees(get_contract_address(), flip_id); + + let to_refund: u256 = (total_fee - total_paid).into(); + + let eth_dispatcher = self.eth_dispatcher.read(); + let success = eth_dispatcher.transfer(caller, to_refund); + assert(success, Errors::TRANSFER_FAILED); + + self.emit(Event::Refunded(Refunded { flip_id, flipper, amount: to_refund })); + } } #[abi(embed_v0)] @@ -140,17 +168,14 @@ pub mod CoinFlip { contract_address: randomness_contract_address }; - let callback_address = get_contract_address(); - - let caller = get_caller_address(); - let premium_fee = randomness_dispatcher.compute_premium_fee(caller); + let this = get_contract_address(); // Approve the randomness contract to transfer the callback fee let eth_dispatcher = self.eth_dispatcher.read(); eth_dispatcher .approve( randomness_contract_address, - CALLBACK_FEE_LIMIT.into() + premium_fee.into() + CALLBACK_FEE_LIMIT.into() / 5 + self._calculate_total_fee_limit(this).into() ); let nonce = self.nonce.read(); @@ -159,7 +184,7 @@ pub mod CoinFlip { let request_id = randomness_dispatcher .request_random( nonce, - callback_address, + this, CALLBACK_FEE_LIMIT, PUBLISH_DELAY, NUM_OF_WORDS, @@ -175,7 +200,7 @@ pub mod CoinFlip { } fn _process_coin_flip(ref self: ContractState, flip_id: u64, random_value: @felt252) { - let flipper = self.flips.read(flip_id); + let (flipper, total_fee) = self.flips.read(flip_id); assert(flipper.is_non_zero(), Errors::INVALID_FLIP_ID); // The chance of a flipped coin landing sideways is approximately 1 in 6000. @@ -193,5 +218,14 @@ pub mod CoinFlip { self.emit(Event::Landed(Landed { flip_id, flipper, side })); } + + fn _calculate_total_fee_limit(self: ContractState, callback_address: ContractAddress) -> u128 { + let randomness_dispatcher = IRandomnessDispatcher { + contract_address: self.randomness_contract_address.read() + }; + let caller = get_caller_address(); + let premium_fee = randomness_dispatcher.compute_premium_fee(caller); + CALLBACK_FEE_LIMIT.into() + premium_fee.into() + CALLBACK_FEE_LIMIT.into() / 5 + } } } diff --git a/listings/applications/coin_flip/src/tests.cairo b/listings/applications/coin_flip/src/tests.cairo index e3e45d68..0634a451 100644 --- a/listings/applications/coin_flip/src/tests.cairo +++ b/listings/applications/coin_flip/src/tests.cairo @@ -44,7 +44,7 @@ fn deploy() -> (ICoinFlipDispatcher, IRandomnessDispatcher, IERC20Dispatcher, Co // approve the CoinFlip contract to spend the callback fee let eth_dispatcher = IERC20Dispatcher { contract_address: eth_address }; start_cheat_caller_address(eth_address, deployer); - eth_dispatcher.approve(coin_flip_address, CoinFlip::CALLBACK_FEE_LIMIT.into() * 2); + eth_dispatcher.approve(coin_flip_address, CoinFlip::CALLBACK_FEE_LIMIT.into() * 4); stop_cheat_caller_address(eth_address); let randomness_dispatcher = IRandomnessDispatcher { contract_address: randomness_address }; From bf9de2d1dbd2a1a254bd2fef91a7fa01c7aee576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nenad=20Misi=C4=87?= Date: Wed, 31 Jul 2024 07:30:57 +0200 Subject: [PATCH 25/48] Store and use is_refunded flag --- listings/applications/coin_flip/src/contract.cairo | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/listings/applications/coin_flip/src/contract.cairo b/listings/applications/coin_flip/src/contract.cairo index b45701ac..3a730d8f 100644 --- a/listings/applications/coin_flip/src/contract.cairo +++ b/listings/applications/coin_flip/src/contract.cairo @@ -30,7 +30,7 @@ pub mod CoinFlip { #[storage] struct Storage { eth_dispatcher: IERC20Dispatcher, - flips: LegacyMap, + flips: LegacyMap, nonce: u64, randomness_contract_address: ContractAddress, } @@ -71,6 +71,7 @@ pub mod CoinFlip { } pub mod Errors { + pub const ALREADY_REFUNDED: felt252 = 'Already refunded'; pub const CALLER_NOT_RANDOMNESS: felt252 = 'Caller not randomness contract'; pub const INVALID_ADDRESS: felt252 = 'Invalid address'; pub const INVALID_FLIP_ID: felt252 = 'No flip with the given ID'; @@ -112,16 +113,17 @@ pub mod CoinFlip { let flip_id = self._request_my_randomness(); - self.flips.write(flip_id, (flipper, total_fee)); + self.flips.write(flip_id, (flipper, total_fee, false)); self.emit(Event::Flipped(Flipped { flip_id, flipper })); } fn refund(ref self: ContractState, flip_id: u64) { let caller = get_caller_address(); - let (flipper, total_fee) = self.flips.read(flip_id); + let (flipper, total_fee, is_refunded) = self.flips.read(flip_id); assert(flipper.is_non_zero(), Errors::INVALID_FLIP_ID); assert(flipper == caller, Errors::ONLY_FLIPPER_CAN_REFUND); + assert(!is_refunded, Errors::ALREADY_REFUNDED); let randomness_dispatcher = IRandomnessDispatcher { contract_address: self.randomness_contract_address.read() @@ -130,6 +132,8 @@ pub mod CoinFlip { let total_paid = randomness_dispatcher.get_total_fees(get_contract_address(), flip_id); let to_refund: u256 = (total_fee - total_paid).into(); + + self.flips.write(flip_id, (flipper, total_fee, true)); let eth_dispatcher = self.eth_dispatcher.read(); let success = eth_dispatcher.transfer(caller, to_refund); From d8ce40b6da334957c44539c044c1251dd802209e Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 31 Jul 2024 11:46:03 +0200 Subject: [PATCH 26/48] Implement logic necessary to successfully perform & test refund --- .../applications/coin_flip/src/contract.cairo | 56 ++++++++----------- .../coin_flip/src/mock_randomness.cairo | 38 +++++-------- .../applications/coin_flip/src/tests.cairo | 30 ++++++---- 3 files changed, 57 insertions(+), 67 deletions(-) diff --git a/listings/applications/coin_flip/src/contract.cairo b/listings/applications/coin_flip/src/contract.cairo index 3a730d8f..361521b1 100644 --- a/listings/applications/coin_flip/src/contract.cairo +++ b/listings/applications/coin_flip/src/contract.cairo @@ -3,7 +3,8 @@ use starknet::ContractAddress; #[starknet::interface] pub trait ICoinFlip { fn flip(ref self: TContractState); - fn refund(ref self: TContractState); + fn get_callback_fee_limit(self: @TContractState) -> u128; + fn refund(ref self: TContractState, flip_id: u64); } #[starknet::interface] @@ -105,10 +106,9 @@ pub mod CoinFlip { // we pass the PragmaVRF fee to the flipper // we take twice the callback fee amount just to make sure we // can cover the fee + the premium - let total_fee = self._calculate_total_fee_limit(this); + let total_fee = self.get_callback_fee_limit(); let eth_dispatcher = self.eth_dispatcher.read(); - let success = eth_dispatcher - .transfer_from(flipper, this, total_fee.into()); + let success = eth_dispatcher.transfer_from(flipper, this, total_fee.into()); assert(success, Errors::TRANSFER_FAILED); let flip_id = self._request_my_randomness(); @@ -117,28 +117,37 @@ pub mod CoinFlip { self.emit(Event::Flipped(Flipped { flip_id, flipper })); } - + + fn get_callback_fee_limit(self: @ContractState) -> u128 { + let randomness_dispatcher = IRandomnessDispatcher { + contract_address: self.randomness_contract_address.read() + }; + let this = get_contract_address(); + let premium_fee = randomness_dispatcher.compute_premium_fee(this); + CALLBACK_FEE_LIMIT.into() + premium_fee.into() + CALLBACK_FEE_LIMIT.into() / 5 + } + fn refund(ref self: ContractState, flip_id: u64) { let caller = get_caller_address(); let (flipper, total_fee, is_refunded) = self.flips.read(flip_id); assert(flipper.is_non_zero(), Errors::INVALID_FLIP_ID); assert(flipper == caller, Errors::ONLY_FLIPPER_CAN_REFUND); assert(!is_refunded, Errors::ALREADY_REFUNDED); - + let randomness_dispatcher = IRandomnessDispatcher { contract_address: self.randomness_contract_address.read() }; - + let total_paid = randomness_dispatcher.get_total_fees(get_contract_address(), flip_id); - - let to_refund: u256 = (total_fee - total_paid).into(); + + let to_refund: u256 = total_fee.into() - total_paid; self.flips.write(flip_id, (flipper, total_fee, true)); - + let eth_dispatcher = self.eth_dispatcher.read(); - let success = eth_dispatcher.transfer(caller, to_refund); + let success = eth_dispatcher.transfer(flipper, to_refund); assert(success, Errors::TRANSFER_FAILED); - + self.emit(Event::Refunded(Refunded { flip_id, flipper, amount: to_refund })); } } @@ -177,22 +186,14 @@ pub mod CoinFlip { // Approve the randomness contract to transfer the callback fee let eth_dispatcher = self.eth_dispatcher.read(); eth_dispatcher - .approve( - randomness_contract_address, - self._calculate_total_fee_limit(this).into() - ); + .approve(randomness_contract_address, self.get_callback_fee_limit().into()); let nonce = self.nonce.read(); // Request the randomness to be used to construct the winning combination let request_id = randomness_dispatcher .request_random( - nonce, - this, - CALLBACK_FEE_LIMIT, - PUBLISH_DELAY, - NUM_OF_WORDS, - array![] + nonce, this, CALLBACK_FEE_LIMIT, PUBLISH_DELAY, NUM_OF_WORDS, array![] ); // remove approval once the randomness is paid for @@ -204,7 +205,7 @@ pub mod CoinFlip { } fn _process_coin_flip(ref self: ContractState, flip_id: u64, random_value: @felt252) { - let (flipper, total_fee) = self.flips.read(flip_id); + let (flipper, _, _) = self.flips.read(flip_id); assert(flipper.is_non_zero(), Errors::INVALID_FLIP_ID); // The chance of a flipped coin landing sideways is approximately 1 in 6000. @@ -222,14 +223,5 @@ pub mod CoinFlip { self.emit(Event::Landed(Landed { flip_id, flipper, side })); } - - fn _calculate_total_fee_limit(self: ContractState, callback_address: ContractAddress) -> u128 { - let randomness_dispatcher = IRandomnessDispatcher { - contract_address: self.randomness_contract_address.read() - }; - let caller = get_caller_address(); - let premium_fee = randomness_dispatcher.compute_premium_fee(caller); - CALLBACK_FEE_LIMIT.into() + premium_fee.into() + CALLBACK_FEE_LIMIT.into() / 5 - } } } diff --git a/listings/applications/coin_flip/src/mock_randomness.cairo b/listings/applications/coin_flip/src/mock_randomness.cairo index b5480427..9012e608 100644 --- a/listings/applications/coin_flip/src/mock_randomness.cairo +++ b/listings/applications/coin_flip/src/mock_randomness.cairo @@ -1,12 +1,5 @@ use starknet::ContractAddress; -#[starknet::interface] -pub trait IMockRandomnessFee { - fn calculate_total_fee( - self: @TContractState, callback_fee_limit: u128, callback_address: ContractAddress - ) -> u128; -} - #[starknet::contract] pub mod MockRandomness { use pragma_lib::abi::IRandomness; @@ -21,7 +14,8 @@ pub mod MockRandomness { #[storage] struct Storage { eth_dispatcher: IERC20Dispatcher, - next_request_id: u64 + next_request_id: u64, + total_fees: LegacyMap::<(ContractAddress, u64), u256>, } #[event] @@ -41,15 +35,6 @@ pub mod MockRandomness { self.eth_dispatcher.write(IERC20Dispatcher { contract_address: eth_address }); } - #[abi(embed_v0)] - impl MockRandomnessFee of super::IMockRandomnessFee { - fn calculate_total_fee( - self: @ContractState, callback_fee_limit: u128, callback_address: ContractAddress - ) -> u128 { - callback_fee_limit / 2 + self.compute_premium_fee(callback_address) - } - } - #[abi(embed_v0)] impl MockRandomness of IRandomness { fn request_random( @@ -64,17 +49,21 @@ pub mod MockRandomness { let caller = get_caller_address(); let this = get_contract_address(); + let total_fee = (callback_fee_limit / 2 + self.compute_premium_fee(callback_address)).into(); let eth_dispatcher = self.eth_dispatcher.read(); let success = eth_dispatcher .transfer_from( caller, this, - self.calculate_total_fee(callback_fee_limit, callback_address).into() + total_fee ); assert(success, Errors::TRANSFER_FAILED); - + let request_id = self.next_request_id.read(); self.next_request_id.write(request_id + 1); + + self.total_fees.write((caller, request_id), total_fee); + request_id } @@ -98,6 +87,12 @@ pub mod MockRandomness { fn compute_premium_fee(self: @ContractState, caller_address: ContractAddress) -> u128 { PREMIUM_FEE } + + fn get_total_fees( + self: @ContractState, caller_address: ContractAddress, request_id: u64 + ) -> u256 { + self.total_fees.read((caller_address, request_id)) + } fn update_status( @@ -155,11 +150,6 @@ pub mod MockRandomness { ) { panic!("unimplemented") } - fn get_total_fees( - self: @ContractState, caller_address: ContractAddress, request_id: u64 - ) -> u256 { - panic!("unimplemented") - } fn get_out_of_gas_requests( self: @ContractState, requestor_address: ContractAddress, ) -> Span { diff --git a/listings/applications/coin_flip/src/tests.cairo b/listings/applications/coin_flip/src/tests.cairo index 0634a451..8667b4c2 100644 --- a/listings/applications/coin_flip/src/tests.cairo +++ b/listings/applications/coin_flip/src/tests.cairo @@ -2,9 +2,7 @@ use coin_flip::contract::{ CoinFlip, ICoinFlipDispatcher, ICoinFlipDispatcherTrait, IPragmaVRFDispatcher, IPragmaVRFDispatcherTrait }; -use coin_flip::mock_randomness::{ - MockRandomness, IMockRandomnessFeeDispatcher, IMockRandomnessFeeDispatcherTrait -}; +use coin_flip::mock_randomness::MockRandomness; use starknet::{ ContractAddress, ClassHash, get_block_timestamp, contract_address_const, get_caller_address }; @@ -58,14 +56,11 @@ fn deploy() -> (ICoinFlipDispatcher, IRandomnessDispatcher, IERC20Dispatcher, Co fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { let (coin_flip, randomness, eth, deployer) = deploy(); - let expected_fee = IMockRandomnessFeeDispatcher { - contract_address: randomness.contract_address - } - .calculate_total_fee(CoinFlip::CALLBACK_FEE_LIMIT, coin_flip.contract_address); + let callback_fee_limit = coin_flip.get_callback_fee_limit(); let mut spy = spy_events(SpyOn::One(coin_flip.contract_address)); - let previous_balance = eth.balance_of(deployer); + let original_balance = eth.balance_of(deployer); start_cheat_caller_address(coin_flip.contract_address, deployer); coin_flip.flip(); stop_cheat_caller_address(coin_flip.contract_address); @@ -84,7 +79,7 @@ fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { ] ); - assert_eq!(eth.balance_of(deployer), previous_balance - expected_fee.into()); + assert_eq!(eth.balance_of(deployer), original_balance - callback_fee_limit.into()); randomness .submit_random( @@ -123,7 +118,13 @@ fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { ] ); - let previous_balance = eth.balance_of(deployer); + start_cheat_caller_address(coin_flip.contract_address, deployer); + coin_flip.refund(expected_request_id); + stop_cheat_caller_address(coin_flip.contract_address); + + assert_eq!(eth.balance_of(deployer), original_balance - randomness.get_total_fees(coin_flip.contract_address, expected_request_id)); + + let original_balance = eth.balance_of(deployer); start_cheat_caller_address(coin_flip.contract_address, deployer); coin_flip.flip(); @@ -142,7 +143,8 @@ fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { ) ] ); - assert_eq!(eth.balance_of(deployer), previous_balance - expected_fee.into()); + + assert_eq!(eth.balance_of(deployer), original_balance - callback_fee_limit.into()); randomness .submit_random( @@ -180,6 +182,12 @@ fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { ) ] ); + + start_cheat_caller_address(coin_flip.contract_address, deployer); + coin_flip.refund(expected_request_id); + stop_cheat_caller_address(coin_flip.contract_address); + + assert_eq!(eth.balance_of(deployer), original_balance - randomness.get_total_fees(coin_flip.contract_address, expected_request_id)); } #[test] From c83546390efa8988b11257530d6f701ad49a2329 Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 31 Jul 2024 12:58:00 +0200 Subject: [PATCH 27/48] Update callback fee limit based on manual testing + update term to deposit --- .../applications/coin_flip/src/contract.cairo | 31 ++++++++----------- .../applications/coin_flip/src/tests.cairo | 18 +++++------ 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/listings/applications/coin_flip/src/contract.cairo b/listings/applications/coin_flip/src/contract.cairo index 361521b1..ba4fe1f7 100644 --- a/listings/applications/coin_flip/src/contract.cairo +++ b/listings/applications/coin_flip/src/contract.cairo @@ -3,7 +3,7 @@ use starknet::ContractAddress; #[starknet::interface] pub trait ICoinFlip { fn flip(ref self: TContractState); - fn get_callback_fee_limit(self: @TContractState) -> u128; + fn get_expected_deposit(self: @TContractState) -> u256; fn refund(ref self: TContractState, flip_id: u64); } @@ -31,7 +31,7 @@ pub mod CoinFlip { #[storage] struct Storage { eth_dispatcher: IERC20Dispatcher, - flips: LegacyMap, + flips: LegacyMap, nonce: u64, randomness_contract_address: ContractAddress, } @@ -83,7 +83,7 @@ pub mod CoinFlip { pub const PUBLISH_DELAY: u64 = 0; // return the random value asap pub const NUM_OF_WORDS: u64 = 1; // one random value is sufficient - pub const CALLBACK_FEE_LIMIT: u128 = 1_000_000_000_000_000; // 0.001 ETH + pub const CALLBACK_FEE_LIMIT: u128 = 100_000_000_000_000; // 0.0001 ETH #[constructor] fn constructor( @@ -106,30 +106,25 @@ pub mod CoinFlip { // we pass the PragmaVRF fee to the flipper // we take twice the callback fee amount just to make sure we // can cover the fee + the premium - let total_fee = self.get_callback_fee_limit(); + let deposit: u256 = self.get_expected_deposit(); let eth_dispatcher = self.eth_dispatcher.read(); - let success = eth_dispatcher.transfer_from(flipper, this, total_fee.into()); + let success = eth_dispatcher.transfer_from(flipper, this, deposit); assert(success, Errors::TRANSFER_FAILED); let flip_id = self._request_my_randomness(); - self.flips.write(flip_id, (flipper, total_fee, false)); + self.flips.write(flip_id, (flipper, deposit, false)); self.emit(Event::Flipped(Flipped { flip_id, flipper })); } - fn get_callback_fee_limit(self: @ContractState) -> u128 { - let randomness_dispatcher = IRandomnessDispatcher { - contract_address: self.randomness_contract_address.read() - }; - let this = get_contract_address(); - let premium_fee = randomness_dispatcher.compute_premium_fee(this); - CALLBACK_FEE_LIMIT.into() + premium_fee.into() + CALLBACK_FEE_LIMIT.into() / 5 + fn get_expected_deposit(self: @ContractState) -> u256 { + CALLBACK_FEE_LIMIT.into() * 5 } fn refund(ref self: ContractState, flip_id: u64) { let caller = get_caller_address(); - let (flipper, total_fee, is_refunded) = self.flips.read(flip_id); + let (flipper, deposit, is_refunded) = self.flips.read(flip_id); assert(flipper.is_non_zero(), Errors::INVALID_FLIP_ID); assert(flipper == caller, Errors::ONLY_FLIPPER_CAN_REFUND); assert(!is_refunded, Errors::ALREADY_REFUNDED); @@ -140,9 +135,9 @@ pub mod CoinFlip { let total_paid = randomness_dispatcher.get_total_fees(get_contract_address(), flip_id); - let to_refund: u256 = total_fee.into() - total_paid; + let to_refund: u256 = deposit - total_paid; - self.flips.write(flip_id, (flipper, total_fee, true)); + self.flips.write(flip_id, (flipper, deposit, true)); let eth_dispatcher = self.eth_dispatcher.read(); let success = eth_dispatcher.transfer(flipper, to_refund); @@ -183,10 +178,10 @@ pub mod CoinFlip { let this = get_contract_address(); - // Approve the randomness contract to transfer the callback fee + // Approve the randomness contract to transfer the callback deposit/fee let eth_dispatcher = self.eth_dispatcher.read(); eth_dispatcher - .approve(randomness_contract_address, self.get_callback_fee_limit().into()); + .approve(randomness_contract_address, self.get_expected_deposit().into()); let nonce = self.nonce.read(); diff --git a/listings/applications/coin_flip/src/tests.cairo b/listings/applications/coin_flip/src/tests.cairo index 8667b4c2..f46e6a7d 100644 --- a/listings/applications/coin_flip/src/tests.cairo +++ b/listings/applications/coin_flip/src/tests.cairo @@ -39,16 +39,16 @@ fn deploy() -> (ICoinFlipDispatcher, IRandomnessDispatcher, IERC20Dispatcher, Co (randomness_address, eth_address).serialize(ref coin_flip_ctor_calldata); let (coin_flip_address, _) = coin_flip_contract.deploy(@coin_flip_ctor_calldata).unwrap(); + let randomness_dispatcher = IRandomnessDispatcher { contract_address: randomness_address }; + let coin_flip_dispatcher = ICoinFlipDispatcher { contract_address: coin_flip_address }; + // approve the CoinFlip contract to spend the callback fee let eth_dispatcher = IERC20Dispatcher { contract_address: eth_address }; start_cheat_caller_address(eth_address, deployer); - eth_dispatcher.approve(coin_flip_address, CoinFlip::CALLBACK_FEE_LIMIT.into() * 4); + eth_dispatcher.approve(coin_flip_address, coin_flip_dispatcher.get_expected_deposit() * 2); // the test will flip the coin twice stop_cheat_caller_address(eth_address); - let randomness_dispatcher = IRandomnessDispatcher { contract_address: randomness_address }; - let coin_flip_dispathcer = ICoinFlipDispatcher { contract_address: coin_flip_address }; - - (coin_flip_dispathcer, randomness_dispatcher, eth_dispatcher, deployer) + (coin_flip_dispatcher, randomness_dispatcher, eth_dispatcher, deployer) } #[test] @@ -56,7 +56,7 @@ fn deploy() -> (ICoinFlipDispatcher, IRandomnessDispatcher, IERC20Dispatcher, Co fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { let (coin_flip, randomness, eth, deployer) = deploy(); - let callback_fee_limit = coin_flip.get_callback_fee_limit(); + let callback_fee_limit = coin_flip.get_expected_deposit(); let mut spy = spy_events(SpyOn::One(coin_flip.contract_address)); @@ -79,7 +79,7 @@ fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { ] ); - assert_eq!(eth.balance_of(deployer), original_balance - callback_fee_limit.into()); + assert_eq!(eth.balance_of(deployer), original_balance - callback_fee_limit); randomness .submit_random( @@ -144,7 +144,7 @@ fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { ] ); - assert_eq!(eth.balance_of(deployer), original_balance - callback_fee_limit.into()); + assert_eq!(eth.balance_of(deployer), original_balance - callback_fee_limit); randomness .submit_random( @@ -215,7 +215,7 @@ fn test_flip_without_enough_for_fees() { // approve the CoinFlip contract, but leave the flipper with no balance let flipper_with_no_funds = contract_address_const::<'flipper_with_no_funds'>(); start_cheat_caller_address(eth.contract_address, flipper_with_no_funds); - eth.approve(coin_flip.contract_address, (CoinFlip::CALLBACK_FEE_LIMIT).into() * 2); + eth.approve(coin_flip.contract_address, (coin_flip.get_expected_deposit())); stop_cheat_caller_address(eth.contract_address); start_cheat_caller_address(coin_flip.contract_address, flipper_with_no_funds); From 2089633436aff66e21f49ada02177c0a24c6a506 Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 31 Jul 2024 13:00:40 +0200 Subject: [PATCH 28/48] Format --- .../applications/coin_flip/src/contract.cairo | 3 +-- .../coin_flip/src/mock_randomness.cairo | 18 +++++++----------- .../applications/coin_flip/src/tests.cairo | 19 +++++++++++++++---- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/listings/applications/coin_flip/src/contract.cairo b/listings/applications/coin_flip/src/contract.cairo index ba4fe1f7..4bb8bb9f 100644 --- a/listings/applications/coin_flip/src/contract.cairo +++ b/listings/applications/coin_flip/src/contract.cairo @@ -180,8 +180,7 @@ pub mod CoinFlip { // Approve the randomness contract to transfer the callback deposit/fee let eth_dispatcher = self.eth_dispatcher.read(); - eth_dispatcher - .approve(randomness_contract_address, self.get_expected_deposit().into()); + eth_dispatcher.approve(randomness_contract_address, self.get_expected_deposit().into()); let nonce = self.nonce.read(); diff --git a/listings/applications/coin_flip/src/mock_randomness.cairo b/listings/applications/coin_flip/src/mock_randomness.cairo index 9012e608..683f5abb 100644 --- a/listings/applications/coin_flip/src/mock_randomness.cairo +++ b/listings/applications/coin_flip/src/mock_randomness.cairo @@ -49,21 +49,17 @@ pub mod MockRandomness { let caller = get_caller_address(); let this = get_contract_address(); - let total_fee = (callback_fee_limit / 2 + self.compute_premium_fee(callback_address)).into(); + let total_fee = (callback_fee_limit / 2 + self.compute_premium_fee(callback_address)) + .into(); let eth_dispatcher = self.eth_dispatcher.read(); - let success = eth_dispatcher - .transfer_from( - caller, - this, - total_fee - ); + let success = eth_dispatcher.transfer_from(caller, this, total_fee); assert(success, Errors::TRANSFER_FAILED); - + let request_id = self.next_request_id.read(); self.next_request_id.write(request_id + 1); - + self.total_fees.write((caller, request_id), total_fee); - + request_id } @@ -87,7 +83,7 @@ pub mod MockRandomness { fn compute_premium_fee(self: @ContractState, caller_address: ContractAddress) -> u128 { PREMIUM_FEE } - + fn get_total_fees( self: @ContractState, caller_address: ContractAddress, request_id: u64 ) -> u256 { diff --git a/listings/applications/coin_flip/src/tests.cairo b/listings/applications/coin_flip/src/tests.cairo index f46e6a7d..1d3b534d 100644 --- a/listings/applications/coin_flip/src/tests.cairo +++ b/listings/applications/coin_flip/src/tests.cairo @@ -41,11 +41,14 @@ fn deploy() -> (ICoinFlipDispatcher, IRandomnessDispatcher, IERC20Dispatcher, Co let randomness_dispatcher = IRandomnessDispatcher { contract_address: randomness_address }; let coin_flip_dispatcher = ICoinFlipDispatcher { contract_address: coin_flip_address }; - + // approve the CoinFlip contract to spend the callback fee let eth_dispatcher = IERC20Dispatcher { contract_address: eth_address }; start_cheat_caller_address(eth_address, deployer); - eth_dispatcher.approve(coin_flip_address, coin_flip_dispatcher.get_expected_deposit() * 2); // the test will flip the coin twice + eth_dispatcher + .approve( + coin_flip_address, coin_flip_dispatcher.get_expected_deposit() * 2 + ); // the test will flip the coin twice stop_cheat_caller_address(eth_address); (coin_flip_dispatcher, randomness_dispatcher, eth_dispatcher, deployer) @@ -122,7 +125,11 @@ fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { coin_flip.refund(expected_request_id); stop_cheat_caller_address(coin_flip.contract_address); - assert_eq!(eth.balance_of(deployer), original_balance - randomness.get_total_fees(coin_flip.contract_address, expected_request_id)); + assert_eq!( + eth.balance_of(deployer), + original_balance + - randomness.get_total_fees(coin_flip.contract_address, expected_request_id) + ); let original_balance = eth.balance_of(deployer); @@ -187,7 +194,11 @@ fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { coin_flip.refund(expected_request_id); stop_cheat_caller_address(coin_flip.contract_address); - assert_eq!(eth.balance_of(deployer), original_balance - randomness.get_total_fees(coin_flip.contract_address, expected_request_id)); + assert_eq!( + eth.balance_of(deployer), + original_balance + - randomness.get_total_fees(coin_flip.contract_address, expected_request_id) + ); } #[test] From 468ff9816c5a2b2fca8deede92a1ddaadbf8cd78 Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 31 Jul 2024 13:09:14 +0200 Subject: [PATCH 29/48] Use a FlipData struct instead of tuple --- .../applications/coin_flip/src/contract.cairo | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/listings/applications/coin_flip/src/contract.cairo b/listings/applications/coin_flip/src/contract.cairo index 4bb8bb9f..a59f63cd 100644 --- a/listings/applications/coin_flip/src/contract.cairo +++ b/listings/applications/coin_flip/src/contract.cairo @@ -28,10 +28,17 @@ pub mod CoinFlip { use pragma_lib::abi::{IRandomnessDispatcher, IRandomnessDispatcherTrait}; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + #[derive(Drop, starknet::Store)] + struct FlipData { + flipper: ContractAddress, + deposit: u256, + refunded: bool + } + #[storage] struct Storage { eth_dispatcher: IERC20Dispatcher, - flips: LegacyMap, + flips: LegacyMap, nonce: u64, randomness_contract_address: ContractAddress, } @@ -113,7 +120,7 @@ pub mod CoinFlip { let flip_id = self._request_my_randomness(); - self.flips.write(flip_id, (flipper, deposit, false)); + self.flips.write(flip_id, FlipData { flipper, deposit, refunded: false }); self.emit(Event::Flipped(Flipped { flip_id, flipper })); } @@ -124,10 +131,10 @@ pub mod CoinFlip { fn refund(ref self: ContractState, flip_id: u64) { let caller = get_caller_address(); - let (flipper, deposit, is_refunded) = self.flips.read(flip_id); + let FlipData { flipper, deposit, refunded } = self.flips.read(flip_id); assert(flipper.is_non_zero(), Errors::INVALID_FLIP_ID); assert(flipper == caller, Errors::ONLY_FLIPPER_CAN_REFUND); - assert(!is_refunded, Errors::ALREADY_REFUNDED); + assert(!refunded, Errors::ALREADY_REFUNDED); let randomness_dispatcher = IRandomnessDispatcher { contract_address: self.randomness_contract_address.read() @@ -137,7 +144,7 @@ pub mod CoinFlip { let to_refund: u256 = deposit - total_paid; - self.flips.write(flip_id, (flipper, deposit, true)); + self.flips.write(flip_id, FlipData { flipper, deposit, refunded: true }); let eth_dispatcher = self.eth_dispatcher.read(); let success = eth_dispatcher.transfer(flipper, to_refund); @@ -199,7 +206,8 @@ pub mod CoinFlip { } fn _process_coin_flip(ref self: ContractState, flip_id: u64, random_value: @felt252) { - let (flipper, _, _) = self.flips.read(flip_id); + let flipData = self.flips.read(flip_id); + let flipper = flipData.flipper; assert(flipper.is_non_zero(), Errors::INVALID_FLIP_ID); // The chance of a flipped coin landing sideways is approximately 1 in 6000. From 3ad1b6fd12a6890fda34687633c1469f9a63516d Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 31 Jul 2024 14:58:50 +0200 Subject: [PATCH 30/48] Fix refund --- .../applications/coin_flip/src/contract.cairo | 65 +++++++++++++------ .../coin_flip/src/mock_randomness.cairo | 20 +++--- .../applications/coin_flip/src/tests.cairo | 30 ++++----- 3 files changed, 69 insertions(+), 46 deletions(-) diff --git a/listings/applications/coin_flip/src/contract.cairo b/listings/applications/coin_flip/src/contract.cairo index a59f63cd..c2e356fc 100644 --- a/listings/applications/coin_flip/src/contract.cairo +++ b/listings/applications/coin_flip/src/contract.cairo @@ -29,16 +29,24 @@ pub mod CoinFlip { use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; #[derive(Drop, starknet::Store)] - struct FlipData { + struct RefundData { flipper: ContractAddress, - deposit: u256, - refunded: bool + amount: u256, + } + + #[derive(Drop, starknet::Store)] + struct LastRequestData { + flip_id: u64, + flipper: ContractAddress, + last_balance: u256, } #[storage] struct Storage { eth_dispatcher: IERC20Dispatcher, - flips: LegacyMap, + flips: LegacyMap, + last_received_request_id: Option, + refunds: LegacyMap, nonce: u64, randomness_contract_address: ContractAddress, } @@ -83,6 +91,7 @@ pub mod CoinFlip { pub const CALLER_NOT_RANDOMNESS: felt252 = 'Caller not randomness contract'; pub const INVALID_ADDRESS: felt252 = 'Invalid address'; pub const INVALID_FLIP_ID: felt252 = 'No flip with the given ID'; + pub const NOTHING_TO_REFUND: felt252 = 'Nothing to refund'; pub const ONLY_FLIPPER_CAN_REFUND: felt252 = 'Only the flipper can refund'; pub const REQUESTOR_NOT_SELF: felt252 = 'Requestor is not self'; pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; @@ -120,7 +129,7 @@ pub mod CoinFlip { let flip_id = self._request_my_randomness(); - self.flips.write(flip_id, FlipData { flipper, deposit, refunded: false }); + self.flips.write(flip_id, flipper); self.emit(Event::Flipped(Flipped { flip_id, flipper })); } @@ -131,26 +140,27 @@ pub mod CoinFlip { fn refund(ref self: ContractState, flip_id: u64) { let caller = get_caller_address(); - let FlipData { flipper, deposit, refunded } = self.flips.read(flip_id); - assert(flipper.is_non_zero(), Errors::INVALID_FLIP_ID); + let flipper = self.flips.read(flip_id); assert(flipper == caller, Errors::ONLY_FLIPPER_CAN_REFUND); - assert(!refunded, Errors::ALREADY_REFUNDED); - let randomness_dispatcher = IRandomnessDispatcher { - contract_address: self.randomness_contract_address.read() - }; + let eth_dispatcher = self.eth_dispatcher.read(); - let total_paid = randomness_dispatcher.get_total_fees(get_contract_address(), flip_id); + if let Option::Some(data) = self.last_received_request_id.read() { + let to_refund = eth_dispatcher.balance_of(get_contract_address()) + - data.last_balance; + self.refunds.write(data.flip_id, RefundData { flipper, amount: to_refund }); + self.last_received_request_id.write(Option::None); + } - let to_refund: u256 = deposit - total_paid; + let RefundData { flipper, amount } = self.refunds.read(flip_id); + assert(flipper.is_non_zero(), Errors::NOTHING_TO_REFUND); - self.flips.write(flip_id, FlipData { flipper, deposit, refunded: true }); + self.refunds.write(flip_id, RefundData { flipper: Zero::zero(), amount: 0 }); - let eth_dispatcher = self.eth_dispatcher.read(); - let success = eth_dispatcher.transfer(flipper, to_refund); + let success = eth_dispatcher.transfer(flipper, amount); assert(success, Errors::TRANSFER_FAILED); - self.emit(Event::Refunded(Refunded { flip_id, flipper, amount: to_refund })); + self.emit(Event::Refunded(Refunded { flip_id, flipper, amount })); } } @@ -206,10 +216,27 @@ pub mod CoinFlip { } fn _process_coin_flip(ref self: ContractState, flip_id: u64, random_value: @felt252) { - let flipData = self.flips.read(flip_id); - let flipper = flipData.flipper; + let flipper = self.flips.read(flip_id); assert(flipper.is_non_zero(), Errors::INVALID_FLIP_ID); + let eth_dispatcher = self.eth_dispatcher.read(); + let current_balance = eth_dispatcher.balance_of(get_contract_address()); + if let Option::Some(data) = self.last_received_request_id.read() { + self + .refunds + .write( + data.flip_id, + RefundData { flipper, amount: current_balance - data.last_balance } + ); + } + self + .last_received_request_id + .write( + Option::Some( + LastRequestData { flip_id, flipper, last_balance: current_balance } + ) + ); + // The chance of a flipped coin landing sideways is approximately 1 in 6000. // https://journals.aps.org/pre/abstract/10.1103/PhysRevE.48.2547 // diff --git a/listings/applications/coin_flip/src/mock_randomness.cairo b/listings/applications/coin_flip/src/mock_randomness.cairo index 683f5abb..79750aa3 100644 --- a/listings/applications/coin_flip/src/mock_randomness.cairo +++ b/listings/applications/coin_flip/src/mock_randomness.cairo @@ -1,5 +1,3 @@ -use starknet::ContractAddress; - #[starknet::contract] pub mod MockRandomness { use pragma_lib::abi::IRandomness; @@ -15,7 +13,7 @@ pub mod MockRandomness { struct Storage { eth_dispatcher: IERC20Dispatcher, next_request_id: u64, - total_fees: LegacyMap::<(ContractAddress, u64), u256>, + total_fees: LegacyMap<(ContractAddress, u64), u256>, } #[event] @@ -27,8 +25,6 @@ pub mod MockRandomness { pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; } - pub const PREMIUM_FEE: u128 = 100_000_000; - #[constructor] fn constructor(ref self: ContractState, eth_address: ContractAddress) { assert(eth_address.is_non_zero(), Errors::INVALID_ADDRESS); @@ -49,8 +45,7 @@ pub mod MockRandomness { let caller = get_caller_address(); let this = get_contract_address(); - let total_fee = (callback_fee_limit / 2 + self.compute_premium_fee(callback_address)) - .into(); + let total_fee: u256 = callback_fee_limit.into() * 5; let eth_dispatcher = self.eth_dispatcher.read(); let success = eth_dispatcher.transfer_from(caller, this, total_fee); assert(success, Errors::TRANSFER_FAILED); @@ -78,10 +73,10 @@ pub mod MockRandomness { ) { let requestor = IPragmaVRFDispatcher { contract_address: callback_address }; requestor.receive_random_words(requestor_address, request_id, random_words, calldata); - } - - fn compute_premium_fee(self: @ContractState, caller_address: ContractAddress) -> u128 { - PREMIUM_FEE + let eth_dispatcher = self.eth_dispatcher.read(); + let success = eth_dispatcher + .transfer(requestor_address, (callback_fee_limit - callback_fee).into()); + assert(success, Errors::TRANSFER_FAILED); } fn get_total_fees( @@ -91,6 +86,9 @@ pub mod MockRandomness { } + fn compute_premium_fee(self: @ContractState, caller_address: ContractAddress) -> u128 { + panic!("unimplemented 'compute_premium_fee'") + } fn update_status( ref self: ContractState, requestor_address: ContractAddress, diff --git a/listings/applications/coin_flip/src/tests.cairo b/listings/applications/coin_flip/src/tests.cairo index 1d3b534d..68b61691 100644 --- a/listings/applications/coin_flip/src/tests.cairo +++ b/listings/applications/coin_flip/src/tests.cairo @@ -18,7 +18,7 @@ fn deploy() -> (ICoinFlipDispatcher, IRandomnessDispatcher, IERC20Dispatcher, Co let eth_contract = declare("ERC20Upgradeable").unwrap(); let eth_name: ByteArray = "Ethereum"; let eth_symbol: ByteArray = "ETH"; - let eth_supply: u256 = CoinFlip::CALLBACK_FEE_LIMIT.into() * 10; + let eth_supply: u256 = CoinFlip::CALLBACK_FEE_LIMIT.into() * 20; let mut eth_ctor_calldata = array![]; let deployer = contract_address_const::<'deployer'>(); ((eth_name, eth_symbol, eth_supply, deployer), deployer).serialize(ref eth_ctor_calldata); @@ -59,7 +59,10 @@ fn deploy() -> (ICoinFlipDispatcher, IRandomnessDispatcher, IERC20Dispatcher, Co fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { let (coin_flip, randomness, eth, deployer) = deploy(); - let callback_fee_limit = coin_flip.get_expected_deposit(); + let expected_deposit = coin_flip.get_expected_deposit(); + let expected_callback_fee = CoinFlip::CALLBACK_FEE_LIMIT / 2; + let expected_total_fee: u256 = expected_deposit + - (CoinFlip::CALLBACK_FEE_LIMIT - expected_callback_fee).into(); let mut spy = spy_events(SpyOn::One(coin_flip.contract_address)); @@ -82,7 +85,7 @@ fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { ] ); - assert_eq!(eth.balance_of(deployer), original_balance - callback_fee_limit); + assert_eq!(eth.balance_of(deployer), original_balance - expected_deposit); randomness .submit_random( @@ -92,7 +95,7 @@ fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { 0, coin_flip.contract_address, CoinFlip::CALLBACK_FEE_LIMIT, - CoinFlip::CALLBACK_FEE_LIMIT, + expected_callback_fee, array![random_word_1].span(), array![].span(), array![] @@ -125,13 +128,12 @@ fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { coin_flip.refund(expected_request_id); stop_cheat_caller_address(coin_flip.contract_address); - assert_eq!( - eth.balance_of(deployer), - original_balance - - randomness.get_total_fees(coin_flip.contract_address, expected_request_id) - ); + assert_eq!(eth.balance_of(deployer), original_balance - expected_total_fee); let original_balance = eth.balance_of(deployer); + let expected_callback_fee = CoinFlip::CALLBACK_FEE_LIMIT / 2 + 1000; + let expected_total_fee: u256 = expected_deposit + - (CoinFlip::CALLBACK_FEE_LIMIT - expected_callback_fee).into(); start_cheat_caller_address(coin_flip.contract_address, deployer); coin_flip.flip(); @@ -151,7 +153,7 @@ fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { ] ); - assert_eq!(eth.balance_of(deployer), original_balance - callback_fee_limit); + assert_eq!(eth.balance_of(deployer), original_balance - expected_deposit); randomness .submit_random( @@ -161,7 +163,7 @@ fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { 0, coin_flip.contract_address, CoinFlip::CALLBACK_FEE_LIMIT, - CoinFlip::CALLBACK_FEE_LIMIT, + expected_callback_fee, array![random_word_2].span(), array![].span(), array![] @@ -194,11 +196,7 @@ fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { coin_flip.refund(expected_request_id); stop_cheat_caller_address(coin_flip.contract_address); - assert_eq!( - eth.balance_of(deployer), - original_balance - - randomness.get_total_fees(coin_flip.contract_address, expected_request_id) - ); + assert_eq!(eth.balance_of(deployer), original_balance - expected_total_fee); } #[test] From d037d572407e8548ab43ccba787814ad862da2c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nenad=20Misi=C4=87?= Date: Thu, 1 Aug 2024 07:07:25 +0200 Subject: [PATCH 31/48] Simplify CoinFlip to pay the flips itself --- .../applications/coin_flip/src/contract.cairo | 111 ++---------------- 1 file changed, 13 insertions(+), 98 deletions(-) diff --git a/listings/applications/coin_flip/src/contract.cairo b/listings/applications/coin_flip/src/contract.cairo index c2e356fc..4ee1d6e6 100644 --- a/listings/applications/coin_flip/src/contract.cairo +++ b/listings/applications/coin_flip/src/contract.cairo @@ -1,10 +1,6 @@ -use starknet::ContractAddress; - #[starknet::interface] pub trait ICoinFlip { fn flip(ref self: TContractState); - fn get_expected_deposit(self: @TContractState) -> u256; - fn refund(ref self: TContractState, flip_id: u64); } #[starknet::interface] @@ -28,25 +24,10 @@ pub mod CoinFlip { use pragma_lib::abi::{IRandomnessDispatcher, IRandomnessDispatcherTrait}; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; - #[derive(Drop, starknet::Store)] - struct RefundData { - flipper: ContractAddress, - amount: u256, - } - - #[derive(Drop, starknet::Store)] - struct LastRequestData { - flip_id: u64, - flipper: ContractAddress, - last_balance: u256, - } - #[storage] struct Storage { eth_dispatcher: IERC20Dispatcher, flips: LegacyMap, - last_received_request_id: Option, - refunds: LegacyMap, nonce: u64, randomness_contract_address: ContractAddress, } @@ -56,7 +37,6 @@ pub mod CoinFlip { pub enum Event { Flipped: Flipped, Landed: Landed, - Refunded: Refunded } #[derive(Drop, starknet::Event)] @@ -72,13 +52,6 @@ pub mod CoinFlip { pub side: Side } - #[derive(Drop, starknet::Event)] - pub struct Refunded { - pub flip_id: u64, - pub flipper: ContractAddress, - pub amount: u256 - } - #[derive(Drop, Serde)] pub enum Side { Heads, @@ -87,12 +60,9 @@ pub mod CoinFlip { } pub mod Errors { - pub const ALREADY_REFUNDED: felt252 = 'Already refunded'; pub const CALLER_NOT_RANDOMNESS: felt252 = 'Caller not randomness contract'; pub const INVALID_ADDRESS: felt252 = 'Invalid address'; pub const INVALID_FLIP_ID: felt252 = 'No flip with the given ID'; - pub const NOTHING_TO_REFUND: felt252 = 'Nothing to refund'; - pub const ONLY_FLIPPER_CAN_REFUND: felt252 = 'Only the flipper can refund'; pub const REQUESTOR_NOT_SELF: felt252 = 'Requestor is not self'; pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; } @@ -100,6 +70,7 @@ pub mod CoinFlip { pub const PUBLISH_DELAY: u64 = 0; // return the random value asap pub const NUM_OF_WORDS: u64 = 1; // one random value is sufficient pub const CALLBACK_FEE_LIMIT: u128 = 100_000_000_000_000; // 0.0001 ETH + pub const CALLBACK_FEE_DEPOSIT: u256 = CALLBACK_FEE_LIMIT * 5; // needs to cover the Premium fee #[constructor] fn constructor( @@ -115,53 +86,14 @@ pub mod CoinFlip { #[abi(embed_v0)] impl CoinFlip of super::ICoinFlip { + /// The contract needs to be funded with some ETH in order for this function + /// to be callable. For simplicity, anyone can fund the contract. fn flip(ref self: ContractState) { - let flipper = get_caller_address(); - let this = get_contract_address(); - - // we pass the PragmaVRF fee to the flipper - // we take twice the callback fee amount just to make sure we - // can cover the fee + the premium - let deposit: u256 = self.get_expected_deposit(); - let eth_dispatcher = self.eth_dispatcher.read(); - let success = eth_dispatcher.transfer_from(flipper, this, deposit); - assert(success, Errors::TRANSFER_FAILED); - let flip_id = self._request_my_randomness(); - + let flipper = get_caller_address(); self.flips.write(flip_id, flipper); - self.emit(Event::Flipped(Flipped { flip_id, flipper })); } - - fn get_expected_deposit(self: @ContractState) -> u256 { - CALLBACK_FEE_LIMIT.into() * 5 - } - - fn refund(ref self: ContractState, flip_id: u64) { - let caller = get_caller_address(); - let flipper = self.flips.read(flip_id); - assert(flipper == caller, Errors::ONLY_FLIPPER_CAN_REFUND); - - let eth_dispatcher = self.eth_dispatcher.read(); - - if let Option::Some(data) = self.last_received_request_id.read() { - let to_refund = eth_dispatcher.balance_of(get_contract_address()) - - data.last_balance; - self.refunds.write(data.flip_id, RefundData { flipper, amount: to_refund }); - self.last_received_request_id.write(Option::None); - } - - let RefundData { flipper, amount } = self.refunds.read(flip_id); - assert(flipper.is_non_zero(), Errors::NOTHING_TO_REFUND); - - self.refunds.write(flip_id, RefundData { flipper: Zero::zero(), amount: 0 }); - - let success = eth_dispatcher.transfer(flipper, amount); - assert(success, Errors::TRANSFER_FAILED); - - self.emit(Event::Refunded(Refunded { flip_id, flipper, amount })); - } } #[abi(embed_v0)] @@ -197,7 +129,7 @@ pub mod CoinFlip { // Approve the randomness contract to transfer the callback deposit/fee let eth_dispatcher = self.eth_dispatcher.read(); - eth_dispatcher.approve(randomness_contract_address, self.get_expected_deposit().into()); + eth_dispatcher.approve(randomness_contract_address, CALLBACK_FEE_DEPOSIT); let nonce = self.nonce.read(); @@ -207,40 +139,23 @@ pub mod CoinFlip { nonce, this, CALLBACK_FEE_LIMIT, PUBLISH_DELAY, NUM_OF_WORDS, array![] ); - // remove approval once the randomness is paid for - eth_dispatcher.approve(randomness_contract_address, 0); - self.nonce.write(nonce + 1); request_id } + /// The chance of a flipped coin landing sideways is approximately 1 in 6000. + /// See paper: https://journals.aps.org/pre/abstract/10.1103/PhysRevE.48.2547 + /// + /// Since splitting the remainder (5999) equally between heads and tails is impossible, + /// we double the probability values: + /// - 2 in 12000 chance that it's sideways + /// - 5999 in 12000 chance that it's heads + /// - 5999 in 12000 chance that it's tails fn _process_coin_flip(ref self: ContractState, flip_id: u64, random_value: @felt252) { let flipper = self.flips.read(flip_id); assert(flipper.is_non_zero(), Errors::INVALID_FLIP_ID); - let eth_dispatcher = self.eth_dispatcher.read(); - let current_balance = eth_dispatcher.balance_of(get_contract_address()); - if let Option::Some(data) = self.last_received_request_id.read() { - self - .refunds - .write( - data.flip_id, - RefundData { flipper, amount: current_balance - data.last_balance } - ); - } - self - .last_received_request_id - .write( - Option::Some( - LastRequestData { flip_id, flipper, last_balance: current_balance } - ) - ); - - // The chance of a flipped coin landing sideways is approximately 1 in 6000. - // https://journals.aps.org/pre/abstract/10.1103/PhysRevE.48.2547 - // - // Since splitting the remainder (5999) equally is impossible, we double the values. let random_value: u256 = (*random_value).into() % 12000; let side = if random_value < 5999 { Side::Heads From d141d5e44a5247cd32babfa3edf5636691f74932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nenad=20Misi=C4=87?= Date: Thu, 1 Aug 2024 07:13:51 +0200 Subject: [PATCH 32/48] CALLBACK_FEE_DEPOSIT->MAX_CALLBACK_FEE_DEPOSIT --- listings/applications/coin_flip/src/contract.cairo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/listings/applications/coin_flip/src/contract.cairo b/listings/applications/coin_flip/src/contract.cairo index 4ee1d6e6..dfd7630a 100644 --- a/listings/applications/coin_flip/src/contract.cairo +++ b/listings/applications/coin_flip/src/contract.cairo @@ -70,7 +70,7 @@ pub mod CoinFlip { pub const PUBLISH_DELAY: u64 = 0; // return the random value asap pub const NUM_OF_WORDS: u64 = 1; // one random value is sufficient pub const CALLBACK_FEE_LIMIT: u128 = 100_000_000_000_000; // 0.0001 ETH - pub const CALLBACK_FEE_DEPOSIT: u256 = CALLBACK_FEE_LIMIT * 5; // needs to cover the Premium fee + pub const MAX_CALLBACK_FEE_DEPOSIT: u256 = CALLBACK_FEE_LIMIT * 5; // needs to cover the Premium fee #[constructor] fn constructor( @@ -129,7 +129,7 @@ pub mod CoinFlip { // Approve the randomness contract to transfer the callback deposit/fee let eth_dispatcher = self.eth_dispatcher.read(); - eth_dispatcher.approve(randomness_contract_address, CALLBACK_FEE_DEPOSIT); + eth_dispatcher.approve(randomness_contract_address, MAX_CALLBACK_FEE_DEPOSIT); let nonce = self.nonce.read(); From 9e4cda04ff22ba980bcbb7118864dcbf0275ca38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nenad=20Misi=C4=87?= Date: Thu, 1 Aug 2024 07:39:46 +0200 Subject: [PATCH 33/48] Update tests to test the new CoinFlip contract --- .../applications/coin_flip/src/tests.cairo | 139 ++++++++++-------- 1 file changed, 77 insertions(+), 62 deletions(-) diff --git a/listings/applications/coin_flip/src/tests.cairo b/listings/applications/coin_flip/src/tests.cairo index 68b61691..6f0a0201 100644 --- a/listings/applications/coin_flip/src/tests.cairo +++ b/listings/applications/coin_flip/src/tests.cairo @@ -18,7 +18,7 @@ fn deploy() -> (ICoinFlipDispatcher, IRandomnessDispatcher, IERC20Dispatcher, Co let eth_contract = declare("ERC20Upgradeable").unwrap(); let eth_name: ByteArray = "Ethereum"; let eth_symbol: ByteArray = "ETH"; - let eth_supply: u256 = CoinFlip::CALLBACK_FEE_LIMIT.into() * 20; + let eth_supply: u256 = CoinFlip::CALLBACK_FEE_LIMIT.into() * 100; let mut eth_ctor_calldata = array![]; let deployer = contract_address_const::<'deployer'>(); ((eth_name, eth_symbol, eth_supply, deployer), deployer).serialize(ref eth_ctor_calldata); @@ -39,16 +39,16 @@ fn deploy() -> (ICoinFlipDispatcher, IRandomnessDispatcher, IERC20Dispatcher, Co (randomness_address, eth_address).serialize(ref coin_flip_ctor_calldata); let (coin_flip_address, _) = coin_flip_contract.deploy(@coin_flip_ctor_calldata).unwrap(); + let eth_dispatcher = IERC20Dispatcher { contract_address: eth_address }; let randomness_dispatcher = IRandomnessDispatcher { contract_address: randomness_address }; let coin_flip_dispatcher = ICoinFlipDispatcher { contract_address: coin_flip_address }; - // approve the CoinFlip contract to spend the callback fee - let eth_dispatcher = IERC20Dispatcher { contract_address: eth_address }; + // fund the CoinFlip contract start_cheat_caller_address(eth_address, deployer); eth_dispatcher - .approve( - coin_flip_address, coin_flip_dispatcher.get_expected_deposit() * 2 - ); // the test will flip the coin twice + .transfer( + coin_flip_address, CoinFlip::CALLBACK_FEE_LIMIT.into() * 100 + ); stop_cheat_caller_address(eth_address); (coin_flip_dispatcher, randomness_dispatcher, eth_dispatcher, deployer) @@ -59,20 +59,20 @@ fn deploy() -> (ICoinFlipDispatcher, IRandomnessDispatcher, IERC20Dispatcher, Co fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { let (coin_flip, randomness, eth, deployer) = deploy(); - let expected_deposit = coin_flip.get_expected_deposit(); - let expected_callback_fee = CoinFlip::CALLBACK_FEE_LIMIT / 2; - let expected_total_fee: u256 = expected_deposit - - (CoinFlip::CALLBACK_FEE_LIMIT - expected_callback_fee).into(); + _flip_request(coin_flip, randomness, eth, deployer, 0, CoinFlip::CALLBACK_FEE_LIMIT / 5 * 3); + _flip_request(coin_flip, randomness, eth, deployer, 1, CoinFlip::CALLBACK_FEE_LIMIT / 4 * 3); + _flip_request(coin_flip, randomness, eth, deployer, 2, CoinFlip::CALLBACK_FEE_LIMIT); +} +fn _flip_request(coin_flip: ICoinFlipDispatcher, randomness: IRandomnessDispatcher, eth: IERC20Dispatcher, deployer: ContractAddress, expected_request_id: u64, expected_callback_fee: u128) { let mut spy = spy_events(SpyOn::One(coin_flip.contract_address)); - let original_balance = eth.balance_of(deployer); + let original_balance = eth.balance_of(coin_flip.contract_address); + start_cheat_caller_address(coin_flip.contract_address, deployer); coin_flip.flip(); stop_cheat_caller_address(coin_flip.contract_address); - let expected_request_id = 0; - spy .assert_emitted( @array![ @@ -85,7 +85,8 @@ fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { ] ); - assert_eq!(eth.balance_of(deployer), original_balance - expected_deposit); + let post_flip_balance = eth.balance_of(coin_flip.contract_address); + assert_eq!(post_flip_balance, original_balance - randomness.get_total_fees(coin_flip.contract_address, expected_request_id)); randomness .submit_random( @@ -124,53 +125,87 @@ fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { ] ); - start_cheat_caller_address(coin_flip.contract_address, deployer); - coin_flip.refund(expected_request_id); - stop_cheat_caller_address(coin_flip.contract_address); + assert_eq!(eth.balance_of(coin_flip.contract_address), post_flip_balance + (CoinFlip::CALLBACK_FEE_LIMIT - expected_callback_fee)); +} + +#[test] +fn test_two_consecutive_flips() { + let (coin_flip, randomness, eth, deployer) = deploy(); - assert_eq!(eth.balance_of(deployer), original_balance - expected_total_fee); + let mut spy = spy_events(SpyOn::One(coin_flip.contract_address)); + + let original_balance = eth.balance_of(coin_flip.contract_address); - let original_balance = eth.balance_of(deployer); - let expected_callback_fee = CoinFlip::CALLBACK_FEE_LIMIT / 2 + 1000; - let expected_total_fee: u256 = expected_deposit - - (CoinFlip::CALLBACK_FEE_LIMIT - expected_callback_fee).into(); + let other_flipper = contract_address_const::<'other_flipper'>(); start_cheat_caller_address(coin_flip.contract_address, deployer); coin_flip.flip(); + start_cheat_caller_address(coin_flip.contract_address, other_flipper); + coin_flip.flip(); stop_cheat_caller_address(coin_flip.contract_address); - let expected_request_id = 1; - spy .assert_emitted( @array![ ( coin_flip.contract_address, CoinFlip::Event::Flipped( - CoinFlip::Flipped { flip_id: expected_request_id, flipper: deployer } + CoinFlip::Flipped { flip_id: 0, flipper: deployer } + ) + ), + ( + coin_flip.contract_address, + CoinFlip::Event::Flipped( + CoinFlip::Flipped { flip_id: 1, flipper: other_flipper } ) ) ] ); - assert_eq!(eth.balance_of(deployer), original_balance - expected_deposit); + let post_flip_balance = eth.balance_of(coin_flip.contract_address); + assert_eq!(post_flip_balance, original_balance - randomness.get_total_fees(coin_flip.contract_address, expected_request_id) * 2); + + let expected_callback_fee = CoinFlip::CALLBACK_FEE_LIMIT / 5 * 3; + let random_word_deployer = 'this is a string representation of some felt'; + let random_word_other_user = 'this is another felt value that we need'; randomness .submit_random( - expected_request_id, + 0, coin_flip.contract_address, + 0, + 0, + coin_flip.contract_address, + CoinFlip::CALLBACK_FEE_LIMIT, + expected_callback_fee, + array![random_word_deployer].span(), + array![].span(), + array![] + ); + randomness + .submit_random( 1, + coin_flip.contract_address, + 0, 0, coin_flip.contract_address, CoinFlip::CALLBACK_FEE_LIMIT, expected_callback_fee, - array![random_word_2].span(), + array![random_word_other_user].span(), array![].span(), array![] ); - let random_value: u256 = random_word_2.into() % 12000; - let expected_side = if random_value < 5999 { + let random_value: u256 = random_word_deployer.into() % 12000; + let expected_side_deployer = if random_value < 5999 { + CoinFlip::Side::Heads + } else if random_value > 6000 { + CoinFlip::Side::Tails + } else { + CoinFlip::Side::Sideways + }; + let random_value: u256 = random_word_other_user.into() % 12000; + let expected_side_other_user = if random_value < 5999 { CoinFlip::Side::Heads } else if random_value > 6000 { CoinFlip::Side::Tails @@ -185,48 +220,28 @@ fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { coin_flip.contract_address, CoinFlip::Event::Landed( CoinFlip::Landed { - flip_id: expected_request_id, flipper: deployer, side: expected_side + flip_id: 0, flipper: deployer, side: expected_side_deployer + } + ) + ) + ( + coin_flip.contract_address, + CoinFlip::Event::Landed( + CoinFlip::Landed { + flip_id: 1, flipper: other_flipper, side: expected_side_other_flipper } ) ) ] ); - start_cheat_caller_address(coin_flip.contract_address, deployer); - coin_flip.refund(expected_request_id); - stop_cheat_caller_address(coin_flip.contract_address); - - assert_eq!(eth.balance_of(deployer), original_balance - expected_total_fee); -} - -#[test] -#[should_panic(expected: 'ERC20: insufficient allowance')] -fn test_flip_no_allowance() { - let (coin_flip, _, eth, deployer) = deploy(); - - let new_flipper = contract_address_const::<'new_flipper'>(); - - // ensure new flipper has funds, just that they haven't approved the - // CoinFlip contract to spend them to cover the fee - start_cheat_caller_address(eth.contract_address, deployer); - eth.transfer(new_flipper, (CoinFlip::CALLBACK_FEE_LIMIT).into() * 2); - stop_cheat_caller_address(eth.contract_address); - - start_cheat_caller_address(coin_flip.contract_address, new_flipper); - coin_flip.flip(); + assert_eq!(eth.balance_of(coin_flip.contract_address), post_flip_balance + (CoinFlip::CALLBACK_FEE_LIMIT - expected_callback_fee) * 2); } #[test] #[should_panic(expected: 'ERC20: insufficient balance')] fn test_flip_without_enough_for_fees() { - let (coin_flip, _, eth, _) = deploy(); - - // approve the CoinFlip contract, but leave the flipper with no balance - let flipper_with_no_funds = contract_address_const::<'flipper_with_no_funds'>(); - start_cheat_caller_address(eth.contract_address, flipper_with_no_funds); - eth.approve(coin_flip.contract_address, (coin_flip.get_expected_deposit())); - stop_cheat_caller_address(eth.contract_address); - - start_cheat_caller_address(coin_flip.contract_address, flipper_with_no_funds); + let (coin_flip, _, _, deployer) = deploy(); + start_cheat_caller_address(coin_flip.contract_address, deployer); coin_flip.flip(); } From a81ecad5843f1df4651bdbacb4c40baefe6801ac Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 1 Aug 2024 09:54:58 +0200 Subject: [PATCH 34/48] Fix compile errors --- .../applications/coin_flip/src/contract.cairo | 5 +- .../applications/coin_flip/src/tests.cairo | 85 +++++++++++++------ 2 files changed, 61 insertions(+), 29 deletions(-) diff --git a/listings/applications/coin_flip/src/contract.cairo b/listings/applications/coin_flip/src/contract.cairo index dfd7630a..2ac6929d 100644 --- a/listings/applications/coin_flip/src/contract.cairo +++ b/listings/applications/coin_flip/src/contract.cairo @@ -1,3 +1,5 @@ +use starknet::ContractAddress; + #[starknet::interface] pub trait ICoinFlip { fn flip(ref self: TContractState); @@ -70,7 +72,8 @@ pub mod CoinFlip { pub const PUBLISH_DELAY: u64 = 0; // return the random value asap pub const NUM_OF_WORDS: u64 = 1; // one random value is sufficient pub const CALLBACK_FEE_LIMIT: u128 = 100_000_000_000_000; // 0.0001 ETH - pub const MAX_CALLBACK_FEE_DEPOSIT: u256 = CALLBACK_FEE_LIMIT * 5; // needs to cover the Premium fee + pub const MAX_CALLBACK_FEE_DEPOSIT: u256 = + 500_000_000_000_000; // CALLBACK_FEE_LIMIT * 5; needs to cover the Premium fee #[constructor] fn constructor( diff --git a/listings/applications/coin_flip/src/tests.cairo b/listings/applications/coin_flip/src/tests.cairo index 6f0a0201..64aaafc2 100644 --- a/listings/applications/coin_flip/src/tests.cairo +++ b/listings/applications/coin_flip/src/tests.cairo @@ -43,28 +43,39 @@ fn deploy() -> (ICoinFlipDispatcher, IRandomnessDispatcher, IERC20Dispatcher, Co let randomness_dispatcher = IRandomnessDispatcher { contract_address: randomness_address }; let coin_flip_dispatcher = ICoinFlipDispatcher { contract_address: coin_flip_address }; - // fund the CoinFlip contract - start_cheat_caller_address(eth_address, deployer); - eth_dispatcher - .transfer( - coin_flip_address, CoinFlip::CALLBACK_FEE_LIMIT.into() * 100 - ); - stop_cheat_caller_address(eth_address); - (coin_flip_dispatcher, randomness_dispatcher, eth_dispatcher, deployer) } #[test] #[fuzzer(runs: 10, seed: 22)] -fn test_two_flips(random_word_1: felt252, random_word_2: felt252) { +fn test_two_flips(random_word: felt252) { let (coin_flip, randomness, eth, deployer) = deploy(); - _flip_request(coin_flip, randomness, eth, deployer, 0, CoinFlip::CALLBACK_FEE_LIMIT / 5 * 3); - _flip_request(coin_flip, randomness, eth, deployer, 1, CoinFlip::CALLBACK_FEE_LIMIT / 4 * 3); - _flip_request(coin_flip, randomness, eth, deployer, 2, CoinFlip::CALLBACK_FEE_LIMIT); + // fund the CoinFlip contract + start_cheat_caller_address(eth.contract_address, deployer); + eth.transfer(coin_flip.contract_address, CoinFlip::CALLBACK_FEE_LIMIT.into() * 50); + stop_cheat_caller_address(eth.contract_address); + + _flip_request( + coin_flip, randomness, eth, deployer, 0, CoinFlip::CALLBACK_FEE_LIMIT / 5 * 3, random_word + ); + _flip_request( + coin_flip, randomness, eth, deployer, 1, CoinFlip::CALLBACK_FEE_LIMIT / 4 * 3, random_word + ); + _flip_request( + coin_flip, randomness, eth, deployer, 2, CoinFlip::CALLBACK_FEE_LIMIT, random_word + ); } -fn _flip_request(coin_flip: ICoinFlipDispatcher, randomness: IRandomnessDispatcher, eth: IERC20Dispatcher, deployer: ContractAddress, expected_request_id: u64, expected_callback_fee: u128) { +fn _flip_request( + coin_flip: ICoinFlipDispatcher, + randomness: IRandomnessDispatcher, + eth: IERC20Dispatcher, + deployer: ContractAddress, + expected_request_id: u64, + expected_callback_fee: u128, + random_word: felt252 +) { let mut spy = spy_events(SpyOn::One(coin_flip.contract_address)); let original_balance = eth.balance_of(coin_flip.contract_address); @@ -86,7 +97,11 @@ fn _flip_request(coin_flip: ICoinFlipDispatcher, randomness: IRandomnessDispatch ); let post_flip_balance = eth.balance_of(coin_flip.contract_address); - assert_eq!(post_flip_balance, original_balance - randomness.get_total_fees(coin_flip.contract_address, expected_request_id)); + assert_eq!( + post_flip_balance, + original_balance + - randomness.get_total_fees(coin_flip.contract_address, expected_request_id) + ); randomness .submit_random( @@ -97,12 +112,12 @@ fn _flip_request(coin_flip: ICoinFlipDispatcher, randomness: IRandomnessDispatch coin_flip.contract_address, CoinFlip::CALLBACK_FEE_LIMIT, expected_callback_fee, - array![random_word_1].span(), + array![random_word].span(), array![].span(), array![] ); - let random_value: u256 = random_word_1.into() % 12000; + let random_value: u256 = random_word.into() % 12000; let expected_side = if random_value < 5999 { CoinFlip::Side::Heads } else if random_value > 6000 { @@ -125,13 +140,21 @@ fn _flip_request(coin_flip: ICoinFlipDispatcher, randomness: IRandomnessDispatch ] ); - assert_eq!(eth.balance_of(coin_flip.contract_address), post_flip_balance + (CoinFlip::CALLBACK_FEE_LIMIT - expected_callback_fee)); + assert_eq!( + eth.balance_of(coin_flip.contract_address), + post_flip_balance + (CoinFlip::CALLBACK_FEE_LIMIT - expected_callback_fee).into() + ); } #[test] fn test_two_consecutive_flips() { let (coin_flip, randomness, eth, deployer) = deploy(); + // fund the CoinFlip contract + start_cheat_caller_address(eth.contract_address, deployer); + eth.transfer(coin_flip.contract_address, CoinFlip::CALLBACK_FEE_LIMIT.into() * 50); + stop_cheat_caller_address(eth.contract_address); + let mut spy = spy_events(SpyOn::One(coin_flip.contract_address)); let original_balance = eth.balance_of(coin_flip.contract_address); @@ -149,9 +172,7 @@ fn test_two_consecutive_flips() { @array![ ( coin_flip.contract_address, - CoinFlip::Event::Flipped( - CoinFlip::Flipped { flip_id: 0, flipper: deployer } - ) + CoinFlip::Event::Flipped(CoinFlip::Flipped { flip_id: 0, flipper: deployer }) ), ( coin_flip.contract_address, @@ -163,11 +184,16 @@ fn test_two_consecutive_flips() { ); let post_flip_balance = eth.balance_of(coin_flip.contract_address); - assert_eq!(post_flip_balance, original_balance - randomness.get_total_fees(coin_flip.contract_address, expected_request_id) * 2); + assert_eq!( + post_flip_balance, + original_balance + - randomness.get_total_fees(coin_flip.contract_address, 0) + - randomness.get_total_fees(coin_flip.contract_address, 1) + ); let expected_callback_fee = CoinFlip::CALLBACK_FEE_LIMIT / 5 * 3; - let random_word_deployer = 'this is a string representation of some felt'; - let random_word_other_user = 'this is another felt value that we need'; + let random_word_deployer = 'this is some random word value'; + let random_word_other_flipper = 'this is another random word'; randomness .submit_random( @@ -191,7 +217,7 @@ fn test_two_consecutive_flips() { coin_flip.contract_address, CoinFlip::CALLBACK_FEE_LIMIT, expected_callback_fee, - array![random_word_other_user].span(), + array![random_word_other_flipper].span(), array![].span(), array![] ); @@ -204,8 +230,8 @@ fn test_two_consecutive_flips() { } else { CoinFlip::Side::Sideways }; - let random_value: u256 = random_word_other_user.into() % 12000; - let expected_side_other_user = if random_value < 5999 { + let random_value: u256 = random_word_other_flipper.into() % 12000; + let expected_side_other_flipper = if random_value < 5999 { CoinFlip::Side::Heads } else if random_value > 6000 { CoinFlip::Side::Tails @@ -223,7 +249,7 @@ fn test_two_consecutive_flips() { flip_id: 0, flipper: deployer, side: expected_side_deployer } ) - ) + ), ( coin_flip.contract_address, CoinFlip::Event::Landed( @@ -235,7 +261,10 @@ fn test_two_consecutive_flips() { ] ); - assert_eq!(eth.balance_of(coin_flip.contract_address), post_flip_balance + (CoinFlip::CALLBACK_FEE_LIMIT - expected_callback_fee) * 2); + assert_eq!( + eth.balance_of(coin_flip.contract_address), + post_flip_balance + (CoinFlip::CALLBACK_FEE_LIMIT - expected_callback_fee).into() * 2 + ); } #[test] From 437b15c418d986f88b1f2a4ae3b37bb5e3e7d983 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 1 Aug 2024 10:12:14 +0200 Subject: [PATCH 35/48] Increase publish_delay to 1 & remove unused imports --- listings/applications/coin_flip/src/contract.cairo | 8 +++----- .../coin_flip/src/mock_randomness.cairo | 2 -- listings/applications/coin_flip/src/tests.cairo | 13 ++++--------- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/listings/applications/coin_flip/src/contract.cairo b/listings/applications/coin_flip/src/contract.cairo index 2ac6929d..3955505c 100644 --- a/listings/applications/coin_flip/src/contract.cairo +++ b/listings/applications/coin_flip/src/contract.cairo @@ -5,6 +5,7 @@ pub trait ICoinFlip { fn flip(ref self: TContractState); } +// declares just the pragma_lib::abi::IRandomness.receive_random_words function #[starknet::interface] pub trait IPragmaVRF { fn receive_random_words( @@ -19,10 +20,7 @@ pub trait IPragmaVRF { #[starknet::contract] pub mod CoinFlip { use core::num::traits::zero::Zero; - use starknet::{ - ContractAddress, contract_address_const, get_caller_address, get_contract_address, - get_block_number - }; + use starknet::{ContractAddress, get_caller_address, get_contract_address,}; use pragma_lib::abi::{IRandomnessDispatcher, IRandomnessDispatcherTrait}; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; @@ -69,7 +67,7 @@ pub mod CoinFlip { pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; } - pub const PUBLISH_DELAY: u64 = 0; // return the random value asap + pub const PUBLISH_DELAY: u64 = 1; // return the random value asap pub const NUM_OF_WORDS: u64 = 1; // one random value is sufficient pub const CALLBACK_FEE_LIMIT: u128 = 100_000_000_000_000; // 0.0001 ETH pub const MAX_CALLBACK_FEE_DEPOSIT: u256 = diff --git a/listings/applications/coin_flip/src/mock_randomness.cairo b/listings/applications/coin_flip/src/mock_randomness.cairo index 79750aa3..f37e3f5e 100644 --- a/listings/applications/coin_flip/src/mock_randomness.cairo +++ b/listings/applications/coin_flip/src/mock_randomness.cairo @@ -4,8 +4,6 @@ pub mod MockRandomness { use pragma_lib::types::RequestStatus; use starknet::{ContractAddress, ClassHash, get_caller_address, get_contract_address}; use core::num::traits::zero::Zero; - use core::poseidon::PoseidonTrait; - use core::hash::{HashStateTrait, HashStateExTrait}; use coin_flip::contract::{IPragmaVRFDispatcher, IPragmaVRFDispatcherTrait}; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; diff --git a/listings/applications/coin_flip/src/tests.cairo b/listings/applications/coin_flip/src/tests.cairo index 64aaafc2..82b11234 100644 --- a/listings/applications/coin_flip/src/tests.cairo +++ b/listings/applications/coin_flip/src/tests.cairo @@ -1,14 +1,9 @@ -use coin_flip::contract::{ - CoinFlip, ICoinFlipDispatcher, ICoinFlipDispatcherTrait, IPragmaVRFDispatcher, - IPragmaVRFDispatcherTrait -}; +use coin_flip::contract::{CoinFlip, ICoinFlipDispatcher, ICoinFlipDispatcherTrait,}; use coin_flip::mock_randomness::MockRandomness; -use starknet::{ - ContractAddress, ClassHash, get_block_timestamp, contract_address_const, get_caller_address -}; +use starknet::{ContractAddress, contract_address_const}; use snforge_std::{ - declare, ContractClass, ContractClassTrait, start_cheat_caller_address, - stop_cheat_caller_address, spy_events, SpyOn, EventSpy, EventAssertions, get_class_hash + declare, start_cheat_caller_address, stop_cheat_caller_address, spy_events, EventAssertions, + SpyOn, ContractClassTrait }; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use pragma_lib::abi::{IRandomnessDispatcher, IRandomnessDispatcherTrait}; From 9f3f79064d9be46de89374c1752c1584f76468fc Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 1 Aug 2024 10:28:42 +0200 Subject: [PATCH 36/48] Remove starkli-wallet dir --- .gitignore | 1 + .../coin_flip/starkli-wallet/account.json | 14 -------------- .../coin_flip/starkli-wallet/keystore.json | 1 - 3 files changed, 1 insertion(+), 15 deletions(-) delete mode 100644 listings/applications/coin_flip/starkli-wallet/account.json delete mode 100644 listings/applications/coin_flip/starkli-wallet/keystore.json diff --git a/.gitignore b/.gitignore index 41187080..72306c0f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ output # Others .snfoundry_cache .vscode/settings.json +**/starkli-wallet diff --git a/listings/applications/coin_flip/starkli-wallet/account.json b/listings/applications/coin_flip/starkli-wallet/account.json deleted file mode 100644 index 32132ff9..00000000 --- a/listings/applications/coin_flip/starkli-wallet/account.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "version": 1, - "variant": { - "type": "open_zeppelin", - "version": 1, - "public_key": "0x50fa8885bddcceb6943bdf2ac8567ab1cf31401a0271963c8cb5a7fd66257db", - "legacy": false - }, - "deployment": { - "status": "deployed", - "class_hash": "0x1e60c8722677cfb7dd8dbea5be86c09265db02cdfe77113e77da7d44c017388", - "address": "0x3b43b843034677e6bc57235d390884758b414b7df8fd777c83ce1011f7aa3b3" - } -} diff --git a/listings/applications/coin_flip/starkli-wallet/keystore.json b/listings/applications/coin_flip/starkli-wallet/keystore.json deleted file mode 100644 index 11c65c11..00000000 --- a/listings/applications/coin_flip/starkli-wallet/keystore.json +++ /dev/null @@ -1 +0,0 @@ -{"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"94f17a7492e0e66a7c0baefda3a2dcaf"},"ciphertext":"dc2fb67bed40fa7deb3e30e9d6fb17b5d81da293621fe0d36791dcc2e2dd4edc","kdf":"scrypt","kdfparams":{"dklen":32,"n":8192,"p":1,"r":8,"salt":"a6550c0fc7c3457b9afc19652270bab741c194a3095d7941ada30617d9d22723"},"mac":"d7b9d49ae862737af301f1ac5fc1260af53d17ae7d63c6f2afb75f549c6f56f9"},"id":"fa0e5704-32c6-46fd-a368-362007c923f2","version":3} \ No newline at end of file From a2ed992ba45daf4e356aec89e9b7067ca520a9cc Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 1 Aug 2024 10:30:38 +0200 Subject: [PATCH 37/48] Generate 3 random words for the 1st test --- listings/applications/coin_flip/src/tests.cairo | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/listings/applications/coin_flip/src/tests.cairo b/listings/applications/coin_flip/src/tests.cairo index 82b11234..210ae5cf 100644 --- a/listings/applications/coin_flip/src/tests.cairo +++ b/listings/applications/coin_flip/src/tests.cairo @@ -43,7 +43,7 @@ fn deploy() -> (ICoinFlipDispatcher, IRandomnessDispatcher, IERC20Dispatcher, Co #[test] #[fuzzer(runs: 10, seed: 22)] -fn test_two_flips(random_word: felt252) { +fn test_two_flips(random_word_1: felt252, random_word_2: felt252, random_word_3: felt252) { let (coin_flip, randomness, eth, deployer) = deploy(); // fund the CoinFlip contract @@ -52,13 +52,13 @@ fn test_two_flips(random_word: felt252) { stop_cheat_caller_address(eth.contract_address); _flip_request( - coin_flip, randomness, eth, deployer, 0, CoinFlip::CALLBACK_FEE_LIMIT / 5 * 3, random_word + coin_flip, randomness, eth, deployer, 0, CoinFlip::CALLBACK_FEE_LIMIT / 5 * 3, random_word_1 ); _flip_request( - coin_flip, randomness, eth, deployer, 1, CoinFlip::CALLBACK_FEE_LIMIT / 4 * 3, random_word + coin_flip, randomness, eth, deployer, 1, CoinFlip::CALLBACK_FEE_LIMIT / 4 * 3, random_word_2 ); _flip_request( - coin_flip, randomness, eth, deployer, 2, CoinFlip::CALLBACK_FEE_LIMIT, random_word + coin_flip, randomness, eth, deployer, 2, CoinFlip::CALLBACK_FEE_LIMIT, random_word_3 ); } From 73a97a91a8b8b236de9f24ba263cb413c75a0c06 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 1 Aug 2024 12:33:31 +0200 Subject: [PATCH 38/48] refactor tests --- .../applications/coin_flip/src/contract.cairo | 2 +- .../applications/coin_flip/src/tests.cairo | 195 +++++++++++------- 2 files changed, 120 insertions(+), 77 deletions(-) diff --git a/listings/applications/coin_flip/src/contract.cairo b/listings/applications/coin_flip/src/contract.cairo index 3955505c..256f4f34 100644 --- a/listings/applications/coin_flip/src/contract.cairo +++ b/listings/applications/coin_flip/src/contract.cairo @@ -52,7 +52,7 @@ pub mod CoinFlip { pub side: Side } - #[derive(Drop, Serde)] + #[derive(Drop, Debug, PartialEq, Serde)] pub enum Side { Heads, Tails, diff --git a/listings/applications/coin_flip/src/tests.cairo b/listings/applications/coin_flip/src/tests.cairo index 210ae5cf..8d78aa96 100644 --- a/listings/applications/coin_flip/src/tests.cairo +++ b/listings/applications/coin_flip/src/tests.cairo @@ -1,19 +1,31 @@ use coin_flip::contract::{CoinFlip, ICoinFlipDispatcher, ICoinFlipDispatcherTrait,}; +use coin_flip::contract::CoinFlip::{Side, CALLBACK_FEE_LIMIT}; use coin_flip::mock_randomness::MockRandomness; use starknet::{ContractAddress, contract_address_const}; use snforge_std::{ declare, start_cheat_caller_address, stop_cheat_caller_address, spy_events, EventAssertions, - SpyOn, ContractClassTrait + SpyOn, ContractClassTrait, EventSpy, Event, EventFetcher }; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use pragma_lib::abi::{IRandomnessDispatcher, IRandomnessDispatcherTrait}; +impl FeltIntoSide of core::traits::Into { + fn into(self: felt252) -> Side { + match self { + 0 => Side::Heads, + 1 => Side::Tails, + 2 => Side::Sideways, + _ => panic!("non-existent side, felt value: {self}") + } + } +} + fn deploy() -> (ICoinFlipDispatcher, IRandomnessDispatcher, IERC20Dispatcher, ContractAddress) { // deploy mock ETH token let eth_contract = declare("ERC20Upgradeable").unwrap(); let eth_name: ByteArray = "Ethereum"; let eth_symbol: ByteArray = "ETH"; - let eth_supply: u256 = CoinFlip::CALLBACK_FEE_LIMIT.into() * 100; + let eth_supply: u256 = CALLBACK_FEE_LIMIT.into() * 100; let mut eth_ctor_calldata = array![]; let deployer = contract_address_const::<'deployer'>(); ((eth_name, eth_symbol, eth_supply, deployer), deployer).serialize(ref eth_ctor_calldata); @@ -42,24 +54,73 @@ fn deploy() -> (ICoinFlipDispatcher, IRandomnessDispatcher, IERC20Dispatcher, Co } #[test] -#[fuzzer(runs: 10, seed: 22)] -fn test_two_flips(random_word_1: felt252, random_word_2: felt252, random_word_3: felt252) { +fn test_all_relevant_random_words() { let (coin_flip, randomness, eth, deployer) = deploy(); // fund the CoinFlip contract start_cheat_caller_address(eth.contract_address, deployer); - eth.transfer(coin_flip.contract_address, CoinFlip::CALLBACK_FEE_LIMIT.into() * 50); + eth.transfer(coin_flip.contract_address, CALLBACK_FEE_LIMIT.into() * 100); + stop_cheat_caller_address(eth.contract_address); + + let mut random_words: Array<(felt252, Side, u64)> = array![ + (0, Side::Heads, 0), + (1, Side::Heads, 1), + (2406, Side::Heads, 2), + (5998, Side::Heads, 3), + (12000, Side::Heads, 4), + (15000, Side::Heads, 5), + (123456789, Side::Heads, 6), + (6001, Side::Tails, 7), + (6002, Side::Tails, 8), + (9999, Side::Tails, 9), + (11999, Side::Tails, 10), + (18001, Side::Tails, 11), + (12345654321, Side::Tails, 12), + (5999, Side::Sideways, 13), + (6000, Side::Sideways, 14), + (17999, Side::Sideways, 15), + (18000, Side::Sideways, 16), + (533999, Side::Sideways, 17), + (534000, Side::Sideways, 18) + ]; + while let Option::Some((random_word, expected_side, expected_request_id)) = random_words + .pop_front() { + _flip_request( + coin_flip, + randomness, + eth, + deployer, + expected_request_id, + CALLBACK_FEE_LIMIT / 5 * 3, + random_word, + expected_side + ); + } +} + +#[test] +fn test_multiple_flips() { + let (coin_flip, randomness, eth, deployer) = deploy(); + + // fund the CoinFlip contract + start_cheat_caller_address(eth.contract_address, deployer); + eth.transfer(coin_flip.contract_address, CALLBACK_FEE_LIMIT.into() * 50); stop_cheat_caller_address(eth.contract_address); _flip_request( - coin_flip, randomness, eth, deployer, 0, CoinFlip::CALLBACK_FEE_LIMIT / 5 * 3, random_word_1 - ); - _flip_request( - coin_flip, randomness, eth, deployer, 1, CoinFlip::CALLBACK_FEE_LIMIT / 4 * 3, random_word_2 + coin_flip, randomness, eth, deployer, 0, CALLBACK_FEE_LIMIT / 5 * 3, 123456789, Side::Heads ); _flip_request( - coin_flip, randomness, eth, deployer, 2, CoinFlip::CALLBACK_FEE_LIMIT, random_word_3 + coin_flip, + randomness, + eth, + deployer, + 1, + CALLBACK_FEE_LIMIT / 4 * 3, + 12345654321, + Side::Tails ); + _flip_request(coin_flip, randomness, eth, deployer, 2, CALLBACK_FEE_LIMIT, 3, Side::Heads); } fn _flip_request( @@ -69,27 +130,35 @@ fn _flip_request( deployer: ContractAddress, expected_request_id: u64, expected_callback_fee: u128, - random_word: felt252 + random_word: felt252, + expected_side: Side ) { - let mut spy = spy_events(SpyOn::One(coin_flip.contract_address)); + let test_data = format!( + "\n---\nTest data:\nexpected_request_id: {:?},\nexpected_callback_fee: {:?},\nrandom_word: {:?},\nexpected_side: {:?}\n---\n", + expected_request_id, + expected_callback_fee, + random_word, + expected_side, + ); let original_balance = eth.balance_of(coin_flip.contract_address); + let mut spy = spy_events(SpyOn::One(coin_flip.contract_address)); + start_cheat_caller_address(coin_flip.contract_address, deployer); coin_flip.flip(); stop_cheat_caller_address(coin_flip.contract_address); - spy - .assert_emitted( - @array![ - ( - coin_flip.contract_address, - CoinFlip::Event::Flipped( - CoinFlip::Flipped { flip_id: expected_request_id, flipper: deployer } - ) - ) - ] - ); + // manually asserting event data, as it results in clearer error messages + spy.fetch_events(); + + assert_eq!(spy.events.len(), 1, "{test_data}On flip.\n"); + let (_, event) = spy.events.at(0); + assert_eq!(event.keys.len(), 1, "{test_data}Event should have one key.\n"); + assert_eq!(event.keys.at(0), @selector!("Flipped"), "{test_data}Expected 'Flipped' to emit.\n"); + assert_eq!(event.data.len(), 2, "{test_data}Flipped event should contain 2 fields.\n"); + assert_eq!(event.data.at(0), @expected_request_id.into(), "{test_data}",); + assert_eq!(event.data.at(1), @deployer.into(), "{test_data}",); let post_flip_balance = eth.balance_of(coin_flip.contract_address); assert_eq!( @@ -98,6 +167,9 @@ fn _flip_request( - randomness.get_total_fees(coin_flip.contract_address, expected_request_id) ); + // reset the event spy + let mut spy = spy_events(SpyOn::One(coin_flip.contract_address)); + randomness .submit_random( expected_request_id, @@ -105,39 +177,28 @@ fn _flip_request( 0, 0, coin_flip.contract_address, - CoinFlip::CALLBACK_FEE_LIMIT, + CALLBACK_FEE_LIMIT, expected_callback_fee, array![random_word].span(), array![].span(), array![] ); - let random_value: u256 = random_word.into() % 12000; - let expected_side = if random_value < 5999 { - CoinFlip::Side::Heads - } else if random_value > 6000 { - CoinFlip::Side::Tails - } else { - CoinFlip::Side::Sideways - }; + spy.fetch_events(); - spy - .assert_emitted( - @array![ - ( - coin_flip.contract_address, - CoinFlip::Event::Landed( - CoinFlip::Landed { - flip_id: expected_request_id, flipper: deployer, side: expected_side - } - ) - ) - ] - ); + assert_eq!(spy.events.len(), 1, "{test_data}On receiving the random word.\n"); + let (_, event) = spy.events.at(0); + assert_eq!(event.keys.len(), 1, "{test_data}Landed event should have one key.\n"); + assert_eq!(event.keys.at(0), @selector!("Landed"), "{test_data}Expected 'Landed' to emit.\n"); + assert_eq!(event.data.len(), 3, "{test_data}Landed event should contain 3 fields.\n"); + assert_eq!(event.data.at(0), @expected_request_id.into(), "{test_data}",); + assert_eq!(event.data.at(1), @deployer.into(), "{test_data}",); + let actual_side: Side = (*event.data.at(2)).into(); + assert_eq!(actual_side, expected_side, "{test_data}"); assert_eq!( eth.balance_of(coin_flip.contract_address), - post_flip_balance + (CoinFlip::CALLBACK_FEE_LIMIT - expected_callback_fee).into() + post_flip_balance + (CALLBACK_FEE_LIMIT - expected_callback_fee).into() ); } @@ -147,7 +208,7 @@ fn test_two_consecutive_flips() { // fund the CoinFlip contract start_cheat_caller_address(eth.contract_address, deployer); - eth.transfer(coin_flip.contract_address, CoinFlip::CALLBACK_FEE_LIMIT.into() * 50); + eth.transfer(coin_flip.contract_address, CALLBACK_FEE_LIMIT.into() * 50); stop_cheat_caller_address(eth.contract_address); let mut spy = spy_events(SpyOn::One(coin_flip.contract_address)); @@ -179,16 +240,15 @@ fn test_two_consecutive_flips() { ); let post_flip_balance = eth.balance_of(coin_flip.contract_address); - assert_eq!( - post_flip_balance, - original_balance - - randomness.get_total_fees(coin_flip.contract_address, 0) - - randomness.get_total_fees(coin_flip.contract_address, 1) - ); + let first_flip_fee = randomness.get_total_fees(coin_flip.contract_address, 0); + let second_flip_fee = randomness.get_total_fees(coin_flip.contract_address, 1); + assert_eq!(post_flip_balance, original_balance - first_flip_fee - second_flip_fee); - let expected_callback_fee = CoinFlip::CALLBACK_FEE_LIMIT / 5 * 3; - let random_word_deployer = 'this is some random word value'; - let random_word_other_flipper = 'this is another random word'; + let expected_callback_fee = CALLBACK_FEE_LIMIT / 5 * 3; + let random_word_deployer = 5633; + let expected_side_deployer = Side::Heads; + let random_word_other_flipper = 8000; + let expected_side_other_flipper = Side::Tails; randomness .submit_random( @@ -197,7 +257,7 @@ fn test_two_consecutive_flips() { 0, 0, coin_flip.contract_address, - CoinFlip::CALLBACK_FEE_LIMIT, + CALLBACK_FEE_LIMIT, expected_callback_fee, array![random_word_deployer].span(), array![].span(), @@ -210,30 +270,13 @@ fn test_two_consecutive_flips() { 0, 0, coin_flip.contract_address, - CoinFlip::CALLBACK_FEE_LIMIT, + CALLBACK_FEE_LIMIT, expected_callback_fee, array![random_word_other_flipper].span(), array![].span(), array![] ); - let random_value: u256 = random_word_deployer.into() % 12000; - let expected_side_deployer = if random_value < 5999 { - CoinFlip::Side::Heads - } else if random_value > 6000 { - CoinFlip::Side::Tails - } else { - CoinFlip::Side::Sideways - }; - let random_value: u256 = random_word_other_flipper.into() % 12000; - let expected_side_other_flipper = if random_value < 5999 { - CoinFlip::Side::Heads - } else if random_value > 6000 { - CoinFlip::Side::Tails - } else { - CoinFlip::Side::Sideways - }; - spy .assert_emitted( @array![ @@ -258,7 +301,7 @@ fn test_two_consecutive_flips() { assert_eq!( eth.balance_of(coin_flip.contract_address), - post_flip_balance + (CoinFlip::CALLBACK_FEE_LIMIT - expected_callback_fee).into() * 2 + post_flip_balance + (CALLBACK_FEE_LIMIT - expected_callback_fee).into() * 2 ); } From 49b42d387b1a4e11e6f58cd8c17f465c93570f8d Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 1 Aug 2024 12:36:09 +0200 Subject: [PATCH 39/48] Add missng newline to scarb.toml --- listings/applications/coin_flip/Scarb.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/listings/applications/coin_flip/Scarb.toml b/listings/applications/coin_flip/Scarb.toml index 82f0da9b..22217d18 100644 --- a/listings/applications/coin_flip/Scarb.toml +++ b/listings/applications/coin_flip/Scarb.toml @@ -14,4 +14,4 @@ test.workspace = true [[target.starknet-contract]] casm = true -build-external-contracts = ["openzeppelin::presets::erc20::ERC20Upgradeable"] \ No newline at end of file +build-external-contracts = ["openzeppelin::presets::erc20::ERC20Upgradeable"] From 6022c026cc2b6778e74f35f5f77f8498348bfca3 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 9 Aug 2024 12:53:15 +0200 Subject: [PATCH 40/48] fix typo in md --- src/applications/random_number_generator.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/random_number_generator.md b/src/applications/random_number_generator.md index fb977f55..8be80881 100644 --- a/src/applications/random_number_generator.md +++ b/src/applications/random_number_generator.md @@ -9,7 +9,7 @@ In blockchain and smart contracts, randomness is needed for: - **Security:** Generating cryptographic keys and nonces that are hard to predict. - **Consensus Protocols:** Selecting validators or block producers in some proof-of-stake systems. -However, achieving true randomness on a decentralized platform poses significant challenges. There are numerous sources of entropy, each with its stregths and weaknesses. +However, achieving true randomness on a decentralized platform poses significant challenges. There are numerous sources of entropy, each with its strengths and weaknesses. ### Sources of Entropy From d1174bdab0ae88bae7f410d52268215271e75844 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 9 Aug 2024 12:55:53 +0200 Subject: [PATCH 41/48] reword 'manipulation' def --- src/applications/random_number_generator.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/random_number_generator.md b/src/applications/random_number_generator.md index 8be80881..2e151070 100644 --- a/src/applications/random_number_generator.md +++ b/src/applications/random_number_generator.md @@ -19,7 +19,7 @@ However, achieving true randomness on a decentralized platform poses significant - **Example:** A common approach is to use the hash of a recent block as a seed for random number generation. - **Risks:** - **Predictability:** Miners can influence future block hashes by controlling the nonce they use during mining. - - **Manipulation:** Miners or validators with significant influence can manipulate the block hash to their advantage, especially if they stand to gain from a specific random outcome. + - **Manipulation:** Many of the blockchain properties (block hash, timestamp etc.) can be manipulated by some entities, especially if they stand to gain from a specific random outcome. #### 2. User-Provided Inputs From fb33c55138a43b164f0c2d510a9154d7efc55577 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 9 Aug 2024 12:56:35 +0200 Subject: [PATCH 42/48] Chainlink->Pragma --- src/applications/random_number_generator.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/random_number_generator.md b/src/applications/random_number_generator.md index 2e151070..b0655925 100644 --- a/src/applications/random_number_generator.md +++ b/src/applications/random_number_generator.md @@ -32,7 +32,7 @@ However, achieving true randomness on a decentralized platform poses significant #### 3. External Oracles - **Description:** Using a trusted third-party service to supply randomness. Oracles are off-chain services that provide data to smart contracts. -- **Example:** Chainlink VRF (Verifiable Random Function) is a service that provides cryptographically secure randomness. +- **Example:** Pragma VRF (Verifiable Random Function) is a service that provides cryptographically secure randomness. - **Risks:** - **Trust:** Reliance on a third party undermines the trustless nature of blockchain. - **Centralization:** If the oracle service is compromised, so is the randomness it provides. From fcce3d35415a518582b21d06754db5f242ed4fe5 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 9 Aug 2024 12:57:42 +0200 Subject: [PATCH 43/48] link to Commit-reveal chapter issue --- src/applications/random_number_generator.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/applications/random_number_generator.md b/src/applications/random_number_generator.md index b0655925..822bfe1e 100644 --- a/src/applications/random_number_generator.md +++ b/src/applications/random_number_generator.md @@ -45,6 +45,7 @@ However, achieving true randomness on a decentralized platform poses significant - **Risks:** - **Dishonest Behavior:** Participants may choose not to reveal their values if the outcome is unfavorable. - **Coordination:** Requires honest participation from multiple parties, which can be hard to guarantee. + #### 5. On-Chain Randomness Beacons From 9a0ed91cfb6b3b4a8a07940ed36103641b5c42b5 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 9 Aug 2024 12:58:20 +0200 Subject: [PATCH 44/48] list 'shut down' as possible centr. issue with ext. oracles --- src/applications/random_number_generator.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/random_number_generator.md b/src/applications/random_number_generator.md index 822bfe1e..1baa66c5 100644 --- a/src/applications/random_number_generator.md +++ b/src/applications/random_number_generator.md @@ -35,7 +35,7 @@ However, achieving true randomness on a decentralized platform poses significant - **Example:** Pragma VRF (Verifiable Random Function) is a service that provides cryptographically secure randomness. - **Risks:** - **Trust:** Reliance on a third party undermines the trustless nature of blockchain. - - **Centralization:** If the oracle service is compromised, so is the randomness it provides. + - **Centralization:** If the oracle service is compromised or shut down, so is the randomness it provides. - **Cost:** Using an oracle often involves additional transaction fees. #### 4. Commit-Reveal Schemes From a3b6e3bcf2202270ff228900a291e66e970d1bf8 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 9 Aug 2024 13:03:28 +0200 Subject: [PATCH 45/48] Turn point 5 into a note --- src/applications/random_number_generator.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/applications/random_number_generator.md b/src/applications/random_number_generator.md index 1baa66c5..5e582805 100644 --- a/src/applications/random_number_generator.md +++ b/src/applications/random_number_generator.md @@ -47,13 +47,7 @@ However, achieving true randomness on a decentralized platform poses significant - **Coordination:** Requires honest participation from multiple parties, which can be hard to guarantee. -#### 5. On-Chain Randomness Beacons - -- **Description:** Dedicated smart contracts or mechanisms designed to generate and provide randomness on-chain. -- **Example:** Randomness beacons like RANDAO, which aggregate randomness from multiple participants. -- **Risks:** - - **Collusion:** If participants collude, they can influence the randomness outcome. - - **Disruption:** If participants do not follow through (e.g., not revealing their values), the beacon may fail to produce a valid random number. +> There are other ways to generate randomness on-chain, for more information read the ["Public Randomness and Randomness Beacons"](https://a16zcrypto.com/posts/article/public-randomness-and-randomness-beacons/) article. ## CoinFlip using Pragma VRF From b75676d28852b0f483af25eff80c9d1bb2cce0ea Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 9 Aug 2024 13:18:52 +0200 Subject: [PATCH 46/48] Remove Sideways enum --- .../applications/coin_flip/src/contract.cairo | 9 ++--- .../applications/coin_flip/src/tests.cairo | 36 +++++++------------ 2 files changed, 16 insertions(+), 29 deletions(-) diff --git a/listings/applications/coin_flip/src/contract.cairo b/listings/applications/coin_flip/src/contract.cairo index 256f4f34..22ae8f9f 100644 --- a/listings/applications/coin_flip/src/contract.cairo +++ b/listings/applications/coin_flip/src/contract.cairo @@ -56,7 +56,6 @@ pub mod CoinFlip { pub enum Side { Heads, Tails, - Sideways } pub mod Errors { @@ -157,13 +156,11 @@ pub mod CoinFlip { let flipper = self.flips.read(flip_id); assert(flipper.is_non_zero(), Errors::INVALID_FLIP_ID); - let random_value: u256 = (*random_value).into() % 12000; - let side = if random_value < 5999 { + let random_value: u256 = (*random_value).into(); + let side = if random_value % 2 == 0 { Side::Heads - } else if random_value > 6000 { - Side::Tails } else { - Side::Sideways + Side::Tails }; self.emit(Event::Landed(Landed { flip_id, flipper, side })); diff --git a/listings/applications/coin_flip/src/tests.cairo b/listings/applications/coin_flip/src/tests.cairo index 8d78aa96..3247ca25 100644 --- a/listings/applications/coin_flip/src/tests.cairo +++ b/listings/applications/coin_flip/src/tests.cairo @@ -14,7 +14,6 @@ impl FeltIntoSide of core::traits::Into { match self { 0 => Side::Heads, 1 => Side::Tails, - 2 => Side::Sideways, _ => panic!("non-existent side, felt value: {self}") } } @@ -64,24 +63,15 @@ fn test_all_relevant_random_words() { let mut random_words: Array<(felt252, Side, u64)> = array![ (0, Side::Heads, 0), - (1, Side::Heads, 1), - (2406, Side::Heads, 2), - (5998, Side::Heads, 3), - (12000, Side::Heads, 4), - (15000, Side::Heads, 5), - (123456789, Side::Heads, 6), - (6001, Side::Tails, 7), - (6002, Side::Tails, 8), - (9999, Side::Tails, 9), - (11999, Side::Tails, 10), - (18001, Side::Tails, 11), - (12345654321, Side::Tails, 12), - (5999, Side::Sideways, 13), - (6000, Side::Sideways, 14), - (17999, Side::Sideways, 15), - (18000, Side::Sideways, 16), - (533999, Side::Sideways, 17), - (534000, Side::Sideways, 18) + (2, Side::Heads, 1), + (4, Side::Heads, 2), + (1000, Side::Heads, 3), + (12345654320, Side::Heads, 4), + (1, Side::Tails, 5), + (3, Side::Tails, 6), + (5, Side::Tails, 7), + (1001, Side::Tails, 8), + (12345654321, Side::Tails, 9), ]; while let Option::Some((random_word, expected_side, expected_request_id)) = random_words .pop_front() { @@ -108,7 +98,7 @@ fn test_multiple_flips() { stop_cheat_caller_address(eth.contract_address); _flip_request( - coin_flip, randomness, eth, deployer, 0, CALLBACK_FEE_LIMIT / 5 * 3, 123456789, Side::Heads + coin_flip, randomness, eth, deployer, 0, CALLBACK_FEE_LIMIT / 5 * 3, 123456789, Side::Tails ); _flip_request( coin_flip, @@ -120,7 +110,7 @@ fn test_multiple_flips() { 12345654321, Side::Tails ); - _flip_request(coin_flip, randomness, eth, deployer, 2, CALLBACK_FEE_LIMIT, 3, Side::Heads); + _flip_request(coin_flip, randomness, eth, deployer, 2, CALLBACK_FEE_LIMIT, 3, Side::Tails); } fn _flip_request( @@ -246,9 +236,9 @@ fn test_two_consecutive_flips() { let expected_callback_fee = CALLBACK_FEE_LIMIT / 5 * 3; let random_word_deployer = 5633; - let expected_side_deployer = Side::Heads; + let expected_side_deployer = Side::Tails; let random_word_other_flipper = 8000; - let expected_side_other_flipper = Side::Tails; + let expected_side_other_flipper = Side::Heads; randomness .submit_random( From 5c68e776b9e9736e2251e5e2fccde50e098fd2be Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 9 Aug 2024 13:33:28 +0200 Subject: [PATCH 47/48] add contract description --- src/applications/random_number_generator.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/applications/random_number_generator.md b/src/applications/random_number_generator.md index 5e582805..8b7c5990 100644 --- a/src/applications/random_number_generator.md +++ b/src/applications/random_number_generator.md @@ -53,6 +53,12 @@ However, achieving true randomness on a decentralized platform poses significant Below is an implementation of a `CoinFlip` contract that utilizes a [Pragma Verifiable Random Function (VRF)](https://docs.pragma.build/Resources/Cairo%201/randomness/randomness) to generate random numbers on-chain. +- Players can flip a virtual coin and receive a random outcome of `Heads` or `Tails` +- The contract needs to be funded with enough ETH to perform the necessary operations, including paying fees to Pragma's Randomness Oracle which returns a random value +- When the coin is "flipped", the contract makes a call to the Randomness Oracle to request a random value and the `Flipped` event is emitted +- Randomness is generated off-chain, and then submitted to the contract using the `receive_random_words` callback +- Based on this random value, the contract determines whether the coin "landed" on `Heads` or on `Tails`, and the `Landed` event is emitted + ```rust {{#include ../../listings/applications/coin_flip/src/contract.cairo}} ``` From 3f99abf80b821d8b59f6a5926266ac92a35de7ac Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 27 Sep 2024 16:31:46 +0200 Subject: [PATCH 48/48] Remove ResultTrait from crowdfunding tests.cairo --- listings/applications/crowdfunding/src/tests.cairo | 1 - 1 file changed, 1 deletion(-) diff --git a/listings/applications/crowdfunding/src/tests.cairo b/listings/applications/crowdfunding/src/tests.cairo index ba01cf83..11f2aaa5 100644 --- a/listings/applications/crowdfunding/src/tests.cairo +++ b/listings/applications/crowdfunding/src/tests.cairo @@ -1,4 +1,3 @@ -use core::result::ResultTrait; use starknet::{ContractAddress, get_block_timestamp, contract_address_const,}; use snforge_std::{ declare, ContractClass, ContractClassTrait, start_cheat_caller_address,