From 6f8f4c7301b82774e5536e91f54f6d7439aa09b1 Mon Sep 17 00:00:00 2001 From: Iwayemi Date: Fri, 19 Jun 2026 04:43:56 +0000 Subject: [PATCH 1/3] feat: add schema_version storage, migrate() functions, and automatic migration in upgrade() across all 6 Soroban contracts --- contracts/creditline-contract/src/errors.rs | 1 + contracts/creditline-contract/src/lib.rs | 19 ++++++++- contracts/creditline-contract/src/storage.rs | 14 +++++++ .../liquidity-pool-contract/src/errors.rs | 1 + contracts/liquidity-pool-contract/src/lib.rs | 21 +++++++++- .../liquidity-pool-contract/src/storage.rs | 14 +++++++ contracts/parameters-contract/src/errors.rs | 1 + contracts/parameters-contract/src/lib.rs | 21 +++++++++- contracts/parameters-contract/src/storage.rs | 14 +++++++ contracts/reputation-contract/src/errors.rs | 1 + contracts/reputation-contract/src/lib.rs | 21 +++++++++- contracts/reputation-contract/src/storage.rs | 14 +++++++ .../vendor-registry-contract/src/errors.rs | 1 + contracts/vendor-registry-contract/src/lib.rs | 40 ++++++++++++++++++- .../vendor-registry-contract/src/storage.rs | 13 ++++++ .../vendor-registry-contract/src/types.rs | 1 + contracts/vouching-contract/src/errors.rs | 1 + contracts/vouching-contract/src/lib.rs | 35 ++++++++++++++++ contracts/vouching-contract/src/storage.rs | 13 ++++++ contracts/vouching-contract/src/types.rs | 1 + 20 files changed, 239 insertions(+), 8 deletions(-) diff --git a/contracts/creditline-contract/src/errors.rs b/contracts/creditline-contract/src/errors.rs index a639875..8d80ad7 100644 --- a/contracts/creditline-contract/src/errors.rs +++ b/contracts/creditline-contract/src/errors.rs @@ -31,4 +31,5 @@ pub enum CreditLineError { InstallmentAlreadyPaid = 24, InvalidLoanStatus = 25, NotInitialized = 26, + MigrationRequired = 27, } diff --git a/contracts/creditline-contract/src/lib.rs b/contracts/creditline-contract/src/lib.rs index c4dcaf2..fa2599b 100644 --- a/contracts/creditline-contract/src/lib.rs +++ b/contracts/creditline-contract/src/lib.rs @@ -189,11 +189,28 @@ impl CreditLineContract { storage::set_admin(&env, &new_admin); } - /// Upgrade the contract WASM — admin only + /// Migrate contract storage to the latest schema version. + /// Called automatically during upgrade() to handle any data migrations. + /// This is idempotent and safe to call multiple times. + pub fn migrate(env: Env) { + let stored = storage::get_schema_version(&env); + let current = storage::CURRENT_SCHEMA_VERSION; + if stored >= current { + return; + } + // Future: add per-version data migration steps here + storage::set_schema_version(&env, current); + } + + /// Upgrade the contract WASM — admin only. + /// After replacing the WASM, automatically runs migrate() to ensure + /// contract storage is up to date with the new code version. pub fn upgrade(env: Env, new_wasm_hash: soroban_sdk::BytesN<32>) { let admin = storage::get_admin(&env).unwrap_or_else(|err| panic_with_error!(&env, err)); admin.require_auth(); env.deployer().update_current_contract_wasm(new_wasm_hash); + // Self-invoke migrate to run post-upgrade migration logic + env.invoke_contract::<()>(&env.current_contract_address(), &Symbol::new(&env, "migrate"), ().into_val(&env)); } 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 d83117b..cfba5a9 100644 --- a/contracts/creditline-contract/src/storage.rs +++ b/contracts/creditline-contract/src/storage.rs @@ -3,6 +3,20 @@ use soroban_sdk::{contracttype, symbol_short, Address, Env, Symbol, Vec}; use crate::errors::CreditLineError; use crate::types::Loan; +// Schema version key +pub const SCHEMA_VERSION_KEY: Symbol = symbol_short!("SCHEMA_V"); +pub const CURRENT_SCHEMA_VERSION: u32 = 1; + +/// Get the current schema version (0 = unset) +pub fn get_schema_version(env: &Env) -> u32 { + env.storage().instance().get(&SCHEMA_VERSION_KEY).unwrap_or(0) +} + +/// Set the schema version +pub fn set_schema_version(env: &Env, version: u32) { + env.storage().instance().set(&SCHEMA_VERSION_KEY, &version); +} + // Storage keys pub const ADMIN_KEY: Symbol = symbol_short!("ADMIN"); pub const LOAN_COUNTER: Symbol = symbol_short!("LOANCNT"); diff --git a/contracts/liquidity-pool-contract/src/errors.rs b/contracts/liquidity-pool-contract/src/errors.rs index 219f0d5..1fcc261 100644 --- a/contracts/liquidity-pool-contract/src/errors.rs +++ b/contracts/liquidity-pool-contract/src/errors.rs @@ -15,4 +15,5 @@ pub enum LiquidityPoolError { NotCreditLine = 9, ZeroTotalShares = 10, ReentrancyDetected = 11, + MigrationRequired = 12, } diff --git a/contracts/liquidity-pool-contract/src/lib.rs b/contracts/liquidity-pool-contract/src/lib.rs index fb5d68a..7f12d16 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, IntoVal, Symbol}; mod errors; mod events; @@ -71,11 +71,28 @@ impl LiquidityPoolContract { storage::set_admin(&env, &new_admin); } - /// Upgrade the contract WASM — admin only + /// Migrate contract storage to the latest schema version. + /// Called automatically during upgrade() to handle any data migrations. + /// This is idempotent and safe to call multiple times. + pub fn migrate(env: Env) { + let stored = storage::get_schema_version(&env); + let current = storage::CURRENT_SCHEMA_VERSION; + if stored >= current { + return; + } + // Future: add per-version data migration steps here + storage::set_schema_version(&env, current); + } + + /// Upgrade the contract WASM — admin only. + /// After replacing the WASM, automatically runs migrate() to ensure + /// contract storage is up to date with the new code version. pub fn upgrade(env: Env, new_wasm_hash: soroban_sdk::BytesN<32>) { let admin = storage::get_admin(&env).unwrap_or_else(|err| panic_with_error!(&env, err)); admin.require_auth(); env.deployer().update_current_contract_wasm(new_wasm_hash); + // Self-invoke migrate to run post-upgrade migration logic + env.invoke_contract::<()>(&env.current_contract_address(), &Symbol::new(&env, "migrate"), ().into_val(&env)); } pub fn get_admin(env: Env) -> Result { storage::get_admin(&env) diff --git a/contracts/liquidity-pool-contract/src/storage.rs b/contracts/liquidity-pool-contract/src/storage.rs index cc35085..eecd5f8 100644 --- a/contracts/liquidity-pool-contract/src/storage.rs +++ b/contracts/liquidity-pool-contract/src/storage.rs @@ -2,6 +2,20 @@ use soroban_sdk::{symbol_short, Address, Env, Symbol}; use crate::errors::LiquidityPoolError; +// Schema version key +pub const SCHEMA_VERSION_KEY: Symbol = symbol_short!("SCHEMA_V"); +pub const CURRENT_SCHEMA_VERSION: u32 = 1; + +/// Get the current schema version (0 = unset) +pub fn get_schema_version(env: &Env) -> u32 { + env.storage().instance().get(&SCHEMA_VERSION_KEY).unwrap_or(0) +} + +/// Set the schema version +pub fn set_schema_version(env: &Env, version: u32) { + env.storage().instance().set(&SCHEMA_VERSION_KEY, &version); +} + // Instance storage keys pub const ADMIN_KEY: Symbol = symbol_short!("ADMIN"); pub const TOKEN_KEY: Symbol = symbol_short!("TOKEN"); diff --git a/contracts/parameters-contract/src/errors.rs b/contracts/parameters-contract/src/errors.rs index 0159c8c..83d1047 100644 --- a/contracts/parameters-contract/src/errors.rs +++ b/contracts/parameters-contract/src/errors.rs @@ -8,4 +8,5 @@ pub enum ParametersError { NotAdmin = 2, InvalidParameters = 3, NotInitialized = 4, + MigrationRequired = 5, } diff --git a/contracts/parameters-contract/src/lib.rs b/contracts/parameters-contract/src/lib.rs index 6f16ce0..76b7ac9 100644 --- a/contracts/parameters-contract/src/lib.rs +++ b/contracts/parameters-contract/src/lib.rs @@ -9,7 +9,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, IntoVal, Symbol}; #[contract] pub struct ParametersContract; @@ -33,11 +33,28 @@ impl ParametersContract { Self::initialize(env, admin, default_parameters()); } - /// Upgrade the contract WASM — admin only + /// Migrate contract storage to the latest schema version. + /// Called automatically during upgrade() to handle any data migrations. + /// This is idempotent and safe to call multiple times. + pub fn migrate(env: Env) { + let stored = storage::get_schema_version(&env); + let current = storage::CURRENT_SCHEMA_VERSION; + if stored >= current { + return; + } + // Future: add per-version data migration steps here + storage::set_schema_version(&env, current); + } + + /// Upgrade the contract WASM — admin only. + /// After replacing the WASM, automatically runs migrate() to ensure + /// contract storage is up to date with the new code version. pub fn upgrade(env: Env, new_wasm_hash: soroban_sdk::BytesN<32>) { let admin = storage::get_admin(&env).unwrap_or_else(|err| panic_with_error!(&env, err)); admin.require_auth(); env.deployer().update_current_contract_wasm(new_wasm_hash); + // Self-invoke migrate to run post-upgrade migration logic + env.invoke_contract::<()>(&env.current_contract_address(), &Symbol::new(&env, "migrate"), ().into_val(&env)); } pub fn get_admin(env: Env) -> Result { storage::get_admin(&env) diff --git a/contracts/parameters-contract/src/storage.rs b/contracts/parameters-contract/src/storage.rs index ad54818..2c629a3 100644 --- a/contracts/parameters-contract/src/storage.rs +++ b/contracts/parameters-contract/src/storage.rs @@ -3,6 +3,20 @@ use soroban_sdk::{symbol_short, Address, Env, Symbol}; use crate::errors::ParametersError; use crate::types::ProtocolParameters; +// Schema version key +pub const SCHEMA_VERSION_KEY: Symbol = symbol_short!("SCHEMA_V"); +pub const CURRENT_SCHEMA_VERSION: u32 = 1; + +/// Get the current schema version (0 = unset) +pub fn get_schema_version(env: &Env) -> u32 { + env.storage().instance().get(&SCHEMA_VERSION_KEY).unwrap_or(0) +} + +/// Set the schema version +pub fn set_schema_version(env: &Env, version: u32) { + env.storage().instance().set(&SCHEMA_VERSION_KEY, &version); +} + pub const ADMIN_KEY: Symbol = symbol_short!("ADMIN"); pub const PARAMS_KEY: Symbol = symbol_short!("PARAMS"); diff --git a/contracts/reputation-contract/src/errors.rs b/contracts/reputation-contract/src/errors.rs index fe8bc42..d42c825 100644 --- a/contracts/reputation-contract/src/errors.rs +++ b/contracts/reputation-contract/src/errors.rs @@ -11,4 +11,5 @@ pub enum ReputationError { Overflow = 4, Underflow = 5, NotInitialized = 6, + MigrationRequired = 7, } diff --git a/contracts/reputation-contract/src/lib.rs b/contracts/reputation-contract/src/lib.rs index 00575c1..b1f1e79 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, Symbol}; +use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, IntoVal, Symbol}; // Module imports mod access; @@ -166,12 +166,29 @@ impl ReputationContract { } } - /// Upgrade the contract WASM — admin only + /// Migrate contract storage to the latest schema version. + /// Called automatically during upgrade() to handle any data migrations. + /// This is idempotent and safe to call multiple times. + pub fn migrate(env: Env) { + let stored = storage::get_schema_version(&env); + let current = storage::CURRENT_SCHEMA_VERSION; + if stored >= current { + return; + } + // Future: add per-version data migration steps here + storage::set_schema_version(&env, current); + } + + /// Upgrade the contract WASM — admin only. + /// After replacing the WASM, automatically runs migrate() to ensure + /// contract storage is up to date with the new code version. pub fn upgrade(env: Env, new_wasm_hash: soroban_sdk::BytesN<32>) { let admin = storage::get_admin(&env) .unwrap_or_else(|err| soroban_sdk::panic_with_error!(&env, err)); admin.require_auth(); env.deployer().update_current_contract_wasm(new_wasm_hash); + // Self-invoke migrate to run post-upgrade migration logic + env.invoke_contract::<()>(&env.current_contract_address(), &Symbol::new(&env, "migrate"), ().into_val(&env)); } pub fn get_admin(env: Env) -> Result { storage::get_admin(&env) diff --git a/contracts/reputation-contract/src/storage.rs b/contracts/reputation-contract/src/storage.rs index 47824f4..659a387 100644 --- a/contracts/reputation-contract/src/storage.rs +++ b/contracts/reputation-contract/src/storage.rs @@ -2,6 +2,20 @@ use soroban_sdk::{symbol_short, Address, Env, Map, Symbol}; use crate::errors::ReputationError; +// Schema version key +pub const SCHEMA_VERSION_KEY: Symbol = symbol_short!("SCHEMA_V"); +pub const CURRENT_SCHEMA_VERSION: u32 = 1; + +/// Get the current schema version (0 = unset) +pub fn get_schema_version(env: &Env) -> u32 { + env.storage().instance().get(&SCHEMA_VERSION_KEY).unwrap_or(0) +} + +/// Set the schema version +pub fn set_schema_version(env: &Env, version: u32) { + env.storage().instance().set(&SCHEMA_VERSION_KEY, &version); +} + // Storage keys for the reputation contract pub const ADMIN_KEY: Symbol = symbol_short!("ADMIN"); pub const UPDATERS_MAP: Symbol = symbol_short!("UPDATERS"); diff --git a/contracts/vendor-registry-contract/src/errors.rs b/contracts/vendor-registry-contract/src/errors.rs index cebbe84..bed157a 100644 --- a/contracts/vendor-registry-contract/src/errors.rs +++ b/contracts/vendor-registry-contract/src/errors.rs @@ -11,4 +11,5 @@ pub enum Error { InvalidName = 5, Unauthorized = 6, Overflow = 7, + MigrationRequired = 8, } diff --git a/contracts/vendor-registry-contract/src/lib.rs b/contracts/vendor-registry-contract/src/lib.rs index f22d5ed..f4c539c 100644 --- a/contracts/vendor-registry-contract/src/lib.rs +++ b/contracts/vendor-registry-contract/src/lib.rs @@ -10,7 +10,7 @@ mod types; mod tests; use errors::Error; -use soroban_sdk::{contract, contractimpl, Address, Env, String}; +use soroban_sdk::{contract, contractimpl, Address, Env, IntoVal, String, Symbol, Val}; use types::VendorInfo; // Export Error type for external use @@ -139,4 +139,42 @@ impl VendorRegistryContract { storage::get_vendor_count(&env) } + + /// Set the admin address for this contract. + /// Requires authorization from the current admin. + pub fn set_admin(env: Env, new_admin: Address) -> Result<(), Error> { + let old_admin = storage::get_admin(&env)?; + old_admin.require_auth(); + access::require_admin(&env, &old_admin)?; + + storage::set_admin(&env, &new_admin); + Ok(()) + } + + /// Migrate contract storage to the latest schema version. + /// Called automatically during upgrade() to handle any data migrations. + /// This is idempotent and safe to call multiple times. + pub fn migrate(env: Env) -> Result<(), Error> { + let stored = storage::get_schema_version(&env); + let current = storage::CURRENT_SCHEMA_VERSION; + if stored >= current { + return Ok(()); + } + // Future: add per-version data migration steps here + storage::set_schema_version(&env, current); + Ok(()) + } + + /// Upgrade the contract WASM — admin only. + /// After replacing the WASM, automatically runs migrate() to ensure + /// contract storage is up to date with the new code version. + pub fn upgrade(env: Env, new_wasm_hash: soroban_sdk::BytesN<32>) -> Result<(), Error> { + let admin = storage::get_admin(&env)?; + admin.require_auth(); + access::require_admin(&env, &admin)?; + env.deployer().update_current_contract_wasm(new_wasm_hash); + // Self-invoke migrate to run post-upgrade migration logic + env.invoke_contract::(&env.current_contract_address(), &Symbol::new(&env, "migrate"), ().into_val(&env)); + Ok(()) + } } diff --git a/contracts/vendor-registry-contract/src/storage.rs b/contracts/vendor-registry-contract/src/storage.rs index 292eb18..4c043ff 100644 --- a/contracts/vendor-registry-contract/src/storage.rs +++ b/contracts/vendor-registry-contract/src/storage.rs @@ -4,6 +4,19 @@ use crate::{ }; use soroban_sdk::{Address, Env}; +// Schema version +pub const CURRENT_SCHEMA_VERSION: u32 = 1; + +/// Get the current schema version (0 = unset) +pub fn get_schema_version(env: &Env) -> u32 { + env.storage().instance().get(&DataKey::SchemaVersion).unwrap_or(0) +} + +/// Set the schema version +pub fn set_schema_version(env: &Env, version: u32) { + env.storage().instance().set(&DataKey::SchemaVersion, &version); +} + pub const PERSISTENT_TTL_THRESHOLD: u32 = 1_036_800; pub const PERSISTENT_TTL_EXTEND_TO: u32 = 2_073_600; diff --git a/contracts/vendor-registry-contract/src/types.rs b/contracts/vendor-registry-contract/src/types.rs index 08e60e0..36cae46 100644 --- a/contracts/vendor-registry-contract/src/types.rs +++ b/contracts/vendor-registry-contract/src/types.rs @@ -5,6 +5,7 @@ use soroban_sdk::{contracttype, Address, String}; pub enum DataKey { // Instance storage Admin, + SchemaVersion, // Persistent storage Vendor(Address), diff --git a/contracts/vouching-contract/src/errors.rs b/contracts/vouching-contract/src/errors.rs index 5d555e2..f400990 100644 --- a/contracts/vouching-contract/src/errors.rs +++ b/contracts/vouching-contract/src/errors.rs @@ -13,4 +13,5 @@ pub enum VouchingError { VouchNotActive = 7, InvalidBoost = 8, ReputationCallFailed = 9, + MigrationRequired = 10, } diff --git a/contracts/vouching-contract/src/lib.rs b/contracts/vouching-contract/src/lib.rs index df9a23c..446330c 100644 --- a/contracts/vouching-contract/src/lib.rs +++ b/contracts/vouching-contract/src/lib.rs @@ -108,6 +108,41 @@ impl VouchingContract { storage::is_mentor(&env, &mentor) } + /// Set the admin address for this contract. + /// Requires authorization from the current admin. + 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(); + Self::require_admin(&env, &old_admin); + + storage::set_admin(&env, &new_admin); + } + + /// Migrate contract storage to the latest schema version. + /// Called automatically during upgrade() to handle any data migrations. + /// This is idempotent and safe to call multiple times. + pub fn migrate(env: Env) { + let stored = storage::get_schema_version(&env); + let current = storage::CURRENT_SCHEMA_VERSION; + if stored >= current { + return; + } + // Future: add per-version data migration steps here + storage::set_schema_version(&env, current); + } + + /// Upgrade the contract WASM — admin only. + /// After replacing the WASM, automatically runs migrate() to ensure + /// contract storage is up to date with the new code version. + pub fn upgrade(env: Env, new_wasm_hash: soroban_sdk::BytesN<32>) { + let admin = storage::get_admin(&env).unwrap_or_else(|err| panic_with_error!(&env, err)); + admin.require_auth(); + Self::require_admin(&env, &admin); + env.deployer().update_current_contract_wasm(new_wasm_hash); + // Self-invoke migrate to run post-upgrade migration logic + env.invoke_contract::<()>(&env.current_contract_address(), &Symbol::new(&env, "migrate"), ().into_val(&env)); + } + fn add_reputation_boost(env: &Env, learner: &Address, boost_amount: u32) { let reputation_contract = storage::get_reputation_contract(env).unwrap_or_else(|err| panic_with_error!(env, err)); diff --git a/contracts/vouching-contract/src/storage.rs b/contracts/vouching-contract/src/storage.rs index 5b41fad..843f287 100644 --- a/contracts/vouching-contract/src/storage.rs +++ b/contracts/vouching-contract/src/storage.rs @@ -5,6 +5,19 @@ use crate::{ types::{DataKey, VouchRecord}, }; +// Schema version +pub const CURRENT_SCHEMA_VERSION: u32 = 1; + +/// Get the current schema version (0 = unset) +pub fn get_schema_version(env: &Env) -> u32 { + env.storage().instance().get(&DataKey::SchemaVersion).unwrap_or(0) +} + +/// Set the schema version +pub fn set_schema_version(env: &Env, version: u32) { + env.storage().instance().set(&DataKey::SchemaVersion, &version); +} + pub const PERSISTENT_TTL_THRESHOLD: u32 = 1_036_800; pub const PERSISTENT_TTL_EXTEND_TO: u32 = 2_073_600; diff --git a/contracts/vouching-contract/src/types.rs b/contracts/vouching-contract/src/types.rs index 7eda96e..e652898 100644 --- a/contracts/vouching-contract/src/types.rs +++ b/contracts/vouching-contract/src/types.rs @@ -16,6 +16,7 @@ pub struct VouchRecord { #[derive(Clone)] pub enum DataKey { Admin, + SchemaVersion, ReputationContract, VouchBoost, Mentor(Address), From 11058e4a8d78240be5dd06a6e542d7e9756d16f7 Mon Sep 17 00:00:00 2001 From: Kehinde Iwayemi <146611338+Iwayemi-Kehinde@users.noreply.github.com> Date: Fri, 19 Jun 2026 22:12:05 +0100 Subject: [PATCH 2/3] Added mcp.json --- .bolt/mcp.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .bolt/mcp.json diff --git a/.bolt/mcp.json b/.bolt/mcp.json new file mode 100644 index 0000000..28c6548 --- /dev/null +++ b/.bolt/mcp.json @@ -0,0 +1,7 @@ +{ + "mcpServers": { + "github": { + "enabled": true + } + } +} From 63001888de3dc69a7df997c1a630e2fa24be42bf Mon Sep 17 00:00:00 2001 From: Kehinde Iwayemi <146611338+Iwayemi-Kehinde@users.noreply.github.com> Date: Fri, 19 Jun 2026 22:31:33 +0100 Subject: [PATCH 3/3] Added package-lock.json --- package-lock.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..af2503e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "project", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}