Skip to content

Commit

Permalink
Random Number Generator (#238)
Browse files Browse the repository at this point in the history
* feat: dice game vrf application

* feat: add summary nav

* fix: ran scarb fmt

* fix: ran scarb fmt

* Fix new lines

* Add more info on randomness sources

* Rename dice_game_vrf.md->random_number_generator.md and update titles

* minor rewording of 1 entropy source

* remove anchors

* Minor changes to fn names

* Implement dice game scaffold

* Implement Pragma randomness

* minor refactor in randomness request

* Implement powerball scaffold

* Turn Dice Game into CoinFlip

* Implement coin_flip test

* Add more tests

* Update titles

* Remove redundant blank line

* Add premium fee calculation into tests

* Assert leftover balance

* Remove comment about fees

* Increase the expected callback fee, update mock to expose fee calc fn

* Unfinished: refunded

* Store and use is_refunded flag

* Implement logic necessary to successfully perform & test refund

* Update callback fee limit based on manual testing + update term to deposit

* Format

* Use a FlipData struct instead of tuple

* Fix refund

* Simplify CoinFlip to pay the flips itself

* CALLBACK_FEE_DEPOSIT->MAX_CALLBACK_FEE_DEPOSIT

* Update tests to test the new CoinFlip contract

* Fix compile errors

* Increase publish_delay to 1 & remove unused imports

* Remove starkli-wallet dir

* Generate 3 random words for the 1st test

* refactor tests

* Add missng newline to scarb.toml

* fix typo in md

* reword 'manipulation' def

* Chainlink->Pragma

* link to Commit-reveal chapter issue

* list 'shut down' as possible centr. issue with ext. oracles

* Turn point 5 into a note

* Remove Sideways enum

* add contract description

* Remove ResultTrait from crowdfunding tests.cairo

---------

Co-authored-by: Tony Stark <afemiadom@icloud.com>
Co-authored-by: Nenad <nenad.misic@nethermind.io>
Co-authored-by: Nenad <nenad@better.giving>
  • Loading branch information
4 people authored Oct 1, 2024
1 parent 3b416c6 commit 1e22588
Show file tree
Hide file tree
Showing 13 changed files with 726 additions and 3 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ output
# Others
.snfoundry_cache
.vscode/settings.json
**/starkli-wallet
14 changes: 14 additions & 0 deletions Scarb.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
2 changes: 0 additions & 2 deletions listings/applications/advanced_factory/src/tests.cairo
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use core::clone::Clone;
use core::result::ResultTrait;
use advanced_factory::contract::{
CampaignFactory, ICampaignFactoryDispatcher, ICampaignFactoryDispatcherTrait
};
Expand Down
2 changes: 2 additions & 0 deletions listings/applications/coin_flip/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target
.snfoundry_cache/
21 changes: 21 additions & 0 deletions listings/applications/coin_flip/Scarb.toml
Original file line number Diff line number Diff line change
@@ -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"]
164 changes: 164 additions & 0 deletions listings/applications/coin_flip/src/contract.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
use starknet::ContractAddress;

#[starknet::interface]
pub trait ICoinFlip<TContractState> {
fn flip(ref self: TContractState);
}

// declares just the pragma_lib::abi::IRandomness.receive_random_words function
#[starknet::interface]
pub trait IPragmaVRF<TContractState> {
fn receive_random_words(
ref self: TContractState,
requestor_address: ContractAddress,
request_id: u64,
random_words: Span<felt252>,
calldata: Array<felt252>
);
}

#[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<u64, ContractAddress>,
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<ContractState> {
/// 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<ContractState> {
fn receive_random_words(
ref self: ContractState,
requestor_address: ContractAddress,
request_id: u64,
random_words: Span<felt252>,
calldata: Array<felt252>
) {
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 }));
}
}
}
5 changes: 5 additions & 0 deletions listings/applications/coin_flip/src/lib.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod contract;
mod mock_randomness;

#[cfg(test)]
mod tests;
Loading

0 comments on commit 1e22588

Please sign in to comment.