Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions contracts/predict-iq/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ pub enum ErrorCode {
UpgradeFailed = 132,
ParentMarketNotResolved = 133,
ParentMarketInvalidOutcome = 134,
MarketIdOverflow = 135,
MarketIdCollision = 136,
InsufficientVotingWeight = 135,
MarketNotCancelled = 136,
BetNotFound = 137,
Expand Down
25 changes: 19 additions & 6 deletions contracts/predict-iq/src/modules/markets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -164,6 +159,24 @@ pub fn create_market(
Ok(count)
}

pub fn allocate_market_id(e: &Env) -> Result<u64, ErrorCode> {
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(
Expand Down
149 changes: 149 additions & 0 deletions contracts/predict-iq/src/test.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
Loading