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.