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);
+}
+