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 0000000..9b7034a
--- /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 379d25f..4609658 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 eba4abc..1c3fb7b 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 038d8a4..d197490 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]