From ce5cb6bdb78065f5cba7d10a9db98a1fba9bfd8b Mon Sep 17 00:00:00 2001 From: Devops Date: Fri, 23 Oct 2026 04:35:54 +0000 Subject: [PATCH] fix: address oracle, analytics, and market issues --- contracts/analytics/src/lib.rs | 9 ++++ contracts/analytics/tests/time_series.rs | 18 ++++++++ .../markets/tests/issues_007_012_014_022.rs | 3 ++ contracts/oracles/tests/auth_snap.rs | 43 ++++++++++++++++++- contracts/predictify-hybrid/src/lib.rs | 6 +-- .../src/reflector_auth_tests.rs | 1 + 6 files changed, 76 insertions(+), 4 deletions(-) diff --git a/contracts/analytics/src/lib.rs b/contracts/analytics/src/lib.rs index d95ef3a3..253c57c1 100644 --- a/contracts/analytics/src/lib.rs +++ b/contracts/analytics/src/lib.rs @@ -43,6 +43,10 @@ pub use errors::ContractError; /// Type alias used by generated client code and test harnesses. pub type Error = ContractError; +/// Maximum distance between a supplied metric timestamp and ledger time. +/// Adjust this deployment constant when the expected ingestion delay changes. +const MAX_TIMESTAMP_DRIFT_SECONDS: u64 = 86_400; + /// Persistent-storage keys used by the Analytics contract. #[contracttype] #[derive(Clone)] @@ -256,6 +260,11 @@ impl Analytics { // Require caller authentication caller.require_auth(); + let current_timestamp = env.ledger().timestamp(); + if timestamp.abs_diff(current_timestamp) > MAX_TIMESTAMP_DRIFT_SECONDS { + return Err(ContractError::ValueOutOfRange); + } + // Check for duplicate entry (same metric_name, timestamp) let key = DataKey::Metric(metric_name.clone(), timestamp); if env.storage().persistent().has(&key) { diff --git a/contracts/analytics/tests/time_series.rs b/contracts/analytics/tests/time_series.rs index 4301f0bc..56b7c8ed 100644 --- a/contracts/analytics/tests/time_series.rs +++ b/contracts/analytics/tests/time_series.rs @@ -82,6 +82,24 @@ fn test_pause_and_unpause() { assert_eq!(client.record_metric_at(&metric, &200, &2000), 200); } +#[test] +fn test_record_metric_at_rejects_timestamp_outside_ledger_drift() { + let (env, client, _admin) = setup_env(); + env.ledger().set_timestamp(100_000); + let metric = symbol_short!("future"); + let timestamp = env.ledger().timestamp() + 86_401; + let stale_timestamp = env.ledger().timestamp() - 86_401; + + assert_eq!( + client.try_record_metric_at(&metric, ×tamp, &100), + Err(Ok(ContractError::ValueOutOfRange)) + ); + assert_eq!( + client.try_record_metric_at(&metric, &stale_timestamp, &100), + Err(Ok(ContractError::ValueOutOfRange)) + ); +} + #[test] fn test_time_bucketed_aggregation() { let (env, client, _admin) = setup_env(); diff --git a/contracts/markets/tests/issues_007_012_014_022.rs b/contracts/markets/tests/issues_007_012_014_022.rs index 2880bb33..c575e1c5 100644 --- a/contracts/markets/tests/issues_007_012_014_022.rs +++ b/contracts/markets/tests/issues_007_012_014_022.rs @@ -336,6 +336,8 @@ fn test_user_bet_enumeration_reflects_market_cancellation() { fn test_default_winner_takes_all_payout() { let te = setup_test_env(); let m1 = create_standard_market(&te, 2); + let token_client = soroban_sdk::token::Client::new(&te.env, &te.token_address); + let starting_balance = token_client.balance(&te.bettor1); // Bettor1 stakes 200 on Outcome 0 (Yes) // Bettor2 stakes 600 on Outcome 1 (No) @@ -351,6 +353,7 @@ fn test_default_winner_takes_all_payout() { let payout = te.client.claim_winnings(&te.bettor1, &m1); assert_eq!(payout, 800); + assert_eq!(token_client.balance(&te.bettor1), starting_balance - 200 + payout); // Losing bettor gets error let lose_res = te.client.try_claim_winnings(&te.bettor2, &m1); diff --git a/contracts/oracles/tests/auth_snap.rs b/contracts/oracles/tests/auth_snap.rs index 66c154e3..195b4556 100644 --- a/contracts/oracles/tests/auth_snap.rs +++ b/contracts/oracles/tests/auth_snap.rs @@ -1,7 +1,8 @@ #![cfg(test)] use soroban_sdk::{ - contract, contractimpl, testutils::Address as _, Address, Env, String, + contract, contractimpl, testutils::{Address as _, MockAuth, MockAuthInvoke}, Address, Env, + IntoVal, String, }; use oracles::{OraclesContract, OraclesContractClient, OraclePriceData}; @@ -121,6 +122,46 @@ fn test_remove_oracle_fails_without_auth() { client.remove_oracle(&admin, &oracle); } +#[test] +#[should_panic] +fn test_add_oracle_rejects_non_admin_signer() { + let setup = setup_test_env(); + let outsider = Address::generate(&setup.env); + setup.env.mock_all_auths(); + setup.client.add_oracle(&setup.admin, &setup.oracle); + + setup.env.mock_auths(&[MockAuth { + address: &setup.admin, + invoke: &MockAuthInvoke { + contract: &setup.contract_id, + fn_name: "add_oracle", + args: (&setup.admin, &setup.oracle).into_val(&setup.env), + sub_invokes: &[], + }, + }]); + setup.client.add_oracle(&outsider, &setup.oracle); +} + +#[test] +#[should_panic] +fn test_remove_oracle_rejects_non_admin_signer() { + let setup = setup_test_env(); + let outsider = Address::generate(&setup.env); + setup.env.mock_all_auths(); + setup.client.add_oracle(&setup.admin, &setup.oracle); + + setup.env.mock_auths(&[MockAuth { + address: &setup.admin, + invoke: &MockAuthInvoke { + contract: &setup.contract_id, + fn_name: "remove_oracle", + args: (&setup.admin, &setup.oracle).into_val(&setup.env), + sub_invokes: &[], + }, + }]); + setup.client.remove_oracle(&outsider, &setup.oracle); +} + // --------------------------------------------------------------------------- // 2. Read-only entrypoints do not require auth // --------------------------------------------------------------------------- diff --git a/contracts/predictify-hybrid/src/lib.rs b/contracts/predictify-hybrid/src/lib.rs index 4ee9785d..b426f4ab 100644 --- a/contracts/predictify-hybrid/src/lib.rs +++ b/contracts/predictify-hybrid/src/lib.rs @@ -1820,7 +1820,7 @@ impl PredictifyHybrid { /// State-changing paths may emit events through internal managers; read-only query paths emit no events. /// Set the minimum oracle price-confidence threshold in basis points. /// Prices from oracles with confidence ratio above this threshold are rejected. - /// `min_confidence_bps` = 0 disables the check. + /// `min_confidence_bps` = 0 disables the check. The default is 5000 (50%). pub fn set_oracle_confidence_threshold(env: Env, admin: Address, min_confidence_bps: u32) { admin.require_auth(); if min_confidence_bps > 10_000 { @@ -1831,12 +1831,12 @@ impl PredictifyHybrid { .set(&Symbol::new(&env, "oracle_conf_bps"), &min_confidence_bps); } - /// Get the configured oracle confidence threshold in bps (0 = disabled). + /// Get the configured oracle confidence threshold in bps (default 5000; 0 = disabled). pub fn get_oracle_confidence_threshold(env: Env) -> u32 { env.storage() .instance() .get(&Symbol::new(&env, "oracle_conf_bps")) - .unwrap_or(0u32) + .unwrap_or(5_000u32) } /// Set configurable weight for a specific oracle source diff --git a/contracts/predictify-hybrid/src/reflector_auth_tests.rs b/contracts/predictify-hybrid/src/reflector_auth_tests.rs index 13f95360..2591eebc 100644 --- a/contracts/predictify-hybrid/src/reflector_auth_tests.rs +++ b/contracts/predictify-hybrid/src/reflector_auth_tests.rs @@ -201,6 +201,7 @@ fn test_fetch_oracle_result_mock_all_auths_any_caller_passes_auth() { fn test_set_oracle_confidence_threshold_precise_admin_mock_auth_passes() { let (env, cid, admin) = setup(); let _market_id = make_reflector_market(&env, &cid, &admin); + assert_eq!(client(&env, &cid).get_oracle_confidence_threshold(), 5_000u32); env.mock_auths(&[MockAuth { address: &admin,