diff --git a/contracts/predictify-hybrid/Cargo.toml b/contracts/predictify-hybrid/Cargo.toml index 53ed4312..8e95bcd8 100644 --- a/contracts/predictify-hybrid/Cargo.toml +++ b/contracts/predictify-hybrid/Cargo.toml @@ -21,6 +21,10 @@ path = "tests/datakey_collision.rs" name = "oracle_callback_fuzz" path = "tests/oracle_callback_fuzz.rs" +[[test]] +name = "force_resolve_test" +path = "tests/force_resolve_test.rs" + [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } proptest = "1.4" diff --git a/contracts/predictify-hybrid/src/force_resolve_tests.rs b/contracts/predictify-hybrid/src/force_resolve_tests.rs index fcf93f40..4a4eb902 100644 --- a/contracts/predictify-hybrid/src/force_resolve_tests.rs +++ b/contracts/predictify-hybrid/src/force_resolve_tests.rs @@ -1,11 +1,10 @@ #![cfg(test)] use crate::err::Error; -use crate::force_resolve::ForceResolveManager; use crate::types::{MarketState, OracleConfig, OracleProvider}; use crate::{PredictifyHybrid, PredictifyHybridClient}; use soroban_sdk::{ - testutils::{Address as _, Ledger, LedgerInfo}, + testutils::{Address as _, Ledger}, vec, Address, Env, String, Symbol, Vec, }; @@ -21,20 +20,8 @@ impl Ctx { env.mock_all_auths(); let admin = Address::generate(&env); let contract_id = env.register(PredictifyHybrid, ()); - let token_contract = - env.register_stellar_asset_contract_v2(Address::generate(&env)); - let token_id = token_contract.address(); - env.as_contract(&contract_id, || { - env.storage() - .persistent() - .set(&Symbol::new(&env, "TokenID"), &token_id); - env.storage() - .persistent() - .set(&Symbol::new(&env, "platform_fee"), &200i128); - crate::circuit_breaker::CircuitBreaker::initialize(&env).unwrap(); - }); PredictifyHybridClient::new(&env, &contract_id).initialize(&admin, &None, &None); - Ctx { env, contract_id, admin } + Self { env, contract_id, admin } } fn client(&self) -> PredictifyHybridClient<'_> { @@ -342,3 +329,19 @@ fn test_force_resolve_unauthorized() { ); assert_eq!(result, Err(Ok(Error::Unauthorized))); } + +#[test] +fn test_force_resolve_rejects_empty_outcome() { + let ctx = Ctx::new(); + let market_id = ctx.create_market(); + + let result = ctx.client().try_force_resolve_market( + &ctx.admin, + &market_id, + &outcomes(&ctx.env, &[""]), + &reason(&ctx.env, "Empty outcome string"), + &key(&ctx.env, "key_009"), + ); + + assert_eq!(result, Err(Ok(Error::InvalidOutcome))); +} diff --git a/contracts/predictify-hybrid/src/lib.rs b/contracts/predictify-hybrid/src/lib.rs index 09cb54e6..0a21de1d 100644 --- a/contracts/predictify-hybrid/src/lib.rs +++ b/contracts/predictify-hybrid/src/lib.rs @@ -90,6 +90,8 @@ mod voting_invariants; #[cfg(test)] mod override_audit_tests; +#[cfg(test)] +mod force_resolve_tests; #[cfg(any())] mod test_audit_trail; // #[cfg(any())] diff --git a/contracts/predictify-hybrid/src/market_id_generator.rs b/contracts/predictify-hybrid/src/market_id_generator.rs index 1573d27b..ecb2e350 100644 --- a/contracts/predictify-hybrid/src/market_id_generator.rs +++ b/contracts/predictify-hybrid/src/market_id_generator.rs @@ -362,28 +362,6 @@ pub struct MarketIdGenerator; /// # Panics /// /// - [`Error::InvalidState`] if attempting to seal an already sealed seed - /// - /// # Examples - /// - /// ```rust - /// #[cfg(test)] - /// fn test_seed_sealing() { - /// let env = Env::default(); - /// let contract_id = env.register(crate::PredictifyHybrid, ())); - /// - /// // Seed must be unsealed initially - /// assert!(!MarketIdGenerator::is_seed_sealed(&env)); - /// - /// // Seal the seed (one-time operation) - /// MarketIdGenerator::seal_seed(&env); - /// - /// // After sealing, regeneration is prohibited - /// assert!(MarketIdGenerator::is_seed_sealed(&env)); - /// - /// // Any attempt to generate IDs will fail - /// // (this would be tested with a failing test case) - /// } - /// ``` // ── Registry write-or-fail methods ──────────────────────────────────────── diff --git a/contracts/predictify-hybrid/tests/force_resolve_test.rs b/contracts/predictify-hybrid/tests/force_resolve_test.rs new file mode 100644 index 00000000..7c7ca979 --- /dev/null +++ b/contracts/predictify-hybrid/tests/force_resolve_test.rs @@ -0,0 +1,212 @@ +#![cfg(test)] + +extern crate std; + +use predictify_hybrid::{ + Error, MarketState, OracleConfig, OracleProvider, + PredictifyHybrid, PredictifyHybridClient, +}; +use soroban_sdk::{ + testutils::{Address as _, Ledger, LedgerInfo}, + vec, Address, Env, String, Symbol, +}; + +struct Ctx { + env: Env, + contract_id: Address, + admin: Address, +} + +impl Ctx { + fn new() -> Self { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let contract_id = env.register(PredictifyHybrid, ()); + PredictifyHybridClient::new(&env, &contract_id).initialize(&admin, &None, &None); + Self { env, contract_id, admin } + } + + fn client(&self) -> PredictifyHybridClient<'_> { + PredictifyHybridClient::new(&self.env, &self.contract_id) + } + + fn create_market(&self) -> Symbol { + self.client().create_market( + &self.admin, + &String::from_str(&self.env, "Will BTC exceed $100k?"), + &vec![ + &self.env, + String::from_str(&self.env, "yes"), + String::from_str(&self.env, "no"), + ], + &30u32, + &OracleConfig { + provider: OracleProvider::reflector(), + oracle_address: Address::generate(&self.env), + feed_id: String::from_str(&self.env, "BTC"), + threshold: 100_000_00, + comparison: String::from_str(&self.env, "gt"), + }, + &None, + &0u64, + &None, + &None, + &None, + ) + } + + fn market_state(&self, market_id: &Symbol) -> MarketState { + self.client().get_market(market_id).unwrap().state + } +} + +#[test] +fn test_force_resolve_requires_admin_auth() { + let ctx = Ctx::new(); + let market_id = ctx.create_market(); + + let non_admin = Address::generate(&ctx.env); + let result = PredictifyHybridClient::new(&ctx.env, &ctx.contract_id) + .try_admin_force_resolve( + &non_admin, + &market_id, + &String::from_str(&ctx.env, "yes"), + &Symbol::new(&ctx.env, "key_001"), + ); + + assert_eq!(result, Err(Ok(Error::Unauthorized))); +} + +#[test] +fn test_force_resolve_rejects_nonexistent_market() { + let ctx = Ctx::new(); + let fake_id = Symbol::new(&ctx.env, "nonexistent"); + + let result = ctx.client().try_admin_force_resolve( + &ctx.admin, + &fake_id, + &String::from_str(&ctx.env, "yes"), + &Symbol::new(&ctx.env, "key_002"), + ); + + assert_eq!(result, Err(Ok(Error::MarketNotFound))); +} + +#[test] +fn test_force_resolve_rejects_invalid_outcome() { + let ctx = Ctx::new(); + let market_id = ctx.create_market(); + + let result = ctx.client().try_admin_force_resolve( + &ctx.admin, + &market_id, + &String::from_str(&ctx.env, "invalid_outcome"), + &Symbol::new(&ctx.env, "key_003"), + ); + + assert_eq!(result, Err(Ok(Error::InvalidOutcome))); +} + +#[test] +fn test_force_resolve_success() { + let ctx = Ctx::new(); + let market_id = ctx.create_market(); + + assert_eq!(ctx.market_state(&market_id), MarketState::Active); + + let result = ctx.client().try_admin_force_resolve( + &ctx.admin, + &market_id, + &String::from_str(&ctx.env, "yes"), + &Symbol::new(&ctx.env, "key_004"), + ); + + assert_eq!(result, Ok(Ok(()))); + assert_eq!(ctx.market_state(&market_id), MarketState::Resolved); +} + +#[test] +fn test_force_resolve_rejects_replayed_idempotency_key() { + let ctx = Ctx::new(); + let market_id = ctx.create_market(); + + let result = ctx.client().try_admin_force_resolve( + &ctx.admin, + &market_id, + &String::from_str(&ctx.env, "yes"), + &Symbol::new(&ctx.env, "key_005"), + ); + assert_eq!(result, Ok(Ok(()))); + + let result = ctx.client().try_admin_force_resolve( + &ctx.admin, + &market_id, + &String::from_str(&ctx.env, "yes"), + &Symbol::new(&ctx.env, "key_005"), + ); + assert_eq!(result, Err(Ok(Error::ForceResolveReplayed))); +} + +#[test] +fn test_force_resolve_different_keys_both_succeed() { + let ctx = Ctx::new(); + let market_id = ctx.create_market(); + + let result = ctx.client().try_admin_force_resolve( + &ctx.admin, + &market_id, + &String::from_str(&ctx.env, "yes"), + &Symbol::new(&ctx.env, "key_006"), + ); + assert_eq!(result, Ok(Ok(()))); + + let result = ctx.client().try_admin_force_resolve( + &ctx.admin, + &market_id, + &String::from_str(&ctx.env, "yes"), + &Symbol::new(&ctx.env, "key_007"), + ); + assert_eq!(result, Ok(Ok(()))); +} + +#[test] +fn test_force_resolve_ended_market() { + let ctx = Ctx::new(); + let market_id = ctx.create_market(); + + ctx.env.ledger().set(LedgerInfo { + timestamp: 30 * 24 * 60 * 60 + 1, + protocol_version: 25, + sequence_number: ctx.env.ledger().sequence(), + network_id: Default::default(), + base_reserve: 10, + min_persistent_entry_ttl: 1, + min_temp_entry_ttl: 1, + max_entry_ttl: 535680, + }); + + let result = ctx.client().try_admin_force_resolve( + &ctx.admin, + &market_id, + &String::from_str(&ctx.env, "yes"), + &Symbol::new(&ctx.env, "key_008"), + ); + assert_eq!(result, Ok(Ok(()))); + assert_eq!(ctx.market_state(&market_id), MarketState::Resolved); +} + +#[test] +fn test_force_resolve_rejects_empty_outcome() { + let ctx = Ctx::new(); + let market_id = ctx.create_market(); + + let result = ctx.client().try_admin_force_resolve( + &ctx.admin, + &market_id, + &String::from_str(&ctx.env, ""), + &Symbol::new(&ctx.env, "key_009"), + ); + + assert_eq!(result, Err(Ok(Error::InvalidOutcome))); +}