Skip to content
Open
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
4 changes: 4 additions & 0 deletions contracts/predictify-hybrid/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
33 changes: 18 additions & 15 deletions contracts/predictify-hybrid/src/force_resolve_tests.rs
Original file line number Diff line number Diff line change
@@ -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,
};

Expand All @@ -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<'_> {
Expand Down Expand Up @@ -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)));
}
2 changes: 2 additions & 0 deletions contracts/predictify-hybrid/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())]
Expand Down
22 changes: 0 additions & 22 deletions contracts/predictify-hybrid/src/market_id_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ────────────────────────────────────────

Expand Down
212 changes: 212 additions & 0 deletions contracts/predictify-hybrid/tests/force_resolve_test.rs
Original file line number Diff line number Diff line change
@@ -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)));
}