Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::analytics_snapshot::{AnalyticsSnapshotEnvelope, AnalyticsSnapshotMana
use crate::err::Error;
use crate::types::{Market, MarketState, OracleConfig};
use crate::PredictifyHybrid;
use soroban_sdk::testutils::Address as _;
use soroban_sdk::{symbol_short, Address, Env, String, Symbol, Vec};

fn make_market(env: &Env, market_id: Symbol) -> Market {
Expand Down
4 changes: 2 additions & 2 deletions contracts/predictify-hybrid/src/audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,9 +247,9 @@ impl MarketAuditManager {
if index > head.total_entries {
return None;
}
Self::get_entry_unchecked(env, market_id, index).unwrap_or_else(|| {
Some(Self::get_entry_unchecked(env, market_id, index).unwrap_or_else(|| {
panic!("audit log corruption: missing entry {index}");
})
}))
}

fn get_entry_unchecked(env: &Env, market_id: &Symbol, index: u32) -> Option<MarketAuditEntry> {
Expand Down
1 change: 1 addition & 0 deletions contracts/predictify-hybrid/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3088,6 +3088,7 @@ impl ConfigTesting {
#[cfg(test)]
mod tests {
use super::*;
use soroban_sdk::testutils::Address as _;

#[test]
fn test_config_manager_default_configs() {
Expand Down
38 changes: 32 additions & 6 deletions contracts/predictify-hybrid/src/deprecated_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ mod deprecated_registry_tests {
use soroban_sdk::{
symbol_short,
testutils::{Address as _, Events},
Address, Env, Symbol, String,
Address, Env, Symbol, String, TryIntoVal,
};

// -----------------------------------------------------------------------
Expand Down Expand Up @@ -339,13 +339,31 @@ mod deprecated_registry_tests {
&sym(&env, "fetch_or"),
);

let events = env.events().all().events();
let contract_events = env.events().all();
let events = contract_events.events();
assert!(!events.is_empty(), "must emit at least one event");

// Verify the emitted event contains expected fields
let found = events.iter().any(|e| {
e.0 .0 == symbol_short!("depr_call")
&& e.0 .1 == sym(&env, "verify_r")
if let soroban_sdk::xdr::ContractEventBody::V0(v0) = &e.body {
let topic0: Symbol = v0
.topics
.get(0)
.unwrap()
.clone()
.try_into_val(&env)
.unwrap();
let topic1: Symbol = v0
.topics
.get(1)
.unwrap()
.clone()
.try_into_val(&env)
.unwrap();
topic0 == symbol_short!("depr_call") && topic1 == sym(&env, "verify_r")
} else {
false
}
});
assert!(found, "depr_call event must be present with correct entrypoint");
}
Expand All @@ -364,12 +382,20 @@ mod deprecated_registry_tests {
&sym(&env, "new_fn"),
);

let events = env.events().all().events();
let contract_events = env.events().all();
let events = contract_events.events();
assert!(!events.is_empty(), "must emit at least one event");

// The first topic in the tuple is the event type, entrypoint is second
let found = events.iter().any(|e| {
e.0 .0 == symbol_short!("depr_call")
if let soroban_sdk::xdr::ContractEventBody::V0(v0) = &e.body {
v0.topics
.get(0)
.map(|t| t.clone().try_into_val(&env).ok())
== Some(Some(symbol_short!("depr_call")))
} else {
false
}
});
assert!(found, "depr_call topic must be present");
}
Expand Down
19 changes: 13 additions & 6 deletions contracts/predictify-hybrid/src/disputes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1166,7 +1166,7 @@ impl DisputeManager {

// SECURITY: Enhanced validation - check market has active disputes
// and is not already resolved (prevents race conditions)
DisputeValidator::validate_market_for_resolution(env, &market)?;
DisputeValidator::validate_market_for_resolution(env, &market_id, &market)?;

// Calculate dispute impact
let dispute_impact = DisputeAnalytics::calculate_dispute_impact(&market);
Expand Down Expand Up @@ -2173,7 +2173,7 @@ impl DisputeManager {
// SECURITY: Validate market state before mutating dispute status
// Prevents race condition where market gets resolved between timeout check and update
let market = MarketStateManager::get_market(env, &timeout.market_id)?;
DisputeValidator::validate_market_for_resolution(env, &market)?;
DisputeValidator::validate_market_for_resolution(env, &timeout.market_id, &market)?;

// Update timeout status
timeout.status = DisputeTimeoutStatus::AutoResolved;
Expand Down Expand Up @@ -2519,7 +2519,11 @@ impl DisputeValidator {
/// - Market must have active disputes
/// - Market must not already be resolved
/// - At least one dispute must be in Active status
pub fn validate_market_for_resolution(env: &Env, market: &Market) -> Result<(), Error> {
pub fn validate_market_for_resolution(
env: &Env,
market_id: &Symbol,
market: &Market,
) -> Result<(), Error> {
// Check if market is already resolved
if market.winning_outcomes.is_some() {
return Err(Error::MarketResolved);
Expand All @@ -2532,7 +2536,7 @@ impl DisputeValidator {

// SECURITY: Verify at least one dispute is Active to prevent race conditions
// where all disputes become finalized between check and resolution
let has_active_dispute = Self::verify_has_active_dispute(env, market)?;
let has_active_dispute = Self::verify_has_active_dispute(env, market_id, market)?;
if !has_active_dispute {
return Err(Error::InvalidState);
}
Expand All @@ -2545,9 +2549,12 @@ impl DisputeValidator {
/// - Check passes: market has disputes
/// - Between check and resolution: all disputes finalized
/// - Resolution executes: market state corrupted
pub fn verify_has_active_dispute(env: &Env, market: &Market) -> Result<bool, Error> {
pub fn verify_has_active_dispute(
env: &Env,
market_id: &Symbol,
market: &Market,
) -> Result<bool, Error> {
// Get dispute history for this market
let market_id = market.market_id.clone();
let history = env.storage().persistent()
.get::<_, Vec<Dispute>>(&DataKey::DisputeHistory(market_id.clone()))
.unwrap_or_else(|| Vec::new(env));
Expand Down
5 changes: 4 additions & 1 deletion contracts/predictify-hybrid/src/err.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ pub enum Error {
/// Market cannot be archived from current state. Archive only allowed from Resolved or Cancelled.
CannotArchiveFromState = 442,
/// Market cannot be restored from current state. Restore only allowed from Archived.
CannotRestoreFromState = 444,
CannotRestoreFromState = 447,
/// Market is already archived. Cannot perform modification operations on archived markets.
MarketAlreadyArchived = 445,
/// Market is already restored. Cannot restore a market that is not archived.
Expand Down Expand Up @@ -1945,6 +1945,9 @@ impl Error {
Error::ArchiveFull => 1917,
Error::ReasonTableFull => 1918,
Error::RegistryFull => 1919,
// Catch-all for variants not yet assigned an off-chain code. Callers
// that hit this value should treat it as an unmapped error.
_ => 0,
}
}

Expand Down
Loading
Loading