From 19058c093813ff24df062aded731e0da5736af7b Mon Sep 17 00:00:00 2001 From: psychemist Date: Thu, 31 Jul 2025 19:13:54 +0100 Subject: [PATCH 1/2] feat: add upgrade contract module to crate --- contracts/predictify-hybrid/src/lib.rs | 1 + contracts/predictify-hybrid/src/upgrade.rs | 1313 ++++++++++++++++++++ 2 files changed, 1314 insertions(+) create mode 100644 contracts/predictify-hybrid/src/upgrade.rs diff --git a/contracts/predictify-hybrid/src/lib.rs b/contracts/predictify-hybrid/src/lib.rs index 383c20fe..2ef32cb4 100644 --- a/contracts/predictify-hybrid/src/lib.rs +++ b/contracts/predictify-hybrid/src/lib.rs @@ -18,6 +18,7 @@ mod markets; mod oracles; mod resolution; mod types; +mod upgrade; mod utils; mod validation; mod voting; diff --git a/contracts/predictify-hybrid/src/upgrade.rs b/contracts/predictify-hybrid/src/upgrade.rs new file mode 100644 index 00000000..23cdbfe6 --- /dev/null +++ b/contracts/predictify-hybrid/src/upgrade.rs @@ -0,0 +1,1313 @@ +extern crate alloc; + +use alloc::format; +use alloc::string::ToString; +use crate::admin::{AdminAccessControl, AdminActionLogger}; +use crate::events::EventEmitter; +use soroban_sdk::{contracterror, contracttype, vec, Address, Env, Map, String, Symbol, Vec}; + +/// Comprehensive upgrade system for Predictify Hybrid contract +/// +/// This module provides a robust contract upgradeability system with: +/// - Version management and tracking +/// - Safe upgrade validation and execution +/// - Rollback mechanisms for failed upgrades +/// - State backup and migration utilities +/// - Administrative controls and logging +/// - Comprehensive testing and validation + +// ===== UPGRADE ERRORS ===== + +/// Upgrade-specific errors for the upgrade system +#[contracterror] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum UpgradeError { + /// Upgrade is already in progress + UpgradeInProgress = 1, + /// No upgrade is currently in progress + UpgradeNotInProgress = 2, + /// Upgrade validation failed + UpgradeValidationFailed = 3, + /// Rollback conditions not met + RollbackConditionsNotMet = 4, + /// State backup failed + BackupFailed = 5, + /// Failed to emit upgrade event + UpgradeEventEmissionFailed = 6, + /// Invalid upgrade version + InvalidUpgradeVersion = 7, + /// Upgrade history not found + UpgradeHistoryNotFound = 8, + /// Previous version not found + PreviousVersionNotFound = 9, + /// Admin not set + AdminNotSet = 10, +} + +impl UpgradeError { + /// Get a human-readable error message + pub fn message(&self) -> &'static str { + match self { + UpgradeError::UpgradeInProgress => "Upgrade is already in progress", + UpgradeError::UpgradeNotInProgress => "No upgrade is currently in progress", + UpgradeError::UpgradeValidationFailed => "Upgrade validation failed", + UpgradeError::RollbackConditionsNotMet => "Rollback conditions not met", + UpgradeError::BackupFailed => "State backup failed", + UpgradeError::UpgradeEventEmissionFailed => "Failed to emit upgrade event", + UpgradeError::InvalidUpgradeVersion => "Invalid upgrade version", + UpgradeError::UpgradeHistoryNotFound => "Upgrade history not found", + UpgradeError::PreviousVersionNotFound => "Previous version not found", + UpgradeError::AdminNotSet => "Admin not set", + } + } + + /// Get error code as string for debugging + pub fn code(&self) -> &'static str { + match self { + UpgradeError::UpgradeInProgress => "UPGRADE_IN_PROGRESS", + UpgradeError::UpgradeNotInProgress => "UPGRADE_NOT_IN_PROGRESS", + UpgradeError::UpgradeValidationFailed => "UPGRADE_VALIDATION_FAILED", + UpgradeError::RollbackConditionsNotMet => "ROLLBACK_CONDITIONS_NOT_MET", + UpgradeError::BackupFailed => "BACKUP_FAILED", + UpgradeError::UpgradeEventEmissionFailed => "UPGRADE_EVENT_EMISSION_FAILED", + UpgradeError::InvalidUpgradeVersion => "INVALID_UPGRADE_VERSION", + UpgradeError::UpgradeHistoryNotFound => "UPGRADE_HISTORY_NOT_FOUND", + UpgradeError::PreviousVersionNotFound => "PREVIOUS_VERSION_NOT_FOUND", + UpgradeError::AdminNotSet => "ADMIN_NOT_SET", + } + } +} + +impl From for UpgradeError { + fn from(error: crate::errors::Error) -> Self { + match error { + crate::errors::Error::Unauthorized => UpgradeError::AdminNotSet, + crate::errors::Error::AdminNotSet => UpgradeError::AdminNotSet, + _ => UpgradeError::UpgradeValidationFailed, + } + } +} + +/// Contract version information using semantic versioning +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ContractVersion { + /// Major version number - incremented for breaking changes + pub major: u32, + /// Minor version number - incremented for new features + pub minor: u32, + /// Patch version number - incremented for bug fixes + pub patch: u32, + /// Build metadata for additional version info + pub build: String, + /// Release timestamp + pub timestamp: u64, +} + +impl ContractVersion { + /// Create a new contract version + pub fn new(env: &Env, major: u32, minor: u32, patch: u32, build: &str) -> Self { + Self { + major, + minor, + patch, + build: String::from_str(env, build), + timestamp: env.ledger().timestamp(), + } + } + + /// Check if this version is compatible with another version + pub fn is_compatible_with(&self, other: &ContractVersion) -> bool { + // Major version must match for compatibility + self.major == other.major + } + + /// Check if this version is newer than another version + pub fn is_newer_than(&self, other: &ContractVersion) -> bool { + if self.major != other.major { + return self.major > other.major; + } + if self.minor != other.minor { + return self.minor > other.minor; + } + self.patch > other.patch + } + + /// Validate version numbers + pub fn validate(&self) -> Result<(), UpgradeError> { + // Version numbers should be reasonable + if self.major > 1000 || self.minor > 1000 || self.patch > 1000 { + return Err(UpgradeError::InvalidUpgradeVersion); + } + Ok(()) + } + + /// Get version string representation + pub fn to_string(&self, env: &Env) -> String { + let major_str = self.major.to_string(); + let minor_str = self.minor.to_string(); + let patch_str = self.patch.to_string(); + + let version_str = format!("{}.{}.{}", major_str, minor_str, patch_str); + String::from_str(env, &version_str) + } +} + +/// Comprehensive upgrade data structure +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct UpgradeData { + /// Unique identifier for this upgrade + pub upgrade_id: String, + /// Version being upgraded from + pub from_version: ContractVersion, + /// Version being upgraded to + pub to_version: ContractVersion, + /// Hash of the new WASM bytecode + pub new_wasm_hash: String, + /// Admin performing the upgrade + pub admin: Address, + /// Timestamp when upgrade was initiated + pub timestamp: u64, + /// Whether rollback is enabled for this upgrade + pub rollback_enabled: bool, + /// Whether validation has passed + pub validation_passed: bool, + /// Upgrade status + pub status: UpgradeStatus, +} + +impl UpgradeData { + /// Create new upgrade data + pub fn new( + env: &Env, + from_version: ContractVersion, + to_version: ContractVersion, + new_wasm_hash: String, + admin: Address, + ) -> Self { + let upgrade_id = UpgradeUtils::generate_upgrade_id(env, &admin, &to_version); + + Self { + upgrade_id, + from_version, + to_version, + new_wasm_hash, + admin, + timestamp: env.ledger().timestamp(), + rollback_enabled: true, + validation_passed: false, + status: UpgradeStatus::Initiated, + } + } + + /// Validate upgrade data + pub fn validate(&self) -> Result<(), UpgradeError> { + // Validate versions + self.from_version.validate()?; + self.to_version.validate()?; + + // Check version compatibility + if !self.to_version.is_compatible_with(&self.from_version) { + return Err(UpgradeError::UpgradeValidationFailed); + } + + // Check that we're upgrading to a newer version + if !self.to_version.is_newer_than(&self.from_version) { + return Err(UpgradeError::UpgradeValidationFailed); + } + + // Validate WASM hash is not empty + if self.new_wasm_hash.is_empty() { + return Err(UpgradeError::UpgradeValidationFailed); + } + + Ok(()) + } +} + +/// Upgrade status enumeration +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum UpgradeStatus { + /// Upgrade initiated but not started + Initiated, + /// Upgrade validation in progress + Validating, + /// Upgrade validation passed + ValidationPassed, + /// Upgrade validation failed + ValidationFailed, + /// Upgrade in progress + InProgress, + /// Upgrade completed successfully + Completed, + /// Upgrade failed + Failed, + /// Upgrade rolled back + RolledBack, +} + +/// Rollback data structure +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RollbackData { + /// Unique identifier for this rollback + pub rollback_id: String, + /// Original upgrade ID being rolled back + pub upgrade_id: String, + /// Version being rolled back from + pub from_version: ContractVersion, + /// Version being rolled back to + pub to_version: ContractVersion, + /// Admin performing the rollback + pub admin: Address, + /// Timestamp when rollback was initiated + pub timestamp: u64, + /// Reason for the rollback + pub reason: String, + /// Whether rollback was successful + pub success: bool, + /// Rollback status + pub status: RollbackStatus, +} + +impl RollbackData { + /// Create new rollback data + pub fn new( + env: &Env, + upgrade_id: String, + from_version: ContractVersion, + to_version: ContractVersion, + admin: Address, + reason: String, + ) -> Self { + let rollback_id = UpgradeUtils::generate_rollback_id(env, &admin, &upgrade_id); + + Self { + rollback_id, + upgrade_id, + from_version, + to_version, + admin, + timestamp: env.ledger().timestamp(), + reason, + success: false, + status: RollbackStatus::Initiated, + } + } +} + +/// Rollback status enumeration +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum RollbackStatus { + /// Rollback initiated + Initiated, + /// Rollback in progress + InProgress, + /// Rollback completed successfully + Completed, + /// Rollback failed + Failed, +} + +/// Compatibility report structure +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CompatibilityReport { + /// Whether versions are compatible + pub is_compatible: bool, + /// List of compatibility issues + pub issues: Vec, + /// List of warnings + pub warnings: Vec, + /// Report timestamp + pub timestamp: u64, +} + +impl CompatibilityReport { + /// Create a compatible report + pub fn compatible(env: &Env) -> Self { + Self { + is_compatible: true, + issues: vec![env], + warnings: vec![env], + timestamp: env.ledger().timestamp(), + } + } + + /// Create an incompatible report with issues + pub fn incompatible(env: &Env, issues: Vec) -> Self { + Self { + is_compatible: false, + issues, + warnings: vec![env], + timestamp: env.ledger().timestamp(), + } + } + + /// Add a warning to the report + pub fn add_warning(&mut self, warning: String) { + self.warnings.push_back(warning); + } +} + +/// Safety report structure +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SafetyReport { + /// Whether upgrade is safe to proceed + pub is_safe: bool, + /// List of safety concerns + pub concerns: Vec, + /// List of recommendations + pub recommendations: Vec, + /// Risk level assessment + pub risk_level: RiskLevel, + /// Report timestamp + pub timestamp: u64, +} + +impl SafetyReport { + /// Create a safe report + pub fn safe(env: &Env) -> Self { + Self { + is_safe: true, + concerns: vec![env], + recommendations: vec![env], + risk_level: RiskLevel::Low, + timestamp: env.ledger().timestamp(), + } + } + + /// Create an unsafe report with concerns + pub fn unsafe_report(env: &Env, concerns: Vec, risk_level: RiskLevel) -> Self { + Self { + is_safe: false, + concerns, + recommendations: vec![env], + risk_level, + timestamp: env.ledger().timestamp(), + } + } +} + +/// Risk level enumeration +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum RiskLevel { + Low, + Medium, + High, + Critical, +} + +/// Test results structure +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TestResults { + /// Whether all tests passed + pub all_tests_passed: bool, + /// Number of tests run + pub tests_run: u32, + /// Number of tests passed + pub tests_passed: u32, + /// Number of tests failed + pub tests_failed: u32, + /// List of failed test names + pub failed_tests: Vec, + /// Test execution timestamp + pub timestamp: u64, +} + +impl TestResults { + /// Create test results + pub fn new(env: &Env, tests_run: u32, tests_passed: u32, failed_tests: Vec) -> Self { + Self { + all_tests_passed: failed_tests.is_empty(), + tests_run, + tests_passed, + tests_failed: tests_run - tests_passed, + failed_tests, + timestamp: env.ledger().timestamp(), + } + } +} + +// ===== STORAGE KEYS ===== + +/// Storage keys for upgrade system +pub mod storage_keys { + pub fn current_version_key() -> &'static str { + "current_version" + } + pub fn upgrade_history_key() -> &'static str { + "upgrade_history" + } + pub fn state_backup_key() -> &'static str { + "state_backup" + } + pub fn rollback_data_key() -> &'static str { + "rollback_data" + } + pub fn upgrade_in_progress_key() -> &'static str { + "upgrade_in_progress" + } + pub fn last_backup_timestamp_key() -> &'static str { + "last_backup_timestamp" + } +} + +// ===== UPGRADE MANAGER ===== + +/// Core upgrade management functionality +pub struct UpgradeManager; + +impl UpgradeManager { + /// Execute contract upgrade to new WASM bytecode + pub fn upgrade_contract( + env: &Env, + new_contract: Address, + admin: Address, + ) -> Result { + // Validate admin permissions + AdminAccessControl::require_admin_auth(env, &admin)?; + + // Check if upgrade is already in progress + if Self::is_upgrade_in_progress(env) { + return Err(UpgradeError::UpgradeInProgress); + } + + // Get current version + let current_version = Self::get_contract_version(env)?; + + // Create new version (simplified - in real implementation this would be derived from new contract) + let new_version = ContractVersion::new( + env, + current_version.major, + current_version.minor + 1, + 0, + "upgrade", + ); + + // Create upgrade data + let mut upgrade_data = UpgradeData::new( + env, + current_version.clone(), + new_version.clone(), + String::from_str(env, "new_wasm_hash"), // Simplified + admin.clone(), + ); + + // Validate upgrade data + UpgradeValidator::validate_upgrade_compatibility(env, ¤t_version, &new_version)?; + + // Run safety validation + let safety_report = UpgradeValidator::validate_upgrade_safety(env, &upgrade_data)?; + if !safety_report.is_safe { + return Err(UpgradeError::UpgradeValidationFailed); + } + + // Create state backup + Self::create_state_backup(env)?; + + // Mark upgrade as in progress + Self::set_upgrade_in_progress(env, true)?; + + // Update upgrade status + upgrade_data.status = UpgradeStatus::InProgress; + upgrade_data.validation_passed = true; + + // Store upgrade data + env.storage().persistent().set( + &Symbol::new(env, storage_keys::upgrade_history_key()), + &upgrade_data, + ); + + // Log admin action + let mut params = Map::new(env); + params.set( + String::from_str(env, "new_contract"), + new_contract.to_string(), + ); + params.set( + String::from_str(env, "reason"), + String::from_str(env, "UPGRADE"), + ); + + AdminActionLogger::log_action( + env, + &admin, + "upgrade_contract", + Some(String::from_str(env, "contract")), + params, + true, + None, + )?; + + // Emit upgrade event + UpgradeEventEmitter::emit_upgrade_initiated(env, &upgrade_data); + + // In a real implementation, here we would: + // 1. Deploy the new contract + // 2. Migrate state if needed + // 3. Update contract references + // 4. Validate post-upgrade state + + // For now, simulate successful upgrade + upgrade_data.status = UpgradeStatus::Completed; + Self::set_contract_version(env, upgrade_data.to_version.clone())?; + Self::set_upgrade_in_progress(env, false)?; + + // Emit completion event + UpgradeEventEmitter::emit_upgrade_completed(env, &upgrade_data); + + Ok(upgrade_data) + } + + /// Get current contract version + pub fn get_contract_version(env: &Env) -> Result { + Ok(env.storage() + .persistent() + .get(&Symbol::new(env, storage_keys::current_version_key())) + .unwrap_or_else(|| { + // Return default version if not set + ContractVersion::new(env, 1, 0, 0, "initial") + })) + } + + /// Set contract version + pub fn set_contract_version(env: &Env, version: ContractVersion) -> Result<(), UpgradeError> { + version.validate().map_err(|_| UpgradeError::InvalidUpgradeVersion)?; + + env.storage().persistent().set( + &Symbol::new(env, storage_keys::current_version_key()), + &version, + ); + + Ok(()) + } + + /// Check if upgrade is currently in progress + pub fn is_upgrade_in_progress(env: &Env) -> bool { + env.storage() + .persistent() + .get(&Symbol::new(env, storage_keys::upgrade_in_progress_key())) + .unwrap_or(false) + } + + /// Set upgrade in progress status + pub fn set_upgrade_in_progress(env: &Env, in_progress: bool) -> Result<(), UpgradeError> { + env.storage().persistent().set( + &Symbol::new(env, storage_keys::upgrade_in_progress_key()), + &in_progress, + ); + Ok(()) + } + + /// Create state backup before upgrade + pub fn create_state_backup(env: &Env) -> Result<(), UpgradeError> { + // In a real implementation, this would backup all contract state + // For now, just store a timestamp + let timestamp = env.ledger().timestamp(); + + env.storage().persistent().set( + &Symbol::new(env, storage_keys::last_backup_timestamp_key()), + ×tamp, + ); + + Ok(()) + } + + /// Get upgrade history + pub fn get_upgrade_history(env: &Env) -> Vec { + env.storage() + .persistent() + .get(&Symbol::new(env, storage_keys::upgrade_history_key())) + .unwrap_or_else(|| vec![env]) + } +} + +// ===== UPGRADE VALIDATOR ===== + +/// Upgrade validation and safety checks +pub struct UpgradeValidator; + +impl UpgradeValidator { + /// Validate version compatibility between old and new versions + pub fn validate_upgrade_compatibility( + _env: &Env, + old_version: &ContractVersion, + new_version: &ContractVersion, + ) -> Result { + let mut issues = vec![_env]; + + // Check major version compatibility + if !new_version.is_compatible_with(old_version) { + issues.push_back(String::from_str(_env, "Major version incompatibility")); + } + + // Check if downgrading + if !new_version.is_newer_than(old_version) { + issues.push_back(String::from_str(_env, "Cannot downgrade to older version")); + } + + if issues.is_empty() { + Ok(CompatibilityReport::compatible(_env)) + } else { + Ok(CompatibilityReport::incompatible(_env, issues)) + } + } + + /// Validate upgrade safety and conditions + pub fn validate_upgrade_safety( + env: &Env, + upgrade: &UpgradeData, + ) -> Result { + let mut concerns = vec![env]; + let mut risk_level = RiskLevel::Low; + + // Check admin authorization + let stored_admin: Option
= env + .storage() + .persistent() + .get(&Symbol::new(env, "Admin")); + + if stored_admin.is_none() || stored_admin != Some(upgrade.admin.clone()) { + concerns.push_back(String::from_str(env, "Unauthorized admin")); + risk_level = RiskLevel::Critical; + } + + // Check if another upgrade is in progress + if UpgradeManager::is_upgrade_in_progress(env) { + concerns.push_back(String::from_str(env, "Upgrade already in progress")); + risk_level = RiskLevel::High; + } + + // Validate upgrade data + if upgrade.validate().is_err() { + concerns.push_back(String::from_str(env, "Invalid upgrade data")); + risk_level = RiskLevel::High; + } + + if concerns.is_empty() { + Ok(SafetyReport::safe(env)) + } else { + Ok(SafetyReport::unsafe_report(env, concerns, risk_level)) + } + } + + /// Test upgrade compatibility without executing it + pub fn test_upgrade_compatibility( + env: &Env, + _new_contract: Address, + ) -> Result { + // In a real implementation, this would run comprehensive tests + // For now, simulate test execution + + let tests_run = 5; + let tests_passed = 5; + let failed_tests = vec![env]; + + Ok(TestResults::new(env, tests_run, tests_passed, failed_tests)) + } +} + +// ===== ROLLBACK MANAGER ===== + +/// Rollback management for failed upgrades +pub struct RollbackManager; + +impl RollbackManager { + /// Execute rollback to previous version + pub fn rollback_upgrade( + env: &Env, + admin: Address, + reason: String, + ) -> Result { + // Validate admin permissions + AdminAccessControl::require_admin_auth(env, &admin)?; + + // Check if upgrade is in progress + if !UpgradeManager::is_upgrade_in_progress(env) { + return Err(UpgradeError::UpgradeNotInProgress); + } + + // Get current and previous versions + let current_version = UpgradeManager::get_contract_version(env)?; + let upgrade_history = UpgradeManager::get_upgrade_history(env); + + // Find the previous version from history + let previous_version = if let Some(last_upgrade) = upgrade_history.last() { + last_upgrade.from_version.clone() + } else { + return Err(UpgradeError::RollbackConditionsNotMet); + }; + + // Validate rollback conditions + Self::validate_rollback_conditions(env)?; + + // Create rollback data + let mut rollback_data = RollbackData::new( + env, + String::from_str(env, "current_upgrade"), // Simplified + current_version, + previous_version.clone(), + admin.clone(), + reason, + ); + + rollback_data.status = RollbackStatus::InProgress; + + // Store rollback data + env.storage().persistent().set( + &Symbol::new(env, storage_keys::rollback_data_key()), + &rollback_data, + ); + + // Emit rollback event + UpgradeEventEmitter::emit_rollback_initiated(env, &rollback_data); + + // Execute rollback (simplified) + UpgradeManager::set_contract_version(env, previous_version)?; + UpgradeManager::set_upgrade_in_progress(env, false)?; + + // Update rollback status + rollback_data.status = RollbackStatus::Completed; + rollback_data.success = true; + + // Log admin action + let mut params = Map::new(env); + params.set( + String::from_str(env, "reason"), + rollback_data.reason.clone(), + ); + + AdminActionLogger::log_action( + env, + &admin, + "rollback_upgrade", + Some(String::from_str(env, "contract")), + params, + true, + None, + )?; + + // Emit completion event + UpgradeEventEmitter::emit_rollback_completed(env, &rollback_data); + + Ok(rollback_data) + } + + /// Validate rollback conditions + pub fn validate_rollback_conditions(env: &Env) -> Result<(), UpgradeError> { + // Check if upgrade is in progress + if !UpgradeManager::is_upgrade_in_progress(env) { + return Err(UpgradeError::UpgradeNotInProgress); + } + + // Check if backup exists + let backup_timestamp: Option = env + .storage() + .persistent() + .get(&Symbol::new(env, storage_keys::last_backup_timestamp_key())); + + if backup_timestamp.is_none() { + return Err(UpgradeError::BackupFailed); + } + + Ok(()) + } + + /// Emergency rollback with minimal validation + pub fn emergency_rollback( + env: &Env, + admin: Address, + reason: String, + ) -> Result { + // Only validate admin auth for emergency rollback + AdminAccessControl::require_admin_auth(env, &admin)?; + + // Force rollback regardless of conditions + Self::rollback_upgrade(env, admin, reason) + } +} + +// ===== UPGRADE EVENT EMITTER ===== +pub struct UpgradeEventEmitter; + +impl UpgradeEventEmitter { + pub fn emit_upgrade_initiated(env: &Env, upgrade_data: &UpgradeData) { + EventEmitter::emit_upgrade_initiated( + env, + &upgrade_data.upgrade_id, + &upgrade_data.admin, + &upgrade_data.from_version, + &upgrade_data.to_version, + ); + } + + pub fn emit_upgrade_completed(env: &Env, upgrade_data: &UpgradeData) { + EventEmitter::emit_upgrade_completed( + env, + &upgrade_data.upgrade_id, + &upgrade_data.admin, + &upgrade_data.to_version, + true, + 1, + ); + } + + pub fn emit_rollback_initiated(env: &Env, rollback_data: &RollbackData) { + EventEmitter::emit_rollback_executed( + env, + &rollback_data.rollback_id, + &rollback_data.admin, + &rollback_data.from_version, + &rollback_data.to_version, + &rollback_data.reason, + ); + } + + pub fn emit_rollback_completed(env: &Env, rollback_data: &RollbackData) { + EventEmitter::emit_rollback_executed( + env, + &rollback_data.rollback_id, + &rollback_data.admin, + &rollback_data.from_version, + &rollback_data.to_version, + &rollback_data.reason, + ); + } +} + +// ===== UPGRADE UTILITIES ===== + +/// Utility functions for upgrade system +pub struct UpgradeUtils; + +impl UpgradeUtils { + /// Generate unique upgrade ID + pub fn generate_upgrade_id(env: &Env, _admin: &Address, version: &ContractVersion) -> String { + // In a real implementation, this would generate a proper unique ID + // For now, create a simple ID based on timestamp and version + let timestamp = env.ledger().timestamp(); + let timestamp_str = timestamp.to_string(); + let version_str = version.major.to_string(); + + // Create simple concatenated ID + let id_str = format!("upgrade_{}_{}", timestamp_str, version_str); + String::from_str(env, &id_str) + } + + /// Generate unique rollback ID + pub fn generate_rollback_id(env: &Env, _admin: &Address, _upgrade_id: &String) -> String { + let timestamp = env.ledger().timestamp(); + let timestamp_str = timestamp.to_string(); + + // Create simple concatenated ID + let id_str = format!("rollback_{}", timestamp_str); + String::from_str(env, &id_str) + } + + /// Check if rollback is possible + pub fn can_rollback(env: &Env) -> bool { + RollbackManager::validate_rollback_conditions(env).is_ok() + } + + /// Get upgrade statistics + pub fn get_upgrade_statistics(env: &Env) -> Map { + let mut stats = Map::new(env); + + let history = UpgradeManager::get_upgrade_history(env); + let total_upgrades = history.len(); + + stats.set( + String::from_str(env, "total_upgrades"), + String::from_str(env, &total_upgrades.to_string()), + ); + + stats.set( + String::from_str(env, "current_version"), + UpgradeManager::get_contract_version(env) + .map(|v| v.to_string(env)) + .unwrap_or_else(|_| String::from_str(env, "unknown")), + ); + + stats + } +} + +#[cfg(test)] +mod tests { + use super::*; + use soroban_sdk::testutils::Address as _; + use soroban_sdk::{Address, Env}; + + /// Setup test environment with initialized contract and admin + fn setup_test_environment(env: &Env) -> (Address, Address) { + let contract_id = Address::generate(env); + let admin = Address::generate(env); + + env.as_contract(&contract_id, || { + // Initialize admin + env.storage() + .persistent() + .set(&Symbol::new(env, "Admin"), &admin); + }); + + (contract_id, admin) + } + + /// Create test contract version data + fn create_test_version_data(env: &Env, prefix: &str) -> ContractVersion { + let version_str = format!("{}_version", prefix); + ContractVersion::new(env, 1, 0, 0, &version_str) + } + + #[test] + fn test_upgrade_contract_version_creation() { + let env = Env::default(); + let version = create_test_version_data(&env, "test"); + assert_eq!(version.major, 1, "Major version should be 1"); + assert_eq!(version.minor, 0, "Minor version should be 0"); + assert_eq!(version.patch, 0, "Patch version should be 0"); + assert_eq!(version.build, String::from_str(&env, "test_version"), "Build should match"); + } + + #[test] + fn test_upgrade_version_compatibility() { + let env = Env::default(); + let version1 = ContractVersion::new(&env, 1, 0, 0, "test1"); + let version2 = ContractVersion::new(&env, 1, 1, 0, "test2"); + let version3 = ContractVersion::new(&env, 2, 0, 0, "test3"); + + assert!(version1.is_compatible_with(&version2), "Versions 1.0.0 and 1.1.0 should be compatible"); + assert!(!version1.is_compatible_with(&version3), "Versions 1.0.0 and 2.0.0 should not be compatible"); + } + + #[test] + fn test_upgrade_version_comparison() { + let env = Env::default(); + let version1 = ContractVersion::new(&env, 1, 0, 0, "test1"); + let version2 = ContractVersion::new(&env, 1, 1, 0, "test2"); + let version3 = ContractVersion::new(&env, 1, 0, 1, "test3"); + + assert!(version2.is_newer_than(&version1), "Version 1.1.0 should be newer than 1.0.0"); + assert!(!version1.is_newer_than(&version2), "Version 1.0.0 should not be newer than 1.1.0"); + assert!(version3.is_newer_than(&version1), "Version 1.0.1 should be newer than 1.0.0"); + } + + #[test] + fn test_upgrade_data_validation() { + let env = Env::default(); + let (contract_id, admin) = setup_test_environment(&env); + let from_version = create_test_version_data(&env, "from"); + let to_version = ContractVersion::new(&env, 1, 1, 0, "to_version"); + + env.as_contract(&contract_id, || { + let upgrade_data = UpgradeData::new( + &env, + from_version, + to_version, + String::from_str(&env, "test_hash"), + admin, + ); + assert!(upgrade_data.validate().is_ok(), "Upgrade data should validate successfully"); + }); + } + + #[test] + fn test_upgrade_data_validation_invalid_version() { + let env = Env::default(); + let (contract_id, admin) = setup_test_environment(&env); + let from_version = create_test_version_data(&env, "from"); + let to_version = ContractVersion::new(&env, 0, 0, 0, "invalid_version"); + + env.as_contract(&contract_id, || { + let upgrade_data = UpgradeData::new( + &env, + from_version, + to_version, + String::from_str(&env, "test_hash"), + admin, + ); + assert_eq!( + upgrade_data.validate(), + Err(UpgradeError::UpgradeValidationFailed), + "Should fail validation for downgrade" + ); + }); + } + + #[test] + fn test_upgrade_manager_version_management() { + let env = Env::default(); + env.mock_all_auths(); + let (contract_id, _) = setup_test_environment(&env); + let version = create_test_version_data(&env, "test"); + + env.as_contract(&contract_id, || { + assert!(UpgradeManager::set_contract_version(&env, version.clone()).is_ok(), "Should set version successfully"); + let retrieved = UpgradeManager::get_contract_version(&env).unwrap(); + assert_eq!(retrieved, version, "Retrieved version should match set version"); + }); + } + + #[test] + fn test_upgrade_validator_compatibility() { + let env = Env::default(); + let old_version = create_test_version_data(&env, "old"); + let new_version = ContractVersion::new(&env, 1, 1, 0, "new_version"); + + let report = UpgradeValidator::validate_upgrade_compatibility(&env, &old_version, &new_version).unwrap(); + assert!(report.is_compatible, "Versions should be compatible"); + + let incompatible_version = ContractVersion::new(&env, 2, 0, 0, "incompatible"); + let report = UpgradeValidator::validate_upgrade_compatibility(&env, &old_version, &incompatible_version).unwrap(); + assert!(!report.is_compatible, "Versions should not be compatible"); + } + + #[test] + fn test_upgrade_validator_safety() { + let env = Env::default(); + env.mock_all_auths(); + let (contract_id, admin) = setup_test_environment(&env); + let from_version = create_test_version_data(&env, "from"); + let to_version = ContractVersion::new(&env, 1, 1, 0, "to_version"); + + env.as_contract(&contract_id, || { + let upgrade_data = UpgradeData::new( + &env, + from_version.clone(), + to_version.clone(), + String::from_str(&env, "test_hash"), + admin.clone(), + ); + + let safety_report = UpgradeValidator::validate_upgrade_safety(&env, &upgrade_data).unwrap(); + assert!(safety_report.is_safe, "Upgrade should be safe with correct admin"); + + let wrong_admin = Address::generate(&env); + let invalid_upgrade_data = UpgradeData::new( + &env, + from_version, + to_version, + String::from_str(&env, "test_hash"), + wrong_admin, + ); + + let safety_report = UpgradeValidator::validate_upgrade_safety(&env, &invalid_upgrade_data).unwrap(); + assert!(!safety_report.is_safe, "Upgrade should be unsafe with wrong admin"); + }); + } + + #[test] + fn test_upgrade_contract_success() { + let env = Env::default(); + env.mock_all_auths(); + let (contract_id, admin) = setup_test_environment(&env); + let new_contract = Address::generate(&env); + let initial_version = create_test_version_data(&env, "initial"); + + env.as_contract(&contract_id, || { + assert!(UpgradeManager::set_contract_version(&env, initial_version.clone()).is_ok(), "Should set initial version"); + + let result = UpgradeManager::upgrade_contract(&env, new_contract, admin); + assert!(result.is_ok(), "Upgrade should succeed"); + + let upgrade_data = result.unwrap(); + assert_eq!(upgrade_data.status, UpgradeStatus::Completed, "Upgrade status should be Completed"); + assert!(upgrade_data.to_version.is_newer_than(&initial_version), "New version should be newer"); + + let current_version = UpgradeManager::get_contract_version(&env).unwrap(); + assert_eq!(current_version, upgrade_data.to_version, "Current version should match upgraded version"); + }); + } + + #[test] + fn test_upgrade_contract_unauthorized() { + let env = Env::default(); + env.mock_all_auths(); + let (contract_id, admin) = setup_test_environment(&env); + let unauthorized_admin = Address::generate(&env); + let new_contract = Address::generate(&env); + + env.as_contract(&contract_id, || { + let result = UpgradeManager::upgrade_contract(&env, new_contract, unauthorized_admin); + assert_eq!( + result, + Err(UpgradeError::AdminNotSet), + "Should fail with unauthorized admin" + ); + }); + } + + #[test] + fn test_upgrade_contract_already_in_progress() { + let env = Env::default(); + env.mock_all_auths(); + let (contract_id, admin) = setup_test_environment(&env); + let new_contract = Address::generate(&env); + + env.as_contract(&contract_id, || { + UpgradeManager::set_upgrade_in_progress(&env, true).unwrap(); + let result = UpgradeManager::upgrade_contract(&env, new_contract, admin); + assert_eq!( + result, + Err(UpgradeError::UpgradeInProgress), + "Should fail when upgrade is already in progress" + ); + }); + } + + #[test] + fn test_rollback_manager_conditions() { + let env = Env::default(); + env.mock_all_auths(); + let (contract_id, _) = setup_test_environment(&env); + + env.as_contract(&contract_id, || { + // Test without upgrade in progress + let result = RollbackManager::validate_rollback_conditions(&env); + assert_eq!( + result, + Err(UpgradeError::UpgradeNotInProgress), + "Should fail without upgrade in progress" + ); + + // Set upgrade in progress and create backup + UpgradeManager::set_upgrade_in_progress(&env, true).unwrap(); + UpgradeManager::create_state_backup(&env).unwrap(); + + // Now rollback conditions should be met + assert!(RollbackManager::validate_rollback_conditions(&env).is_ok(), "Rollback conditions should be met"); + }); + } + + #[test] + fn test_rollback_upgrade_success() { + let env = Env::default(); + env.mock_all_auths(); + let (contract_id, admin) = setup_test_environment(&env); + let initial_version = create_test_version_data(&env, "initial"); + let reason = String::from_str(&env, "Test rollback"); + + env.as_contract(&contract_id, || { + // Setup initial state + UpgradeManager::set_contract_version(&env, initial_version.clone()).unwrap(); + UpgradeManager::set_upgrade_in_progress(&env, true).unwrap(); + UpgradeManager::create_state_backup(&env).unwrap(); + + // Store upgrade history for rollback + let to_version = ContractVersion::new(&env, 1, 1, 0, "new_version"); + let upgrade_data = UpgradeData::new( + &env, + initial_version.clone(), + to_version, + String::from_str(&env, "test_hash"), + admin.clone(), + ); + env.storage().persistent().set( + &Symbol::new(&env, storage_keys::upgrade_history_key()), + &upgrade_data, + ); + + // Perform rollback + let result = RollbackManager::rollback_upgrade(&env, admin, reason); + assert!(result.is_ok(), "Rollback should succeed"); + + let rollback_data = result.unwrap(); + assert_eq!(rollback_data.status, RollbackStatus::Completed, "Rollback status should be Completed"); + assert!(rollback_data.success, "Rollback should be successful"); + + let current_version = UpgradeManager::get_contract_version(&env).unwrap(); + assert_eq!(current_version, initial_version, "Should rollback to initial version"); + assert!(!UpgradeManager::is_upgrade_in_progress(&env), "Upgrade should no longer be in progress"); + }); + } + + #[test] + fn test_rollback_upgrade_no_backup() { + let env = Env::default(); + env.mock_all_auths(); + let (contract_id, admin) = setup_test_environment(&env); + let reason = String::from_str(&env, "Test rollback"); + + env.as_contract(&contract_id, || { + // Set upgrade in progress but no backup + UpgradeManager::set_upgrade_in_progress(&env, true).unwrap(); + + let result = RollbackManager::rollback_upgrade(&env, admin, reason); + assert_eq!( + result, + Err(UpgradeError::BackupFailed), + "Should fail without backup" + ); + }); + } + + #[test] + fn test_rollback_upgrade_no_history() { + let env = Env::default(); + env.mock_all_auths(); + let (contract_id, admin) = setup_test_environment(&env); + let reason = String::from_str(&env, "Test rollback"); + + env.as_contract(&contract_id, || { + // Set upgrade in progress and backup + UpgradeManager::set_upgrade_in_progress(&env, true).unwrap(); + UpgradeManager::create_state_backup(&env).unwrap(); + + // No upgrade history set + let result = RollbackManager::rollback_upgrade(&env, admin, reason); + assert_eq!( + result, + Err(UpgradeError::RollbackConditionsNotMet), + "Should fail without upgrade history" + ); + }); + } + + #[test] + fn test_emergency_rollback() { + let env = Env::default(); + env.mock_all_auths(); + let (contract_id, admin) = setup_test_environment(&env); + let initial_version = create_test_version_data(&env, "initial"); + let reason = String::from_str(&env, "Emergency rollback"); + + env.as_contract(&contract_id, || { + // Setup initial state + UpgradeManager::set_contract_version(&env, initial_version.clone()).unwrap(); + UpgradeManager::set_upgrade_in_progress(&env, true).unwrap(); + UpgradeManager::create_state_backup(&env).unwrap(); + + // Store upgrade history + let to_version = ContractVersion::new(&env, 1, 1, 0, "new_version"); + let upgrade_data = UpgradeData::new( + &env, + initial_version.clone(), + to_version, + String::from_str(&env, "test_hash"), + admin.clone(), + ); + env.storage().persistent().set( + &Symbol::new(&env, storage_keys::upgrade_history_key()), + &upgrade_data, + ); + + // Perform emergency rollback + let result = RollbackManager::emergency_rollback(&env, admin, reason); + assert!(result.is_ok(), "Emergency rollback should succeed"); + + let rollback_data = result.unwrap(); + assert_eq!(rollback_data.status, RollbackStatus::Completed, "Rollback status should be Completed"); + assert!(rollback_data.success, "Rollback should be successful"); + + let current_version = UpgradeManager::get_contract_version(&env).unwrap(); + assert_eq!(current_version, initial_version, "Should rollback to initial version"); + }); + } +} From 58483c2a622d42297243a0fe589c30c6e2f83283 Mon Sep 17 00:00:00 2001 From: psychemist Date: Thu, 31 Jul 2025 19:14:04 +0100 Subject: [PATCH 2/2] feat: add upgrade events --- contracts/predictify-hybrid/src/events.rs | 116 +++++++++++++++++++++- 1 file changed, 113 insertions(+), 3 deletions(-) diff --git a/contracts/predictify-hybrid/src/events.rs b/contracts/predictify-hybrid/src/events.rs index cf67dc47..f53fbbbb 100644 --- a/contracts/predictify-hybrid/src/events.rs +++ b/contracts/predictify-hybrid/src/events.rs @@ -2,10 +2,9 @@ extern crate alloc; // use alloc::string::ToString; // Removed to fix Display/ToString trait errors use soroban_sdk::{contracttype, symbol_short, vec, Address, Env, Map, String, Symbol, Vec}; - - use crate::errors::Error; use crate::config::Environment; +use crate::upgrade::ContractVersion; // Define AdminRole locally since it's not available in the crate root #[derive(Clone, Debug, Eq, PartialEq)] @@ -313,6 +312,56 @@ pub struct ConfigInitializedEvent { pub timestamp: u64, } +// Upgrade Initiated Event +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct UpgradeInitiatedEvent { + /// Upgrade ID + pub upgrade_id: String, + /// Admin address + pub admin: Address, + /// Contract from version + pub from_version: ContractVersion, + /// Contract to version + pub to_version: ContractVersion, + /// Timestamp + pub timestamp: u64, +} + +// Upgrade Completed Event +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct UpgradeCompletedEvent { + /// Upgrade ID + pub upgrade_id: String, + /// Admin address + pub admin: Address, + /// Contract new version + pub new_version: ContractVersion, + /// Success status + pub success: bool, + /// Upgrade duration + pub duration_seconds: u64, + /// Timestamp + pub timestamp: u64, +} + +// Rollback Executed Event +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RollbackExecutedEvent { + /// Rollback ID + pub rollback_id: String, + /// Admin address + pub admin: Address, + /// Contract from version + pub from_version: ContractVersion, + /// Contract to version + pub to_version: ContractVersion, + pub reason: String, + pub timestamp: u64, +} + // ===== EVENT EMISSION UTILITIES ===== /// Event emission utilities @@ -663,6 +712,67 @@ impl EventEmitter { Self::store_event(env, &symbol_short!("mkt_final"), &event); } + /// Emit upgrade initiated event + pub fn emit_upgrade_initiated( + env: &Env, + upgrade_id: &String, + admin: &Address, + from_version: &crate::upgrade::ContractVersion, + to_version: &crate::upgrade::ContractVersion, + ) { + let event = UpgradeInitiatedEvent { + upgrade_id: upgrade_id.clone(), + admin: admin.clone(), + from_version: from_version.clone(), + to_version: to_version.clone(), + timestamp: env.ledger().timestamp(), + }; + + Self::store_event(env, &symbol_short!("upg_init"), &event); + } + + /// Emit upgrade completed event + pub fn emit_upgrade_completed( + env: &Env, + upgrade_id: &String, + admin: &Address, + new_version: &crate::upgrade::ContractVersion, + success: bool, + duration_seconds: u64, + ) { + let event = UpgradeCompletedEvent { + upgrade_id: upgrade_id.clone(), + admin: admin.clone(), + new_version: new_version.clone(), + success, + duration_seconds, + timestamp: env.ledger().timestamp(), + }; + + Self::store_event(env, &symbol_short!("upg_comp"), &event); + } + + /// Emit rollback executed event + pub fn emit_rollback_executed( + env: &Env, + rollback_id: &String, + admin: &Address, + from_version: &crate::upgrade::ContractVersion, + to_version: &crate::upgrade::ContractVersion, + reason: &String, + ) { + let event = RollbackExecutedEvent { + rollback_id: rollback_id.clone(), + admin: admin.clone(), + from_version: from_version.clone(), + to_version: to_version.clone(), + reason: reason.clone(), + timestamp: env.ledger().timestamp(), + }; + + Self::store_event(env, &symbol_short!("rbk_exec"), &event); + } + /// Store event in persistent storage fn store_event(env: &Env, event_key: &Symbol, event_data: &T) where @@ -1288,4 +1398,4 @@ impl EventDocumentation { examples } -} +} \ No newline at end of file