diff --git a/contracts/predictify-hybrid/src/err.rs b/contracts/predictify-hybrid/src/err.rs index dbfa75ce..1d0c9ae0 100644 --- a/contracts/predictify-hybrid/src/err.rs +++ b/contracts/predictify-hybrid/src/err.rs @@ -189,6 +189,11 @@ pub enum Error { CBError = 504, /// Rate limit exceeded. Too many requests in the time window. RateLimitExceeded = 505, + + // ===== UPGRADE ERRORS ===== + /// Upgrade WASM hash does not match the expected predecessor in the chain. + /// This prevents out-of-order or forked upgrades. + UpgradeChainMismatch = 600, } // ===== ERROR CATEGORIZATION AND RECOVERY SYSTEM ===== @@ -1428,6 +1433,9 @@ impl Error { Error::CBOpen => "Circuit breaker is open (operations blocked)", Error::CBError => "Generic circuit breaker subsystem error", Error::RateLimitExceeded => "Rate limit exceeded; too many requests in the time window", + + // Upgrade errors + Error::UpgradeChainMismatch => "Upgrade WASM hash does not match the expected predecessor", } } @@ -1522,6 +1530,9 @@ impl Error { Error::CBOpen => "CIRCUIT_BREAKER_OPEN", Error::CBError => "CIRCUIT_BREAKER_ERROR", Error::RateLimitExceeded => "RATE_LIMIT_EXCEEDED", + + // Upgrade errors + Error::UpgradeChainMismatch => "UPGRADE_CHAIN_MISMATCH", } } } diff --git a/contracts/predictify-hybrid/src/events.rs b/contracts/predictify-hybrid/src/events.rs index ee6b1de4..ccf066f9 100644 --- a/contracts/predictify-hybrid/src/events.rs +++ b/contracts/predictify-hybrid/src/events.rs @@ -1570,6 +1570,33 @@ pub struct ContractUpgradedEvent { pub timestamp: u64, } +/// Event emitted when WASM hash chain verification fails during upgrade. +/// +/// This event is critical for security as it indicates an attempt to apply +/// an out-of-order or forked upgrade. The upgrade was rejected because the +/// expected predecessor hash did not match the current contract's WASM hash. +/// +/// # Security Implications +/// +/// - Prevents downgrade attacks +/// - Blocks forked upgrade chains +/// - Ensures linear upgrade progression +/// - Detects potential compromise scenarios +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct UpgradeChainMismatchEvent { + /// Expected predecessor hash (from upgrade plan) + pub expected_predecessor: soroban_sdk::BytesN<32>, + /// Actual current hash (from contract) + pub actual_current_hash: soroban_sdk::BytesN<32>, + /// Proposed new hash (that was rejected) + pub proposed_new_hash: soroban_sdk::BytesN<32>, + /// Admin who attempted the upgrade + pub admin: Address, + /// Timestamp of the failed attempt + pub timestamp: u64, +} + /// Event emitted when market deadline is extended /// /// This event tracks market deadline extensions, providing transparency @@ -3496,6 +3523,27 @@ impl EventEmitter { env.events().publish((symbol_short!("rollback"),), event); } + /// Emit upgrade chain mismatch event when hash verification fails + pub fn emit_upgrade_chain_mismatch_event( + env: &Env, + expected_predecessor: &soroban_sdk::BytesN<32>, + actual_current_hash: &soroban_sdk::BytesN<32>, + proposed_new_hash: &soroban_sdk::BytesN<32>, + admin: &Address, + ) { + let event = UpgradeChainMismatchEvent { + expected_predecessor: expected_predecessor.clone(), + actual_current_hash: actual_current_hash.clone(), + proposed_new_hash: proposed_new_hash.clone(), + admin: admin.clone(), + timestamp: env.ledger().timestamp(), + }; + + Self::store_event(env, &symbol_short!("chain_mismatch"), &event); + env.events() + .publish((symbol_short!("chain_mismatch"), admin.clone()), event); + } + /// Emit upgrade proposal created event pub fn emit_upgrade_proposal_created_event( env: &Env, diff --git a/contracts/predictify-hybrid/src/lib.rs b/contracts/predictify-hybrid/src/lib.rs index 96324a3f..a0dcd963 100644 --- a/contracts/predictify-hybrid/src/lib.rs +++ b/contracts/predictify-hybrid/src/lib.rs @@ -18,6 +18,11 @@ extern crate wee_alloc; #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; +// Short symbol keys (max length 9 for Soroban compatibility) +const SYM_PLATFORM_FEE: &str = "plat_fee"; // was "platform_fee" (12 chars) +const SYM_ALLOWED_ASSETS: &str = "allowed"; // was "allowed_assets" (14 chars) +const SYM_ADMIN: &str = "Admin"; // kept as is (5 chars) + // Module declarations - all modules enabled mod admin; #[cfg(test)] @@ -288,7 +293,7 @@ impl PredictifyHybrid { if env .storage() .persistent() - .has(&Symbol::new(&env, "platform_fee")) + .has(&Symbol::new(&env, SYM_PLATFORM_FEE)) { return Err(Error::InvalidState); } @@ -315,7 +320,7 @@ impl PredictifyHybrid { // Store platform fee configuration in persistent storage env.storage() .persistent() - .set(&Symbol::new(&env, "platform_fee"), &fee_percentage); + .set(&Symbol::new(&env, SYM_PLATFORM_FEE), &fee_percentage); // Store default contract configuration so validators have deterministic bounds let mut default_config = crate::config::ConfigManager::get_development_config(&env); @@ -364,7 +369,7 @@ impl PredictifyHybrid { // Store custom allowed assets env.storage() .persistent() - .set(&Symbol::new(&env, "allowed_assets"), &assets); + .set(&Symbol::new(&env, SYM_ALLOWED_ASSETS), &assets); } else { // Initialize with defaults crate::tokens::TokenRegistry::initialize_with_defaults(&env); @@ -382,7 +387,7 @@ impl PredictifyHybrid { fn stored_primary_admin(env: &Env) -> Result { env.storage() .persistent() - .get(&Symbol::new(env, "Admin")) + .get(&Symbol::new(env, SYM_ADMIN)) .ok_or(Error::AdminNotSet) } @@ -1839,7 +1844,7 @@ impl PredictifyHybrid { let stored_admin: Address = env .storage() .persistent() - .get(&Symbol::new(&env, "Admin")) + .get(&Symbol::new(&env, SYM_ADMIN)) .unwrap_or_else(|| panic_with_error!(env, Error::AdminNotSet)); if admin != stored_admin { @@ -1868,7 +1873,7 @@ impl PredictifyHybrid { let stored_admin: Address = env .storage() .persistent() - .get(&Symbol::new(&env, "Admin")) + .get(&Symbol::new(&env, SYM_ADMIN)) .unwrap_or_else(|| panic_with_error!(env, Error::AdminNotSet)); if admin != stored_admin { @@ -1899,7 +1904,7 @@ impl PredictifyHybrid { let stored_admin: Address = env .storage() .persistent() - .get(&Symbol::new(&env, "Admin")) + .get(&Symbol::new(&env, SYM_ADMIN)) .unwrap_or_else(|| panic_with_error!(env, Error::AdminNotSet)); if admin != stored_admin { @@ -1925,7 +1930,7 @@ impl PredictifyHybrid { let stored_admin: Address = env .storage() .persistent() - .get(&Symbol::new(&env, "Admin")) + .get(&Symbol::new(&env, SYM_ADMIN)) .ok_or(Error::AdminNotSet)?; if admin != stored_admin { @@ -1959,10 +1964,13 @@ impl PredictifyHybrid { let fee_percent = crate::config::ConfigManager::get_config(&env) .map(|cfg| cfg.fees.platform_fee_percentage) .unwrap_or_else(|_| { - env.storage() - .persistent() - .get(&Symbol::new(&env, "platform_fee")) - .unwrap_or(2) + // Use the short platform fee key (backwards-compat fallback to legacy long keys + // is not possible here because Soroban restricts symbols to <=9 chars). + // If you need to read old on-chain keys created with long symbols, + // perform a storage migration on-chain (one-time) to move legacy values + // under the new short key. + let new_key = Symbol::new(&env, SYM_PLATFORM_FEE); + env.storage().persistent().get(&new_key).unwrap_or(2) }); if fee_percent < 0 || fee_percent > PERCENTAGE_DENOMINATOR { @@ -5968,27 +5976,37 @@ impl PredictifyHybrid { /// /// - Requires Soroban `require_auth()` from the caller /// - Requires the caller to match the stored primary admin in persistent storage + /// - Validates WASM hash chain to prevent out-of-order/forked upgrades /// - Validates version compatibility /// - Performs safety checks before upgrade /// - Logs all upgrade attempts for audit trail /// + /// # Parameters + /// + /// * `env` - Soroban environment + /// * `admin` - Admin performing the upgrade (must be authorized) + /// * `new_wasm_hash` - Hash of new Wasm bytecode to deploy + /// * `expected_predecessor` - Expected current WASM hash (for chain verification) + /// /// # Example /// /// ```rust /// # use soroban_sdk::{Env, Address, BytesN}; /// # let env = Env::default(); /// # let admin = Address::generate(&env); - /// # let new_wasm_hash = BytesN::from_array(&env, &[0u8; 32]); + /// # let new_wasm_hash = BytesN::from_array(&env, &[1u8; 32]); + /// # let current_hash = BytesN::from_array(&env, &[0u8; 32]); /// - /// // Perform upgrade with admin authorization + /// // Perform upgrade with admin authorization and chain verification /// admin.require_auth(); - /// PredictifyHybrid::upgrade_contract(env, admin, new_wasm_hash)?; + /// PredictifyHybrid::upgrade_contract(env, admin, new_wasm_hash, current_hash)?; /// # Ok::<(), predictify_hybrid::errors::Error>(()) /// ``` /// /// # Errors /// /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. + /// Returns [`Error::UpgradeChainMismatch`] if the expected predecessor does not match the current WASM hash. /// /// # Events /// @@ -5997,9 +6015,15 @@ impl PredictifyHybrid { env: Env, admin: Address, new_wasm_hash: soroban_sdk::BytesN<32>, + expected_predecessor: soroban_sdk::BytesN<32>, ) -> Result<(), Error> { Self::require_primary_admin(&env, &admin)?; - let result = upgrade_manager::UpgradeManager::upgrade_contract(&env, &admin, new_wasm_hash); + let result = upgrade_manager::UpgradeManager::upgrade_contract( + &env, + &admin, + new_wasm_hash, + expected_predecessor, + ); crate::audit_trail::AuditTrailManager::append_record( &env, diff --git a/contracts/predictify-hybrid/src/upgrade_manager.rs b/contracts/predictify-hybrid/src/upgrade_manager.rs index f5209dc7..300d2c79 100644 --- a/contracts/predictify-hybrid/src/upgrade_manager.rs +++ b/contracts/predictify-hybrid/src/upgrade_manager.rs @@ -87,6 +87,7 @@ use crate::versioning::{IrreversibleAcknowledgement, Version, VersionManager, Ve /// - Upgrade description and rationale /// - Validation requirements and safety checks /// - Rollback plan and recovery procedures +/// - WASM hash chain verification for security #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct UpgradeProposal { @@ -116,6 +117,10 @@ pub struct UpgradeProposal { pub required_validations: Vec, /// Validation results pub validation_results: Vec, + /// Expected predecessor WASM hash (for chain verification) + /// Must match the current contract's WASM hash for upgrade to proceed. + /// For genesis upgrades (first deployment), this should be all zeros. + pub expected_predecessor: BytesN<32>, } impl UpgradeProposal { @@ -148,6 +153,7 @@ impl UpgradeProposal { has_rollback_hash: false, required_validations: Vec::new(env), validation_results: Vec::new(env), + expected_predecessor: BytesN::from_array(env, &[0u8; 32]), // Default to genesis } } @@ -197,6 +203,18 @@ impl UpgradeProposal { true } + + /// Set the expected predecessor WASM hash for chain verification + /// + /// This hash must match the current contract's WASM hash for the upgrade + /// to be accepted. This prevents out-of-order and forked upgrades. + /// + /// # Parameters + /// + /// * `predecessor_hash` - The expected current WASM hash before this upgrade + pub fn set_expected_predecessor(&mut self, predecessor_hash: BytesN<32>) { + self.expected_predecessor = predecessor_hash; + } } /// Validation result for upgrade safety checks @@ -297,30 +315,44 @@ impl UpgradeManager { /// /// This is the primary upgrade function that: /// 1. Validates admin authorization - /// 2. Checks version compatibility - /// 3. Performs pre-upgrade safety checks - /// 4. Updates contract Wasm bytecode - /// 5. Records upgrade in history - /// 6. Emits upgrade event + /// 2. Verifies WASM hash chain (prevents out-of-order/forked upgrades) + /// 3. Checks version compatibility + /// 4. Performs pre-upgrade safety checks + /// 5. Updates contract Wasm bytecode + /// 6. Records upgrade in history + /// 7. Emits upgrade event /// /// # Parameters /// /// * `env` - Soroban environment /// * `admin` - Admin performing the upgrade (must be authorized) /// * `new_wasm_hash` - Hash of new Wasm bytecode to deploy + /// * `expected_predecessor` - Expected current WASM hash (for chain verification) /// /// # Returns /// /// * `Ok(())` if upgrade succeeds - /// * `Err(Error)` if authorization fails or upgrade is incompatible + /// * `Err(Error)` if authorization fails, hash chain mismatch, or upgrade is incompatible /// /// # Security /// /// - Requires admin authentication via `require_auth()` + /// - Validates WASM hash chain to prevent out-of-order upgrades /// - Validates version compatibility /// - Performs safety checks before upgrade /// - Logs all upgrade attempts for audit /// + /// # Hash Chain Verification + /// + /// The upgrade verifies that the `expected_predecessor` matches the current + /// contract's WASM hash. This ensures: + /// - Upgrades are applied in the correct order + /// - No forked upgrade chains can be applied + /// - Downgrade attacks are prevented + /// + /// For genesis upgrades (first deployment), both the current hash and + /// expected_predecessor should be all zeros. + /// /// # Example /// /// ```rust @@ -328,19 +360,21 @@ impl UpgradeManager { /// # use predictify_hybrid::upgrade_manager::UpgradeManager; /// # let env = Env::default(); /// # let admin = Address::generate(&env); - /// # let new_wasm_hash = BytesN::from_array(&env, &[0u8; 32]); + /// # let new_wasm_hash = BytesN::from_array(&env, &[1u8; 32]); + /// # let current_hash = BytesN::from_array(&env, &[0u8; 32]); /// /// // Admin authorization required /// admin.require_auth(); /// - /// // Perform upgrade - /// UpgradeManager::upgrade_contract(&env, &admin, new_wasm_hash)?; + /// // Perform upgrade with chain verification + /// UpgradeManager::upgrade_contract(&env, &admin, new_wasm_hash, current_hash)?; /// # Ok::<(), predictify_hybrid::errors::Error>(()) /// ``` pub fn upgrade_contract( env: &Env, admin: &Address, new_wasm_hash: BytesN<32>, + expected_predecessor: BytesN<32>, ) -> Result<(), Error> { // Validate admin permissions Self::validate_admin_permissions(env, admin)?; @@ -349,6 +383,40 @@ impl UpgradeManager { let current_version = Self::get_contract_version(env)?; let current_wasm_hash = Self::get_current_wasm_hash(env); + // ── WASM HASH CHAIN VERIFICATION ───────────────────────────────────── + // Verify that the expected predecessor matches the current contract's WASM hash. + // This prevents out-of-order upgrades, forked chains, and downgrade attacks. + let zero_hash = BytesN::from_array(env, &[0u8; 32]); + + // Genesis case: if current hash is zero, allow upgrade if predecessor is also zero + let is_genesis = current_wasm_hash == zero_hash; + let predecessor_is_genesis = expected_predecessor == zero_hash; + + if is_genesis && !predecessor_is_genesis { + // Current is genesis but predecessor is not - reject + EventEmitter::emit_upgrade_chain_mismatch_event( + env, + &expected_predecessor, + ¤t_wasm_hash, + &new_wasm_hash, + admin, + ); + return Err(Error::UpgradeChainMismatch); + } + + // Non-genesis case: predecessor must exactly match current hash + if !is_genesis && current_wasm_hash != expected_predecessor { + // Hash mismatch - reject upgrade + EventEmitter::emit_upgrade_chain_mismatch_event( + env, + &expected_predecessor, + ¤t_wasm_hash, + &new_wasm_hash, + admin, + ); + return Err(Error::UpgradeChainMismatch); + } + // Create upgrade record let upgrade_id = Symbol::new(env, &format!("upgrade_{}", env.ledger().timestamp())); @@ -632,6 +700,45 @@ impl UpgradeManager { Ok(stats) } + /// Get WASM hash chain history + /// + /// Returns the complete chain of WASM hashes from all upgrades, providing + /// a verifiable history of contract bytecode evolution. This is critical for + /// security audits and verifying upgrade integrity. + /// + /// The chain includes: + /// - Previous WASM hash (before upgrade) + /// - New WASM hash (after upgrade) + /// - Upgrade timestamp + /// - Admin who performed the upgrade + /// + /// # Returns + /// + /// * `Ok(Vec)` - Complete upgrade history with hash chain + /// + /// # Security + /// + /// This function enables verification that: + /// - All upgrades followed the hash chain + /// - No forks or out-of-order upgrades occurred + /// - The upgrade history is complete and linear + pub fn get_wasm_hash_chain(env: &Env) -> Result, Error> { + Self::get_upgrade_history(env) + } + + /// Get the current WASM hash + /// + /// Returns the currently active contract's WASM bytecode hash. + /// This is the hash that should be used as the `expected_predecessor` + /// for the next upgrade. + /// + /// # Returns + /// + /// * `BytesN<32>` - Current WASM hash (zero if not set) + pub fn get_current_wasm_hash_public(env: &Env) -> BytesN<32> { + Self::get_current_wasm_hash(env) + } + /// Test upgrade safety without executing /// /// Performs dry-run validation of upgrade proposal without actually diff --git a/contracts/predictify-hybrid/src/upgrade_manager_tests.rs b/contracts/predictify-hybrid/src/upgrade_manager_tests.rs index 9fe865e9..1b7606f2 100644 --- a/contracts/predictify-hybrid/src/upgrade_manager_tests.rs +++ b/contracts/predictify-hybrid/src/upgrade_manager_tests.rs @@ -893,6 +893,335 @@ fn test_apply_migration_persists_history() { }); } +// ============================================================ +// ===== WASM HASH CHAIN VERIFICATION TESTS (#661) ============ +// ============================================================ + +/// Test helper to create a WASM hash from a seed byte +fn create_wasm_hash(env: &Env, seed: u8) -> BytesN<32> { + BytesN::from_array(env, &[seed; 32]) +} + +/// Test genesis upgrade (first deployment) with zero hashes +#[test] +fn test_upgrade_chain_genesis_case() { + let (env, admin, contract_id) = setup_test_env(); + + env.as_contract(&contract_id, || { + // Initialize version + let version_manager = VersionManager::new(&env); + let current_version = Version::new( + &env, + 1, + 0, + 0, + String::from_str(&env, "Genesis version"), + false, + ); + version_manager + .track_contract_version(&env, current_version) + .unwrap(); + + // Genesis: current hash is zero, predecessor is zero + let zero_hash = BytesN::from_array(&env, &[0u8; 32]); + let new_wasm_hash = create_wasm_hash(&env, 1); + + // Should succeed - both are zero (genesis case) + let result = UpgradeManager::upgrade_contract( + &env, + &admin, + new_wasm_hash.clone(), + zero_hash.clone(), + ); + assert!(result.is_ok(), "Genesis upgrade should succeed with zero predecessor"); + + // Verify the new hash is now current + let current_hash = UpgradeManager::get_current_wasm_hash_public(&env); + assert_eq!(current_hash, new_wasm_hash, "Current hash should be updated"); + }); +} + +/// Test successful upgrade with correct predecessor hash +#[test] +fn test_upgrade_chain_success_with_correct_predecessor() { + let (env, admin, contract_id) = setup_test_env(); + + env.as_contract(&contract_id, || { + // Initialize version + let version_manager = VersionManager::new(&env); + let current_version = Version::new( + &env, + 1, + 0, + 0, + String::from_str(&env, "Initial version"), + false, + ); + version_manager + .track_contract_version(&env, current_version) + .unwrap(); + + // First upgrade (genesis) + let zero_hash = BytesN::from_array(&env, &[0u8; 32]); + let hash_v1 = create_wasm_hash(&env, 1); + UpgradeManager::upgrade_contract(&env, &admin, hash_v1.clone(), zero_hash).unwrap(); + + // Second upgrade with correct predecessor + let hash_v2 = create_wasm_hash(&env, 2); + let result = UpgradeManager::upgrade_contract( + &env, + &admin, + hash_v2.clone(), + hash_v1.clone(), + ); + assert!(result.is_ok(), "Upgrade should succeed with correct predecessor"); + + // Verify the chain + let current_hash = UpgradeManager::get_current_wasm_hash_public(&env); + assert_eq!(current_hash, hash_v2, "Current hash should be v2"); + }); +} + +/// Test failed upgrade with wrong predecessor (out-of-order) +#[test] +fn test_upgrade_chain_rejects_wrong_predecessor() { + let (env, admin, contract_id) = setup_test_env(); + + env.as_contract(&contract_id, || { + // Initialize version + let version_manager = VersionManager::new(&env); + let current_version = Version::new( + &env, + 1, + 0, + 0, + String::from_str(&env, "Initial version"), + false, + ); + version_manager + .track_contract_version(&env, current_version) + .unwrap(); + + // First upgrade + let zero_hash = BytesN::from_array(&env, &[0u8; 32]); + let hash_v1 = create_wasm_hash(&env, 1); + UpgradeManager::upgrade_contract(&env, &admin, hash_v1.clone(), zero_hash).unwrap(); + + // Try to upgrade with wrong predecessor (out-of-order) + let hash_v3 = create_wasm_hash(&env, 3); + let wrong_predecessor = create_wasm_hash(&env, 99); // Wrong hash + let result = UpgradeManager::upgrade_contract( + &env, + &admin, + hash_v3.clone(), + wrong_predecessor, + ); + + assert!(result.is_err(), "Upgrade should fail with wrong predecessor"); + assert_eq!( + result.unwrap_err(), + crate::errors::Error::UpgradeChainMismatch, + "Should return UpgradeChainMismatch error" + ); + + // Verify current hash is unchanged + let current_hash = UpgradeManager::get_current_wasm_hash_public(&env); + assert_eq!(current_hash, hash_v1, "Current hash should still be v1"); + }); +} + +/// Test failed upgrade when genesis case is violated +#[test] +fn test_upgrade_chain_rejects_genesis_violation() { + let (env, admin, contract_id) = setup_test_env(); + + env.as_contract(&contract_id, || { + // Initialize version + let version_manager = VersionManager::new(&env); + let current_version = Version::new( + &env, + 1, + 0, + 0, + String::from_str(&env, "Initial version"), + false, + ); + version_manager + .track_contract_version(&env, current_version) + .unwrap(); + + // Current is genesis (zero), but predecessor is non-zero + let zero_hash = BytesN::from_array(&env, &[0u8; 32]); + let new_wasm_hash = create_wasm_hash(&env, 1); + let non_zero_predecessor = create_wasm_hash(&env, 99); + + let result = UpgradeManager::upgrade_contract( + &env, + &admin, + new_wasm_hash, + non_zero_predecessor, + ); + + assert!(result.is_err(), "Upgrade should fail when genesis case is violated"); + assert_eq!( + result.unwrap_err(), + crate::errors::Error::UpgradeChainMismatch, + "Should return UpgradeChainMismatch error" + ); + }); +} + +/// Test query WASM hash chain history +#[test] +fn test_get_wasm_hash_chain() { + let (env, admin, contract_id) = setup_test_env(); + + env.as_contract(&contract_id, || { + // Initialize version + let version_manager = VersionManager::new(&env); + let current_version = Version::new( + &env, + 1, + 0, + 0, + String::from_str(&env, "Initial version"), + false, + ); + version_manager + .track_contract_version(&env, current_version) + .unwrap(); + + // Perform multiple upgrades + let zero_hash = BytesN::from_array(&env, &[0u8; 32]); + let hash_v1 = create_wasm_hash(&env, 1); + let hash_v2 = create_wasm_hash(&env, 2); + let hash_v3 = create_wasm_hash(&env, 3); + + UpgradeManager::upgrade_contract(&env, &admin, hash_v1.clone(), zero_hash).unwrap(); + UpgradeManager::upgrade_contract(&env, &admin, hash_v2.clone(), hash_v1.clone()).unwrap(); + UpgradeManager::upgrade_contract(&env, &admin, hash_v3.clone(), hash_v2.clone()).unwrap(); + + // Query the hash chain + let chain = UpgradeManager::get_wasm_hash_chain(&env).unwrap(); + assert_eq!(chain.len(), 3, "Should have 3 upgrades in chain"); + + // Verify chain integrity + assert_eq!(chain.get(0).unwrap().previous_wasm_hash, zero_hash); + assert_eq!(chain.get(0).unwrap().new_wasm_hash, hash_v1); + assert_eq!(chain.get(1).unwrap().previous_wasm_hash, hash_v1); + assert_eq!(chain.get(1).unwrap().new_wasm_hash, hash_v2); + assert_eq!(chain.get(2).unwrap().previous_wasm_hash, hash_v2); + assert_eq!(chain.get(2).unwrap().new_wasm_hash, hash_v3); + }); +} + +/// Test get current WASM hash +#[test] +fn test_get_current_wasm_hash_public() { + let (env, admin, contract_id) = setup_test_env(); + + env.as_contract(&contract_id, || { + // Initialize version + let version_manager = VersionManager::new(&env); + let current_version = Version::new( + &env, + 1, + 0, + 0, + String::from_str(&env, "Initial version"), + false, + ); + version_manager + .track_contract_version(&env, current_version) + .unwrap(); + + // Initially should be zero + let initial_hash = UpgradeManager::get_current_wasm_hash_public(&env); + let zero_hash = BytesN::from_array(&env, &[0u8; 32]); + assert_eq!(initial_hash, zero_hash, "Initial hash should be zero"); + + // After upgrade, should be new hash + let new_wasm_hash = create_wasm_hash(&env, 1); + UpgradeManager::upgrade_contract(&env, &admin, new_wasm_hash.clone(), zero_hash).unwrap(); + + let current_hash = UpgradeManager::get_current_wasm_hash_public(&env); + assert_eq!(current_hash, new_wasm_hash, "Current hash should be updated"); + }); +} + +/// Test UpgradeProposal with expected_predecessor +#[test] +fn test_upgrade_proposal_with_expected_predecessor() { + let env = Env::default(); + let new_wasm_hash = BytesN::from_array(&env, &[1u8; 32]); + let target_version = Version::new( + &env, + 1, + 1, + 0, + String::from_str(&env, "Upgrade to v1.1.0"), + false, + ); + + let mut proposal = UpgradeProposal::new( + &env, + new_wasm_hash, + target_version, + String::from_str(&env, "Add new features"), + ); + + // Set expected predecessor + let predecessor_hash = BytesN::from_array(&env, &[0u8; 32]); + proposal.set_expected_predecessor(predecessor_hash.clone()); + + assert_eq!(proposal.expected_predecessor, predecessor_hash); +} + +/// Test forked upgrade chain detection +#[test] +fn test_upgrade_chain_detects_fork() { + let (env, admin, contract_id) = setup_test_env(); + + env.as_contract(&contract_id, || { + // Initialize version + let version_manager = VersionManager::new(&env); + let current_version = Version::new( + &env, + 1, + 0, + 0, + String::from_str(&env, "Initial version"), + false, + ); + version_manager + .track_contract_version(&env, current_version) + .unwrap(); + + // First upgrade + let zero_hash = BytesN::from_array(&env, &[0u8; 32]); + let hash_v1 = create_wasm_hash(&env, 1); + UpgradeManager::upgrade_contract(&env, &admin, hash_v1.clone(), zero_hash).unwrap(); + + // Simulate a fork: try to upgrade from a different hash + let forked_predecessor = create_wasm_hash(&env, 99); // Not the actual current hash + let hash_v2_fork = create_wasm_hash(&env, 2); + + let result = UpgradeManager::upgrade_contract( + &env, + &admin, + hash_v2_fork, + forked_predecessor, + ); + + assert!(result.is_err(), "Forked upgrade should be rejected"); + assert_eq!( + result.unwrap_err(), + crate::errors::Error::UpgradeChainMismatch, + "Should return UpgradeChainMismatch error for fork" + ); + }); +} + // ── Failed migration also stored in history ─────────────────────────────────── /// A migration that fails validation is recorded as Failed in the history.