This guide documents the implementation of lifecycle-bound archive and restore transitions for GitHub issue #1403. The changes enforce strict state machine rules for market lifecycle transitions, ensuring data consistency and preventing invalid operations.
Archived: Market is archived (immutable, read-only state)Restored: Market is restored from archive
CannotArchiveFromState (442): Archive only allowed fromResolvedorCancelledCannotRestoreFromState (444): Restore only allowed fromArchivedMarketAlreadyArchived (445): Market is already archivedMarketAlreadyRestored (446): Market is already restored
archive_event(admin, market_id): Transition market from Resolved/Cancelled to Archivedrestore_event(admin, market_id, reason): Transition market from Archived to Restoredis_archived(market_id): Check if market is archivedis_restored(market_id): Check if market is restoredvalidate_archive_consistency(market_id): Validate archive state consistencyvalidate_restore_consistency(market_id): Validate restore state consistencyvalidate_market_lifecycle(market_id): Comprehensive lifecycle validationvalidate_state_transition(from, to): Validate state transition legality
ArchiveTransitionEvent: Emitted when market transitions to ArchivedRestoreTransitionEvent: Emitted when market transitions to Restored
The implementation is fully backward compatible. Existing contract callers can continue using all existing functions without modifications:
- Market Creation:
create_market()works unchanged - Voting:
vote()works unchanged - Betting:
place_bet(),place_bets()work unchanged - Claims:
claim_winnings()works unchanged - Resolution:
resolve_market_manual(),force_resolve_market()work unchanged - Queries:
get_market(),query_events_history()work unchanged
Why compatible?
- Archive and restore are new optional features that do not affect existing workflows
- Existing market lifecycle (Active → Ended → Resolved → Closed) continues unchanged
- Archive/restore only apply to markets that explicitly call
archive_event()andrestore_event() - Archived markets are still queryable via existing functions
Active → Ended, Disputed, Closed, Cancelled
Ended → Disputed, Resolved, Closed
Disputed → Resolved, Closed
Resolved → Archived, Closed
Cancelled → Archived, Closed
Archived → Restored
Restored → Closed (or reactivation path if implemented)
Closed → (terminal, no transitions)
- Resolved → Active (cannot reopen a resolved market)
- Closed → Ended (terminal state, no transitions allowed)
- Active → Active (self-loops are not valid transitions)
- Any → Archived except from Resolved or Cancelled
- Any → Restored except from Archived
- Any → Closed except from terminal or non-terminal states
Both archive and restore operations are admin-only:
// Only the stored admin address can archive/restore
archive_event(&admin, &market_id) → requires admin.require_auth()
restore_event(&admin, &market_id, &reason) → requires admin.require_auth()Non-admin callers will receive Error::Unauthorized (100).
Once a market is Resolved or Cancelled, you can archive it:
// After market is resolved
resolve_market_manual(&admin, &market_id, &winning_outcome);
// Now archive it
archive_event(&admin, &market_id);// Check if archived
if is_archived(&market_id) {
println!("Market is archived");
}
// Get archive details
if let Some(entry) = get_archive_entry(&market_id) {
println!("Archived at: {}", entry.archived_at);
}// If correction needed, restore from archive
restore_event(&admin, &market_id, &String::from_str(&env, "Dispute resolution"))?;Archived markets are:
- Queryable:
get_market()still returns the market - Immutable: No voting, betting, or resolution changes allowed
- Retention: Kept in archive until manually pruned via
prune_archive() - Metadata: Archive timestamp and admin details are recorded
When archive capacity is reached (1,000 entries), prune oldest entries:
// Prune oldest 10 archived markets
let (pruned_count, next_cursor) = prune_archive(&admin, 10, None)?;
// Resume pruning with cursor
loop {
let (count, cursor) = prune_archive(&admin, 30, next_cursor)?;
if cursor.done {
break; // No more entries to prune
}
next_cursor = Some(cursor);
}-
Test Archive Flow
#[test] fn test_archive_resolved_market() { // Create and resolve market // Archive it // Verify is_archived() returns true }
-
Test Restore Flow
#[test] fn test_restore_archived_market() { // Create, resolve, archive market // Restore it // Verify is_restored() returns true }
-
Test Authorization
#[test] fn test_archive_requires_admin() { // Verify non-admin gets Unauthorized error }
-
Test Invalid Transitions
#[test] fn test_archive_fails_from_active() { // Verify CannotArchiveFromState error for Active market }
All existing tests should continue to pass without modification. The implementation does not change:
- Market creation flow
- Voting mechanics
- Betting mechanics
- Resolution logic
- Claim logic
Archives a market. Market must be in Resolved or Cancelled state.
Errors:
Unauthorized: Caller is not adminMarketNotFound: Market does not existCannotArchiveFromState: Market state is not Resolved or CancelledMarketAlreadyArchived: Market already archivedArchiveFull: Archive capacity (1,000) reached
Events Emitted:
ArchiveTransitionEvent(topicarch_trn)
Restores a market from archive. Market must be in Archived state.
Errors:
Unauthorized: Caller is not adminMarketNotFound: Market does not existCannotRestoreFromState: Market state is not ArchivedMarketAlreadyRestored: Market already restored
Events Emitted:
RestoreTransitionEvent(topicrest_trn)
Returns true if market is in Archived state with valid archive metadata.
Returns true if market is in Restored state with valid restore metadata.
Comprehensive consistency validation for a market's lifecycle state.
Returns:
is_valid: Boolean indicating validation successerror: Error code if validation failedmessage: Diagnostic messagechecked_at: Validation timestamp
Validates if a state transition is legal.
Returns:
Ok(()): Transition is legalErr(IllegalMarketStateTransition): Transition is illegal
All archive and restore operations emit events for audit trails:
// Archive event
pub struct ArchiveTransitionEvent {
pub market_id: Symbol,
pub admin: Address,
pub from_state: String,
pub archived_at: u64,
pub nonce: u64, // Replay protection
pub timestamp: u64,
}
// Restore event
pub struct RestoreTransitionEvent {
pub market_id: Symbol,
pub admin: Address,
pub reason: String,
pub restored_at: u64,
pub nonce: u64, // Replay protection
pub timestamp: u64,
}- Archive:
arch_trn(with market_id as second topic) - Restore:
rest_trn(with market_id as second topic)
Use these topics to filter events in your indexer:
// Listen for archive events
filter.topic(0) == "arch_trn"
// Listen for restore events
filter.topic(0) == "rest_trn"- Atomic Transitions: Archive and restore are atomic operations
- Idempotency: Duplicate requests are rejected deterministically
- State Consistency: Archive/restore metadata is always synchronized with market state
- Deterministic Pruning: Archive pruning is deterministic and resumable
Soroban's storage model guarantees:
- No partial updates (transactions are atomic)
- Consistent state across concurrent calls
- Deterministic key derivation prevents collisions
-
Archive is One-Way by Default: Markets can be archived from Resolved/Cancelled, but once archived, they can only be restored or pruned (no automatic cleanup)
-
Archive Capacity: Maximum 1,000 archived entries. Older entries must be pruned to make room for new ones.
-
Restore is Optional: Restore functionality is included but not required for basic archive/prune workflows.
-
No Auto-Expiry: Archived entries do not automatically expire. Admins must explicitly prune old entries.
Cause: Trying to archive a market that is not in Resolved or Cancelled state
Solution:
- Verify market state with
get_market(&market_id) - Only archive after market is resolved:
resolve_market_manual()→archive_event()
Cause: Attempting to archive a market that is already archived
Solution: This is expected behavior and indicates idempotency protection is working. Check if market is already archived with is_archived().
Cause: Archive has reached capacity (1,000 entries)
Solution: Call prune_archive() to remove oldest entries before archiving new ones.
Cause: Attempting to restore a market that is already restored
Solution: This is expected behavior. Check with is_restored() before restoring.
If issues arise with archive/restore functionality:
- Disable Archive/Restore: Do not call
archive_event()orrestore_event()on new markets - Existing Archived Markets: Can still be queried and pruned
- No Data Loss: Archive/restore do not modify market resolution or payouts
Archive/restore are completely independent from the core market lifecycle and can be disabled without affecting existing operations.
Refer to:
LIFECYCLE_VALIDATION.md- State validation rules and consistency checkstests/lifecycle.rs- Complete test suite with examplessrc/lifecycle_validation.rs- Validation implementationsrc/event_archive.rs- Archive implementationsrc/restore_archive.rs- Restore implementation
For issues or questions, create a GitHub issue with:
- Error code received
- Market ID and state
- Steps to reproduce
- Contract version