diff --git a/.env.local b/.env.local new file mode 100644 index 00000000..c65ff9a2 --- /dev/null +++ b/.env.local @@ -0,0 +1,7 @@ +NEXT_PUBLIC_API_KEY="AIzaSyBs3rH_4KQnO1FO7xqigU33uv654_aFI5E", +NEXT_PUBLIC_AUTH_DOMAIN="realtor-clone-ad19c.firebaseapp.com", +NEXT_PUBLIC_PROJECT_ID="realtor-clone-ad19c", +NEXT_PUBLIC_STORAGE_BUCKET="realtor-clone-ad19c.appspot.com", +NEXT_PUBLIC_MESSAGING_SENDER_ID="347563487697", +NEXT_PUBLIC_APP_ID="1:347563487697:web:ecc1c0eff0221f743f30f0", +NEXT_PUBLIC_RPC_URL="https://starknet-mainnet.g.alchemy.com/starknet/version/rpc/v0_7/dvHmwiGiA_uE22lKpZKLk4FoGlC_Xzy4" \ No newline at end of file diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml index b105357c..4c1930d4 100644 --- a/.github/workflows/frontend-ci.yml +++ b/.github/workflows/frontend-ci.yml @@ -25,14 +25,12 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v4 with: - version: 9 + version: 10 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "20" - cache: "pnpm" - cache-dependency-path: frontend/pnpm-lock.yaml - name: Install dependencies run: pnpm install diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml new file mode 100644 index 00000000..f1082c5b --- /dev/null +++ b/.github/workflows/frontend.yml @@ -0,0 +1,23 @@ +name: Run Frontend Tests + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: "20" + + - name: Install dependencies + run: npm install --legacy-peer-deps + working-directory: frontend + + - name: Run build + run: npm run build + working-directory: frontend diff --git a/.github/workflows/test_contract.yml b/.github/workflows/test_contract.yml new file mode 100644 index 00000000..20ea814f --- /dev/null +++ b/.github/workflows/test_contract.yml @@ -0,0 +1,45 @@ +name: Build and Test + +on: [push, pull_request] +permissions: read-all + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: software-mansion/setup-scarb@v1 + with: + scarb-version: 2.11.3 + - uses: actions/cache@v3 + with: + path: ~/.cache/scarb + key: ${{ runner.os }}-scarb-${{ hashFiles('contract/Scarb.lock') }} + restore-keys: | + ${{ runner.os }}-scarb- + - name: Check cairo format + working-directory: contract + run: scarb fmt --check + - name: Build cairo programs + working-directory: contract + run: scarb build + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: software-mansion/setup-scarb@v1 + with: + scarb-version: 2.11.2 + - uses: actions/cache@v3 + with: + path: ~/.cache/scarb + key: ${{ runner.os }}-scarb-${{ hashFiles('contract/Scarb.lock') }} + restore-keys: | + ${{ runner.os }}-scarb- + - uses: foundry-rs/setup-snfoundry@v3 + with: + starknet-foundry-version: 0.39.0 + - name: Run cairo tests + working-directory: contract + run: snforge test \ No newline at end of file diff --git a/contract/.tool-versions b/contract/.tool-versions new file mode 100644 index 00000000..45075dc9 --- /dev/null +++ b/contract/.tool-versions @@ -0,0 +1,2 @@ +scarb 2.11.3 +starknet-foundry 0.39.0 \ No newline at end of file diff --git a/contract/Scarb.lock b/contract/Scarb.lock new file mode 100644 index 00000000..442e3772 --- /dev/null +++ b/contract/Scarb.lock @@ -0,0 +1,150 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "contract" +version = "0.1.0" +dependencies = [ + "pragma_lib", + "snforge_std", +] + +[[package]] +name = "openzeppelin" +version = "1.0.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:320185f3e17cf9fafda88b1ce490f5eaed0bfcc273036b56cd22ce4fb8de628f" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_finance", + "openzeppelin_governance", + "openzeppelin_introspection", + "openzeppelin_merkle_tree", + "openzeppelin_presets", + "openzeppelin_security", + "openzeppelin_token", + "openzeppelin_upgrades", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_access" +version = "1.0.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:a39a4ea1582916c637bf7e3aee0832c3fe1ea3a3e39191955e8dc39d08327f9b" +dependencies = [ + "openzeppelin_introspection", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_account" +version = "1.0.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:7e943a2de32ddca4d48e467e52790e380ab1f49c4daddbbbc4634dd930d0243f" +dependencies = [ + "openzeppelin_introspection", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_finance" +version = "1.0.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:9fa9e91d39b6ccdfa31eef32fdc087cd06c0269cc9c6b86e32d57f5a6997d98b" +dependencies = [ + "openzeppelin_access", + "openzeppelin_token", +] + +[[package]] +name = "openzeppelin_governance" +version = "1.0.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:c05add2974b3193c3a5c022b9586a84cf98c5970cdb884dcf201c77dbe359f55" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_introspection", + "openzeppelin_token", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_introspection" +version = "1.0.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:34e088ecf19e0b3012481a29f1fbb20e600540cb9a5db1c3002a97ebb7f5a32a" + +[[package]] +name = "openzeppelin_merkle_tree" +version = "1.0.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:a5341705514a3d9beeeb39cf11464111f7355be621639740d2c5006786aa63dc" + +[[package]] +name = "openzeppelin_presets" +version = "1.0.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:4eb098e2ee3ac0e67b6828115a7de62f781418beab767d4e80b54e176808369d" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_finance", + "openzeppelin_introspection", + "openzeppelin_token", + "openzeppelin_upgrades", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_security" +version = "1.0.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:1deb811a239c4f9cc28fc302039e2ffcb19911698a8c612487207448d70d2e6e" + +[[package]] +name = "openzeppelin_token" +version = "1.0.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:33fcb84a1a76d2d3fff9302094ff564f78d45b743548fd7568c130b272473f66" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_introspection", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_upgrades" +version = "1.0.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:36f7a03e7e7111577916aacf31f88ad0053de20f33ee10b0ab3804849c3aa373" + +[[package]] +name = "openzeppelin_utils" +version = "1.0.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:fd348b31c4a4407add33adc3c2b8f26dca71dbd7431faaf726168f37a91db0c1" + +[[package]] +name = "pragma_lib" +version = "1.0.0" +source = "git+https://github.com/astraly-labs/pragma-lib#c429179ed6153004b79657337c589548b73d5151" +dependencies = [ + "openzeppelin", +] + +[[package]] +name = "snforge_scarb_plugin" +version = "0.39.0" +source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.39.0#13ee73d44807f57b7e2c64be766161f48ca04c27" + +[[package]] +name = "snforge_std" +version = "0.39.0" +source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.39.0#13ee73d44807f57b7e2c64be766161f48ca04c27" +dependencies = [ + "snforge_scarb_plugin", +] diff --git a/contract/Scarb.toml b/contract/Scarb.toml new file mode 100644 index 00000000..59b5f115 --- /dev/null +++ b/contract/Scarb.toml @@ -0,0 +1,50 @@ +[package] +name = "contract" +version = "0.1.0" +edition = "2024_07" + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html + +[dependencies] +starknet = ">=2.11.2" +pragma_lib = { git = "https://github.com/astraly-labs/pragma-lib" } + +[dev-dependencies] +snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.39.0" } +assert_macros = "2.8.4" + +[[target.starknet-contract]] +sierra = true + +[scripts] +test = "snforge test" + +# Visit https://foundry-rs.github.io/starknet-foundry/appendix/scarb-toml.html for more information + +# [tool.snforge] # Define `snforge` tool section +# exit_first = true # Stop tests execution immediately upon the first failure +# fuzzer_runs = 1234 # Number of runs of the random fuzzer +# fuzzer_seed = 1111 # Seed for the random fuzzer + +# [[tool.snforge.fork]] # Used for fork testing +# name = "SOME_NAME" # Fork name +# url = "http://your.rpc.url" # Url of the RPC provider +# block_id.tag = "latest" # Block to fork from (block tag) + +# [[tool.snforge.fork]] +# name = "SOME_SECOND_NAME" +# url = "http://your.second.rpc.url" +# block_id.number = "123" # Block to fork from (block number) + +# [[tool.snforge.fork]] +# name = "SOME_THIRD_NAME" +# url = "http://your.third.rpc.url" +# block_id.hash = "0x123" # Block to fork from (block hash) + +# [profile.dev.cairo] # Configure Cairo compiler +# unstable-add-statements-code-locations-debug-info = true # Should be used if you want to use coverage +# unstable-add-statements-functions-debug-info = true # Should be used if you want to use coverage/profiler +# inlining-strategy = "avoid" # Should be used if you want to use coverage + +# [features] # Used for conditional compilation +# enable_for_tests = [] # Feature name and list of other features that should be enabled with it diff --git a/contract/snfoundry.toml b/contract/snfoundry.toml new file mode 100644 index 00000000..5f7b1c3e --- /dev/null +++ b/contract/snfoundry.toml @@ -0,0 +1,20 @@ +# Visit https://foundry-rs.github.io/starknet-foundry/appendix/snfoundry-toml.html for more information + +# [sncast.myprofile1] # Define a profile name +# url = "http://127.0.0.1:5050/" # Url of the RPC provider +# accounts_file = "../account-file" # Path to the file with the account data +# account = "mainuser" # Account from `accounts_file` or default account file that will be used for the transactions +# keystore = "~/keystore" # Path to the keystore file +# wait_params = { timeout = 500, retry_interval = 10 } # Wait for submitted transaction parameters +# block_explorer = "StarkScan" # Block explorer service used to display links to transaction details + +[sncast.predifi] +account = "predifi" +accounts-file = "/home/akshola00/.starknet_accounts/starknet_open_zeppelin_accounts.json" +url = "https://free-rpc.nethermind.io/sepolia-juno/" + +# 0x4287a17fc57912782dbcbf43b9cdf8b9d8133e275ded2f03bd2617c3fa1974d +[sncast.predifi2] +account = "predifi2" +accounts-file = "/home/codespace/.starknet_accounts/starknet_open_zeppelin_accounts.json" +url = "https://free-rpc.nethermind.io/sepolia-juno/" diff --git a/contract/src/base/errors.cairo b/contract/src/base/errors.cairo new file mode 100644 index 00000000..c85a9845 --- /dev/null +++ b/contract/src/base/errors.cairo @@ -0,0 +1,18 @@ +pub mod Errors { + pub const REQUIRED_PAYMENT: u128 = 1000; + pub const INVALID_POOL_OPTION: felt252 = 'Invalid Pool Option'; + pub const INACTIVE_POOL: felt252 = 'Pool is inactive'; + pub const AMOUNT_BELOW_MINIMUM: felt252 = 'Amount is below minimum'; + pub const AMOUNT_ABOVE_MAXIMUM: felt252 = 'Amount is above maximum'; + pub const INVALID_POOL_DETAILS: felt252 = 'Invalid Pool Details'; + pub const INVALID_VOTE_DETAILS: felt252 = 'Invalid Vote Details'; + pub const LOCKED_PREDICTION_POOL: felt252 = 'PREDICTION POOL HAS BEEN LOCKED'; + pub const PAYMENT_FAILED: felt252 = 'TRANSFER FAILED'; + pub const TOTAL_STAKE_MUST_BE_ONE_STRK: felt252 = 'Total stake should be 1 STRK'; + pub const TOTAL_SHARE_MUST_BE_ONE_STRK: felt252 = 'Total shares should be 1 STRK'; + pub const USER_SHARE_MUST_BE_ONE_STRK: felt252 = 'User shares should be 1 STRK'; + pub const UNAUTHORIZED: felt252 = 'Caller is not authorized'; + pub const INVALID_ROLE: felt252 = 'Role is invalid'; + pub const SELF_REVOKE_ERROR: felt252 = 'Cannot revoke own admin role'; + pub const ROLE_NOT_ASSIGNED: felt252 = 'Role not assigned to address'; +} diff --git a/contract/src/base/types.cairo b/contract/src/base/types.cairo new file mode 100644 index 00000000..b20f9b58 --- /dev/null +++ b/contract/src/base/types.cairo @@ -0,0 +1,148 @@ +#[derive(Copy, Drop, Serde, PartialEq, starknet::Store, Debug)] +pub enum Pool { + #[default] + WinBet, + VoteBet, + OverUnderBet, + ParlayPool, +} + +#[derive(Copy, Drop, Serde, PartialEq, Debug, starknet::Store)] +pub enum Status { + #[default] + Active, + Locked, + Settled, + Closed, +} + + +#[derive(Copy, Drop, Serde, PartialEq, starknet::Store, Clone)] +pub struct UserStake { + pub amount: u256, + pub shares: u256, + pub option: bool, + pub timestamp: u64, +} + +#[derive(Drop, Serde, PartialEq, starknet::Store, Clone)] +pub struct PoolOdds { + pub option1_odds: u256, // Stored in basis points (10000 = 1.0) + pub option2_odds: u256, + pub option1_probability: u256, // Stored in basis points (10000 = 100%) + pub option2_probability: u256, + pub implied_probability1: u256, + pub implied_probability2: u256, +} + + +fn StatusType(status: Status) -> felt252 { + match status { + Status::Active => 'active', + Status::Locked => 'locked', + Status::Settled => 'settled', + Status::Closed => 'closed', + } +} + +fn PoolType(PoolType: Pool) -> felt252 { + match PoolType { + Pool::WinBet => 'win bet', + Pool::VoteBet => 'vote bet', + Pool::OverUnderBet => 'over under bet', + Pool::ParlayPool => 'parlay pool', + } +} + + +#[derive(Copy, Drop, Serde, PartialEq, Debug, starknet::Store)] +pub enum Category { + #[default] + Sports, + Politics, + Entertainment, + Crypto, + Other, +} + +#[derive(Copy, Drop, Serde, PartialEq, Debug, starknet::Store)] +pub enum ValidateOptions { + #[default] + Win, + Loss, + Void, +} + +pub fn ValidateOptionsType(validate_option: ValidateOptions) -> felt252 { + match validate_option { + ValidateOptions::Win => 'win', + ValidateOptions::Loss => 'loss', + ValidateOptions::Void => 'void', + } +} + + +pub fn CategoryType(category: Category) -> felt252 { + match category { + Category::Sports => 'sports', + Category::Politics => 'politics', + Category::Entertainment => 'entertainment', + Category::Crypto => 'crypto', + Category::Other => 'other', + } +} + +#[derive(Drop, Serde, PartialEq, Debug, starknet::Store, Clone)] +pub struct ValidatorData { + pub status: bool, + pub preodifiTokenAmount: u256, +} + + +#[derive(Drop, Serde, PartialEq, Debug, starknet::Store, Clone)] +pub struct WinaAndLoss { + pub win: u32, + pub loss: u32, + pub null: u32, +} + + +#[derive(Drop, Serde, PartialEq, Debug, starknet::Store, Clone)] +pub struct PoolDetails { + // basic pool details + pub pool_id: u256, + pub address: starknet::ContractAddress, + pub poolName: felt252, + pub poolType: Pool, + pub poolDescription: ByteArray, + pub poolImage: ByteArray, + // event url where users can see more event details and verify event + pub poolEventSourceUrl: ByteArray, + pub createdTimeStamp: u64, + // pool timings: start time, lock time, end time + pub poolStartTime: u64, + pub poolLockTime: u64, + pub poolEndTime: u64, + // pool options, the options that users can bet on + // pub option1: felt252, + // pub option2: felt252, + pub option1: felt252, + pub option2: felt252, + // betamounts in strk + pub minBetAmount: u256, + pub maxBetAmount: u256, + // the fee that the creator gets + pub creatorFee: u8, + pub status: Status, + pub isPrivate: bool, + pub category: Category, + pub totalBetAmountStrk: u256, + pub totalBetCount: u8, + pub totalStakeOption1: u256, + pub totalStakeOption2: u256, + pub totalSharesOption1: u256, + pub totalSharesOption2: u256, + pub initial_share_price: u16, + pub exists: bool, +} + diff --git a/contract/src/interfaces/ipredifi.cairo b/contract/src/interfaces/ipredifi.cairo new file mode 100644 index 00000000..031054c3 --- /dev/null +++ b/contract/src/interfaces/ipredifi.cairo @@ -0,0 +1,46 @@ +use starknet::ContractAddress; +use crate::base::types::{Category, Pool, PoolDetails, PoolOdds, UserStake}; +#[starknet::interface] +pub trait IPredifi { + // Pool Creation and Management + fn create_pool( + ref self: TContractState, + poolName: felt252, + poolType: Pool, + poolDescription: ByteArray, + poolImage: ByteArray, + poolEventSourceUrl: ByteArray, + poolStartTime: u64, + poolLockTime: u64, + poolEndTime: u64, + option1: felt252, + option2: felt252, + minBetAmount: u256, + maxBetAmount: u256, + creatorFee: u8, + isPrivate: bool, + category: Category, + ) -> u256; + + fn pool_count(self: @TContractState) -> u256; + fn pool_odds(self: @TContractState, pool_id: u256) -> PoolOdds; + fn get_pool(self: @TContractState, pool_id: u256) -> PoolDetails; + fn vote(ref self: TContractState, pool_id: u256, option: felt252, amount: u256); + fn get_user_stake(self: @TContractState, pool_id: u256, address: ContractAddress) -> UserStake; + fn get_pool_stakes(self: @TContractState, pool_id: u256) -> UserStake; + fn get_pool_vote(self: @TContractState, pool_id: u256) -> bool; + fn get_pool_count(self: @TContractState) -> u256; + fn retrieve_pool(self: @TContractState, pool_id: u256) -> bool; + + // Role Management + fn assign_role(ref self: TContractState, role: felt252, account: ContractAddress); + fn revoke_role(ref self: TContractState, role: felt252, account: ContractAddress); + fn transfer_role( + ref self: TContractState, + role: felt252, + new_account: ContractAddress, + old_account: ContractAddress, + ); + fn has_role(self: @TContractState, role: felt252, account: ContractAddress) -> bool; +} + diff --git a/contract/src/lib.cairo b/contract/src/lib.cairo new file mode 100644 index 00000000..667e83a4 --- /dev/null +++ b/contract/src/lib.cairo @@ -0,0 +1,14 @@ +pub mod base { + pub mod errors; + pub mod types; +} + +pub mod interfaces { + pub mod ipredifi; +} + +pub mod presets { + pub mod ERC20; +} + +pub mod predifi; diff --git a/contract/src/predifi.cairo b/contract/src/predifi.cairo new file mode 100644 index 00000000..fd37cf59 --- /dev/null +++ b/contract/src/predifi.cairo @@ -0,0 +1,440 @@ +#[starknet::contract] +pub mod Predifi { + // Cairo imports + use core::hash::{HashStateExTrait, HashStateTrait}; + use core::pedersen::PedersenTrait; + use core::poseidon::PoseidonTrait; + use starknet::storage::{ + Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, + StoragePointerWriteAccess, + }; + use starknet::{ContractAddress, get_block_timestamp, get_caller_address, get_contract_address}; + use crate::base::errors::Errors::{ + AMOUNT_ABOVE_MAXIMUM, AMOUNT_BELOW_MINIMUM, INACTIVE_POOL, INVALID_POOL_OPTION, + ROLE_NOT_ASSIGNED, SELF_REVOKE_ERROR, UNAUTHORIZED, + }; + // oz imports + + // package imports + use crate::base::types::{Category, Pool, PoolDetails, PoolOdds, Status, UserStake}; + use crate::interfaces::ipredifi::IPredifi; + + // 1 STRK in WEI + const ONE_STRK: u256 = 1_000_000_000_000_000_000; + + #[storage] + struct Storage { + pools: Map, // pool id to pool details struct + pool_count: u256, // number of pools available totally + pool_odds: Map, + pool_stakes: Map, + pool_vote: Map, // pool id to vote + user_stakes: Map<(u256, ContractAddress), UserStake>, // Mapping user -> stake details + user_hash_poseidon: felt252, + user_hash_pedersen: felt252, + nonce: felt252, + roles: Map<(felt252, ContractAddress), bool>, + } + + // Events + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + BetPlaced: BetPlaced, + RoleGranted: RoleGranted, + RoleRevoked: RoleRevoked, + } + + #[derive(Drop, starknet::Event)] + struct RoleGranted { + role: felt252, + account: ContractAddress, + sender: ContractAddress, + } + + #[derive(Drop, starknet::Event)] + struct RoleRevoked { + role: felt252, + account: ContractAddress, + sender: ContractAddress, + } + + #[derive(Drop, starknet::Event)] + struct BetPlaced { + pool_id: u256, + address: ContractAddress, + option: felt252, + amount: u256, + shares: u256, + } + + #[derive(Drop, Hash)] + struct HashingProperties { + username: felt252, + password: felt252, + } + + #[derive(Drop, Hash)] + struct Hashed { + id: felt252, + login: HashingProperties, + } + + #[constructor] + fn constructor(ref self: ContractState) { + let admin = get_caller_address(); + self.roles.write(('ADMIN', admin), true); + self.emit(Event::RoleGranted(RoleGranted { role: 'ADMIN', account: admin, sender: admin })); + } + + #[abi(embed_v0)] + impl predifi of IPredifi { + fn create_pool( + ref self: ContractState, + poolName: felt252, + poolType: Pool, + poolDescription: ByteArray, + poolImage: ByteArray, + poolEventSourceUrl: ByteArray, + poolStartTime: u64, + poolLockTime: u64, + poolEndTime: u64, + option1: felt252, + option2: felt252, + minBetAmount: u256, + maxBetAmount: u256, + creatorFee: u8, + isPrivate: bool, + category: Category, + ) -> u256 { + // Validation checks + assert!(poolStartTime < poolLockTime, "Start time must be before lock time"); + assert!(poolLockTime < poolEndTime, "Lock time must be before end time"); + assert!(minBetAmount > 0, "Minimum bet must be greater than 0"); + assert!( + maxBetAmount >= minBetAmount, "Max bet must be greater than or equal to min bet", + ); + let current_time = get_block_timestamp(); + assert!(current_time < poolStartTime, "Start time must be in the future"); + assert!(creatorFee <= 5, "Creator fee cannot exceed 5%"); + + // Collect pool creation fee (1 STRK) + self.collect_pool_creation_fee(get_caller_address()); + + let mut pool_id = self.generate_deterministic_number(); + + // While a pool with this pool_id already exists, generate a new one. + while self.retrieve_pool(pool_id) { + pool_id = self.generate_deterministic_number(); + } + + // Create pool details structure + let creator_address = get_caller_address(); + let pool_details = PoolDetails { + pool_id: pool_id, + address: creator_address, + poolName, + poolType, + poolDescription, + poolImage, + poolEventSourceUrl, + createdTimeStamp: current_time, + poolStartTime, + poolLockTime, + poolEndTime, + option1, + option2, + minBetAmount, + maxBetAmount, + creatorFee, + status: Status::Active, + isPrivate, + category, + totalBetAmountStrk: 0_u256, + totalBetCount: 0_u8, + totalStakeOption1: 0_u256, + totalStakeOption2: 0_u256, + totalSharesOption1: 0_u256, + totalSharesOption2: 0_u256, + initial_share_price: 5000, // 0.5 in basis points (10000 = 1.0) + exists: true, + }; + + self.pools.write(pool_id, pool_details); + + let initial_odds = PoolOdds { + option1_odds: 5000, // 0.5 in decimal (5000/10000) + option2_odds: 5000, + option1_probability: 5000, // 50% probability + option2_probability: 5000, + implied_probability1: 5000, + implied_probability2: 5000, + }; + + self.pool_odds.write(pool_id, initial_odds); + + pool_id + } + + fn pool_count(self: @ContractState) -> u256 { + self.pool_count.read() + } + + fn pool_odds(self: @ContractState, pool_id: u256) -> PoolOdds { + self.pool_odds.read(pool_id) + } + + fn get_pool(self: @ContractState, pool_id: u256) -> PoolDetails { + self.pools.read(pool_id) + } + + fn vote(ref self: ContractState, pool_id: u256, option: felt252, amount: u256) { + let pool = self.pools.read(pool_id); + let option1: felt252 = pool.option1; + let option2: felt252 = pool.option2; + assert(option == option1 || option == option2, INVALID_POOL_OPTION); + assert(pool.status == Status::Active, INACTIVE_POOL); + assert(amount >= pool.minBetAmount, AMOUNT_BELOW_MINIMUM); + assert(amount <= pool.maxBetAmount, AMOUNT_ABOVE_MAXIMUM); + + let mut pool = self.pools.read(pool_id); + if option == option1 { + pool.totalStakeOption1 += amount; + pool + .totalSharesOption1 += self + .calculate_shares(amount, pool.totalStakeOption1, pool.totalStakeOption2); + } else { + pool.totalStakeOption2 += amount; + pool + .totalSharesOption2 += self + .calculate_shares(amount, pool.totalStakeOption2, pool.totalStakeOption1); + } + pool.totalBetAmountStrk += amount; + pool.totalBetCount += 1; + + // Update pool odds + let odds = self + .calculate_odds(pool.pool_id, pool.totalStakeOption1, pool.totalStakeOption2); + self.pool_odds.write(pool_id, odds); + + // Calculate the user's shares + let shares: u256 = if option == option1 { + self.calculate_shares(amount, pool.totalStakeOption1, pool.totalStakeOption2) + } else { + self.calculate_shares(amount, pool.totalStakeOption2, pool.totalStakeOption1) + }; + + // Store user stake + let user_stake = UserStake { + option: option == option2, amount, shares, timestamp: get_block_timestamp(), + }; + let address: ContractAddress = get_caller_address(); + self.user_stakes.write((pool.pool_id, address), user_stake); + self.pool_vote.write(pool.pool_id, option == option2); + self.pool_stakes.write(pool.pool_id, user_stake); + self.pools.write(pool.pool_id, pool); + // Emit event + self.emit(Event::BetPlaced(BetPlaced { pool_id, address, option, amount, shares })); + } + + fn get_user_stake( + self: @ContractState, pool_id: u256, address: ContractAddress, + ) -> UserStake { + self.user_stakes.read((pool_id, address)) + } + fn get_pool_stakes(self: @ContractState, pool_id: u256) -> UserStake { + self.pool_stakes.read(pool_id) + } + + fn get_pool_vote(self: @ContractState, pool_id: u256) -> bool { + self.pool_vote.read(pool_id) + } + fn get_pool_count(self: @ContractState) -> u256 { + self.pool_count.read() + } + + + fn retrieve_pool(self: @ContractState, pool_id: u256) -> bool { + let pool = self.pools.read(pool_id); + pool.exists + } + + fn assign_role(ref self: ContractState, role: felt252, account: ContractAddress) { + let caller = get_caller_address(); + assert(self.roles.read(('ADMIN', caller)), UNAUTHORIZED); + self.roles.write((role, account), true); + self.emit(Event::RoleGranted(RoleGranted { role, account, sender: caller })); + } + + fn revoke_role(ref self: ContractState, role: felt252, account: ContractAddress) { + let caller = get_caller_address(); + assert(self.roles.read(('ADMIN', caller)), UNAUTHORIZED); + assert(!(role == 'ADMIN' && account == caller), SELF_REVOKE_ERROR); + self.roles.write((role, account), false); + self.emit(Event::RoleRevoked(RoleRevoked { role, account, sender: caller })); + } + + fn transfer_role( + ref self: ContractState, + role: felt252, + new_account: ContractAddress, + old_account: ContractAddress, + ) { + let caller = get_caller_address(); + assert(self.roles.read(('ADMIN', caller)), UNAUTHORIZED); + assert(self.roles.read((role, old_account)), ROLE_NOT_ASSIGNED); + + self.roles.write((role, new_account), true); + self.roles.write((role, old_account), false); + self + .emit( + Event::RoleGranted(RoleGranted { role, account: new_account, sender: caller }), + ); + self + .emit( + Event::RoleRevoked(RoleRevoked { role, account: old_account, sender: caller }), + ); + } + + fn has_role(self: @ContractState, role: felt252, account: ContractAddress) -> bool { + self.roles.read((role, account)) + } + } + + #[generate_trait] + impl Private of PrivateTrait { + fn collect_pool_creation_fee( + ref self: ContractState, creator: ContractAddress, + ) { // TODO: Uncomment code after ERC20 implementation + // let strk_token = IErc20Dispatcher { contract_address: self.strk_token_address.read() }; + // strk_token.transfer_from(creator, get_contract_address(), ONE_STRK); + } + + + /// Generates a deterministic `u256` with 6 decimal places. + /// Combines block number, timestamp, and sender address for uniqueness. + + fn generate_deterministic_number(ref self: ContractState) -> u256 { + let nonce: felt252 = self.nonce.read(); + let nonci: felt252 = self.save_user_with_pedersen(nonce); + // Increment the nonce and update storage. + self.nonce.write(nonci); + + let username: felt252 = get_contract_address().into(); + let id: felt252 = get_caller_address().into(); + let password: felt252 = nonce.into(); + let login = HashingProperties { username, password }; + let user = Hashed { id, login }; + + let poseidon_hash: felt252 = PoseidonTrait::new().update_with(user).finalize(); + self.user_hash_poseidon.write(poseidon_hash); + + // Convert poseidon_hash from felt252 to u256. + let hash_as_u256: u256 = poseidon_hash.try_into().unwrap(); + + // Define divisor for 6 digits: 1,000,000. + let divisor: u256 = 1000000; + + // Calculate quotient and remainder manually. + let quotient: u256 = hash_as_u256 / divisor; + let remainder: u256 = hash_as_u256 - quotient * divisor; + + remainder + } + + + fn save_user_with_pedersen(ref self: ContractState, salt: felt252) -> felt252 { + let username: felt252 = salt; + let id: felt252 = get_caller_address().into(); + let password: felt252 = get_block_timestamp().into(); + let login = HashingProperties { username, password }; + let user = Hashed { id, login }; + + let pedersen_hash = PedersenTrait::new(0).update_with(user).finalize(); + + self.user_hash_pedersen.write(pedersen_hash); + pedersen_hash + } + + + fn calculate_shares( + ref self: ContractState, + amount: u256, + total_stake_selected_option: u256, + total_stake_other_option: u256, + ) -> u256 { + let total_pool_amount = total_stake_selected_option + total_stake_other_option; + + if total_stake_selected_option == 0 { + return amount; + } + + let shares = (amount * total_pool_amount) / (total_stake_selected_option + 1); + shares + } + + fn calculate_odds( + ref self: ContractState, + pool_id: u256, + total_stake_option1: u256, + total_stake_option2: u256, + ) -> PoolOdds { + // Fetch the current pool odds + let current_pool_odds = self.pool_odds.read(pool_id); + + // If no current pool odds exist, use the initial odds (5000 for both options) + let initial_odds = 5000; // 0.5 in decimal (5000/10000) + let current_option1_odds = if current_pool_odds.option1_odds == 0 { + initial_odds + } else { + current_pool_odds.option1_odds + }; + let current_option2_odds = if current_pool_odds.option2_odds == 0 { + initial_odds + } else { + current_pool_odds.option2_odds + }; + + // Calculate the total pool amount + let total_pool_amount = total_stake_option1 + total_stake_option2; + + // If no stakes are placed, return the current pool odds + if total_pool_amount == 0 { + return PoolOdds { + option1_odds: current_option1_odds, + option2_odds: current_option2_odds, + option1_probability: current_option1_odds, + option2_probability: current_option2_odds, + implied_probability1: 10000 / current_option1_odds, + implied_probability2: 10000 / current_option2_odds, + }; + } + + // Calculate the new odds based on the stakes + let new_option1_odds = (total_stake_option2 * 10000) / total_pool_amount; + let new_option2_odds = (total_stake_option1 * 10000) / total_pool_amount; + + // update the new odds with the current odds (weighted average) + let option1_odds = (current_option1_odds + new_option1_odds) / 2; + let option2_odds = (current_option2_odds + new_option2_odds) / 2; + + // Calculate probabilities + let option1_probability = option1_odds; + let option2_probability = option2_odds; + + // Calculate implied probabilities + let implied_probability1 = 10000 / option1_odds; + let implied_probability2 = 10000 / option2_odds; + + // Return the updated PoolOdds struct + PoolOdds { + option1_odds: option1_odds, + option2_odds: option2_odds, + option1_probability, + option2_probability, + implied_probability1, + implied_probability2, + } + } + } +} diff --git a/contract/src/presets/ERC20.cairo b/contract/src/presets/ERC20.cairo new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/contract/src/presets/ERC20.cairo @@ -0,0 +1 @@ + diff --git a/contract/tests/test_contract.cairo b/contract/tests/test_contract.cairo new file mode 100644 index 00000000..9abecaf2 --- /dev/null +++ b/contract/tests/test_contract.cairo @@ -0,0 +1,687 @@ +use contract::base::types::{Category, Pool}; +use contract::interfaces::ipredifi::{IPredifiDispatcher, IPredifiDispatcherTrait}; +use core::felt252; +use core::traits::Into; +use snforge_std::{ + ContractClassTrait, DeclareResultTrait, declare, start_cheat_caller_address, + start_cheat_caller_address_global, stop_cheat_caller_address, stop_cheat_caller_address_global, +}; +use starknet::{ContractAddress, get_block_timestamp}; + +fn owner() -> ContractAddress { + 'owner'.try_into().unwrap() +} + +fn deploy_predifi() -> IPredifiDispatcher { + let contract_class = declare("Predifi").unwrap().contract_class(); + + let (contract_address, _) = contract_class.deploy(@array![].into()).unwrap(); + (IPredifiDispatcher { contract_address }) +} + +const ONE_STRK: u256 = 1_000_000_000_000_000_000; + +#[test] +fn test_create_pool() { + let contract = deploy_predifi(); + let pool_id = contract + .create_pool( + 'Example Pool', + Pool::WinBet, + "A simple betting pool", + "image.png", + "event.com/details", + 1710000000, + 1710003600, + 1710007200, + 'Team A', + 'Team B', + 100, + 10000, + 5, + false, + Category::Sports, + ); + + assert!(pool_id != 0, "not created"); +} + +#[test] +#[should_panic(expected: "Start time must be before lock time")] +fn test_invalid_time_sequence_start_after_lock() { + let contract = deploy_predifi(); + let ( + poolName, + poolType, + poolDescription, + poolImage, + poolEventSourceUrl, + _, + _, + poolEndTime, + option1, + option2, + minBetAmount, + maxBetAmount, + creatorFee, + isPrivate, + category, + ) = + get_default_pool_params(); + + let current_time = get_block_timestamp(); + let invalid_start_time = current_time + 3600; // 1 hour from now + let invalid_lock_time = current_time + + 1800; // 30 minutes from now (before start), should not be able to lock before starting + + contract + .create_pool( + poolName, + poolType, + poolDescription, + poolImage, + poolEventSourceUrl, + invalid_start_time, + invalid_lock_time, + poolEndTime, + option1, + option2, + minBetAmount, + maxBetAmount, + creatorFee, + isPrivate, + category, + ); +} + +#[test] +#[should_panic(expected: "Minimum bet must be greater than 0")] +fn test_zero_min_bet() { + let contract = deploy_predifi(); + let ( + poolName, + poolType, + poolDescription, + poolImage, + poolEventSourceUrl, + poolStartTime, + poolLockTime, + poolEndTime, + option1, + option2, + _, + maxBetAmount, + creatorFee, + isPrivate, + category, + ) = + get_default_pool_params(); + + contract + .create_pool( + poolName, + poolType, + poolDescription, + poolImage, + poolEventSourceUrl, + poolStartTime, + poolLockTime, + poolEndTime, + option1, + option2, + 0, + maxBetAmount, + creatorFee, + isPrivate, + category, + ); +} + +#[test] +#[should_panic(expected: "Creator fee cannot exceed 5%")] +fn test_excessive_creator_fee() { + let contract = deploy_predifi(); + let ( + poolName, + poolType, + poolDescription, + poolImage, + poolEventSourceUrl, + poolStartTime, + poolLockTime, + poolEndTime, + option1, + option2, + minBetAmount, + maxBetAmount, + _, + isPrivate, + category, + ) = + get_default_pool_params(); + + contract + .create_pool( + poolName, + poolType, + poolDescription, + poolImage, + poolEventSourceUrl, + poolStartTime, + poolLockTime, + poolEndTime, + option1, + option2, + minBetAmount, + maxBetAmount, + 6, + isPrivate, + category, + ); +} + +fn get_default_pool_params() -> ( + felt252, + Pool, + ByteArray, + ByteArray, + ByteArray, + u64, + u64, + u64, + felt252, + felt252, + u256, + u256, + u8, + bool, + Category, +) { + let current_time = get_block_timestamp(); + ( + 'Default Pool', // poolName + Pool::WinBet, // poolType + "Default Description", // poolDescription + "default_image.jpg", // poolImage + "https://example.com", // poolEventSourceUrl + current_time + 86400, // poolStartTime (1 day from now) + current_time + 172800, // poolLockTime (2 days from now) + current_time + 259200, // poolEndTime (3 days from now) + 'Option A', // option1 + 'Option B', // option2 + 1_000_000_000_000_000_000, // minBetAmount (1 STRK) + 10_000_000_000_000_000_000, // maxBetAmount (10 STRK) + 5, // creatorFee (5%) + false, // isPrivate + Category::Sports // category + ) +} + +#[test] +fn test_vote() { + let contract = deploy_predifi(); + let pool_id = contract + .create_pool( + 'Example Pool', + Pool::WinBet, + "A simple betting pool", + "image.png", + "event.com/details", + 1710000000, + 1710003600, + 1710007200, + 'Team A', + 'Team B', + 100, + 10000, + 5, + false, + Category::Sports, + ); + contract.vote(pool_id, 'Team A', 200); + + let pool = contract.get_pool(pool_id); + assert(pool.totalBetCount == 1, 'Total bet count should be 1'); + assert(pool.totalStakeOption1 == 200, 'Total stake should be 200'); + assert(pool.totalSharesOption1 == 199, 'Total share should be 199'); +} + +#[test] +fn test_vote_with_user_stake() { + let contract = deploy_predifi(); + let pool_id = contract + .create_pool( + 'Example Pool', + Pool::WinBet, + "A simple betting pool", + "image.png", + "event.com/details", + 1710000000, + 1710003600, + 1710007200, + 'Team A', + 'Team B', + 100, + 10000, + 5, + false, + Category::Sports, + ); + + let pool = contract.get_pool(pool_id); + + contract.vote(pool_id, 'Team A', 200); + + let user_stake = contract.get_user_stake(pool_id, pool.address); + + assert(user_stake.amount == 200, 'Incorrect amount'); + assert(user_stake.shares == 199, 'Incorrect shares'); + assert(!user_stake.option, 'Incorrect option'); +} + +#[test] +fn test_successful_get_pool() { + let contract = deploy_predifi(); + let pool_id = contract + .create_pool( + 'Example Pool1', + Pool::WinBet, + "A simple betting pool1", + "image.png", + "event.com/details", + 1710000000, + 1710003600, + 1710007200, + 'Team A', + 'Team B', + 100, + 10000, + 5, + false, + Category::Sports, + ); + let pool = contract.get_pool(pool_id); + assert(pool.poolName == 'Example Pool1', 'Pool not found'); +} + +#[test] +#[should_panic(expected: 'Invalid Pool Option')] +fn test_when_invalid_option_is_pass() { + let contract = deploy_predifi(); + let pool_id = contract + .create_pool( + 'Example Pool', + Pool::WinBet, + "A simple betting pool", + "image.png", + "event.com/details", + 1710000000, + 1710003600, + 1710007200, + 'Team A', + 'Team B', + 100, + 10000, + 5, + false, + Category::Sports, + ); + contract.vote(pool_id, 'Team C', 200); +} + +#[test] +#[should_panic(expected: 'Amount is below minimum')] +fn test_when_min_bet_amount_less_than_required() { + let contract = deploy_predifi(); + let pool_id = contract + .create_pool( + 'Example Pool', + Pool::WinBet, + "A simple betting pool", + "image.png", + "event.com/details", + 1710000000, + 1710003600, + 1710007200, + 'Team A', + 'Team B', + 100, + 10000, + 5, + false, + Category::Sports, + ); + contract.vote(pool_id, 'Team A', 10); +} + +#[test] +#[should_panic(expected: 'Amount is above maximum')] +fn test_when_max_bet_amount_greater_than_required() { + let contract = deploy_predifi(); + let pool_id = contract + .create_pool( + 'Example Pool', + Pool::WinBet, + "A simple betting pool", + "image.png", + "event.com/details", + 1710000000, + 1710003600, + 1710007200, + 'Team A', + 'Team B', + 100, + 10000, + 5, + false, + Category::Sports, + ); + contract.vote(pool_id, 'Team B', 1000000); +} + +#[test] +fn test_get_pool_odds() { + let contract = deploy_predifi(); + + // Create a new pool + let pool_id = contract + .create_pool( + 'Example Pool', + Pool::WinBet, + "A simple betting pool", + "image.png", + "event.com/details", + 1710000000, + 1710003600, + 1710007200, + 'Team A', + 'Team B', + 100, + 10000, + 5, + false, + Category::Sports, + ); + + contract.vote(pool_id, 'Team A', 100); + + let pool_odds = contract.pool_odds(pool_id); + + assert(pool_odds.option1_odds == 2500, 'Incorrect odds for option 1'); + assert(pool_odds.option2_odds == 7500, 'Incorrect odds for option 2'); +} + +#[test] +fn test_get_pool_stakes() { + let contract = deploy_predifi(); + + // Create a new pool + let pool_id = contract + .create_pool( + 'Example Pool', + Pool::WinBet, + "A simple betting pool", + "image.png", + "event.com/details", + 1710000000, + 1710003600, + 1710007200, + 'Team A', + 'Team B', + 100, + 10000, + 5, + false, + Category::Sports, + ); + + contract.vote(pool_id, 'Team A', 200); + + let pool_stakes = contract.get_pool_stakes(pool_id); + + assert(pool_stakes.amount == 200, 'Incorrect pool stake amount'); + assert(pool_stakes.shares == 199, 'Incorrect pool stake shares'); + assert(!pool_stakes.option, 'Incorrect pool stake option'); +} + +#[test] +fn test_unique_pool_id() { + let contract = deploy_predifi(); + let pool_id = contract + .create_pool( + 'Example Pool', + Pool::WinBet, + "A simple betting pool", + "image.png", + "event.com/details", + 1710000000, + 1710003600, + 1710007200, + 'Team A', + 'Team B', + 100, + 10000, + 5, + false, + Category::Sports, + ); + assert!(pool_id != 0, "not created"); + println!("Pool id: {}", pool_id); +} + + +#[test] +fn test_unique_pool_id_when_called_twice_in_the_same_execution() { + let contract = deploy_predifi(); + let pool_id = contract + .create_pool( + 'Example Pool', + Pool::WinBet, + "A simple betting pool", + "image.png", + "event.com/details", + 1710000000, + 1710003600, + 1710007200, + 'Team A', + 'Team B', + 100, + 10000, + 5, + false, + Category::Sports, + ); + let pool_id1 = contract + .create_pool( + 'Example Pool', + Pool::WinBet, + "A simple betting pool", + "image.png", + "event.com/details", + 1710000000, + 1710003600, + 1710007200, + 'Team A', + 'Team B', + 100, + 10000, + 5, + false, + Category::Sports, + ); + + assert!(pool_id != 0, "not created"); + assert!(pool_id != pool_id1, "they are the same"); + + println!("Pool id: {}", pool_id); + println!("Pool id: {}", pool_id1); +} +#[test] +fn test_get_pool_vote() { + let contract = deploy_predifi(); + + // Create a new pool + let pool_id = contract + .create_pool( + 'Example Pool', + Pool::WinBet, + "A simple betting pool", + "image.png", + "event.com/details", + 1710000000, + 1710003600, + 1710007200, + 'Team A', + 'Team B', + 100, + 10000, + 5, + false, + Category::Sports, + ); + + contract.vote(pool_id, 'Team A', 200); + + let pool_vote = contract.get_pool_vote(pool_id); + + assert(!pool_vote, 'Incorrect pool vote'); +} + +#[test] +fn test_role_management() { + let admin: ContractAddress = 'admin'.try_into().unwrap(); + let user: ContractAddress = 'user'.try_into().unwrap(); + + // Cheat caller for the constructor + start_cheat_caller_address_global(admin); + let contract = deploy_predifi(); + stop_cheat_caller_address_global(); + + // Verify admin + assert(contract.has_role('ADMIN', admin), 'Admin should have ADMIN role'); + + // Now prank as admin to assign role + start_cheat_caller_address(contract.contract_address, admin); + contract.assign_role('OPERATOR', user); + stop_cheat_caller_address(contract.contract_address); + + assert(contract.has_role('OPERATOR', user), 'User should have OPERATOR role'); + + // Revoke + start_cheat_caller_address(contract.contract_address, admin); + contract.revoke_role('OPERATOR', user); + stop_cheat_caller_address(contract.contract_address); + + assert(!contract.has_role('OPERATOR', user), 'User should not have role'); +} + +#[test] +fn test_transfer_role() { + let admin: ContractAddress = 'admin'.try_into().unwrap(); + let user1: ContractAddress = 'user1'.try_into().unwrap(); + let user2: ContractAddress = 'user2'.try_into().unwrap(); + + start_cheat_caller_address_global(admin); + let contract = deploy_predifi(); + stop_cheat_caller_address_global(); + + start_cheat_caller_address(contract.contract_address, admin); + contract.assign_role('OPERATOR', user1); + assert(contract.has_role('OPERATOR', user1), 'User1 should have role'); + + contract.transfer_role('OPERATOR', user2, user1); + stop_cheat_caller_address(contract.contract_address); + + assert(!contract.has_role('OPERATOR', user1), 'User1 should not have role'); + assert(contract.has_role('OPERATOR', user2), 'User2 should have role'); +} + +#[test] +#[should_panic(expected: 'Caller is not authorized')] +fn test_unauthorized_assign_role() { + let admin: ContractAddress = 'admin'.try_into().unwrap(); + let user: ContractAddress = 'user'.try_into().unwrap(); + let attacker: ContractAddress = 'attacker'.try_into().unwrap(); + + start_cheat_caller_address_global(admin); + let contract = deploy_predifi(); + stop_cheat_caller_address_global(); + + start_cheat_caller_address(contract.contract_address, attacker); + contract.assign_role('OPERATOR', user); + stop_cheat_caller_address(contract.contract_address); +} + +#[test] +#[should_panic(expected: 'Caller is not authorized')] +fn test_unauthorized_revoke_role() { + let admin: ContractAddress = 'admin'.try_into().unwrap(); + let user: ContractAddress = 'user'.try_into().unwrap(); + let attacker: ContractAddress = 'attacker'.try_into().unwrap(); + + start_cheat_caller_address_global(admin); + let contract = deploy_predifi(); + stop_cheat_caller_address_global(); + + // Assign role first (as admin) + start_cheat_caller_address(contract.contract_address, admin); + contract.assign_role('OPERATOR', user); + stop_cheat_caller_address(contract.contract_address); + + // Try to revoke as attacker + start_cheat_caller_address(contract.contract_address, attacker); + contract.revoke_role('OPERATOR', user); + stop_cheat_caller_address(contract.contract_address); +} + +#[test] +#[should_panic(expected: 'Caller is not authorized')] +fn test_unauthorized_transfer_role() { + let admin: ContractAddress = 'admin'.try_into().unwrap(); + let user1: ContractAddress = 'user1'.try_into().unwrap(); + let user2: ContractAddress = 'user2'.try_into().unwrap(); + let attacker: ContractAddress = 'attacker'.try_into().unwrap(); + + start_cheat_caller_address_global(admin); + let contract = deploy_predifi(); + stop_cheat_caller_address_global(); + + start_cheat_caller_address(contract.contract_address, admin); + contract.assign_role('OPERATOR', user1); + stop_cheat_caller_address(contract.contract_address); + + start_cheat_caller_address(contract.contract_address, attacker); + contract.transfer_role('OPERATOR', user2, user1); + stop_cheat_caller_address(contract.contract_address); +} + +#[test] +#[should_panic(expected: 'Role not assigned to address')] +fn test_transfer_role_not_assigned() { + let admin: ContractAddress = 'admin'.try_into().unwrap(); + let user1: ContractAddress = 'user1'.try_into().unwrap(); + let user2: ContractAddress = 'user2'.try_into().unwrap(); + + start_cheat_caller_address_global(admin); + let contract = deploy_predifi(); + stop_cheat_caller_address_global(); + + start_cheat_caller_address(contract.contract_address, admin); + // user1 does not have the role + contract.transfer_role('OPERATOR', user2, user1); + stop_cheat_caller_address(contract.contract_address); +} + +#[test] +#[should_panic(expected: 'Cannot revoke own admin role')] +fn test_revoke_own_admin_role() { + let admin: ContractAddress = 'admin'.try_into().unwrap(); + + start_cheat_caller_address_global(admin); + let contract = deploy_predifi(); + stop_cheat_caller_address_global(); + + start_cheat_caller_address(contract.contract_address, admin); + contract.revoke_role('ADMIN', admin); + stop_cheat_caller_address(contract.contract_address); +} diff --git a/frontend/app/(marketing)/components/Features.tsx b/frontend/app/(marketing)/components/Features.tsx index ad0ab678..cf4597e2 100644 --- a/frontend/app/(marketing)/components/Features.tsx +++ b/frontend/app/(marketing)/components/Features.tsx @@ -1,5 +1,6 @@ import React from "react"; import { ArrowRight } from "lucide-react"; +import Image from "next/image"; const featuresData = [ { @@ -52,11 +53,14 @@ function Features() { `} > {/* IMAGE */} -
- + {feature.title}
diff --git a/frontend/app/(marketing)/components/Footer.tsx b/frontend/app/(marketing)/components/Footer.tsx index 37b70374..0b154895 100644 --- a/frontend/app/(marketing)/components/Footer.tsx +++ b/frontend/app/(marketing)/components/Footer.tsx @@ -1,15 +1,16 @@ import React from "react"; +import Image from "next/image"; function Footer() { return (
- +
- - - - + + + +
diff --git a/frontend/app/(marketing)/components/HeroSection.tsx b/frontend/app/(marketing)/components/HeroSection.tsx index 37ff9269..ad7c544f 100644 --- a/frontend/app/(marketing)/components/HeroSection.tsx +++ b/frontend/app/(marketing)/components/HeroSection.tsx @@ -1,14 +1,16 @@ import React from "react"; +import Image from "next/image"; function HeroSection() { return ( // Added min-h to ensure it covers screen on mobile, and overflow adjustments
{/* Background Pattern */} - diff --git a/frontend/app/(marketing)/components/NavBar.tsx b/frontend/app/(marketing)/components/NavBar.tsx index 4bb8070b..e74882de 100644 --- a/frontend/app/(marketing)/components/NavBar.tsx +++ b/frontend/app/(marketing)/components/NavBar.tsx @@ -1,6 +1,7 @@ "use client"; import Link from "next/link"; import React, { useState } from "react"; +import Image from "next/image"; import { Menu, X } from "lucide-react"; function Navbar() { @@ -14,7 +15,7 @@ function Navbar() {
{/* LOGO */} - Logo + Logo {/* DESKTOP NAVIGATION (Hidden on mobile) */} diff --git a/frontend/app/(marketing)/components/PredictionProtocol.tsx b/frontend/app/(marketing)/components/PredictionProtocol.tsx index 4a039796..99bbc653 100644 --- a/frontend/app/(marketing)/components/PredictionProtocol.tsx +++ b/frontend/app/(marketing)/components/PredictionProtocol.tsx @@ -1,6 +1,8 @@ "use client"; import React, { useState, useRef } from "react"; +import Image from "next/image"; + // define your content and images here const steps = [ { @@ -69,12 +71,15 @@ function PredictionProtocol() { onTouchEnd={handleTouchEnd} >
- {steps[activeTab].title}
@@ -87,9 +92,8 @@ function PredictionProtocol() { +
+ +
+
+ Pool Image +
+
+

+ PredFi to win the hackathon Pool ID: {stakePoolId} +

+