From 001fb61dde880e3a4fdb673b395226f8befcd5af Mon Sep 17 00:00:00 2001 From: Jagadeesh Date: Thu, 26 Feb 2026 11:52:52 +0530 Subject: [PATCH] feat: implement rate limiting for bets and admin events - Added rate limiting functionality to restrict the number of bets per user and events per admin within a specified time window. - Introduced new configuration options in RateLimitConfig for bet limits and admin event limits. - Updated relevant functions in lib.rs and rate_limiter.rs to enforce these limits. - Added tests to ensure rate limits are correctly enforced and function as expected. - Created a new file to store seeds for failure cases generated by proptest. --- .../property_based_tests.txt | 11 + contracts/predictify-hybrid/src/lib.rs | 49 +- .../predictify-hybrid/src/rate_limiter.rs | 68 ++- contracts/predictify-hybrid/src/test.rs | 479 ++++++++++++++++++ 4 files changed, 595 insertions(+), 12 deletions(-) create mode 100644 contracts/predictify-hybrid/proptest-regressions/property_based_tests.txt diff --git a/contracts/predictify-hybrid/proptest-regressions/property_based_tests.txt b/contracts/predictify-hybrid/proptest-regressions/property_based_tests.txt new file mode 100644 index 00000000..9b7034a3 --- /dev/null +++ b/contracts/predictify-hybrid/proptest-regressions/property_based_tests.txt @@ -0,0 +1,11 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc d5beda84e86a79af46fd30e3b919c1b5e26f7dd893272eaff176639eb01f9b82 # shrinks to question = "Will BTC reach $100k by year end?", duration_days = 1 +cc ec287376804edf904d828c9deab125362ba03a6a97487fe2fcf8fd95a56abf2c # shrinks to question = "Will BTC reach $100k by year end?", outcome_count = 2, user_index = 0, stake_amount = 1000000, outcome_choice = 0 +cc 683c735e4e7a0d574dd602cdce52927faac4d2f500788735556d60e47c330bec # shrinks to question = "Will BTC reach $100k by year end?", outcome_count = 2, duration_days = 1, threshold = 1, comparison = "gt" +cc 37decdae6683c73fab564d279240ad9de15b9e6edd3871d740ae4a0e26c62b23 # shrinks to question = "Will BTC reach $100k by year end?", outcome_count = 2, duration_days = 1, threshold = 1, comparison = "gt" +cc 7ec0c802dcefd5fa82fa4ee7cbbde7e177ea6d94bd772961214529af44625848 # shrinks to question = "Will BTC reach $100k by year end?", user_count = 2, stakes = [1000000, 1000000] diff --git a/contracts/predictify-hybrid/src/lib.rs b/contracts/predictify-hybrid/src/lib.rs index 379d25f0..4609658e 100644 --- a/contracts/predictify-hybrid/src/lib.rs +++ b/contracts/predictify-hybrid/src/lib.rs @@ -556,6 +556,16 @@ impl PredictifyHybrid { panic_with_error!(env, Error::InvalidInput); } + // Rate limit: max events per admin per time window (when config set) + match rate_limiter::RateLimiter::new(env.clone()).rate_limit_admin_events(admin.clone()) { + Ok(()) => {} + Err(rate_limiter::RateLimiterError::ConfigNotFound) => {} + Err(rate_limiter::RateLimiterError::RateLimitExceeded) => { + panic_with_error!(env, Error::InvalidInput); + } + Err(_) => panic_with_error!(env, Error::InvalidInput), + } + // Validate metadata using InputValidator if let Err(_) = crate::validation::InputValidator::validate_question_length(&question) { panic_with_error!(env, Error::InvalidQuestion); @@ -1210,6 +1220,15 @@ impl PredictifyHybrid { if ReentrancyGuard::check_reentrancy_state(&env).is_err() { panic_with_error!(env, Error::InvalidState); } + // Rate limit: max bets per user per time window (when config set) + match rate_limiter::RateLimiter::new(env.clone()).rate_limit_bets(user.clone()) { + Ok(()) => {} + Err(rate_limiter::RateLimiterError::ConfigNotFound) => {} + Err(rate_limiter::RateLimiterError::RateLimitExceeded) => { + panic_with_error!(env, Error::InvalidInput); + } + Err(_) => panic_with_error!(env, Error::InvalidInput), + } // Use the BetManager to handle the bet placement match bets::BetManager::place_bet(&env, user.clone(), market_id, outcome, amount) { Ok(bet) => { @@ -4774,8 +4793,12 @@ impl PredictifyHybrid { let stored_admin: Option
= env.storage().persistent().get(&Symbol::new(&env, "Admin")); let is_admin = stored_admin.as_ref().map_or(false, |a| a == &caller); - let timeout_passed = current_time.saturating_sub(market.end_time) - >= config::DEFAULT_RESOLUTION_TIMEOUT_SECONDS; + let effective_timeout = if market.resolution_timeout == 0 { + config::DEFAULT_RESOLUTION_TIMEOUT_SECONDS + } else { + market.resolution_timeout + }; + let timeout_passed = current_time.saturating_sub(market.end_time) >= effective_timeout; if !is_admin && !timeout_passed { return Err(Error::Unauthorized); } @@ -5325,6 +5348,28 @@ impl PredictifyHybrid { admin::ContractPauseManager::unpause(&env, &admin) } + /// Set or update rate limits (admin only). Configures max bets per user per time window, + /// max events per admin per time window, and existing voting/dispute/oracle limits. + /// When config is set, place_bet and create_market enforce these limits. + pub fn set_rate_limits( + env: Env, + admin: Address, + config: rate_limiter::RateLimitConfig, + ) -> Result<(), Error> { + admin.require_auth(); + let stored_admin: Address = env + .storage() + .persistent() + .get(&Symbol::new(&env, "Admin")) + .ok_or(Error::AdminNotSet)?; + if admin != stored_admin { + return Err(Error::Unauthorized); + } + rate_limiter::RateLimiter::new(env) + .update_rate_limits(admin, config) + .map_err(|_| Error::InvalidInput) + } + /// Returns true if the contract is currently paused. pub fn is_contract_paused(env: Env) -> bool { admin::ContractPauseManager::is_contract_paused(&env) diff --git a/contracts/predictify-hybrid/src/rate_limiter.rs b/contracts/predictify-hybrid/src/rate_limiter.rs index eba4abc9..1c3fb7bf 100644 --- a/contracts/predictify-hybrid/src/rate_limiter.rs +++ b/contracts/predictify-hybrid/src/rate_limiter.rs @@ -3,10 +3,12 @@ use soroban_sdk::{contract, contracterror, contractimpl, contracttype, Address, #[contracttype] #[derive(Clone, Debug, PartialEq)] pub struct RateLimitConfig { - pub voting_limit: u32, // Max votes per time window - pub dispute_limit: u32, // Max disputes per time window - pub oracle_call_limit: u32, // Max oracle calls per time window - pub time_window_seconds: u64, // Time window in seconds + pub voting_limit: u32, // Max votes per time window + pub dispute_limit: u32, // Max disputes per time window + pub oracle_call_limit: u32, // Max oracle calls per time window + pub bet_limit: u32, // Max bets per user per time window (0 = no limit) + pub events_per_admin_limit: u32, // Max events per admin per time window (0 = no limit) + pub time_window_seconds: u64, // Time window in seconds } // Rate limit tracking @@ -21,9 +23,11 @@ pub struct RateLimit { #[contracttype] pub enum RateLimiterData { Config, - UserVoting(Address, Symbol), // user, market_id - UserDisputes(Address, Symbol), // user, market_id + UserVoting(Address, Symbol), // user, market_id + UserDisputes(Address, Symbol), // user, market_id OracleCalls(Symbol), // market_id + UserBets(Address), // user (global bet count per window) + AdminEvents(Address), // admin (events created per window) } pub struct RateLimiter { @@ -154,14 +158,41 @@ impl RateLimiter { Ok(()) } - // Update rate limits (admin only) + /// Rate limit bets: max bets per user per time window (global across markets). + /// Returns Ok(()) if within limit or config not set; ConfigNotFound is used by caller to skip check. + /// Caller (e.g. place_bet) must have already authenticated user. + pub fn rate_limit_bets(&self, user: Address) -> Result<(), RateLimiterError> { + let config = self.get_config()?; + if config.bet_limit == 0 { + return Ok(()); + } + let key = RateLimiterData::UserBets(user.clone()); + let limit = self.get_or_create_limit(&key); + self.check_limit(limit.count, config.bet_limit)?; + self.update_limit(&key, limit, config.time_window_seconds)?; + Ok(()) + } + + /// Rate limit event creation: max events per admin per time window. + /// Caller (e.g. create_market) must have already authenticated admin. + pub fn rate_limit_admin_events(&self, admin: Address) -> Result<(), RateLimiterError> { + let config = self.get_config()?; + if config.events_per_admin_limit == 0 { + return Ok(()); + } + let key = RateLimiterData::AdminEvents(admin.clone()); + let limit = self.get_or_create_limit(&key); + self.check_limit(limit.count, config.events_per_admin_limit)?; + self.update_limit(&key, limit, config.time_window_seconds)?; + Ok(()) + } + + // Update rate limits (admin only). Caller must have already authenticated admin. pub fn update_rate_limits( &self, - admin: Address, + _admin: Address, limits: RateLimitConfig, ) -> Result<(), RateLimiterError> { - admin.require_auth(); - self.validate_rate_limit_configuration(&limits)?; self.env @@ -213,6 +244,13 @@ impl RateLimiter { return Err(RateLimiterError::InvalidOracleCallLimit); } + if config.bet_limit > 10000 { + return Err(RateLimiterError::InvalidBetLimit); + } + if config.events_per_admin_limit > 1000 { + return Err(RateLimiterError::InvalidEventsLimit); + } + // Time window should be between 1 minute and 30 days if config.time_window_seconds < 60 || config.time_window_seconds > 2592000 { return Err(RateLimiterError::InvalidTimeWindow); @@ -244,6 +282,8 @@ pub enum RateLimiterError { InvalidOracleCallLimit = 5, InvalidTimeWindow = 6, Unauthorized = 7, + InvalidBetLimit = 8, + InvalidEventsLimit = 9, } #[contract] @@ -334,6 +374,8 @@ mod tests { voting_limit: 10, dispute_limit: 5, oracle_call_limit: 20, + bet_limit: 50, + events_per_admin_limit: 10, time_window_seconds: 3600, // 1 hour } } @@ -426,6 +468,8 @@ mod tests { voting_limit: 20000, dispute_limit: 5, oracle_call_limit: 20, + bet_limit: 0, + events_per_admin_limit: 0, time_window_seconds: 3600, }; let result = RateLimiterContract::validate_rate_limit_config(env.clone(), invalid_config); @@ -436,6 +480,8 @@ mod tests { voting_limit: 10, dispute_limit: 5, oracle_call_limit: 20, + bet_limit: 0, + events_per_admin_limit: 0, time_window_seconds: 30, // Less than 60 }; let result = RateLimiterContract::validate_rate_limit_config(env.clone(), invalid_config); @@ -461,6 +507,8 @@ mod tests { voting_limit: 20, dispute_limit: 10, oracle_call_limit: 30, + bet_limit: 100, + events_per_admin_limit: 20, time_window_seconds: 7200, }; diff --git a/contracts/predictify-hybrid/src/test.rs b/contracts/predictify-hybrid/src/test.rs index 038d8a4a..d1974905 100644 --- a/contracts/predictify-hybrid/src/test.rs +++ b/contracts/predictify-hybrid/src/test.rs @@ -2801,6 +2801,485 @@ fn test_refund_on_oracle_failure_after_timeout_any_caller() { assert_eq!(total_refunded, 10_000_000); } +/// #251: Refund uses per-market resolution_timeout when set (not default). +#[test] +fn test_refund_on_oracle_failure_uses_market_resolution_timeout() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + let outcomes = vec![ + &test.env, + String::from_str(&test.env, "yes"), + String::from_str(&test.env, "no"), + ]; + let resolution_timeout: u64 = 3600; // 1 hour + test.env.mock_all_auths(); + let market_id = client.create_market( + &test.admin, + &String::from_str(&test.env, "Will BTC hit $50k?"), + &outcomes, + &30, + &OracleConfig { + provider: OracleProvider::Reflector, + oracle_address: Address::generate(&test.env), + feed_id: String::from_str(&test.env, "BTC"), + threshold: 50_000_00, + comparison: String::from_str(&test.env, "gt"), + }, + &None, + &resolution_timeout, + &None, + &None, + &None, + ); + let user1 = test.create_funded_user(); + test.env.mock_all_auths(); + client.place_bet( + &user1, + &market_id, + &String::from_str(&test.env, "yes"), + &10_000_000, + ); + let market = test.env.as_contract(&test.contract_id, || { + test.env + .storage() + .persistent() + .get::(&market_id) + .unwrap() + }); + let any_caller = test.create_funded_user(); + // After market resolution_timeout: any caller can refund (per-market timeout) + test.env.ledger().set(LedgerInfo { + timestamp: market.end_time + resolution_timeout + 1, + protocol_version: 22, + sequence_number: test.env.ledger().sequence(), + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 1, + min_persistent_entry_ttl: 1, + max_entry_ttl: 10000, + }); + test.env.mock_all_auths(); + let total_refunded = client.refund_on_oracle_failure(&any_caller, &market_id); + assert_eq!(total_refunded, 10_000_000); +} + +// ===== TESTS FOR RATE LIMITING (#259) ===== + +#[test] +fn test_bet_rate_limit_enforced_when_config_set() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + let market1 = test.create_test_market(); + let market2 = test.create_test_market(); + let market3 = test.create_test_market(); + let user = test.create_funded_user(); + let config = crate::rate_limiter::RateLimitConfig { + voting_limit: 10, + dispute_limit: 5, + oracle_call_limit: 20, + bet_limit: 2, + events_per_admin_limit: 10, + time_window_seconds: 3600, + }; + test.env.mock_all_auths(); + client.set_rate_limits(&test.admin, &config); + test.env.mock_all_auths(); + client.place_bet(&user, &market1, &String::from_str(&test.env, "yes"), &1_000_000); + test.env.mock_all_auths(); + client.place_bet(&user, &market2, &String::from_str(&test.env, "yes"), &1_000_000); + test.env.mock_all_auths(); + let res = client.try_place_bet(&user, &market3, &String::from_str(&test.env, "yes"), &1_000_000); + assert!(res.is_err()); +} + +#[test] +fn test_bet_rate_limit_at_limit_ok() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + let market1 = test.create_test_market(); + let market2 = test.create_test_market(); + let user = test.create_funded_user(); + let config = crate::rate_limiter::RateLimitConfig { + voting_limit: 10, + dispute_limit: 5, + oracle_call_limit: 20, + bet_limit: 2, + events_per_admin_limit: 10, + time_window_seconds: 3600, + }; + test.env.mock_all_auths(); + client.set_rate_limits(&test.admin, &config); + test.env.mock_all_auths(); + client.place_bet(&user, &market1, &String::from_str(&test.env, "yes"), &1_000_000); + test.env.mock_all_auths(); + client.place_bet(&user, &market2, &String::from_str(&test.env, "yes"), &1_000_000); + // Exactly at limit: both bets succeeded (different markets) +} + +#[test] +fn test_bet_rate_limit_without_config_ok() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + let market_id = test.create_test_market(); + let user = test.create_funded_user(); + // No set_rate_limits called - place_bet should succeed + test.env.mock_all_auths(); + client.place_bet(&user, &market_id, &String::from_str(&test.env, "yes"), &1_000_000); + // No limit enforced when config not set +} + +// ===== TESTS FOR MULTI-OUTCOME MARKETS (#248) ===== + +#[test] +fn test_multi_outcome_creation_binary() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + let outcomes = vec![ + &test.env, + String::from_str(&test.env, "yes"), + String::from_str(&test.env, "no"), + ]; + test.env.mock_all_auths(); + let market_id = client.create_market( + &test.admin, + &String::from_str(&test.env, "Binary outcome question?"), + &outcomes, + &30, + &OracleConfig { + provider: OracleProvider::Reflector, + oracle_address: Address::generate(&test.env), + feed_id: String::from_str(&test.env, "BTC"), + threshold: 100, + comparison: String::from_str(&test.env, "gt"), + }, + &None, + &0, + &None, + &None, + &None, + ); + let market = test.env.as_contract(&test.contract_id, || { + test.env + .storage() + .persistent() + .get::(&market_id) + .unwrap() + }); + assert_eq!(market.outcomes.len(), 2); +} + +#[test] +fn test_multi_outcome_creation_three_outcomes() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + let outcomes = vec![ + &test.env, + String::from_str(&test.env, "A1"), + String::from_str(&test.env, "B2"), + String::from_str(&test.env, "C3"), + ]; + test.env.mock_all_auths(); + let market_id = client.create_market( + &test.admin, + &String::from_str(&test.env, "Which outcome will win?"), + &outcomes, + &30, + &OracleConfig { + provider: OracleProvider::Reflector, + oracle_address: Address::generate(&test.env), + feed_id: String::from_str(&test.env, "X"), + threshold: 100, + comparison: String::from_str(&test.env, "gt"), + }, + &None, + &0, + &None, + &None, + &None, + ); + let market = test.env.as_contract(&test.contract_id, || { + test.env + .storage() + .persistent() + .get::(&market_id) + .unwrap() + }); + assert_eq!(market.outcomes.len(), 3); +} + +#[test] +fn test_multi_outcome_invalid_outcome_rejected() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + let outcomes = vec![ + &test.env, + String::from_str(&test.env, "A1"), + String::from_str(&test.env, "B2"), + String::from_str(&test.env, "C3"), + ]; + test.env.mock_all_auths(); + let market_id = client.create_market( + &test.admin, + &String::from_str(&test.env, "Which outcome will win?"), + &outcomes, + &30, + &OracleConfig { + provider: OracleProvider::Reflector, + oracle_address: Address::generate(&test.env), + feed_id: String::from_str(&test.env, "X"), + threshold: 100, + comparison: String::from_str(&test.env, "gt"), + }, + &None, + &0, + &None, + &None, + &None, + ); + let user = test.create_funded_user(); + test.env.mock_all_auths(); + let res = client.try_place_bet( + &user, + &market_id, + &String::from_str(&test.env, "D4"), + &10_000_000, + ); + assert!(res.is_err()); +} + +#[test] +fn test_multi_outcome_single_winner_payout() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + let outcomes = vec![ + &test.env, + String::from_str(&test.env, "win"), + String::from_str(&test.env, "lose"), + ]; + test.env.mock_all_auths(); + let market_id = client.create_market( + &test.admin, + &String::from_str(&test.env, "Single winner test market"), + &outcomes, + &30, + &OracleConfig { + provider: OracleProvider::Reflector, + oracle_address: Address::generate(&test.env), + feed_id: String::from_str(&test.env, "X"), + threshold: 100, + comparison: String::from_str(&test.env, "gt"), + }, + &None, + &0, + &None, + &None, + &None, + ); + let winner = test.create_funded_user(); + let loser = test.create_funded_user(); + test.env.mock_all_auths(); + client.place_bet(&winner, &market_id, &String::from_str(&test.env, "win"), &100_0000000); + client.place_bet(&loser, &market_id, &String::from_str(&test.env, "lose"), &200_0000000); + let market = test.env.as_contract(&test.contract_id, || { + test.env + .storage() + .persistent() + .get::(&market_id) + .unwrap() + }); + test.env.ledger().set(LedgerInfo { + timestamp: market.end_time + market.dispute_window_seconds + 1, + protocol_version: 22, + sequence_number: test.env.ledger().sequence(), + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 1, + min_persistent_entry_ttl: 1, + max_entry_ttl: 10000, + }); + test.env.mock_all_auths(); + client.resolve_market_manual(&test.admin, &market_id, &String::from_str(&test.env, "win")); + test.env.mock_all_auths(); + client.claim_winnings(&winner, &market_id); + test.env.mock_all_auths(); + client.claim_winnings(&loser, &market_id); +} + +#[test] +fn test_multi_outcome_tie_split_payout() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + let outcomes = vec![ + &test.env, + String::from_str(&test.env, "A1"), + String::from_str(&test.env, "B2"), + String::from_str(&test.env, "C3"), + ]; + test.env.mock_all_auths(); + let market_id = client.create_market( + &test.admin, + &String::from_str(&test.env, "Tie split test"), + &outcomes, + &30, + &OracleConfig { + provider: OracleProvider::Reflector, + oracle_address: Address::generate(&test.env), + feed_id: String::from_str(&test.env, "X"), + threshold: 100, + comparison: String::from_str(&test.env, "gt"), + }, + &None, + &0, + &None, + &None, + &None, + ); + let u1 = test.create_funded_user(); + let u2 = test.create_funded_user(); + test.env.mock_all_auths(); + client.place_bet(&u1, &market_id, &String::from_str(&test.env, "A1"), &100_0000000); + client.place_bet(&u2, &market_id, &String::from_str(&test.env, "B2"), &100_0000000); + let market = test.env.as_contract(&test.contract_id, || { + test.env + .storage() + .persistent() + .get::(&market_id) + .unwrap() + }); + test.env.ledger().set(LedgerInfo { + timestamp: market.end_time + market.dispute_window_seconds + 1, + protocol_version: 22, + sequence_number: test.env.ledger().sequence(), + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 1, + min_persistent_entry_ttl: 1, + max_entry_ttl: 10000, + }); + let winning = vec![ + &test.env, + String::from_str(&test.env, "A1"), + String::from_str(&test.env, "B2"), + ]; + test.env.mock_all_auths(); + client.resolve_market_with_ties(&test.admin, &market_id, &winning); + test.env.mock_all_auths(); + client.claim_winnings(&u1, &market_id); + client.claim_winnings(&u2, &market_id); +} + +#[test] +fn test_multi_outcome_one_outcome_no_bets() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + let outcomes = vec![ + &test.env, + String::from_str(&test.env, "A1"), + String::from_str(&test.env, "B2"), + String::from_str(&test.env, "C3"), + ]; + test.env.mock_all_auths(); + let market_id = client.create_market( + &test.admin, + &String::from_str(&test.env, "One outcome no bets"), + &outcomes, + &30, + &OracleConfig { + provider: OracleProvider::Reflector, + oracle_address: Address::generate(&test.env), + feed_id: String::from_str(&test.env, "X"), + threshold: 100, + comparison: String::from_str(&test.env, "gt"), + }, + &None, + &0, + &None, + &None, + &None, + ); + let user = test.create_funded_user(); + test.env.mock_all_auths(); + client.place_bet(&user, &market_id, &String::from_str(&test.env, "A1"), &50_0000000); + let market = test.env.as_contract(&test.contract_id, || { + test.env + .storage() + .persistent() + .get::(&market_id) + .unwrap() + }); + test.env.ledger().set(LedgerInfo { + timestamp: market.end_time + market.dispute_window_seconds + 1, + protocol_version: 22, + sequence_number: test.env.ledger().sequence(), + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 1, + min_persistent_entry_ttl: 1, + max_entry_ttl: 10000, + }); + test.env.mock_all_auths(); + client.resolve_market_manual(&test.admin, &market_id, &String::from_str(&test.env, "A1")); + test.env.mock_all_auths(); + client.claim_winnings(&user, &market_id); +} + +#[test] +fn test_multi_outcome_all_same_outcome() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + let outcomes = vec![ + &test.env, + String::from_str(&test.env, "yes"), + String::from_str(&test.env, "no"), + ]; + test.env.mock_all_auths(); + let market_id = client.create_market( + &test.admin, + &String::from_str(&test.env, "All same outcome"), + &outcomes, + &30, + &OracleConfig { + provider: OracleProvider::Reflector, + oracle_address: Address::generate(&test.env), + feed_id: String::from_str(&test.env, "X"), + threshold: 100, + comparison: String::from_str(&test.env, "gt"), + }, + &None, + &0, + &None, + &None, + &None, + ); + let u1 = test.create_funded_user(); + let u2 = test.create_funded_user(); + test.env.mock_all_auths(); + client.place_bet(&u1, &market_id, &String::from_str(&test.env, "yes"), &10_0000000); + client.place_bet(&u2, &market_id, &String::from_str(&test.env, "yes"), &20_0000000); + let market = test.env.as_contract(&test.contract_id, || { + test.env + .storage() + .persistent() + .get::(&market_id) + .unwrap() + }); + test.env.ledger().set(LedgerInfo { + timestamp: market.end_time + market.dispute_window_seconds + 1, + protocol_version: 22, + sequence_number: test.env.ledger().sequence(), + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 1, + min_persistent_entry_ttl: 1, + max_entry_ttl: 10000, + }); + test.env.mock_all_auths(); + client.resolve_market_manual(&test.admin, &market_id, &String::from_str(&test.env, "yes")); + test.env.mock_all_auths(); + client.claim_winnings(&u1, &market_id); + client.claim_winnings(&u2, &market_id); +} + // ===== TESTS FOR MANUAL DISPUTE RESOLUTION (#218, #219) ===== #[test]