diff --git a/contracts/creditline-contract/src/lib.rs b/contracts/creditline-contract/src/lib.rs index 0bf0eea..2ac387f 100644 --- a/contracts/creditline-contract/src/lib.rs +++ b/contracts/creditline-contract/src/lib.rs @@ -198,6 +198,42 @@ impl CreditLineContract { env.deployer().update_current_contract_wasm(new_wasm_hash); events::emit_contract_upgraded(&env, old_version, new_version); + + // Automatically execute migrate() during upgrade() by calling self. + // This spins up a new VM running the upgraded WASM hash. + env.invoke_contract::<()>( + &env.current_contract_address(), + &Symbol::new(&env, "migrate"), + (), + ); + } + + pub fn get_schema_version(env: Env) -> u32 { + storage::get_schema_version(&env) + } + + pub fn migrate(env: Env) { + let admin = storage::get_admin(&env).unwrap_or_else(|err| panic_with_error!(&env, err)); + admin.require_auth(); + + let mut current_version = storage::get_schema_version(&env); + let target_version = 2; + + if current_version >= target_version { + return; + } + + if current_version == 0 { + // Version 0 -> 1 migration logic (idempotent & safe) + current_version = 1; + storage::set_schema_version(&env, current_version); + } + + if current_version == 1 { + // Version 1 -> 2 migration logic (idempotent & safe) + current_version = 2; + storage::set_schema_version(&env, current_version); + } } pub fn get_admin(env: Env) -> Result { storage::get_admin(&env) diff --git a/contracts/creditline-contract/src/storage.rs b/contracts/creditline-contract/src/storage.rs index 99d2c17..abe4098 100644 --- a/contracts/creditline-contract/src/storage.rs +++ b/contracts/creditline-contract/src/storage.rs @@ -269,3 +269,22 @@ pub fn get_version(env: &Env) -> Result { pub fn set_version(env: &Env, version: u32) { env.storage().instance().set(&VERSION_KEY, &version); } + +pub const SCHEMA_VERSION_KEY: Symbol = symbol_short!("SCH_VER"); + +/// Get the contract schema version (persistent storage). Defaults to 0 when not set. +pub fn get_schema_version(env: &Env) -> u32 { + env.storage() + .persistent() + .get(&SCHEMA_VERSION_KEY) + .unwrap_or(0u32) +} + +/// Set the contract schema version in persistent storage. +pub fn set_schema_version(env: &Env, version: u32) { + env.storage().persistent().set(&SCHEMA_VERSION_KEY, &version); + env.storage() + .persistent() + .extend_ttl(&SCHEMA_VERSION_KEY, PERSISTENT_TTL_THRESHOLD, PERSISTENT_TTL_EXTEND_TO); +} + diff --git a/contracts/creditline-contract/src/tests.rs b/contracts/creditline-contract/src/tests.rs index c96cb74..6d8ecb2 100644 --- a/contracts/creditline-contract/src/tests.rs +++ b/contracts/creditline-contract/src/tests.rs @@ -3125,3 +3125,40 @@ fn test_safe_math_boundaries() { assert_eq!(safe_math::mul_i128(max, 2), Err(CreditLineError::Overflow)); assert_eq!(safe_math::div_i128(max, 0), Err(CreditLineError::Overflow)); } + +#[test] +fn test_schema_version_and_migration() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(CreditLineContract, ()); + let client = CreditLineContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let reputation_contract = Address::generate(&env); + let vendor_registry = Address::generate(&env); + let liquidity_pool = Address::generate(&env); + let token = Address::generate(&env); + + client.initialize( + &admin, + &reputation_contract, + &vendor_registry, + &liquidity_pool, + &token, + ); + + // Default version should be 0 + assert_eq!(client.get_schema_version(), 0); + + // Call migrate() directly + client.migrate(); + + // After migration, version should be 2 + assert_eq!(client.get_schema_version(), 2); + + // Call migrate() again (idempotent check) + client.migrate(); + assert_eq!(client.get_schema_version(), 2); +} + diff --git a/contracts/liquidity-pool-contract/src/lib.rs b/contracts/liquidity-pool-contract/src/lib.rs index c1a8622..442238b 100644 --- a/contracts/liquidity-pool-contract/src/lib.rs +++ b/contracts/liquidity-pool-contract/src/lib.rs @@ -1,5 +1,5 @@ #![no_std] -use soroban_sdk::{contract, contractimpl, panic_with_error, token, Address, Env}; +use soroban_sdk::{contract, contractimpl, panic_with_error, token, Address, Env, Symbol}; mod errors; mod events; @@ -83,6 +83,14 @@ impl LiquidityPoolContract { env.deployer().update_current_contract_wasm(new_wasm_hash); events::emit_contract_upgraded(&env, old_version, new_version); + + // Automatically execute migrate() during upgrade() by calling self. + // This spins up a new VM running the upgraded WASM hash. + env.invoke_contract::<()>( + &env.current_contract_address(), + &Symbol::new(&env, "migrate"), + (), + ); } pub fn get_admin(env: Env) -> Result { storage::get_admin(&env) @@ -92,6 +100,34 @@ impl LiquidityPoolContract { storage::get_version(&env).unwrap_or_else(|err| panic_with_error!(&env, err)) } + pub fn get_schema_version(env: Env) -> u32 { + storage::get_schema_version(&env) + } + + pub fn migrate(env: Env) { + let admin = storage::get_admin(&env).unwrap_or_else(|err| panic_with_error!(&env, err)); + admin.require_auth(); + + let mut current_version = storage::get_schema_version(&env); + let target_version = 2; + + if current_version >= target_version { + return; + } + + if current_version == 0 { + // Version 0 -> 1 migration logic (idempotent & safe) + current_version = 1; + storage::set_schema_version(&env, current_version); + } + + if current_version == 1 { + // Version 1 -> 2 migration logic (idempotent & safe) + current_version = 2; + storage::set_schema_version(&env, current_version); + } + } + // ------------------------------------------------------------------------- // LP Operations // ------------------------------------------------------------------------- diff --git a/contracts/liquidity-pool-contract/src/storage.rs b/contracts/liquidity-pool-contract/src/storage.rs index 5841059..6077598 100644 --- a/contracts/liquidity-pool-contract/src/storage.rs +++ b/contracts/liquidity-pool-contract/src/storage.rs @@ -157,3 +157,22 @@ pub fn get_version(env: &Env) -> Result { pub fn set_version(env: &Env, v: u32) { env.storage().instance().set(&VERSION_KEY, &v); } + +pub const SCHEMA_VERSION_KEY: Symbol = symbol_short!("SCH_VER"); + +/// Get the contract schema version (persistent storage). Defaults to 0 when not set. +pub fn get_schema_version(env: &Env) -> u32 { + env.storage() + .persistent() + .get(&SCHEMA_VERSION_KEY) + .unwrap_or(0u32) +} + +/// Set the contract schema version in persistent storage. +pub fn set_schema_version(env: &Env, version: u32) { + env.storage().persistent().set(&SCHEMA_VERSION_KEY, &version); + env.storage() + .persistent() + .extend_ttl(&SCHEMA_VERSION_KEY, PERSISTENT_TTL_THRESHOLD, PERSISTENT_TTL_EXTEND_TO); +} + diff --git a/contracts/liquidity-pool-contract/src/tests.rs b/contracts/liquidity-pool-contract/src/tests.rs index c2aff34..cbdb3b1 100644 --- a/contracts/liquidity-pool-contract/src/tests.rs +++ b/contracts/liquidity-pool-contract/src/tests.rs @@ -2417,3 +2417,33 @@ fn test_loan_funding_and_guarantee_recovery_cycle() { assert_eq!(stats_final.locked_liquidity, 2_500); assert_eq!(stats_final.available_liquidity, 3_000); } + +#[test] +fn test_schema_version_and_migration() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(LiquidityPoolContract, ()); + let client = LiquidityPoolContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let token = Address::generate(&env); + let treasury = Address::generate(&env); + let merchant_fund = Address::generate(&env); + + client.initialize(&admin, &token, &treasury, &merchant_fund); + + // Default version should be 0 + assert_eq!(client.get_schema_version(), 0); + + // Call migrate() directly + client.migrate(); + + // After migration, version should be 2 + assert_eq!(client.get_schema_version(), 2); + + // Call migrate() again (idempotent check) + client.migrate(); + assert_eq!(client.get_schema_version(), 2); +} + diff --git a/contracts/parameters-contract/src/lib.rs b/contracts/parameters-contract/src/lib.rs index 77b6248..c5c7fd9 100644 --- a/contracts/parameters-contract/src/lib.rs +++ b/contracts/parameters-contract/src/lib.rs @@ -10,7 +10,7 @@ mod types; pub use errors::ParametersError; pub use types::{default_parameters, ProtocolParameters}; -use soroban_sdk::{contract, contractimpl, panic_with_error, Address, Env}; +use soroban_sdk::{contract, contractimpl, panic_with_error, Address, Env, Symbol}; #[contract] pub struct ParametersContract; @@ -45,6 +45,15 @@ impl ParametersContract { env.deployer().update_current_contract_wasm(new_wasm_hash); events::emit_contract_upgraded(&env, old, new); + + // Automatically execute migrate() during upgrade() by calling self. + // This spins up a new VM running the upgraded WASM hash. + env.invoke_contract::<()>( + &env.current_contract_address(), + &Symbol::new(&env, "migrate"), + (), + ); + Self::exit_non_reentrant(&env); } pub fn get_admin(env: Env) -> Result { @@ -55,6 +64,34 @@ impl ParametersContract { storage::get_version(&env).unwrap_or_else(|err| panic_with_error!(&env, err)) } + pub fn get_schema_version(env: Env) -> u32 { + storage::get_schema_version(&env) + } + + pub fn migrate(env: Env) { + let admin = storage::get_admin(&env).unwrap_or_else(|err| panic_with_error!(&env, err)); + admin.require_auth(); + + let mut current_version = storage::get_schema_version(&env); + let target_version = 2; + + if current_version >= target_version { + return; + } + + if current_version == 0 { + // Version 0 -> 1 migration logic (idempotent & safe) + current_version = 1; + storage::set_schema_version(&env, current_version); + } + + if current_version == 1 { + // Version 1 -> 2 migration logic (idempotent & safe) + current_version = 2; + storage::set_schema_version(&env, current_version); + } + } + pub fn set_admin(env: Env, new_admin: Address) { let old_admin = storage::get_admin(&env).unwrap_or_else(|err| panic_with_error!(&env, err)); old_admin.require_auth(); diff --git a/contracts/parameters-contract/src/storage.rs b/contracts/parameters-contract/src/storage.rs index 3fb6608..1635258 100644 --- a/contracts/parameters-contract/src/storage.rs +++ b/contracts/parameters-contract/src/storage.rs @@ -53,3 +53,24 @@ pub fn get_version(env: &Env) -> Result { pub fn set_version(env: &Env, v: u32) { env.storage().instance().set(&VERSION_KEY, &v); } + +pub const SCHEMA_VERSION_KEY: Symbol = symbol_short!("SCH_VER"); +pub const PERSISTENT_TTL_THRESHOLD: u32 = 1_036_800; +pub const PERSISTENT_TTL_EXTEND_TO: u32 = 2_073_600; + +/// Get the contract schema version (persistent storage). Defaults to 0 when not set. +pub fn get_schema_version(env: &Env) -> u32 { + env.storage() + .persistent() + .get(&SCHEMA_VERSION_KEY) + .unwrap_or(0u32) +} + +/// Set the contract schema version in persistent storage. +pub fn set_schema_version(env: &Env, version: u32) { + env.storage().persistent().set(&SCHEMA_VERSION_KEY, &version); + env.storage() + .persistent() + .extend_ttl(&SCHEMA_VERSION_KEY, PERSISTENT_TTL_THRESHOLD, PERSISTENT_TTL_EXTEND_TO); +} + diff --git a/contracts/parameters-contract/src/tests.rs b/contracts/parameters-contract/src/tests.rs index 9fb3ece..9059d34 100644 --- a/contracts/parameters-contract/src/tests.rs +++ b/contracts/parameters-contract/src/tests.rs @@ -123,3 +123,23 @@ fn test_admin_upgrade_increments_version() { } assert!(found, "CONTRACTUPGRADED event not found"); } + +#[test] +fn test_schema_version_and_migration() { + let (env, client, admin) = setup(); + client.initialize_defaults(&admin); + + // Default version should be 0 + assert_eq!(client.get_schema_version(), 0); + + // Call migrate() directly + client.migrate(); + + // After migration, version should be 2 + assert_eq!(client.get_schema_version(), 2); + + // Call migrate() again (idempotent check) + client.migrate(); + assert_eq!(client.get_schema_version(), 2); +} + diff --git a/contracts/reputation-contract/src/lib.rs b/contracts/reputation-contract/src/lib.rs index e125889..38e19c6 100644 --- a/contracts/reputation-contract/src/lib.rs +++ b/contracts/reputation-contract/src/lib.rs @@ -1,5 +1,5 @@ #![no_std] -use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env}; +use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, Symbol}; // Module imports mod access; @@ -220,12 +220,49 @@ impl ReputationContract { storage::set_version(&env, new); env.deployer().update_current_contract_wasm(new_wasm_hash); events::emit_contract_upgraded(&env, old, new); + + // Automatically execute migrate() during upgrade() by calling self. + // This spins up a new VM running the upgraded WASM hash. + env.invoke_contract::<()>( + &env.current_contract_address(), + &Symbol::new(&env, "migrate"), + (), + ); + Self::exit_non_reentrant(&env); } pub fn get_admin(env: Env) -> Result { storage::get_admin(&env) } + pub fn get_schema_version(env: Env) -> u32 { + storage::get_schema_version(&env) + } + + pub fn migrate(env: Env) { + let admin = storage::get_admin(&env).unwrap_or_else(|err| soroban_sdk::panic_with_error!(&env, err)); + admin.require_auth(); + + let mut current_version = storage::get_schema_version(&env); + let target_version = 2; + + if current_version >= target_version { + return; + } + + if current_version == 0 { + // Version 0 -> 1 migration logic (idempotent & safe) + current_version = 1; + storage::set_schema_version(&env, current_version); + } + + if current_version == 1 { + // Version 1 -> 2 migration logic (idempotent & safe) + current_version = 2; + storage::set_schema_version(&env, current_version); + } + } + fn enter_non_reentrant(env: &Env) { if storage::is_reentrancy_locked(env) .unwrap_or_else(|err| soroban_sdk::panic_with_error!(env, err)) diff --git a/contracts/reputation-contract/src/storage.rs b/contracts/reputation-contract/src/storage.rs index c099f44..2247719 100644 --- a/contracts/reputation-contract/src/storage.rs +++ b/contracts/reputation-contract/src/storage.rs @@ -92,3 +92,24 @@ pub fn get_version(env: &Env) -> Result { pub fn set_version(env: &Env, v: u32) { env.storage().instance().set(&VERSION_KEY, &v); } + +pub const SCHEMA_VERSION_KEY: Symbol = symbol_short!("SCH_VER"); +pub const PERSISTENT_TTL_THRESHOLD: u32 = 1_036_800; +pub const PERSISTENT_TTL_EXTEND_TO: u32 = 2_073_600; + +/// Get the contract schema version (persistent storage). Defaults to 0 when not set. +pub fn get_schema_version(env: &Env) -> u32 { + env.storage() + .persistent() + .get(&SCHEMA_VERSION_KEY) + .unwrap_or(0u32) +} + +/// Set the contract schema version in persistent storage. +pub fn set_schema_version(env: &Env, version: u32) { + env.storage().persistent().set(&SCHEMA_VERSION_KEY, &version); + env.storage() + .persistent() + .extend_ttl(&SCHEMA_VERSION_KEY, PERSISTENT_TTL_THRESHOLD, PERSISTENT_TTL_EXTEND_TO); +} + diff --git a/contracts/reputation-contract/src/tests.rs b/contracts/reputation-contract/src/tests.rs index 8a9b64d..4b133a7 100644 --- a/contracts/reputation-contract/src/tests.rs +++ b/contracts/reputation-contract/src/tests.rs @@ -1351,3 +1351,29 @@ fn it_allows_setting_score_to_current_value() { client.set_score(&updater, &user, &75); assert_eq!(client.get_score(&user), 75); } + +#[test] +fn test_schema_version_and_migration() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(ReputationContract, ()); + let client = ReputationContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + client.set_admin(&admin); + + // Default version should be 0 + assert_eq!(client.get_schema_version(), 0); + + // Call migrate() directly + client.migrate(); + + // After migration, version should be 2 + assert_eq!(client.get_schema_version(), 2); + + // Call migrate() again (idempotent check) + client.migrate(); + assert_eq!(client.get_schema_version(), 2); +} + diff --git a/contracts/vendor-registry-contract/src/lib.rs b/contracts/vendor-registry-contract/src/lib.rs index c4c193e..3a34131 100644 --- a/contracts/vendor-registry-contract/src/lib.rs +++ b/contracts/vendor-registry-contract/src/lib.rs @@ -11,7 +11,7 @@ mod types; mod tests; use errors::Error; -use soroban_sdk::{contract, contractimpl, Address, Env, String}; +use soroban_sdk::{contract, contractimpl, Address, Env, String, Symbol}; use types::VendorInfo; // Export Error type for external use @@ -188,5 +188,41 @@ impl VendorRegistryContract { env.deployer().update_current_contract_wasm(new_wasm_hash); events::emit_contract_upgraded(&env, old, new); + + // Automatically execute migrate() during upgrade() by calling self. + // This spins up a new VM running the upgraded WASM hash. + env.invoke_contract::<()>( + &env.current_contract_address(), + &Symbol::new(&env, "migrate"), + (), + ); + } + + pub fn get_schema_version(env: Env) -> u32 { + storage::get_schema_version(&env) + } + + pub fn migrate(env: Env) { + let admin = storage::get_admin(&env).unwrap_or_else(|err| soroban_sdk::panic_with_error!(&env, err)); + admin.require_auth(); + + let mut current_version = storage::get_schema_version(&env); + let target_version = 2; + + if current_version >= target_version { + return; + } + + if current_version == 0 { + // Version 0 -> 1 migration logic (idempotent & safe) + current_version = 1; + storage::set_schema_version(&env, current_version); + } + + if current_version == 1 { + // Version 1 -> 2 migration logic (idempotent & safe) + current_version = 2; + storage::set_schema_version(&env, current_version); + } } } diff --git a/contracts/vendor-registry-contract/src/storage.rs b/contracts/vendor-registry-contract/src/storage.rs index a6865c3..11f9804 100644 --- a/contracts/vendor-registry-contract/src/storage.rs +++ b/contracts/vendor-registry-contract/src/storage.rs @@ -86,3 +86,24 @@ pub fn get_version(env: &Env) -> Result { pub fn set_version(env: &Env, v: u32) { env.storage().instance().set(&VERSION_KEY, &v); } + +pub const SCHEMA_VERSION_KEY: soroban_sdk::Symbol = symbol_short!("SCH_VER"); + +/// Get the contract schema version (persistent storage). Defaults to 0 when not set. +pub fn get_schema_version(env: &Env) -> u32 { + env.storage() + .persistent() + .get(&SCHEMA_VERSION_KEY) + .unwrap_or(0u32) +} + +/// Set the contract schema version in persistent storage. +pub fn set_schema_version(env: &Env, version: u32) { + env.storage().persistent().set(&SCHEMA_VERSION_KEY, &version); + env.storage().persistent().extend_ttl( + &SCHEMA_VERSION_KEY, + PERSISTENT_TTL_THRESHOLD, + PERSISTENT_TTL_EXTEND_TO, + ); +} + diff --git a/contracts/vendor-registry-contract/src/tests.rs b/contracts/vendor-registry-contract/src/tests.rs index 27757a7..bc0ec8a 100644 --- a/contracts/vendor-registry-contract/src/tests.rs +++ b/contracts/vendor-registry-contract/src/tests.rs @@ -305,3 +305,24 @@ fn test_admin_upgrade_increments_version_and_emits_event() { } assert!(found, "CONTRACTUPGRADED event not found"); } + +#[test] +fn test_schema_version_and_migration() { + let env = Env::default(); + let (client, admin, _vendor) = setup(&env); + env.mock_all_auths(); + + // Default version should be 0 + assert_eq!(client.get_schema_version(), 0); + + // Call migrate() directly + client.migrate(); + + // After migration, version should be 2 + assert_eq!(client.get_schema_version(), 2); + + // Call migrate() again (idempotent check) + client.migrate(); + assert_eq!(client.get_schema_version(), 2); +} +