Skip to content

Commit 129e21b

Browse files
feat: preserve archived event states via metadata-only archiving (#1427)
* feat: preserve archived event states via metadata-only archiving Archiving and restoring are now non-destructive metadata operations that leave Market.state untouched, so archived events keep their terminal Resolved/Cancelled state and remain discoverable by status. Adds a new query_archived_events view, metadata-driven is_archived/unarchive with legacy back-compat, reworked lifecycle validation, and an error-code fix (CannotRestoreFromState 444 -> 447). Includes archive discoverability integration tests. Generated with Codebuff 🤖 Co-Authored-By: Codebuff <noreply@codebuff.com> * fixed budget cl failing --------- Co-authored-by: Codebuff <noreply@codebuff.com>
1 parent 9507a93 commit 129e21b

43 files changed

Lines changed: 1469 additions & 917 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

contracts/predictify-hybrid/src/analytics_snapshot_tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::analytics_snapshot::{AnalyticsSnapshotEnvelope, AnalyticsSnapshotMana
22
use crate::err::Error;
33
use crate::types::{Market, MarketState, OracleConfig};
44
use crate::PredictifyHybrid;
5+
use soroban_sdk::testutils::Address as _;
56
use soroban_sdk::{symbol_short, Address, Env, String, Symbol, Vec};
67

78
fn make_market(env: &Env, market_id: Symbol) -> Market {

contracts/predictify-hybrid/src/audit.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,9 @@ impl MarketAuditManager {
247247
if index > head.total_entries {
248248
return None;
249249
}
250-
Self::get_entry_unchecked(env, market_id, index).unwrap_or_else(|| {
250+
Some(Self::get_entry_unchecked(env, market_id, index).unwrap_or_else(|| {
251251
panic!("audit log corruption: missing entry {index}");
252-
})
252+
}))
253253
}
254254

255255
fn get_entry_unchecked(env: &Env, market_id: &Symbol, index: u32) -> Option<MarketAuditEntry> {

contracts/predictify-hybrid/src/config.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3088,6 +3088,7 @@ impl ConfigTesting {
30883088
#[cfg(test)]
30893089
mod tests {
30903090
use super::*;
3091+
use soroban_sdk::testutils::Address as _;
30913092

30923093
#[test]
30933094
fn test_config_manager_default_configs() {

contracts/predictify-hybrid/src/deprecated_tests.rs

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ mod deprecated_registry_tests {
1414
use soroban_sdk::{
1515
symbol_short,
1616
testutils::{Address as _, Events},
17-
Address, Env, Symbol, String,
17+
Address, Env, Symbol, String, TryIntoVal,
1818
};
1919

2020
// -----------------------------------------------------------------------
@@ -339,13 +339,31 @@ mod deprecated_registry_tests {
339339
&sym(&env, "fetch_or"),
340340
);
341341

342-
let events = env.events().all().events();
342+
let contract_events = env.events().all();
343+
let events = contract_events.events();
343344
assert!(!events.is_empty(), "must emit at least one event");
344345

345346
// Verify the emitted event contains expected fields
346347
let found = events.iter().any(|e| {
347-
e.0 .0 == symbol_short!("depr_call")
348-
&& e.0 .1 == sym(&env, "verify_r")
348+
if let soroban_sdk::xdr::ContractEventBody::V0(v0) = &e.body {
349+
let topic0: Symbol = v0
350+
.topics
351+
.get(0)
352+
.unwrap()
353+
.clone()
354+
.try_into_val(&env)
355+
.unwrap();
356+
let topic1: Symbol = v0
357+
.topics
358+
.get(1)
359+
.unwrap()
360+
.clone()
361+
.try_into_val(&env)
362+
.unwrap();
363+
topic0 == symbol_short!("depr_call") && topic1 == sym(&env, "verify_r")
364+
} else {
365+
false
366+
}
349367
});
350368
assert!(found, "depr_call event must be present with correct entrypoint");
351369
}
@@ -364,12 +382,20 @@ mod deprecated_registry_tests {
364382
&sym(&env, "new_fn"),
365383
);
366384

367-
let events = env.events().all().events();
385+
let contract_events = env.events().all();
386+
let events = contract_events.events();
368387
assert!(!events.is_empty(), "must emit at least one event");
369388

370389
// The first topic in the tuple is the event type, entrypoint is second
371390
let found = events.iter().any(|e| {
372-
e.0 .0 == symbol_short!("depr_call")
391+
if let soroban_sdk::xdr::ContractEventBody::V0(v0) = &e.body {
392+
v0.topics
393+
.get(0)
394+
.map(|t| t.clone().try_into_val(&env).ok())
395+
== Some(Some(symbol_short!("depr_call")))
396+
} else {
397+
false
398+
}
373399
});
374400
assert!(found, "depr_call topic must be present");
375401
}

contracts/predictify-hybrid/src/disputes.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,7 +1166,7 @@ impl DisputeManager {
11661166

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

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

21782178
// Update timeout status
21792179
timeout.status = DisputeTimeoutStatus::AutoResolved;
@@ -2519,7 +2519,11 @@ impl DisputeValidator {
25192519
/// - Market must have active disputes
25202520
/// - Market must not already be resolved
25212521
/// - At least one dispute must be in Active status
2522-
pub fn validate_market_for_resolution(env: &Env, market: &Market) -> Result<(), Error> {
2522+
pub fn validate_market_for_resolution(
2523+
env: &Env,
2524+
market_id: &Symbol,
2525+
market: &Market,
2526+
) -> Result<(), Error> {
25232527
// Check if market is already resolved
25242528
if market.winning_outcomes.is_some() {
25252529
return Err(Error::MarketResolved);
@@ -2532,7 +2536,7 @@ impl DisputeValidator {
25322536

25332537
// SECURITY: Verify at least one dispute is Active to prevent race conditions
25342538
// where all disputes become finalized between check and resolution
2535-
let has_active_dispute = Self::verify_has_active_dispute(env, market)?;
2539+
let has_active_dispute = Self::verify_has_active_dispute(env, market_id, market)?;
25362540
if !has_active_dispute {
25372541
return Err(Error::InvalidState);
25382542
}
@@ -2545,9 +2549,12 @@ impl DisputeValidator {
25452549
/// - Check passes: market has disputes
25462550
/// - Between check and resolution: all disputes finalized
25472551
/// - Resolution executes: market state corrupted
2548-
pub fn verify_has_active_dispute(env: &Env, market: &Market) -> Result<bool, Error> {
2552+
pub fn verify_has_active_dispute(
2553+
env: &Env,
2554+
market_id: &Symbol,
2555+
market: &Market,
2556+
) -> Result<bool, Error> {
25492557
// Get dispute history for this market
2550-
let market_id = market.market_id.clone();
25512558
let history = env.storage().persistent()
25522559
.get::<_, Vec<Dispute>>(&DataKey::DisputeHistory(market_id.clone()))
25532560
.unwrap_or_else(|| Vec::new(env));

contracts/predictify-hybrid/src/err.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ pub enum Error {
220220
/// Market cannot be archived from current state. Archive only allowed from Resolved or Cancelled.
221221
CannotArchiveFromState = 442,
222222
/// Market cannot be restored from current state. Restore only allowed from Archived.
223-
CannotRestoreFromState = 444,
223+
CannotRestoreFromState = 447,
224224
/// Market is already archived. Cannot perform modification operations on archived markets.
225225
MarketAlreadyArchived = 445,
226226
/// Market is already restored. Cannot restore a market that is not archived.
@@ -1945,6 +1945,9 @@ impl Error {
19451945
Error::ArchiveFull => 1917,
19461946
Error::ReasonTableFull => 1918,
19471947
Error::RegistryFull => 1919,
1948+
// Catch-all for variants not yet assigned an off-chain code. Callers
1949+
// that hit this value should treat it as an unmapped error.
1950+
_ => 0,
19481951
}
19491952
}
19501953

0 commit comments

Comments
 (0)