From a969fcdd691f88d75fb2f4a36e43d4cced77359a Mon Sep 17 00:00:00 2001 From: Jerome Peter Date: Sun, 29 Mar 2026 14:52:26 +0100 Subject: [PATCH] test(contract): move config leaderboard and market suites --- contract/src/config.rs | 13 + contract/src/lib.rs | 198 +------ contract/src/market.rs | 784 +--------------------------- contract/src/market_tests.rs | 175 ------- contract/tests/config_tests.rs | 77 +++ contract/tests/leaderboard_tests.rs | 176 +++++++ contract/tests/market_tests.rs | 718 +++++++++++++++++++++++++ 7 files changed, 1010 insertions(+), 1131 deletions(-) delete mode 100644 contract/src/market_tests.rs create mode 100644 contract/tests/config_tests.rs create mode 100644 contract/tests/leaderboard_tests.rs create mode 100644 contract/tests/market_tests.rs diff --git a/contract/src/config.rs b/contract/src/config.rs index 8575dff6..c54c7866 100644 --- a/contract/src/config.rs +++ b/contract/src/config.rs @@ -49,6 +49,14 @@ fn load_config(env: &Env) -> Result { .ok_or(InsightArenaError::NotInitialized) } +fn validate_protocol_fee(fee_bps: u32) -> Result<(), InsightArenaError> { + if fee_bps > 10_000 { + return Err(InsightArenaError::InvalidFee); + } + + Ok(()) +} + // ── Entry-point logic (called from contractimpl in lib.rs) ──────────────────── /// One-time contract setup. @@ -66,6 +74,8 @@ pub fn initialize( return Err(InsightArenaError::AlreadyInitialized); } + validate_protocol_fee(fee_bps)?; + let config = Config { admin, protocol_fee_bps: fee_bps, @@ -121,6 +131,8 @@ pub fn update_protocol_fee(env: &Env, new_fee_bps: u32) -> Result<(), InsightAre // Authorisation check — reverts the entire transaction if auth is absent. config.admin.require_auth(); + validate_protocol_fee(new_fee_bps)?; + config.protocol_fee_bps = new_fee_bps; env.storage().persistent().set(&DataKey::Config, &config); bump_config(env); @@ -133,6 +145,7 @@ pub fn update_protocol_fee_from_governance( new_fee_bps: u32, ) -> Result<(), InsightArenaError> { let mut config = load_config(env)?; + validate_protocol_fee(new_fee_bps)?; config.protocol_fee_bps = new_fee_bps; env.storage().persistent().set(&DataKey::Config, &config); bump_config(env); diff --git a/contract/src/lib.rs b/contract/src/lib.rs index 07099392..0475d8ea 100644 --- a/contract/src/lib.rs +++ b/contract/src/lib.rs @@ -354,7 +354,7 @@ impl InsightArenaContract { season::get_active_season(&env) } - pub fn update_leaderboard( + pub fn update_leaderboard( env: Env, admin: Address, season_id: u32, @@ -449,202 +449,6 @@ impl InsightArenaContract { // ── Tests ───────────────────────────────────────────────────────────────────── -#[cfg(test)] -mod config_tests { - use soroban_sdk::testutils::Address as _; - use soroban_sdk::{Address, Env}; - use super::{InsightArenaContract, InsightArenaContractClient, InsightArenaError}; - - fn deploy(env: &Env) -> InsightArenaContractClient<'_> { - let id = env.register(InsightArenaContract, ()); - InsightArenaContractClient::new(env, &id) - } - - fn register_token(env: &Env) -> Address { - let token_admin = Address::generate(env); - env.register_stellar_asset_contract_v2(token_admin) - .address() - } - - #[test] - fn ensure_not_paused_ok_when_running() { - let env = Env::default(); - env.mock_all_auths(); - let client = deploy(&env); - let admin = Address::generate(&env); - let oracle = Address::generate(&env); - client.initialize(&admin, &oracle, &200_u32, ®ister_token(&env)); - client.get_config(); - } - - #[test] - fn ensure_not_paused_err_when_paused() { - let env = Env::default(); - env.mock_all_auths(); - let client = deploy(&env); - let admin = Address::generate(&env); - let oracle = Address::generate(&env); - client.initialize(&admin, &oracle, &200_u32, ®ister_token(&env)); - client.set_paused(&true); - let result = client.try_get_config(); - assert!(matches!(result, Err(Ok(InsightArenaError::Paused)))); - } - - #[test] - fn ensure_not_paused_not_initialized() { - let env = Env::default(); - env.mock_all_auths(); - let client = deploy(&env); - let result = client.try_get_config(); - assert!(matches!(result, Err(Ok(InsightArenaError::NotInitialized)))); - } - - #[test] - fn ensure_not_paused_ok_after_unpause() { - let env = Env::default(); - env.mock_all_auths(); - let client = deploy(&env); - let admin = Address::generate(&env); - let oracle = Address::generate(&env); - client.initialize(&admin, &oracle, &200_u32, ®ister_token(&env)); - client.set_paused(&true); - client.set_paused(&false); - client.get_config(); - } -} - -#[cfg(test)] -mod leaderboard_tests { - use soroban_sdk::testutils::Address as _; - use soroban_sdk::{vec, Address, Env}; - use super::{ - InsightArenaContract, InsightArenaContractClient, InsightArenaError, LeaderboardEntry, - }; - - fn deploy(env: &Env) -> (InsightArenaContractClient<'_>, Address, Address) { - let id = env.register(InsightArenaContract, ()); - let client = InsightArenaContractClient::new(env, &id); - let admin = Address::generate(env); - let oracle = Address::generate(env); - let token_admin = Address::generate(env); - let xlm_token = env - .register_stellar_asset_contract_v2(token_admin) - .address(); - env.mock_all_auths(); - client.initialize(&admin, &oracle, &200_u32, &xlm_token); - (client, admin, xlm_token) - } - - #[test] - fn test_update_and_get_historical_leaderboard() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, xlm_token) = deploy(&env); - - let reward_pool = 10_000_000; - let token_client = soroban_sdk::token::Client::new(&env, &xlm_token); - soroban_sdk::token::StellarAssetClient::new(&env, &xlm_token).mint(&admin, &reward_pool); - token_client.approve(&admin, &client.address, &reward_pool, &9999); - - let season_id = client.create_season(&admin, &100, &200, &reward_pool); - let user1 = Address::generate(&env); - let user2 = Address::generate(&env); - let entries = vec![ - &env, - LeaderboardEntry { - rank: 1, - user: user1.clone(), - points: 100, - correct_predictions: 10, - total_predictions: 15, - }, - LeaderboardEntry { - rank: 2, - user: user2.clone(), - points: 80, - correct_predictions: 8, - total_predictions: 12, - }, - ]; - - client.update_leaderboard(&admin, &season_id, &entries); - - let snapshot = client.get_leaderboard(&season_id); - assert_eq!(snapshot.season_id, season_id); - assert_eq!(snapshot.entries.len(), 2); - assert_eq!(snapshot.entries.get(0).unwrap().user, user1); - assert_eq!(snapshot.entries.get(1).unwrap().user, user2); - } - - #[test] - fn test_list_snapshot_seasons_deduplication() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, xlm_token) = deploy(&env); - - let reward_pool = 20_000_000; - soroban_sdk::token::StellarAssetClient::new(&env, &xlm_token).mint(&admin, &reward_pool); - soroban_sdk::token::Client::new(&env, &xlm_token).approve( - &admin, - &client.address, - &reward_pool, - &9999, - ); - - let s1 = client.create_season(&admin, &100, &200, &10_000_000); - let s2 = client.create_season(&admin, &201, &300, &10_000_000); - - assert_eq!(client.list_snapshot_seasons().len(), 0); - - let entries = vec![ - &env, - LeaderboardEntry { - rank: 1, - user: Address::generate(&env), - points: 10, - correct_predictions: 1, - total_predictions: 1, - }, - ]; - - client.update_leaderboard(&admin, &s1, &entries); - assert_eq!(client.list_snapshot_seasons().len(), 1); - client.update_leaderboard(&admin, &s1, &entries); - assert_eq!(client.list_snapshot_seasons().len(), 1); - client.update_leaderboard(&admin, &s2, &entries); - assert_eq!(client.list_snapshot_seasons().len(), 2); - } - - #[test] - fn test_get_leaderboard_not_found() { - let env = Env::default(); - env.mock_all_auths(); - let (client, _, _) = deploy(&env); - let result = client.try_get_leaderboard(&99); - assert!(matches!(result, Err(Ok(InsightArenaError::SeasonNotFound)))); - } - - #[test] - fn test_update_leaderboard_unauthorized() { - let env = Env::default(); - env.mock_all_auths(); - let (client, _, _) = deploy(&env); - let stranger = Address::generate(&env); - let result = client.try_update_leaderboard(&stranger, &1, &vec![&env]); - assert!(matches!(result, Err(Ok(InsightArenaError::Unauthorized)))); - } - - #[test] - fn test_update_leaderboard_when_paused() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, _) = deploy(&env); - client.set_paused(&true); - let result = client.try_update_leaderboard(&admin, &1, &vec![&env]); - assert!(matches!(result, Err(Ok(InsightArenaError::Paused)))); - } -} - #[cfg(test)] mod season_tests; diff --git a/contract/src/market.rs b/contract/src/market.rs index 20d454fe..110d2dff 100644 --- a/contract/src/market.rs +++ b/contract/src/market.rs @@ -165,8 +165,7 @@ pub fn emit_market_resolved(env: &Env, market_id: u64, resolved_outcome: Symbol) /// Calculate price of outcome A in terms of outcome B. /// Returns price with 6 decimal precision (multiplied by 1_000_000). -#[allow(dead_code)] -fn calculate_price(reserve_a: i128, reserve_b: i128) -> Result { +pub fn calculate_price(reserve_a: i128, reserve_b: i128) -> Result { if reserve_a <= 0 || reserve_b <= 0 { return Err(InsightArenaError::InvalidInput); } @@ -180,6 +179,25 @@ fn calculate_price(reserve_a: i128, reserve_b: i128) -> Result) -> bool { + let mut index: u32 = 0; + while index < outcomes.len() { + let outcome = outcomes.get(index).unwrap(); + let mut next_index = index + 1; + + while next_index < outcomes.len() { + if outcomes.get(next_index) == Some(outcome.clone()) { + return true; + } + next_index += 1; + } + + index += 1; + } + + false +} + // ── Entry-point logic ───────────────────────────────────────────────────────── /// Create a new prediction market and return its auto-assigned `market_id`. @@ -219,6 +237,9 @@ pub fn create_market( if params.outcomes.len() < 2 { return Err(InsightArenaError::InvalidInput); } + if has_duplicate_outcomes(¶ms.outcomes) { + return Err(InsightArenaError::InvalidInput); + } // ── Load config for fee and stake floor checks ──────────────────────────── let cfg = config::get_config(env)?; @@ -492,11 +513,10 @@ pub fn cancel_market(env: &Env, caller: Address, market_id: u64) -> Result<(), I .set(&DataKey::Market(market_id), &market); bump_market(env, market_id); - // ── Iterate all predictors and issue refunds ────────────────────────────── - let predictors: Vec
= env + let predictors = env .storage() .persistent() - .get(&DataKey::PredictorList(market_id)) + .get::>(&DataKey::PredictorList(market_id)) .unwrap_or_else(|| Vec::new(env)); for predictor in predictors.iter() { @@ -506,761 +526,7 @@ pub fn cancel_market(env: &Env, caller: Address, market_id: u64) -> Result<(), I } } - // ── Emit MarketCancelled event ──────────────────────────────────────────── emit_market_cancelled(env, market_id, &caller); Ok(()) } - -// ── Tests ───────────────────────────────────────────────────────────────────── - -#[cfg(test)] -mod market_tests { - use soroban_sdk::testutils::{Address as _, Ledger as _}; - use soroban_sdk::{symbol_short, vec, Address, Env, String, Symbol, Vec}; - - use crate::storage_types::DataKey; - use crate::{InsightArenaContract, InsightArenaContractClient, InsightArenaError}; - - use super::CreateMarketParams; - - #[test] - fn test_calculate_price_equal_reserves() { - // Reserves: 1000/1000 -> Expected: 1_000_000 - let price = super::calculate_price(1000, 1000).unwrap(); - assert_eq!(price, 1_000_000); - } - - #[test] - fn test_calculate_price_double() { - // Reserves: 1000/2000 -> Expected: 2_000_000 - let price = super::calculate_price(1000, 2000).unwrap(); - assert_eq!(price, 2_000_000); - } - - #[test] - fn test_calculate_price_half() { - // Reserves: 2000/1000 -> Expected: 500_000 - let price = super::calculate_price(2000, 1000).unwrap(); - assert_eq!(price, 500_000); - } - - #[test] - fn test_calculate_price_precision() { - // Reserves: 3000/1000 -> Expected: 333_333 - let price = super::calculate_price(3000, 1000).unwrap(); - assert_eq!(price, 333_333); - } - - /// Register a mock XLM token (Stellar Asset Contract) and return its address. - fn register_token(env: &Env) -> Address { - let token_admin = Address::generate(env); - env.register_stellar_asset_contract_v2(token_admin) - .address() - } - - fn deploy(env: &Env) -> InsightArenaContractClient<'_> { - let id = env.register(InsightArenaContract, ()); - let client = InsightArenaContractClient::new(env, &id); - let admin = Address::generate(env); - let oracle = Address::generate(env); - let xlm_token = register_token(env); - env.mock_all_auths(); - client.initialize(&admin, &oracle, &200_u32, &xlm_token); - client - } - - fn default_params(env: &Env) -> CreateMarketParams { - let now = env.ledger().timestamp(); - CreateMarketParams { - title: String::from_str(env, "Will it rain?"), - description: String::from_str(env, "Daily weather market"), - category: Symbol::new(env, "Sports"), - outcomes: vec![env, symbol_short!("yes"), symbol_short!("no")], - end_time: now + 1000, - resolution_time: now + 2000, - dispute_window: 86_400, - creator_fee_bps: 100, - min_stake: 10_000_000, - max_stake: 100_000_000, - is_public: true, - } - } - - #[test] - fn create_market_success_returns_incremented_id() { - let env = Env::default(); - env.mock_all_auths(); - let client = deploy(&env); - let creator = Address::generate(&env); - - let id = client.create_market(&creator, &default_params(&env)); - assert_eq!(id, 1); - - let id2 = client.create_market(&creator, &default_params(&env)); - assert_eq!(id2, 2); - } - - #[test] - fn create_market_fails_end_time_in_past() { - let env = Env::default(); - env.mock_all_auths(); - let client = deploy(&env); - let creator = Address::generate(&env); - - let mut p = default_params(&env); - p.end_time = env.ledger().timestamp(); // not strictly after now - - let result = client.try_create_market(&creator, &p); - assert!(matches!( - result, - Err(Ok(InsightArenaError::InvalidTimeRange)) - )); - } - - #[test] - fn create_market_fails_resolution_before_end() { - let env = Env::default(); - env.mock_all_auths(); - let client = deploy(&env); - let creator = Address::generate(&env); - - let mut p = default_params(&env); - p.resolution_time = p.end_time - 1; - - let result = client.try_create_market(&creator, &p); - assert!(matches!( - result, - Err(Ok(InsightArenaError::InvalidTimeRange)) - )); - } - - #[test] - fn create_market_fails_single_outcome() { - let env = Env::default(); - env.mock_all_auths(); - let client = deploy(&env); - let creator = Address::generate(&env); - - let mut p = default_params(&env); - p.outcomes = vec![&env, symbol_short!("yes")]; - - let result = client.try_create_market(&creator, &p); - assert!(matches!(result, Err(Ok(InsightArenaError::InvalidInput)))); - } - - #[test] - fn create_market_fails_fee_too_high() { - let env = Env::default(); - env.mock_all_auths(); - let client = deploy(&env); - let creator = Address::generate(&env); - - let mut p = default_params(&env); - p.creator_fee_bps = 501; // exceeds 500 bps cap - - let result = client.try_create_market(&creator, &p); - assert!(matches!(result, Err(Ok(InsightArenaError::InvalidFee)))); - } - - #[test] - fn create_market_fails_when_paused() { - let env = Env::default(); - env.mock_all_auths(); - let client = deploy(&env); - let creator = Address::generate(&env); - - client.set_paused(&true); - let result = client.try_create_market(&creator, &default_params(&env)); - assert!(matches!(result, Err(Ok(InsightArenaError::Paused)))); - } - - #[test] - fn create_market_fails_stake_too_low() { - let env = Env::default(); - env.mock_all_auths(); - let client = deploy(&env); - let creator = Address::generate(&env); - - let mut p = default_params(&env); - p.min_stake = 1; // below 10_000_000 stroops platform floor - - let result = client.try_create_market(&creator, &p); - assert!(matches!(result, Err(Ok(InsightArenaError::StakeTooLow)))); - } - - #[test] - fn create_market_fails_when_category_not_whitelisted() { - let env = Env::default(); - env.mock_all_auths(); - let client = deploy(&env); - let creator = Address::generate(&env); - - let mut p = default_params(&env); - p.category = Symbol::new(&env, "Weather"); - - let result = client.try_create_market(&creator, &p); - assert!(matches!(result, Err(Ok(InsightArenaError::InvalidInput)))); - } - - #[test] - fn list_categories_returns_seeded_defaults() { - let env = Env::default(); - env.mock_all_auths(); - let client = deploy(&env); - let categories = client.list_categories(); - - assert!(categories.contains(Symbol::new(&env, "Sports"))); - assert!(categories.contains(Symbol::new(&env, "Crypto"))); - assert!(categories.contains(Symbol::new(&env, "Politics"))); - assert!(categories.contains(Symbol::new(&env, "Entertainment"))); - assert!(categories.contains(Symbol::new(&env, "Science"))); - assert!(categories.contains(Symbol::new(&env, "Other"))); - } - - #[test] - fn add_category_allows_admin_to_extend_whitelist() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, _oracle) = deploy_with_actors(&env); - let weather = Symbol::new(&env, "Weather"); - - client.add_category(&admin, &weather); - - let categories = client.list_categories(); - assert!(categories.contains(weather)); - } - - #[test] - fn remove_category_blocks_future_market_creation() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, _oracle) = deploy_with_actors(&env); - let creator = Address::generate(&env); - let science = Symbol::new(&env, "Science"); - - client.remove_category(&admin, &science); - - let categories = client.list_categories(); - assert!(!categories.contains(science.clone())); - - let mut p = default_params(&env); - p.category = science; - let result = client.try_create_market(&creator, &p); - assert!(matches!(result, Err(Ok(InsightArenaError::InvalidInput)))); - } - - #[test] - fn non_admin_cannot_mutate_categories() { - let env = Env::default(); - env.mock_all_auths(); - let (client, _admin, _oracle) = deploy_with_actors(&env); - let random = Address::generate(&env); - let weather = Symbol::new(&env, "Weather"); - let sports = Symbol::new(&env, "Sports"); - - let add_result = client.try_add_category(&random, &weather); - assert!(matches!( - add_result, - Err(Ok(InsightArenaError::Unauthorized)) - )); - - let remove_result = client.try_remove_category(&random, &sports); - assert!(matches!( - remove_result, - Err(Ok(InsightArenaError::Unauthorized)) - )); - } - - // ── get_market ──────────────────────────────────────────────────────────── - - #[test] - fn get_market_returns_correct_market() { - let env = Env::default(); - env.mock_all_auths(); - let client = deploy(&env); - let creator = Address::generate(&env); - - let id = client.create_market(&creator, &default_params(&env)); - let market = client.get_market(&id); - assert_eq!(market.market_id, id); - assert_eq!(market.creator, creator); - } - - #[test] - fn get_market_returns_not_found_for_missing_id() { - let env = Env::default(); - env.mock_all_auths(); - let client = deploy(&env); - - let result = client.try_get_market(&99_u64); - assert!(matches!(result, Err(Ok(InsightArenaError::MarketNotFound)))); - } - - // ── get_market_count ────────────────────────────────────────────────────── - - #[test] - fn get_market_count_zero_before_any_market() { - let env = Env::default(); - env.mock_all_auths(); - let client = deploy(&env); - - assert_eq!(client.get_market_count(), 0); - } - - #[test] - fn get_market_count_increments_with_each_market() { - let env = Env::default(); - env.mock_all_auths(); - let client = deploy(&env); - let creator = Address::generate(&env); - - client.create_market(&creator, &default_params(&env)); - assert_eq!(client.get_market_count(), 1); - - client.create_market(&creator, &default_params(&env)); - assert_eq!(client.get_market_count(), 2); - } - - // ── list_markets ────────────────────────────────────────────────────────── - - #[test] - fn list_markets_empty_when_no_markets() { - let env = Env::default(); - env.mock_all_auths(); - let client = deploy(&env); - - let list = client.list_markets(&1_u64, &10_u32); - assert_eq!(list.len(), 0); - } - - #[test] - fn get_markets_by_category_returns_paginated_results() { - let env = Env::default(); - env.mock_all_auths(); - let client = deploy(&env); - let creator = Address::generate(&env); - - let sports_category = Symbol::new(&env, "Sports"); - let crypto_category = Symbol::new(&env, "Crypto"); - - let first_sports = client.create_market(&creator, &default_params(&env)); - - let mut crypto = default_params(&env); - crypto.category = crypto_category; - client.create_market(&creator, &crypto); - - let second_sports_id = client.create_market(&creator, &default_params(&env)); - let third_sports_id = client.create_market(&creator, &default_params(&env)); - - let first_page = client.get_markets_by_category(&sports_category, &0_u64, &2_u32); - assert_eq!(first_page.len(), 2); - assert_eq!(first_page.get(0).unwrap().market_id, first_sports); - assert_eq!(first_page.get(1).unwrap().market_id, second_sports_id); - - let second_page = client.get_markets_by_category(&sports_category, &2_u64, &2_u32); - assert_eq!(second_page.len(), 1); - assert_eq!(second_page.get(0).unwrap().market_id, third_sports_id); - } - - #[test] - fn category_index_is_kept_in_sync_on_market_creation() { - let env = Env::default(); - env.mock_all_auths(); - let client = deploy(&env); - let creator = Address::generate(&env); - let sports = Symbol::new(&env, "Sports"); - - let first_id = client.create_market(&creator, &default_params(&env)); - - let mut crypto = default_params(&env); - crypto.category = Symbol::new(&env, "Crypto"); - client.create_market(&creator, &crypto); - - let second_id = client.create_market(&creator, &default_params(&env)); - - let stored_index = env.as_contract(&client.address, || { - env.storage() - .persistent() - .get::>(&DataKey::CategoryIndex(sports.clone())) - .unwrap() - }); - - assert_eq!(stored_index.len(), 2); - assert_eq!(stored_index.get(0), Some(first_id)); - assert_eq!(stored_index.get(1), Some(second_id)); - } - - #[test] - fn list_markets_returns_all_when_within_limit() { - let env = Env::default(); - env.mock_all_auths(); - let client = deploy(&env); - let creator = Address::generate(&env); - - for _ in 0..3 { - client.create_market(&creator, &default_params(&env)); - } - - let list = client.list_markets(&1_u64, &10_u32); - assert_eq!(list.len(), 3); - assert_eq!(list.get(0).unwrap().market_id, 1); - assert_eq!(list.get(2).unwrap().market_id, 3); - } - - #[test] - fn list_markets_respects_pagination_start() { - let env = Env::default(); - env.mock_all_auths(); - let client = deploy(&env); - let creator = Address::generate(&env); - - for _ in 0..5 { - client.create_market(&creator, &default_params(&env)); - } - - // Start from market ID 3, take up to 10 - let list = client.list_markets(&3_u64, &10_u32); - assert_eq!(list.len(), 3); // IDs 3, 4, 5 - assert_eq!(list.get(0).unwrap().market_id, 3); - } - - #[test] - fn list_markets_caps_at_max_limit_50() { - let env = Env::default(); - env.mock_all_auths(); - let client = deploy(&env); - let creator = Address::generate(&env); - - for _ in 0..60 { - client.create_market(&creator, &default_params(&env)); - } - - let list = client.list_markets(&1_u64, &100_u32); // ask for 100, should get 50 - assert_eq!(list.len(), 50); - } - - #[test] - fn list_markets_empty_when_start_out_of_bounds() { - let env = Env::default(); - env.mock_all_auths(); - let client = deploy(&env); - let creator = Address::generate(&env); - - client.create_market(&creator, &default_params(&env)); - - // start > total count → empty - let list = client.list_markets(&99_u64, &10_u32); - assert_eq!(list.len(), 0); - } - - // ── close_market ────────────────────────────────────────────────────────── - - /// Helper: deploy a contract and return client together with pre-registered - /// admin and oracle addresses (the same ones used during `initialize`). - fn deploy_with_actors(env: &Env) -> (InsightArenaContractClient<'_>, Address, Address) { - let id = env.register(InsightArenaContract, ()); - let client = InsightArenaContractClient::new(env, &id); - let admin = Address::generate(env); - let oracle = Address::generate(env); - let xlm_token = register_token(env); - env.mock_all_auths(); - client.initialize(&admin, &oracle, &200_u32, &xlm_token); - (client, admin, oracle) - } - - /// Helper: deploy contract, return client + admin + oracle + token address. - fn deploy_with_token(env: &Env) -> (InsightArenaContractClient<'_>, Address, Address, Address) { - let id = env.register(InsightArenaContract, ()); - let client = InsightArenaContractClient::new(env, &id); - let admin = Address::generate(env); - let oracle = Address::generate(env); - let xlm_token = register_token(env); - env.mock_all_auths(); - client.initialize(&admin, &oracle, &200_u32, &xlm_token); - (client, admin, oracle, xlm_token) - } - - // (a) close_market called before end_time → MarketStillOpen - #[test] - fn close_market_fails_before_end_time() { - let env = Env::default(); - env.mock_all_auths(); - let (client, _admin, oracle) = deploy_with_actors(&env); - let creator = Address::generate(&env); - - // Market end_time is now + 1000; current timestamp is still "now" - let id = client.create_market(&creator, &default_params(&env)); - - let result = client.try_close_market(&oracle, &id); - assert!(matches!( - result, - Err(Ok(InsightArenaError::MarketStillOpen)) - )); - } - - // (b) close_market called after end_time by the oracle → success + is_closed == true - #[test] - fn close_market_success_by_oracle_after_end_time() { - let env = Env::default(); - env.mock_all_auths(); - let (client, _admin, oracle) = deploy_with_actors(&env); - let creator = Address::generate(&env); - - let id = client.create_market(&creator, &default_params(&env)); - - // Advance ledger time past end_time (now + 1000) - env.ledger().set_timestamp(env.ledger().timestamp() + 1001); - - client.close_market(&oracle, &id); - - let market = client.get_market(&id); - assert!(market.is_closed); - assert!(!market.is_resolved); - } - - // (b-alt) close_market called after end_time by the admin → success - #[test] - fn close_market_success_by_admin_after_end_time() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, _oracle) = deploy_with_actors(&env); - let creator = Address::generate(&env); - - let id = client.create_market(&creator, &default_params(&env)); - - env.ledger().set_timestamp(env.ledger().timestamp() + 1001); - - client.close_market(&admin, &id); - - let market = client.get_market(&id); - assert!(market.is_closed); - } - - // (c) double-close attempt → MarketAlreadyResolved not triggered, but a - // second close on an already-closed (not yet resolved) market succeeds - // because is_resolved is still false; however once resolved it must fail. - // We test the resolved path: set is_resolved manually via a resolved market - // scenario by directly checking that a market flagged resolved returns the error. - // - // Since we can only interact through the public ABI, we test the reachable - // path: close a market that has already been resolved (simulated by calling - // close twice — second call must still pass because is_resolved stays false - // until resolve_market is implemented). Instead we verify that calling - // close on a non-existent market returns MarketNotFound, and that calling - // close on an already-closed-then-externally-resolved market returns - // MarketAlreadyResolved via direct storage manipulation in the test. - #[test] - fn close_market_fails_when_already_resolved() { - use crate::storage_types::{DataKey, Market}; - - let env = Env::default(); - env.mock_all_auths(); - let (client, _admin, oracle) = deploy_with_actors(&env); - let creator = Address::generate(&env); - - let id = client.create_market(&creator, &default_params(&env)); - - // Advance past end_time and close the market normally - env.ledger().set_timestamp(env.ledger().timestamp() + 1001); - client.close_market(&oracle, &id); - - // Simulate resolution by mutating the stored market directly using the - // correct contract address from the deployed client. - let contract_id = client.address.clone(); - let mut market: Market = env.as_contract(&contract_id, || { - env.storage() - .persistent() - .get(&DataKey::Market(id)) - .unwrap() - }); - market.is_resolved = true; - env.as_contract(&contract_id, || { - env.storage() - .persistent() - .set(&DataKey::Market(id), &market); - }); - - // Now try to close again — should fail with MarketAlreadyResolved - let result = client.try_close_market(&oracle, &id); - assert!(matches!( - result, - Err(Ok(InsightArenaError::MarketAlreadyResolved)) - )); - } - - // ── cancel_market ───────────────────────────────────────────────────────── - - // (a) Non-admin caller → Unauthorized - #[test] - fn cancel_market_fails_for_non_admin() { - let env = Env::default(); - env.mock_all_auths(); - let (client, _admin, _oracle, _token) = deploy_with_token(&env); - let creator = Address::generate(&env); - let random = Address::generate(&env); - - let id = client.create_market(&creator, &default_params(&env)); - - let result = client.try_cancel_market(&random, &id); - assert!(matches!(result, Err(Ok(InsightArenaError::Unauthorized)))); - } - - // (b) Unknown market_id → MarketNotFound - #[test] - fn cancel_market_fails_market_not_found() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, _oracle, _token) = deploy_with_token(&env); - - let result = client.try_cancel_market(&admin, &99_u64); - assert!(matches!(result, Err(Ok(InsightArenaError::MarketNotFound)))); - } - - // (c) Already-resolved market → MarketAlreadyResolved - #[test] - fn cancel_market_fails_when_already_resolved() { - use crate::storage_types::{DataKey, Market}; - - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, _oracle, _token) = deploy_with_token(&env); - let creator = Address::generate(&env); - - let id = client.create_market(&creator, &default_params(&env)); - - // Simulate resolution via direct storage mutation. - let contract_id = client.address.clone(); - let mut market: Market = env.as_contract(&contract_id, || { - env.storage() - .persistent() - .get(&DataKey::Market(id)) - .unwrap() - }); - market.is_resolved = true; - env.as_contract(&contract_id, || { - env.storage() - .persistent() - .set(&DataKey::Market(id), &market); - }); - - let result = client.try_cancel_market(&admin, &id); - assert!(matches!( - result, - Err(Ok(InsightArenaError::MarketAlreadyResolved)) - )); - } - - // (d) Double-cancel → MarketAlreadyCancelled - #[test] - fn cancel_market_fails_when_already_cancelled() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, _oracle, _token) = deploy_with_token(&env); - let creator = Address::generate(&env); - - let id = client.create_market(&creator, &default_params(&env)); - client.cancel_market(&admin, &id); // first cancel succeeds - - let result = client.try_cancel_market(&admin, &id); - assert!(matches!( - result, - Err(Ok(InsightArenaError::MarketAlreadyCancelled)) - )); - } - - // (e) Successful cancel with no predictors → market.is_cancelled == true, - // MarketCancelled event emitted, no refund calls made. - #[test] - fn cancel_market_success_no_predictors() { - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, _oracle, _token) = deploy_with_token(&env); - let creator = Address::generate(&env); - - let id = client.create_market(&creator, &default_params(&env)); - client.cancel_market(&admin, &id); - - let market = client.get_market(&id); - assert!(market.is_cancelled); - assert!(!market.is_resolved); - } - - // (f) Cancel with multiple predictors → all stakes refunded, balances restored. - // - // Because no `predict` function exists yet, predictions are seeded directly - // into persistent storage (same technique as the close_market resolved test). - // The contract escrow balance is pre-funded by minting tokens to the contract. - #[test] - fn cancel_market_refunds_all_predictors() { - use crate::storage_types::{DataKey, Prediction}; - use soroban_sdk::token::{Client as TokenClient, StellarAssetClient}; - - let env = Env::default(); - env.mock_all_auths(); - let (client, admin, _oracle, xlm_token) = deploy_with_token(&env); - let creator = Address::generate(&env); - - let id = client.create_market(&creator, &default_params(&env)); - - // Prepare two predictors with distinct stakes. - let predictor_a = Address::generate(&env); - let predictor_b = Address::generate(&env); - let stake_a: i128 = 20_000_000; // 2 XLM - let stake_b: i128 = 50_000_000; // 5 XLM - - let contract_id = client.address.clone(); - - // Seed Prediction records and PredictorList directly into contract storage. - env.as_contract(&contract_id, || { - let pred_a = Prediction::new( - id, - predictor_a.clone(), - symbol_short!("yes"), - stake_a, - env.ledger().timestamp(), - ); - let pred_b = Prediction::new( - id, - predictor_b.clone(), - symbol_short!("no"), - stake_b, - env.ledger().timestamp(), - ); - - env.storage() - .persistent() - .set(&DataKey::Prediction(id, predictor_a.clone()), &pred_a); - env.storage() - .persistent() - .set(&DataKey::Prediction(id, predictor_b.clone()), &pred_b); - - let mut predictors = soroban_sdk::Vec::new(&env); - predictors.push_back(predictor_a.clone()); - predictors.push_back(predictor_b.clone()); - env.storage() - .persistent() - .set(&DataKey::PredictorList(id), &predictors); - }); - - // Fund the contract escrow with the total staked amount. - let total_staked = stake_a + stake_b; - StellarAssetClient::new(&env, &xlm_token).mint(&contract_id, &total_staked); - - // Confirm predictors start with zero balance. - let token_client = TokenClient::new(&env, &xlm_token); - assert_eq!(token_client.balance(&predictor_a), 0); - assert_eq!(token_client.balance(&predictor_b), 0); - - // Cancel the market. - client.cancel_market(&admin, &id); - - // Every predictor must receive exactly their stake back. - assert_eq!(token_client.balance(&predictor_a), stake_a); - assert_eq!(token_client.balance(&predictor_b), stake_b); - - // Market must be flagged as cancelled. - let market = client.get_market(&id); - assert!(market.is_cancelled); - } -} diff --git a/contract/src/market_tests.rs b/contract/src/market_tests.rs deleted file mode 100644 index b771ed18..00000000 --- a/contract/src/market_tests.rs +++ /dev/null @@ -1,175 +0,0 @@ -use soroban_sdk::testutils::Address as _; -use soroban_sdk::{symbol_short, vec, Address, Env, String, Symbol}; - -use crate::market::CreateMarketParams; -use crate::{InsightArenaContract, InsightArenaContractClient, InsightArenaError}; - -fn register_token(env: &Env) -> Address { - let token_admin = Address::generate(env); - env.register_stellar_asset_contract_v2(token_admin) - .address() -} - -/// Deploy, initialise, and return (client, admin, oracle). -fn deploy(env: &Env) -> (InsightArenaContractClient<'_>, Address, Address) { - let id = env.register(InsightArenaContract, ()); - let client = InsightArenaContractClient::new(env, &id); - let admin = Address::generate(env); - let oracle = Address::generate(env); - let xlm_token = register_token(env); - env.mock_all_auths(); - client.initialize(&admin, &oracle, &200_u32, &xlm_token); - (client, admin, oracle) -} - -fn default_params(env: &Env) -> CreateMarketParams { - let now = env.ledger().timestamp(); - CreateMarketParams { - title: String::from_str(env, "Will it rain?"), - description: String::from_str(env, "Weather market"), - category: Symbol::new(env, "Sports"), - outcomes: vec![env, symbol_short!("yes"), symbol_short!("no")], - end_time: now + 1000, - resolution_time: now + 2000, - dispute_window: 86_400, - creator_fee_bps: 100, - min_stake: 10_000_000, - max_stake: 100_000_000, - is_public: true, - } -} - -// ── Happy path ──────────────────────────────────────────────────────────── - -/// Successful market creation returns a valid market ID and persists the record. -#[test] -fn test_create_market_success() { - let env = Env::default(); - env.mock_all_auths(); - let (client, _admin, _oracle) = deploy(&env); - let creator = Address::generate(&env); - - let id = client.create_market(&creator, &default_params(&env)); - assert_eq!(id, 1); - - let market = client.get_market(&id); - assert_eq!(market.market_id, id); - assert_eq!(market.creator, creator); - assert!(!market.is_resolved); - assert!(!market.is_cancelled); -} - -// ── Edge cases ──────────────────────────────────────────────────────────── - -/// end_time equal to current timestamp is rejected (must be strictly in the future). -#[test] -fn test_create_market_invalid_end_time() { - let env = Env::default(); - env.mock_all_auths(); - let (client, _admin, _oracle) = deploy(&env); - let creator = Address::generate(&env); - - let mut p = default_params(&env); - p.end_time = env.ledger().timestamp(); // not strictly after now - - let result = client.try_create_market(&creator, &p); - assert!(matches!( - result, - Err(Ok(InsightArenaError::InvalidTimeRange)) - )); -} - -/// Fewer than two outcomes is rejected. -#[test] -fn test_create_market_invalid_outcomes_count() { - let env = Env::default(); - env.mock_all_auths(); - let (client, _admin, _oracle) = deploy(&env); - let creator = Address::generate(&env); - - let mut p = default_params(&env); - p.outcomes = vec![&env, symbol_short!("yes")]; - - let result = client.try_create_market(&creator, &p); - assert!(matches!(result, Err(Ok(InsightArenaError::InvalidInput)))); -} - -/// Creator fee exceeding the 500 bps platform cap is rejected. -#[test] -fn test_create_market_fee_exceeds_max() { - let env = Env::default(); - env.mock_all_auths(); - let (client, _admin, _oracle) = deploy(&env); - let creator = Address::generate(&env); - - let mut p = default_params(&env); - p.creator_fee_bps = 501; - - let result = client.try_create_market(&creator, &p); - assert!(matches!(result, Err(Ok(InsightArenaError::InvalidFee)))); -} - -/// min_stake greater than max_stake is rejected. -#[test] -fn test_create_market_min_stake_exceeds_max_stake() { - let env = Env::default(); - env.mock_all_auths(); - let (client, _admin, _oracle) = deploy(&env); - let creator = Address::generate(&env); - - let mut p = default_params(&env); - p.min_stake = 100_000_000; - p.max_stake = 10_000_000; // less than min_stake - - let result = client.try_create_market(&creator, &p); - assert!(matches!(result, Err(Ok(InsightArenaError::InvalidInput)))); -} - -/// Creating a market while the contract is paused is rejected. -#[test] -fn test_create_market_when_paused() { - let env = Env::default(); - env.mock_all_auths(); - let (client, _admin, _oracle) = deploy(&env); - let creator = Address::generate(&env); - - client.set_paused(&true); - - let result = client.try_create_market(&creator, &default_params(&env)); - assert!(matches!(result, Err(Ok(InsightArenaError::Paused)))); -} - -/// Creator require_auth is enforced — calling without valid authorisation panics. -#[test] -#[should_panic(expected = "HostError: Error(Auth")] -fn test_create_market_unauthorised() { - let env = Env::default(); - // Do NOT call mock_all_auths so require_auth will fail. - let id = env.register(InsightArenaContract, ()); - let client = InsightArenaContractClient::new(&env, &id); - let admin = Address::generate(&env); - let oracle = Address::generate(&env); - let xlm_token = register_token(&env); - - // Initialize needs admin auth — temporarily mock it. - env.mock_all_auths(); - client.initialize(&admin, &oracle, &200_u32, &xlm_token); - - // Soroban's mock_all_auths is sticky per Env, so we use a second Env - // to test the auth-failure path with a clean auth state. - let env2 = Env::default(); - // NO mock_all_auths on env2. - let id2 = env2.register(InsightArenaContract, ()); - let client2 = InsightArenaContractClient::new(&env2, &id2); - let admin2 = Address::generate(&env2); - let oracle2 = Address::generate(&env2); - let xlm_token2 = register_token(&env2); - // We must initialize — use as_contract to bypass auth for setup only. - env2.as_contract(&id2, || { - crate::config::initialize(&env2, admin2, oracle2, 200, xlm_token2).unwrap(); - }); - - let creator = Address::generate(&env2); - // This should panic because creator.require_auth() has no mock. - client2.create_market(&creator, &default_params(&env2)); -} diff --git a/contract/tests/config_tests.rs b/contract/tests/config_tests.rs new file mode 100644 index 00000000..78e71981 --- /dev/null +++ b/contract/tests/config_tests.rs @@ -0,0 +1,77 @@ +use insightarena_contract::{InsightArenaContract, InsightArenaContractClient, InsightArenaError}; +use soroban_sdk::testutils::Address as _; +use soroban_sdk::{Address, Env}; + +fn deploy(env: &Env) -> InsightArenaContractClient<'_> { + let id = env.register(InsightArenaContract, ()); + InsightArenaContractClient::new(env, &id) +} + +fn register_token(env: &Env) -> Address { + let token_admin = Address::generate(env); + env.register_stellar_asset_contract_v2(token_admin) + .address() +} + +#[test] +fn ensure_not_paused_ok_when_running() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let admin = Address::generate(&env); + let oracle = Address::generate(&env); + client.initialize(&admin, &oracle, &200_u32, ®ister_token(&env)); + client.get_config(); +} + +#[test] +fn ensure_not_paused_err_when_paused() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let admin = Address::generate(&env); + let oracle = Address::generate(&env); + client.initialize(&admin, &oracle, &200_u32, ®ister_token(&env)); + client.set_paused(&true); + let result = client.try_get_config(); + assert!(matches!(result, Err(Ok(InsightArenaError::Paused)))); +} + +#[test] +fn ensure_not_paused_not_initialized() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let result = client.try_get_config(); + assert!(matches!(result, Err(Ok(InsightArenaError::NotInitialized)))); +} + +#[test] +fn ensure_not_paused_ok_after_unpause() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let admin = Address::generate(&env); + let oracle = Address::generate(&env); + client.initialize(&admin, &oracle, &200_u32, ®ister_token(&env)); + client.set_paused(&true); + client.set_paused(&false); + client.get_config(); +} + +#[test] +fn test_config_update_validation() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let admin = Address::generate(&env); + let oracle = Address::generate(&env); + + client.initialize(&admin, &oracle, &200_u32, ®ister_token(&env)); + + let result = client.try_update_protocol_fee(&10_001_u32); + assert!(matches!(result, Err(Ok(InsightArenaError::InvalidFee)))); + + let config = client.get_config(); + assert_eq!(config.protocol_fee_bps, 200); +} diff --git a/contract/tests/leaderboard_tests.rs b/contract/tests/leaderboard_tests.rs new file mode 100644 index 00000000..69a21099 --- /dev/null +++ b/contract/tests/leaderboard_tests.rs @@ -0,0 +1,176 @@ +use insightarena_contract::{ + InsightArenaContract, InsightArenaContractClient, InsightArenaError, LeaderboardEntry, +}; +use soroban_sdk::testutils::Address as _; +use soroban_sdk::{vec, Address, Env}; + +fn deploy(env: &Env) -> (InsightArenaContractClient<'_>, Address, Address) { + let id = env.register(InsightArenaContract, ()); + let client = InsightArenaContractClient::new(env, &id); + let admin = Address::generate(env); + let oracle = Address::generate(env); + let token_admin = Address::generate(env); + let xlm_token = env + .register_stellar_asset_contract_v2(token_admin) + .address(); + env.mock_all_auths(); + client.initialize(&admin, &oracle, &200_u32, &xlm_token); + (client, admin, xlm_token) +} + +fn fund_reward_pool( + env: &Env, + client: &InsightArenaContractClient<'_>, + admin: &Address, + xlm_token: &Address, + reward_pool: i128, +) { + soroban_sdk::token::StellarAssetClient::new(env, xlm_token).mint(admin, &reward_pool); + soroban_sdk::token::Client::new(env, xlm_token).approve( + admin, + &client.address, + &reward_pool, + &9999, + ); +} + +#[test] +fn test_update_and_get_historical_leaderboard() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin, xlm_token) = deploy(&env); + + let reward_pool = 10_000_000; + fund_reward_pool(&env, &client, &admin, &xlm_token, reward_pool); + + let season_id = client.create_season(&admin, &100, &200, &reward_pool); + let user1 = Address::generate(&env); + let user2 = Address::generate(&env); + let entries = vec![ + &env, + LeaderboardEntry { + rank: 1, + user: user1.clone(), + points: 100, + correct_predictions: 10, + total_predictions: 15, + }, + LeaderboardEntry { + rank: 2, + user: user2.clone(), + points: 80, + correct_predictions: 8, + total_predictions: 12, + }, + ]; + + client.update_leaderboard(&admin, &season_id, &entries); + + let snapshot = client.get_leaderboard(&season_id); + assert_eq!(snapshot.season_id, season_id); + assert_eq!(snapshot.entries.len(), 2); + assert_eq!(snapshot.entries.get(0).unwrap().user, user1); + assert_eq!(snapshot.entries.get(1).unwrap().user, user2); +} + +#[test] +fn test_list_snapshot_seasons_deduplication() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin, xlm_token) = deploy(&env); + + let reward_pool = 20_000_000; + fund_reward_pool(&env, &client, &admin, &xlm_token, reward_pool); + + let s1 = client.create_season(&admin, &100, &200, &10_000_000); + let s2 = client.create_season(&admin, &201, &300, &10_000_000); + + assert_eq!(client.list_snapshot_seasons().len(), 0); + + let entries = vec![ + &env, + LeaderboardEntry { + rank: 1, + user: Address::generate(&env), + points: 10, + correct_predictions: 1, + total_predictions: 1, + }, + ]; + + client.update_leaderboard(&admin, &s1, &entries); + assert_eq!(client.list_snapshot_seasons().len(), 1); + client.update_leaderboard(&admin, &s1, &entries); + assert_eq!(client.list_snapshot_seasons().len(), 1); + client.update_leaderboard(&admin, &s2, &entries); + assert_eq!(client.list_snapshot_seasons().len(), 2); +} + +#[test] +fn test_get_leaderboard_not_found() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _, _) = deploy(&env); + let result = client.try_get_leaderboard(&99); + assert!(matches!(result, Err(Ok(InsightArenaError::SeasonNotFound)))); +} + +#[test] +fn test_update_leaderboard_unauthorized() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _, _) = deploy(&env); + let stranger = Address::generate(&env); + let result = client.try_update_leaderboard(&stranger, &1, &vec![&env]); + assert!(matches!(result, Err(Ok(InsightArenaError::Unauthorized)))); +} + +#[test] +fn test_update_leaderboard_when_paused() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin, _) = deploy(&env); + client.set_paused(&true); + let result = client.try_update_leaderboard(&admin, &1, &vec![&env]); + assert!(matches!(result, Err(Ok(InsightArenaError::Paused)))); +} + +#[test] +fn test_leaderboard_tie_handling() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin, xlm_token) = deploy(&env); + + let reward_pool = 15_000_000; + fund_reward_pool(&env, &client, &admin, &xlm_token, reward_pool); + + let season_id = client.create_season(&admin, &100, &200, &reward_pool); + let user1 = Address::generate(&env); + let user2 = Address::generate(&env); + let entries = vec![ + &env, + LeaderboardEntry { + rank: 1, + user: user1.clone(), + points: 100, + correct_predictions: 9, + total_predictions: 12, + }, + LeaderboardEntry { + rank: 2, + user: user2.clone(), + points: 100, + correct_predictions: 8, + total_predictions: 12, + }, + ]; + + client.update_leaderboard(&admin, &season_id, &entries); + + let snapshot = client.get_leaderboard(&season_id); + assert_eq!(snapshot.entries.len(), 2); + assert_eq!(snapshot.entries.get(0).unwrap().points, 100); + assert_eq!(snapshot.entries.get(1).unwrap().points, 100); + assert_eq!(client.get_user_season_points(&user1, &season_id), 100); + assert_eq!(client.get_user_season_points(&user2, &season_id), 100); +} diff --git a/contract/tests/market_tests.rs b/contract/tests/market_tests.rs new file mode 100644 index 00000000..127fc106 --- /dev/null +++ b/contract/tests/market_tests.rs @@ -0,0 +1,718 @@ +use insightarena_contract::market::{calculate_price, CreateMarketParams}; +use insightarena_contract::storage_types::{DataKey, Market, Prediction}; +use insightarena_contract::{InsightArenaContract, InsightArenaContractClient, InsightArenaError}; +use soroban_sdk::testutils::{Address as _, Ledger as _}; +use soroban_sdk::token::{Client as TokenClient, StellarAssetClient}; +use soroban_sdk::{symbol_short, vec, Address, Env, String, Symbol, Vec}; + +#[test] +fn test_calculate_price_equal_reserves() { + assert_eq!(calculate_price(1000, 1000).unwrap(), 1_000_000); +} + +#[test] +fn test_calculate_price_double() { + assert_eq!(calculate_price(1000, 2000).unwrap(), 2_000_000); +} + +#[test] +fn test_calculate_price_half() { + assert_eq!(calculate_price(2000, 1000).unwrap(), 500_000); +} + +#[test] +fn test_calculate_price_precision() { + assert_eq!(calculate_price(3000, 1000).unwrap(), 333_333); +} + +fn register_token(env: &Env) -> Address { + let token_admin = Address::generate(env); + env.register_stellar_asset_contract_v2(token_admin) + .address() +} + +fn deploy(env: &Env) -> InsightArenaContractClient<'_> { + let id = env.register(InsightArenaContract, ()); + let client = InsightArenaContractClient::new(env, &id); + let admin = Address::generate(env); + let oracle = Address::generate(env); + let xlm_token = register_token(env); + env.mock_all_auths(); + client.initialize(&admin, &oracle, &200_u32, &xlm_token); + client +} + +fn deploy_with_actors(env: &Env) -> (InsightArenaContractClient<'_>, Address, Address) { + let id = env.register(InsightArenaContract, ()); + let client = InsightArenaContractClient::new(env, &id); + let admin = Address::generate(env); + let oracle = Address::generate(env); + let xlm_token = register_token(env); + env.mock_all_auths(); + client.initialize(&admin, &oracle, &200_u32, &xlm_token); + (client, admin, oracle) +} + +fn deploy_with_token(env: &Env) -> (InsightArenaContractClient<'_>, Address, Address, Address) { + let id = env.register(InsightArenaContract, ()); + let client = InsightArenaContractClient::new(env, &id); + let admin = Address::generate(env); + let oracle = Address::generate(env); + let xlm_token = register_token(env); + env.mock_all_auths(); + client.initialize(&admin, &oracle, &200_u32, &xlm_token); + (client, admin, oracle, xlm_token) +} + +fn default_params(env: &Env) -> CreateMarketParams { + let now = env.ledger().timestamp(); + CreateMarketParams { + title: String::from_str(env, "Will it rain?"), + description: String::from_str(env, "Daily weather market"), + category: Symbol::new(env, "Sports"), + outcomes: vec![env, symbol_short!("yes"), symbol_short!("no")], + end_time: now + 1000, + resolution_time: now + 2000, + dispute_window: 86_400, + creator_fee_bps: 100, + min_stake: 10_000_000, + max_stake: 100_000_000, + is_public: true, + } +} + +#[test] +fn test_create_market_success() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let creator = Address::generate(&env); + + let id = client.create_market(&creator, &default_params(&env)); + assert_eq!(id, 1); + + let market = client.get_market(&id); + assert_eq!(market.market_id, id); + assert_eq!(market.creator, creator); + assert!(!market.is_resolved); + assert!(!market.is_cancelled); +} + +#[test] +fn create_market_success_returns_incremented_id() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let creator = Address::generate(&env); + + let id = client.create_market(&creator, &default_params(&env)); + let id2 = client.create_market(&creator, &default_params(&env)); + + assert_eq!(id, 1); + assert_eq!(id2, 2); +} + +#[test] +fn create_market_fails_end_time_in_past() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let creator = Address::generate(&env); + + let mut params = default_params(&env); + params.end_time = env.ledger().timestamp(); + + let result = client.try_create_market(&creator, ¶ms); + assert!(matches!( + result, + Err(Ok(InsightArenaError::InvalidTimeRange)) + )); +} + +#[test] +fn create_market_fails_resolution_before_end() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let creator = Address::generate(&env); + + let mut params = default_params(&env); + params.resolution_time = params.end_time - 1; + + let result = client.try_create_market(&creator, ¶ms); + assert!(matches!( + result, + Err(Ok(InsightArenaError::InvalidTimeRange)) + )); +} + +#[test] +fn create_market_fails_single_outcome() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let creator = Address::generate(&env); + + let mut params = default_params(&env); + params.outcomes = vec![&env, symbol_short!("yes")]; + + let result = client.try_create_market(&creator, ¶ms); + assert!(matches!(result, Err(Ok(InsightArenaError::InvalidInput)))); +} + +#[test] +fn create_market_fails_fee_too_high() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let creator = Address::generate(&env); + + let mut params = default_params(&env); + params.creator_fee_bps = 501; + + let result = client.try_create_market(&creator, ¶ms); + assert!(matches!(result, Err(Ok(InsightArenaError::InvalidFee)))); +} + +#[test] +fn test_create_market_min_stake_exceeds_max_stake() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let creator = Address::generate(&env); + + let mut params = default_params(&env); + params.min_stake = 100_000_000; + params.max_stake = 10_000_000; + + let result = client.try_create_market(&creator, ¶ms); + assert!(matches!(result, Err(Ok(InsightArenaError::InvalidInput)))); +} + +#[test] +fn create_market_fails_when_paused() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let creator = Address::generate(&env); + + client.set_paused(&true); + + let result = client.try_create_market(&creator, &default_params(&env)); + assert!(matches!(result, Err(Ok(InsightArenaError::Paused)))); +} + +#[test] +#[should_panic(expected = "HostError: Error(Auth")] +fn test_create_market_unauthorised() { + let env = Env::default(); + let id = env.register(InsightArenaContract, ()); + let client = InsightArenaContractClient::new(&env, &id); + let admin = Address::generate(&env); + let oracle = Address::generate(&env); + let xlm_token = register_token(&env); + + env.mock_all_auths(); + client.initialize(&admin, &oracle, &200_u32, &xlm_token); + + let env2 = Env::default(); + let id2 = env2.register(InsightArenaContract, ()); + let client2 = InsightArenaContractClient::new(&env2, &id2); + let admin2 = Address::generate(&env2); + let oracle2 = Address::generate(&env2); + let xlm_token2 = register_token(&env2); + env2.as_contract(&id2, || { + insightarena_contract::config::initialize(&env2, admin2, oracle2, 200, xlm_token2).unwrap(); + }); + + let creator = Address::generate(&env2); + client2.create_market(&creator, &default_params(&env2)); +} + +#[test] +fn create_market_fails_stake_too_low() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let creator = Address::generate(&env); + + let mut params = default_params(&env); + params.min_stake = 1; + + let result = client.try_create_market(&creator, ¶ms); + assert!(matches!(result, Err(Ok(InsightArenaError::StakeTooLow)))); +} + +#[test] +fn create_market_fails_when_category_not_whitelisted() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let creator = Address::generate(&env); + + let mut params = default_params(&env); + params.category = Symbol::new(&env, "Weather"); + + let result = client.try_create_market(&creator, ¶ms); + assert!(matches!(result, Err(Ok(InsightArenaError::InvalidInput)))); +} + +#[test] +fn test_create_market_with_duplicate_outcomes() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let creator = Address::generate(&env); + + let mut params = default_params(&env); + params.outcomes = vec![&env, symbol_short!("yes"), symbol_short!("yes")]; + + let result = client.try_create_market(&creator, ¶ms); + assert!(matches!(result, Err(Ok(InsightArenaError::InvalidInput)))); +} + +#[test] +fn list_categories_returns_seeded_defaults() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let categories = client.list_categories(); + + assert!(categories.contains(Symbol::new(&env, "Sports"))); + assert!(categories.contains(Symbol::new(&env, "Crypto"))); + assert!(categories.contains(Symbol::new(&env, "Politics"))); + assert!(categories.contains(Symbol::new(&env, "Entertainment"))); + assert!(categories.contains(Symbol::new(&env, "Science"))); + assert!(categories.contains(Symbol::new(&env, "Other"))); +} + +#[test] +fn add_category_allows_admin_to_extend_whitelist() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin, _) = deploy_with_actors(&env); + let weather = Symbol::new(&env, "Weather"); + + client.add_category(&admin, &weather); + + assert!(client.list_categories().contains(weather)); +} + +#[test] +fn remove_category_blocks_future_market_creation() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin, _) = deploy_with_actors(&env); + let creator = Address::generate(&env); + let science = Symbol::new(&env, "Science"); + + client.remove_category(&admin, &science); + + let mut params = default_params(&env); + params.category = science; + + let result = client.try_create_market(&creator, ¶ms); + assert!(matches!(result, Err(Ok(InsightArenaError::InvalidInput)))); +} + +#[test] +fn non_admin_cannot_mutate_categories() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin, _) = deploy_with_actors(&env); + let random = Address::generate(&env); + + let add_result = client.try_add_category(&random, &Symbol::new(&env, "Weather")); + let remove_result = client.try_remove_category(&random, &Symbol::new(&env, "Sports")); + + assert!(matches!( + add_result, + Err(Ok(InsightArenaError::Unauthorized)) + )); + assert!(matches!( + remove_result, + Err(Ok(InsightArenaError::Unauthorized)) + )); +} + +#[test] +fn get_market_returns_correct_market() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let creator = Address::generate(&env); + + let id = client.create_market(&creator, &default_params(&env)); + let market = client.get_market(&id); + assert_eq!(market.market_id, id); + assert_eq!(market.creator, creator); +} + +#[test] +fn get_market_returns_not_found_for_missing_id() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + + let result = client.try_get_market(&99_u64); + assert!(matches!(result, Err(Ok(InsightArenaError::MarketNotFound)))); +} + +#[test] +fn get_market_count_zero_before_any_market() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + assert_eq!(client.get_market_count(), 0); +} + +#[test] +fn get_market_count_increments_with_each_market() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let creator = Address::generate(&env); + + client.create_market(&creator, &default_params(&env)); + client.create_market(&creator, &default_params(&env)); + + assert_eq!(client.get_market_count(), 2); +} + +#[test] +fn list_markets_empty_when_no_markets() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + assert_eq!(client.list_markets(&1_u64, &10_u32).len(), 0); +} + +#[test] +fn get_markets_by_category_returns_paginated_results() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let creator = Address::generate(&env); + let sports_category = Symbol::new(&env, "Sports"); + + let first_sports = client.create_market(&creator, &default_params(&env)); + + let mut crypto = default_params(&env); + crypto.category = Symbol::new(&env, "Crypto"); + client.create_market(&creator, &crypto); + + let second_sports_id = client.create_market(&creator, &default_params(&env)); + let third_sports_id = client.create_market(&creator, &default_params(&env)); + + let first_page = client.get_markets_by_category(&sports_category, &0_u64, &2_u32); + let second_page = client.get_markets_by_category(&sports_category, &2_u64, &2_u32); + + assert_eq!(first_page.len(), 2); + assert_eq!(first_page.get(0).unwrap().market_id, first_sports); + assert_eq!(first_page.get(1).unwrap().market_id, second_sports_id); + assert_eq!(second_page.len(), 1); + assert_eq!(second_page.get(0).unwrap().market_id, third_sports_id); +} + +#[test] +fn category_index_is_kept_in_sync_on_market_creation() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let creator = Address::generate(&env); + let sports = Symbol::new(&env, "Sports"); + + let first_id = client.create_market(&creator, &default_params(&env)); + + let mut crypto = default_params(&env); + crypto.category = Symbol::new(&env, "Crypto"); + client.create_market(&creator, &crypto); + + let second_id = client.create_market(&creator, &default_params(&env)); + + let stored_index = env.as_contract(&client.address, || { + env.storage() + .persistent() + .get::>(&DataKey::CategoryIndex(sports.clone())) + .unwrap() + }); + + assert_eq!(stored_index.get(0), Some(first_id)); + assert_eq!(stored_index.get(1), Some(second_id)); +} + +#[test] +fn list_markets_returns_all_when_within_limit() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let creator = Address::generate(&env); + + for _ in 0..3 { + client.create_market(&creator, &default_params(&env)); + } + + let list = client.list_markets(&1_u64, &10_u32); + assert_eq!(list.len(), 3); + assert_eq!(list.get(2).unwrap().market_id, 3); +} + +#[test] +fn list_markets_respects_pagination_start() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let creator = Address::generate(&env); + + for _ in 0..5 { + client.create_market(&creator, &default_params(&env)); + } + + let list = client.list_markets(&3_u64, &10_u32); + assert_eq!(list.len(), 3); + assert_eq!(list.get(0).unwrap().market_id, 3); +} + +#[test] +fn list_markets_caps_at_max_limit_50() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let creator = Address::generate(&env); + + for _ in 0..60 { + client.create_market(&creator, &default_params(&env)); + } + + assert_eq!(client.list_markets(&1_u64, &100_u32).len(), 50); +} + +#[test] +fn list_markets_empty_when_start_out_of_bounds() { + let env = Env::default(); + env.mock_all_auths(); + let client = deploy(&env); + let creator = Address::generate(&env); + + client.create_market(&creator, &default_params(&env)); + assert_eq!(client.list_markets(&99_u64, &10_u32).len(), 0); +} + +#[test] +fn close_market_fails_before_end_time() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin, oracle) = deploy_with_actors(&env); + let creator = Address::generate(&env); + + let id = client.create_market(&creator, &default_params(&env)); + let result = client.try_close_market(&oracle, &id); + + assert!(matches!( + result, + Err(Ok(InsightArenaError::MarketStillOpen)) + )); +} + +#[test] +fn close_market_success_by_oracle_after_end_time() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin, oracle) = deploy_with_actors(&env); + let creator = Address::generate(&env); + + let id = client.create_market(&creator, &default_params(&env)); + env.ledger().set_timestamp(env.ledger().timestamp() + 1001); + + client.close_market(&oracle, &id); + + let market = client.get_market(&id); + assert!(market.is_closed); + assert!(!market.is_resolved); +} + +#[test] +fn close_market_success_by_admin_after_end_time() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin, _) = deploy_with_actors(&env); + let creator = Address::generate(&env); + + let id = client.create_market(&creator, &default_params(&env)); + env.ledger().set_timestamp(env.ledger().timestamp() + 1001); + + client.close_market(&admin, &id); + assert!(client.get_market(&id).is_closed); +} + +#[test] +fn close_market_fails_when_already_resolved() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin, oracle) = deploy_with_actors(&env); + let creator = Address::generate(&env); + + let id = client.create_market(&creator, &default_params(&env)); + env.ledger().set_timestamp(env.ledger().timestamp() + 1001); + client.close_market(&oracle, &id); + + let contract_id = client.address.clone(); + let mut market: Market = env.as_contract(&contract_id, || { + env.storage() + .persistent() + .get(&DataKey::Market(id)) + .unwrap() + }); + market.is_resolved = true; + env.as_contract(&contract_id, || { + env.storage() + .persistent() + .set(&DataKey::Market(id), &market); + }); + + let result = client.try_close_market(&oracle, &id); + assert!(matches!( + result, + Err(Ok(InsightArenaError::MarketAlreadyResolved)) + )); +} + +#[test] +fn cancel_market_fails_for_non_admin() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin, _oracle, _) = deploy_with_token(&env); + let creator = Address::generate(&env); + let random = Address::generate(&env); + + let id = client.create_market(&creator, &default_params(&env)); + let result = client.try_cancel_market(&random, &id); + + assert!(matches!(result, Err(Ok(InsightArenaError::Unauthorized)))); +} + +#[test] +fn cancel_market_fails_market_not_found() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin, _oracle, _) = deploy_with_token(&env); + + let result = client.try_cancel_market(&admin, &99_u64); + assert!(matches!(result, Err(Ok(InsightArenaError::MarketNotFound)))); +} + +#[test] +fn cancel_market_fails_when_already_resolved() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin, _oracle, _) = deploy_with_token(&env); + let creator = Address::generate(&env); + + let id = client.create_market(&creator, &default_params(&env)); + let contract_id = client.address.clone(); + let mut market: Market = env.as_contract(&contract_id, || { + env.storage() + .persistent() + .get(&DataKey::Market(id)) + .unwrap() + }); + market.is_resolved = true; + env.as_contract(&contract_id, || { + env.storage() + .persistent() + .set(&DataKey::Market(id), &market); + }); + + let result = client.try_cancel_market(&admin, &id); + assert!(matches!( + result, + Err(Ok(InsightArenaError::MarketAlreadyResolved)) + )); +} + +#[test] +fn cancel_market_fails_when_already_cancelled() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin, _oracle, _) = deploy_with_token(&env); + let creator = Address::generate(&env); + + let id = client.create_market(&creator, &default_params(&env)); + client.cancel_market(&admin, &id); + + let result = client.try_cancel_market(&admin, &id); + assert!(matches!( + result, + Err(Ok(InsightArenaError::MarketAlreadyCancelled)) + )); +} + +#[test] +fn cancel_market_success_no_predictors() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin, _oracle, _) = deploy_with_token(&env); + let creator = Address::generate(&env); + + let id = client.create_market(&creator, &default_params(&env)); + client.cancel_market(&admin, &id); + + let market = client.get_market(&id); + assert!(market.is_cancelled); + assert!(!market.is_resolved); +} + +#[test] +fn cancel_market_refunds_all_predictors() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin, _oracle, xlm_token) = deploy_with_token(&env); + let creator = Address::generate(&env); + + let id = client.create_market(&creator, &default_params(&env)); + let predictor_a = Address::generate(&env); + let predictor_b = Address::generate(&env); + let stake_a: i128 = 20_000_000; + let stake_b: i128 = 50_000_000; + let contract_id = client.address.clone(); + + env.as_contract(&contract_id, || { + let pred_a = Prediction::new( + id, + predictor_a.clone(), + symbol_short!("yes"), + stake_a, + env.ledger().timestamp(), + ); + let pred_b = Prediction::new( + id, + predictor_b.clone(), + symbol_short!("no"), + stake_b, + env.ledger().timestamp(), + ); + + env.storage() + .persistent() + .set(&DataKey::Prediction(id, predictor_a.clone()), &pred_a); + env.storage() + .persistent() + .set(&DataKey::Prediction(id, predictor_b.clone()), &pred_b); + + let mut predictors = Vec::new(&env); + predictors.push_back(predictor_a.clone()); + predictors.push_back(predictor_b.clone()); + env.storage() + .persistent() + .set(&DataKey::PredictorList(id), &predictors); + }); + + StellarAssetClient::new(&env, &xlm_token).mint(&contract_id, &(stake_a + stake_b)); + + let token_client = TokenClient::new(&env, &xlm_token); + client.cancel_market(&admin, &id); + + assert_eq!(token_client.balance(&predictor_a), stake_a); + assert_eq!(token_client.balance(&predictor_b), stake_b); + assert!(client.get_market(&id).is_cancelled); +}