From 6c4ffdf305f4ad751aba3b3ad19249ece1d27277 Mon Sep 17 00:00:00 2001 From: bitstarkbridge Date: Thu, 26 Mar 2026 10:41:38 -0700 Subject: [PATCH] feature:prune_market Relies on Unset resolved_at --- .../predict-iq/src/modules/resolution.rs | 12 +- .../src/test_resolution_state_machine.rs | 145 +++++++++++++++++- 2 files changed, 153 insertions(+), 4 deletions(-) diff --git a/contracts/predict-iq/src/modules/resolution.rs b/contracts/predict-iq/src/modules/resolution.rs index 2a4907c2..6f9ab6d0 100644 --- a/contracts/predict-iq/src/modules/resolution.rs +++ b/contracts/predict-iq/src/modules/resolution.rs @@ -53,6 +53,8 @@ pub fn finalize_resolution(e: &Env, market_id: u64) -> Result<(), ErrorCode> { // No dispute filed, finalize with oracle result let winning_outcome = market.winning_outcome.unwrap(); market.status = MarketStatus::Resolved; + // Populate resolved_at so prune_market can enforce the 30-day grace period + market.resolved_at = Some(e.ledger().timestamp()); markets::update_market(e, market); e.events().publish( @@ -63,9 +65,11 @@ pub fn finalize_resolution(e: &Env, market_id: u64) -> Result<(), ErrorCode> { Ok(()) }, MarketStatus::Disputed => { - // Check if 72h voting period has passed - let dispute_ts = market.dispute_timestamp.ok_or(ErrorCode::MarketNotDisputed)?; - if e.ledger().timestamp() < dispute_ts + VOTING_PERIOD_SECONDS { + // The dispute window opens at pending_resolution_timestamp; voting period + // begins immediately after the 24h dispute window closes. + let pending_ts = market.pending_resolution_timestamp.ok_or(ErrorCode::ResolutionNotReady)?; + let dispute_window_end = pending_ts + DISPUTE_WINDOW_SECONDS; + if e.ledger().timestamp() < dispute_window_end + VOTING_PERIOD_SECONDS { return Err(ErrorCode::VotingNotStarted); } @@ -74,6 +78,8 @@ pub fn finalize_resolution(e: &Env, market_id: u64) -> Result<(), ErrorCode> { market.status = MarketStatus::Resolved; market.winning_outcome = Some(winning_outcome); + // Populate resolved_at so prune_market can enforce the 30-day grace period + market.resolved_at = Some(e.ledger().timestamp()); markets::update_market(e, market); e.events().publish( diff --git a/contracts/predict-iq/src/test_resolution_state_machine.rs b/contracts/predict-iq/src/test_resolution_state_machine.rs index ff19e757..37e7e5a0 100644 --- a/contracts/predict-iq/src/test_resolution_state_machine.rs +++ b/contracts/predict-iq/src/test_resolution_state_machine.rs @@ -145,7 +145,8 @@ fn test_stage3_dispute_filed_within_24h() { let market = client.get_market(&market_id).unwrap(); assert_eq!(market.status, types::MarketStatus::Disputed); - assert!(market.dispute_timestamp.is_some()); + // dispute window is tracked via pending_resolution_timestamp (set at oracle resolution) + assert!(market.pending_resolution_timestamp.is_some()); } #[test] @@ -339,3 +340,145 @@ fn test_payouts_blocked_until_resolved() { let payout = client.claim_winnings(&bettor, &market_id); assert!(payout > 0); } + +#[test] +fn test_resolved_at_populated_after_oracle_finalization() { + let (e, _admin, _, client) = setup_test_env(); + + let resolution_deadline = 2000; + let market_id = create_test_market(&client, &e, resolution_deadline); + + client.set_oracle_result(&market_id, &0); + + e.ledger().with_mut(|li| { + li.timestamp = resolution_deadline; + }); + + client.attempt_oracle_resolution(&market_id); + + let finalize_time = resolution_deadline + 86400; // T+24h + e.ledger().with_mut(|li| { + li.timestamp = finalize_time; + }); + + client.finalize_resolution(&market_id); + + let market = client.get_market(&market_id).unwrap(); + assert_eq!(market.status, types::MarketStatus::Resolved); + // resolved_at must be set so prune_market can enforce the 30-day grace period + assert_eq!(market.resolved_at, Some(finalize_time)); +} + +#[test] +fn test_prune_market_succeeds_after_30_days() { + let (e, _admin, _, client) = setup_test_env(); + + let resolution_deadline = 2000; + let market_id = create_test_market(&client, &e, resolution_deadline); + + client.set_oracle_result(&market_id, &0); + + e.ledger().with_mut(|li| { + li.timestamp = resolution_deadline; + }); + + client.attempt_oracle_resolution(&market_id); + + let finalize_time = resolution_deadline + 86400; + e.ledger().with_mut(|li| { + li.timestamp = finalize_time; + }); + + client.finalize_resolution(&market_id); + + // Advance to exactly 30 days after resolution + let prune_time = finalize_time + 2_592_000; // PRUNE_GRACE_PERIOD + e.ledger().with_mut(|li| { + li.timestamp = prune_time; + }); + + // Should succeed — 30-day grace period has elapsed + client.prune_market(&market_id); + + // Market must no longer exist in storage + assert!(client.get_market(&market_id).is_none()); +} + +#[test] +#[should_panic] +fn test_prune_market_fails_before_30_days() { + let (e, _admin, _, client) = setup_test_env(); + + let resolution_deadline = 2000; + let market_id = create_test_market(&client, &e, resolution_deadline); + + client.set_oracle_result(&market_id, &0); + + e.ledger().with_mut(|li| { + li.timestamp = resolution_deadline; + }); + + client.attempt_oracle_resolution(&market_id); + + let finalize_time = resolution_deadline + 86400; + e.ledger().with_mut(|li| { + li.timestamp = finalize_time; + }); + + client.finalize_resolution(&market_id); + + // Only 15 days after resolution — should fail + e.ledger().with_mut(|li| { + li.timestamp = finalize_time + 1_296_000; // 15 days + }); + + client.prune_market(&market_id); +} + +#[test] +fn test_resolved_at_populated_after_dispute_resolution() { + let (e, _admin, contract_id, client) = setup_test_env(); + + let token_admin = Address::generate(&e); + let token_id = e.register_stellar_asset_contract_v2(token_admin.clone()); + let token_address = token_id.address(); + let token_client = token::StellarAssetClient::new(&e, &token_address); + + client.set_governance_token(&token_address); + + let resolution_deadline = 2000; + let market_id = create_test_market(&client, &e, resolution_deadline); + + client.set_oracle_result(&market_id, &0); + + e.ledger().with_mut(|li| { + li.timestamp = resolution_deadline; + }); + + client.attempt_oracle_resolution(&market_id); + + // File dispute within 24h + let disputer = Address::generate(&e); + e.ledger().with_mut(|li| { + li.timestamp = resolution_deadline + 10000; + }); + client.file_dispute(&disputer, &market_id); + + // Cast votes with clear majority + let voter = Address::generate(&e); + token_client.mint(&voter, &7000); + client.cast_vote(&voter, &market_id, &1, &7000); + + // Advance past 24h dispute window + 72h voting period + let finalize_time = resolution_deadline + 86400 + 259200; + e.ledger().with_mut(|li| { + li.timestamp = finalize_time; + }); + + client.finalize_resolution(&market_id); + + let market = client.get_market(&market_id).unwrap(); + assert_eq!(market.status, types::MarketStatus::Resolved); + // resolved_at must be set on the dispute path too + assert_eq!(market.resolved_at, Some(finalize_time)); +}