diff --git a/contracts/predict-iq/src/errors.rs b/contracts/predict-iq/src/errors.rs index ddc0dd3c..967ea85d 100644 --- a/contracts/predict-iq/src/errors.rs +++ b/contracts/predict-iq/src/errors.rs @@ -89,6 +89,8 @@ pub enum ErrorCode { UpgradeFailed = 132, ParentMarketNotResolved = 133, ParentMarketInvalidOutcome = 134, + MarketIdOverflow = 135, + MarketIdCollision = 136, InsufficientVotingWeight = 135, MarketNotCancelled = 136, BetNotFound = 137, diff --git a/contracts/predict-iq/src/modules/markets.rs b/contracts/predict-iq/src/modules/markets.rs index 0a45704c..e9f85bab 100644 --- a/contracts/predict-iq/src/modules/markets.rs +++ b/contracts/predict-iq/src/modules/markets.rs @@ -101,12 +101,7 @@ pub fn create_market( token_client.transfer(&creator, &e.current_contract_address(), &creation_deposit); } - let mut count: u64 = e - .storage() - .instance() - .get(&DataKey::MarketCount) - .unwrap_or(0); - count += 1; + let count = allocate_market_id(e)?; let num_outcomes = options.len() as u32; @@ -164,6 +159,24 @@ pub fn create_market( Ok(count) } +pub fn allocate_market_id(e: &Env) -> Result { + let current_count: u64 = e + .storage() + .instance() + .get(&DataKey::MarketCount) + .unwrap_or(0); + + let next_id = current_count + .checked_add(1) + .ok_or(ErrorCode::MarketIdOverflow)?; + + if e.storage().persistent().has(&DataKey::Market(next_id)) { + return Err(ErrorCode::MarketIdCollision); + } + + e.storage().instance().set(&DataKey::MarketCount, &next_id); + + Ok(next_id) /// Validates that a parent market exists, is resolved, and resolved to the required outcome. /// Called by both create_market and place_bet to enforce consistent conditional market rules. pub fn validate_parent_market( diff --git a/contracts/predict-iq/src/test.rs b/contracts/predict-iq/src/test.rs index 200b652e..33274f49 100644 --- a/contracts/predict-iq/src/test.rs +++ b/contracts/predict-iq/src/test.rs @@ -1,5 +1,8 @@ #![cfg(test)] use super::*; +use crate::modules::markets::{self, DataKey}; +use soroban_sdk::testutils::{Address as _, Ledger}; +use soroban_sdk::{token, Address, Env, String, Vec}; use soroban_sdk::testutils::{Address as _, Ledger as _}; use soroban_sdk::{token, Address, BytesN, Env, String, Vec}; @@ -63,6 +66,34 @@ fn create_test_market( ) } +fn make_stored_market(e: &Env, id: u64) -> types::Market { + let mut options = Vec::new(e); + options.push_back(String::from_str(e, "Yes")); + options.push_back(String::from_str(e, "No")); + + types::Market { + id, + creator: Address::generate(e), + description: String::from_str(e, "Seeded Market"), + options, + status: types::MarketStatus::Active, + deadline: 1000, + resolution_deadline: 2000, + winning_outcome: None, + oracle_config: types::OracleConfig { + oracle_address: Address::generate(e), + feed_id: String::from_str(e, "seeded_feed"), + min_responses: Some(1), + }, + total_staked: 0, + payout_mode: types::PayoutMode::Pull, + tier: types::MarketTier::Basic, + creation_deposit: 0, + parent_id: 0, + parent_outcome_idx: 0, + } +} + #[test] fn test_market_creation_fails_without_deposit() { let (e, _admin, _contract_id, client) = setup_test_env(); @@ -134,6 +165,124 @@ fn test_market_creation_with_sufficient_deposit() { assert_eq!(market.tier, types::MarketTier::Basic); } +#[test] +fn test_market_ids_are_unique_and_sequential() { + let (e, _admin, _contract_id, client) = setup_test_env(); + client.set_creation_deposit(&0); + + let creator = Address::generate(&e); + let native_token = Address::generate(&e); + + let mut previous_id = 0u64; + + for expected_id in 1..=1_000u64 { + let market_id = create_test_market( + &client, + &e, + &creator, + types::MarketTier::Basic, + &native_token, + ); + + assert_eq!(market_id, expected_id); + assert!(market_id > previous_id); + previous_id = market_id; + } +} + +#[test] +fn test_market_id_overflow_returns_error() { + let (e, _admin, contract_id, client) = setup_test_env(); + client.set_creation_deposit(&0); + + e.as_contract(&contract_id, || { + e.storage().instance().set(&DataKey::MarketCount, &u64::MAX); + }); + + let creator = Address::generate(&e); + let native_token = Address::generate(&e); + + let result = client.try_create_market( + &creator, + &String::from_str(&e, "Overflow Market"), + &{ + let mut opts = Vec::new(&e); + opts.push_back(String::from_str(&e, "Yes")); + opts.push_back(String::from_str(&e, "No")); + opts + }, + &1000, + &2000, + &types::OracleConfig { + oracle_address: Address::generate(&e), + feed_id: String::from_str(&e, "overflow_feed"), + min_responses: Some(1), + }, + &types::MarketTier::Basic, + &native_token, + &0, + &0, + ); + + assert_eq!(result, Err(Ok(ErrorCode::MarketIdOverflow))); +} + +#[test] +fn test_market_id_collision_returns_error() { + let (e, _admin, contract_id, client) = setup_test_env(); + client.set_creation_deposit(&0); + + e.as_contract(&contract_id, || { + e.storage().instance().set(&DataKey::MarketCount, &1u64); + e.storage() + .persistent() + .set(&DataKey::Market(2), &make_stored_market(&e, 2)); + }); + + let creator = Address::generate(&e); + let native_token = Address::generate(&e); + + let result = client.try_create_market( + &creator, + &String::from_str(&e, "Collision Market"), + &{ + let mut opts = Vec::new(&e); + opts.push_back(String::from_str(&e, "Yes")); + opts.push_back(String::from_str(&e, "No")); + opts + }, + &1000, + &2000, + &types::OracleConfig { + oracle_address: Address::generate(&e), + feed_id: String::from_str(&e, "collision_feed"), + min_responses: Some(1), + }, + &types::MarketTier::Basic, + &native_token, + &0, + &0, + ); + + assert_eq!(result, Err(Ok(ErrorCode::MarketIdCollision))); +} + +#[test] +fn test_market_id_allocator_simulates_one_million_unique_ids() { + let (e, _admin, contract_id, _client) = setup_test_env(); + + let mut last_id = 0u64; + e.as_contract(&contract_id, || { + for expected_id in 1..=1_000_000u64 { + let allocated_id = markets::allocate_market_id(&e).unwrap(); + assert_eq!(allocated_id, expected_id); + last_id = allocated_id; + } + }); + + assert_eq!(last_id, 1_000_000); +} + #[test] fn test_pro_reputation_skips_deposit() { let (e, _admin, _contract_id, client) = setup_test_env();