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/Scarb.lock b/Scarb.lock index ee2add0e..2aaddaf7 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -22,6 +22,15 @@ version = "0.1.0" name = "calling_other_contracts" version = "0.1.0" +[[package]] +name = "coin_flip" +version = "0.1.0" +dependencies = [ + "openzeppelin", + "pragma_lib", + "snforge_std", +] + [[package]] name = "components" version = "0.1.0" @@ -206,6 +215,11 @@ version = "0.16.0" source = "registry+https://scarbs.xyz/" checksum = "sha256:a494aeb5f1371db7f22e922196aa41d1d1698877a766a838350c0b6ffe49fda2" +[[package]] +name = "pragma_lib" +version = "1.0.0" +source = "git+https://github.com/astraly-labs/pragma-lib#86d7ccdc15b349b8b48d9796fc8464c947bea6e1" + [[package]] name = "simple_account" version = "0.1.0" diff --git a/Scarb.toml b/Scarb.toml index f8dd7cba..7482a04e 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -18,6 +18,7 @@ assert_macros = "2.8.2" snforge_std = "0.30.0" openzeppelin = "0.16.0" components = { path = "listings/applications/components" } +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/advanced_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo index 759be3d0..41588040 100644 --- a/listings/applications/advanced_factory/src/tests.cairo +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -1,5 +1,3 @@ -use core::clone::Clone; -use core::result::ResultTrait; use advanced_factory::contract::{ CampaignFactory, ICampaignFactoryDispatcher, ICampaignFactoryDispatcherTrait }; diff --git a/listings/applications/coin_flip/.gitignore b/listings/applications/coin_flip/.gitignore new file mode 100644 index 00000000..73aa31e6 --- /dev/null +++ b/listings/applications/coin_flip/.gitignore @@ -0,0 +1,2 @@ +target +.snfoundry_cache/ diff --git a/listings/applications/coin_flip/Scarb.toml b/listings/applications/coin_flip/Scarb.toml new file mode 100644 index 00000000..c1ae15f7 --- /dev/null +++ b/listings/applications/coin_flip/Scarb.toml @@ -0,0 +1,21 @@ +[package] +name = "coin_flip" +version.workspace = true +edition = "2024_07" + +[lib] + +[dependencies] +starknet.workspace = true +openzeppelin.workspace = true +pragma_lib.workspace = true +snforge_std.workspace = true + +[dev-dependencies] +assert_macros.workspace = true + +[scripts] +test.workspace = true + +[[target.starknet-contract]] +build-external-contracts = ["openzeppelin_presets::erc20::ERC20Upgradeable"] diff --git a/listings/applications/coin_flip/src/contract.cairo b/listings/applications/coin_flip/src/contract.cairo new file mode 100644 index 00000000..cefe052e --- /dev/null +++ b/listings/applications/coin_flip/src/contract.cairo @@ -0,0 +1,164 @@ +use starknet::ContractAddress; + +#[starknet::interface] +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( + ref self: TContractState, + requestor_address: ContractAddress, + request_id: u64, + random_words: Span, + calldata: Array + ); +} + +#[starknet::contract] +pub mod CoinFlip { + use core::num::traits::zero::Zero; + use starknet::{ContractAddress, get_caller_address, get_contract_address,}; + use starknet::storage::{ + Map, StoragePointerReadAccess, StoragePathEntry, StoragePointerWriteAccess + }; + use pragma_lib::abi::{IRandomnessDispatcher, IRandomnessDispatcherTrait}; + use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + + #[storage] + struct Storage { + eth_dispatcher: IERC20Dispatcher, + flips: Map, + nonce: u64, + randomness_contract_address: ContractAddress, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + Flipped: Flipped, + Landed: Landed, + } + + #[derive(Drop, starknet::Event)] + pub struct Flipped { + pub flip_id: u64, + pub flipper: ContractAddress, + } + + #[derive(Drop, starknet::Event)] + pub struct Landed { + pub flip_id: u64, + pub flipper: ContractAddress, + pub side: Side + } + + #[derive(Drop, Debug, PartialEq, Serde)] + pub enum Side { + Heads, + Tails, + } + + 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'; + pub const REQUESTOR_NOT_SELF: felt252 = 'Requestor is not self'; + pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; + } + + 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 = + 500_000_000_000_000; // CALLBACK_FEE_LIMIT * 5; needs to cover the Premium fee + + #[constructor] + 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: eth_address }); + } + + #[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 flip_id = self._request_my_randomness(); + let flipper = get_caller_address(); + self.flips.entry(flip_id).write(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 + }; + + let this = get_contract_address(); + + // Approve the randomness contract to transfer the callback deposit/fee + let eth_dispatcher = self.eth_dispatcher.read(); + eth_dispatcher.approve(randomness_contract_address, MAX_CALLBACK_FEE_DEPOSIT); + + 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![] + ); + + self.nonce.write(nonce + 1); + + request_id + } + + fn _process_coin_flip(ref self: ContractState, flip_id: u64, random_value: @felt252) { + let flipper = self.flips.entry(flip_id).read(); + assert(flipper.is_non_zero(), Errors::INVALID_FLIP_ID); + + let random_value: u256 = (*random_value).into(); + let side = if random_value % 2 == 0 { + Side::Heads + } else { + Side::Tails + }; + + 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..a4238d37 --- /dev/null +++ b/listings/applications/coin_flip/src/lib.cairo @@ -0,0 +1,5 @@ +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..54f2f327 --- /dev/null +++ b/listings/applications/coin_flip/src/mock_randomness.cairo @@ -0,0 +1,166 @@ +#[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 starknet::storage::{ + Map, StoragePointerReadAccess, StoragePathEntry, StoragePointerWriteAccess + }; + use core::num::traits::zero::Zero; + use coin_flip::contract::{IPragmaVRFDispatcher, IPragmaVRFDispatcherTrait}; + use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + + #[storage] + struct Storage { + eth_dispatcher: IERC20Dispatcher, + next_request_id: u64, + total_fees: Map<(ContractAddress, u64), u256>, + } + + #[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 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); + + let request_id = self.next_request_id.read(); + self.next_request_id.write(request_id + 1); + + self.total_fees.entry((caller, request_id)).write(total_fee); + + 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 }; + requestor.receive_random_words(requestor_address, request_id, random_words, calldata); + 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( + self: @ContractState, caller_address: ContractAddress, request_id: u64 + ) -> u256 { + self.total_fees.entry((caller_address, request_id)).read() + } + + + fn compute_premium_fee(self: @ContractState, caller_address: ContractAddress) -> u128 { + panic!("unimplemented 'compute_premium_fee'") + } + 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_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 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 new file mode 100644 index 00000000..bdaa0466 --- /dev/null +++ b/listings/applications/coin_flip/src/tests.cairo @@ -0,0 +1,287 @@ +use coin_flip::contract::{ + CoinFlip, CoinFlip::{Side, CALLBACK_FEE_LIMIT}, ICoinFlipDispatcher, ICoinFlipDispatcherTrait, +}; +use starknet::{ContractAddress, contract_address_const}; +use snforge_std::{ + declare, start_cheat_caller_address, stop_cheat_caller_address, spy_events, + EventSpyAssertionsTrait, DeclareResultTrait, ContractClassTrait +}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use pragma_lib::abi::{IRandomnessDispatcher, IRandomnessDispatcherTrait}; + +fn deploy() -> (ICoinFlipDispatcher, IRandomnessDispatcher, IERC20Dispatcher, ContractAddress) { + // deploy mock ETH token + let eth_contract = declare("ERC20Upgradeable").unwrap().contract_class(); + let eth_name: ByteArray = "Ethereum"; + let eth_symbol: ByteArray = "ETH"; + 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); + 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().contract_class(); + 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().contract_class(); + 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(); + + 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 }; + + (coin_flip_dispatcher, randomness_dispatcher, eth_dispatcher, deployer) +} + +#[test] +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, CALLBACK_FEE_LIMIT.into() * 100); + stop_cheat_caller_address(eth.contract_address); + + let random_words: Array<(felt252, Side, u64)> = array![ + (0, Side::Heads, 0), + (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), + ]; + for ( + random_word, expected_side, expected_request_id + ) in random_words { + _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, CALLBACK_FEE_LIMIT / 5 * 3, 123456789, Side::Tails + ); + _flip_request( + 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::Tails); +} + +fn _flip_request( + coin_flip: ICoinFlipDispatcher, + randomness: IRandomnessDispatcher, + eth: IERC20Dispatcher, + deployer: ContractAddress, + expected_request_id: u64, + expected_callback_fee: u128, + random_word: felt252, + expected_side: Side +) { + let original_balance = eth.balance_of(coin_flip.contract_address); + + let mut spy = spy_events(); + + 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 } + ) + ) + ] + ); + + 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( + expected_request_id, + coin_flip.contract_address, + 0, + 0, + coin_flip.contract_address, + CALLBACK_FEE_LIMIT, + expected_callback_fee, + array![random_word].span(), + array![].span(), + array![] + ); + + 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!( + eth.balance_of(coin_flip.contract_address), + post_flip_balance + (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, CALLBACK_FEE_LIMIT.into() * 50); + stop_cheat_caller_address(eth.contract_address); + + let mut spy = spy_events(); + + let original_balance = eth.balance_of(coin_flip.contract_address); + + 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); + + spy + .assert_emitted( + @array![ + ( + coin_flip.contract_address, + CoinFlip::Event::Flipped(CoinFlip::Flipped { flip_id: 0, flipper: deployer }) + ), + ( + coin_flip.contract_address, + CoinFlip::Event::Flipped( + CoinFlip::Flipped { flip_id: 1, flipper: other_flipper } + ) + ) + ] + ); + + let post_flip_balance = eth.balance_of(coin_flip.contract_address); + 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 = CALLBACK_FEE_LIMIT / 5 * 3; + let random_word_deployer = 5633; + let expected_side_deployer = Side::Tails; + let random_word_other_flipper = 8000; + let expected_side_other_flipper = Side::Heads; + + randomness + .submit_random( + 0, + coin_flip.contract_address, + 0, + 0, + coin_flip.contract_address, + 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, + CALLBACK_FEE_LIMIT, + expected_callback_fee, + array![random_word_other_flipper].span(), + array![].span(), + array![] + ); + + spy + .assert_emitted( + @array![ + ( + coin_flip.contract_address, + CoinFlip::Event::Landed( + CoinFlip::Landed { + 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 + } + ) + ) + ] + ); + + assert_eq!( + eth.balance_of(coin_flip.contract_address), + post_flip_balance + (CALLBACK_FEE_LIMIT - expected_callback_fee).into() * 2 + ); +} + +#[test] +#[should_panic(expected: 'ERC20: insufficient balance')] +fn test_flip_without_enough_for_fees() { + let (coin_flip, _, _, deployer) = deploy(); + start_cheat_caller_address(coin_flip.contract_address, deployer); + coin_flip.flip(); +} 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, diff --git a/src/SUMMARY.md b/src/SUMMARY.md index a74bd1cd..54dd0313 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -64,6 +64,7 @@ Summary - [Simple Storage with Starknet-js](./applications/simple_storage_starknetjs.md) - [Crowdfunding Campaign](./applications/crowdfunding.md) - [AdvancedFactory: Crowdfunding](./applications/advanced_factory.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 new file mode 100644 index 00000000..8b7c5990 --- /dev/null +++ b/src/applications/random_number_generator.md @@ -0,0 +1,64 @@ +# 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. + +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 strengths and weaknesses. + +### Sources of Entropy + +#### 1. Block Properties + +- **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. + - **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 + +- **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:** 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 or shut down, 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. + + +> 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 + +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}} +```