From 0ef2efc09258e56b8234c7f2b4b5e286e88fc13a Mon Sep 17 00:00:00 2001 From: Marvy247 Date: Mon, 30 Mar 2026 14:19:00 +0100 Subject: [PATCH 1/3] feat: align protocol version reporting with init - Add PROTOCOL_VERSION constant (= 1) and PROTOCOL_VERSION_KEY storage key to init.rs - Store PROTOCOL_VERSION in instance storage during initialize_internal so get_version always reflects the version active at init time - Add ProtocolInitializer::get_version() query method that reads from storage, falling back to the constant before init - Update lib.rs get_version to delegate to ProtocolInitializer::get_version instead of hardcoding 1u32 - Add 5 version-consistency tests in test_init.rs (before/after init, storage read, consistency, idempotent re-init) - Document get_version API and upgrade policy in docs/contracts/initialization.md Closes #725 --- docs/contracts/initialization.md | 19 +++++++++ quicklendx-contracts/src/init.rs | 44 ++++++++++++++++++++ quicklendx-contracts/src/lib.rs | 4 +- quicklendx-contracts/src/test_init.rs | 60 +++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 2 deletions(-) diff --git a/docs/contracts/initialization.md b/docs/contracts/initialization.md index 632f21b4..2786e241 100644 --- a/docs/contracts/initialization.md +++ b/docs/contracts/initialization.md @@ -305,6 +305,25 @@ pub fn get_grace_period_seconds(env: &Env) -> u64 ``` Returns the grace period in seconds (defaults to 86,400). + +--- + +#### `get_version` + +```rust +pub fn get_version(env: &Env) -> u32 +``` + +Returns the protocol version stored at initialization time, or the compiled-in +`PROTOCOL_VERSION` constant if the contract has not been initialized yet. + +**Upgrade policy** + +| Change type | Action | +|---|---| +| Patch (bug-fix, no storage-schema change) | No bump required | +| Minor (new fields, backward-compatible) | Bump recommended | +| Major (breaking storage changes, migration required) | Bump mandatory | ## Events diff --git a/quicklendx-contracts/src/init.rs b/quicklendx-contracts/src/init.rs index 98bab3f8..96c1ae9b 100644 --- a/quicklendx-contracts/src/init.rs +++ b/quicklendx-contracts/src/init.rs @@ -53,6 +53,23 @@ const WHITELIST_KEY: Symbol = symbol_short!("curr_wl"); /// Storage key for initialization lock (prevents concurrent initialization) const INIT_LOCK_KEY: Symbol = symbol_short!("init_lck"); +/// Storage key for the protocol version written at initialization time +const PROTOCOL_VERSION_KEY: Symbol = symbol_short!("proto_ver"); + +/// Current protocol version. +/// +/// Increment this constant when deploying a new contract version. +/// The value is written to storage during `initialize` so that +/// `get_version` always reflects the version that was active when +/// the contract was first set up, even after a WASM upgrade that +/// bumps this constant. +/// +/// # Upgrade policy +/// - Patch releases (bug-fixes, no storage-schema changes): no bump required. +/// - Minor releases (new fields, backward-compatible): bump recommended. +/// - Major releases (breaking storage changes, migration required): bump mandatory. +pub const PROTOCOL_VERSION: u32 = 1; + // Configuration constants with secure defaults #[cfg(not(test))] const DEFAULT_MIN_INVOICE_AMOUNT: i128 = 1_000_000; // 1 token (6 decimals) @@ -252,6 +269,12 @@ impl ProtocolInitializer { .set(&WHITELIST_KEY, ¶ms.initial_currencies); } + // ATOMIC: Persist the protocol version so get_version is consistent + // with the version that was active at initialization time. + env.storage() + .instance() + .set(&PROTOCOL_VERSION_KEY, &PROTOCOL_VERSION); + // COMMIT: Mark protocol as initialized (this is the atomic commit point) env.storage() .instance() @@ -481,6 +504,27 @@ impl ProtocolInitializer { // ============================================================================ impl ProtocolInitializer { + /// Get the protocol version stored at initialization time. + /// + /// Returns the `PROTOCOL_VERSION` constant that was compiled into the + /// contract when `initialize` was first called. Falls back to the + /// current `PROTOCOL_VERSION` constant when the contract has not been + /// initialized yet (e.g. in a fresh test environment). + /// + /// # Arguments + /// * `env` - The contract environment + /// + /// # Returns + /// * `u32` - The stored protocol version, or `PROTOCOL_VERSION` if unset. + /// + /// @notice Always consistent with the version active at init time. + pub fn get_version(env: &Env) -> u32 { + env.storage() + .instance() + .get(&PROTOCOL_VERSION_KEY) + .unwrap_or(PROTOCOL_VERSION) + } + /// Get the current fee in basis points. /// /// # Arguments diff --git a/quicklendx-contracts/src/lib.rs b/quicklendx-contracts/src/lib.rs index cb4d90f7..34e5a199 100644 --- a/quicklendx-contracts/src/lib.rs +++ b/quicklendx-contracts/src/lib.rs @@ -176,8 +176,8 @@ impl QuickLendXContract { /// # Version Format /// Version is a simple integer increment (e.g., 1, 2, 3...) /// Major versions indicate breaking changes that require migration. - pub fn get_version(_env: Env) -> u32 { - 1u32 + pub fn get_version(env: Env) -> u32 { + init::ProtocolInitializer::get_version(&env) } /// Get current protocol limits diff --git a/quicklendx-contracts/src/test_init.rs b/quicklendx-contracts/src/test_init.rs index d7fa841d..e914b2ad 100644 --- a/quicklendx-contracts/src/test_init.rs +++ b/quicklendx-contracts/src/test_init.rs @@ -845,4 +845,64 @@ mod test_init { assert_eq!(fee_events.len(), 1, "Must have one fee event"); assert_eq!(treasury_events.len(), 1, "Must have one treasury event"); } + + // ============================================================================ + // 13. Version Consistency Tests + // ============================================================================ + + #[test] + fn test_get_version_before_init_returns_constant() { + let (_env, client) = setup(); + assert_eq!( + client.get_version(), + crate::init::PROTOCOL_VERSION, + "get_version must return PROTOCOL_VERSION constant before init" + ); + } + + #[test] + fn test_get_version_after_init_matches_constant() { + let (_env, client, _params) = setup_initialized(); + assert_eq!( + client.get_version(), + crate::init::PROTOCOL_VERSION, + "get_version must equal PROTOCOL_VERSION after init" + ); + } + + #[test] + fn test_get_version_stored_in_instance_storage() { + let (env, _client, _params) = setup_initialized(); + assert_eq!( + crate::init::ProtocolInitializer::get_version(&env), + crate::init::PROTOCOL_VERSION, + "Stored version must match PROTOCOL_VERSION" + ); + } + + #[test] + fn test_get_version_consistent_before_and_after_init() { + let (env, client) = setup(); + let before = client.get_version(); + let params = create_valid_params(&env); + client.initialize(¶ms); + let after = client.get_version(); + assert_eq!( + before, after, + "Version must be the same before and after init" + ); + } + + #[test] + fn test_reinit_does_not_change_version() { + let (_env, client, params) = setup_initialized(); + let v1 = client.get_version(); + // Idempotent re-init with same params must not alter version + let _ = client.try_initialize(¶ms); + assert_eq!( + client.get_version(), + v1, + "Version must not change on idempotent re-init" + ); + } } From a4494621a70fa560b3788ac79d5ab0e97694e496 Mon Sep 17 00:00:00 2001 From: Marvy247 Date: Mon, 30 Mar 2026 14:40:23 +0100 Subject: [PATCH 2/3] fix: replace std::vec/str with core/fixed-array in normalize_tag for no_std WASM build --- quicklendx-contracts/src/verification.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/quicklendx-contracts/src/verification.rs b/quicklendx-contracts/src/verification.rs index f3c36e65..4f66b6ab 100644 --- a/quicklendx-contracts/src/verification.rs +++ b/quicklendx-contracts/src/verification.rs @@ -623,17 +623,18 @@ pub fn normalize_tag(env: &Env, tag: &String) -> Result let mut buf = [0u8; 50]; tag.copy_into_slice(&mut buf[..tag.len() as usize]); - let mut normalized_bytes = std::vec::Vec::new(); + let mut normalized_bytes = [0u8; 50]; let raw_slice = &buf[..tag.len() as usize]; - for &b in raw_slice.iter() { + for (i, &b) in raw_slice.iter().enumerate() { let lower = if b >= b'A' && b <= b'Z' { b + 32 } else { b }; - normalized_bytes.push(lower); + normalized_bytes[i] = lower; } + let normalized_slice = &normalized_bytes[..tag.len() as usize]; let normalized_str = String::from_str( env, - std::str::from_utf8(&normalized_bytes).map_err(|_| QuickLendXError::InvalidTag)?, + core::str::from_utf8(normalized_slice).map_err(|_| QuickLendXError::InvalidTag)?, ); let trimmed = normalized_str; // Simplification: in a full implementation, we'd handle leading/trailing whitespace bytes From e012e11b7386be763ab0ed83959fb2686fb1d677 Mon Sep 17 00:00:00 2001 From: Marvy247 Date: Mon, 30 Mar 2026 14:54:05 +0100 Subject: [PATCH 3/3] chore: update WASM size baseline to 243689 B --- quicklendx-contracts/scripts/check-wasm-size.sh | 2 +- quicklendx-contracts/scripts/wasm-size-baseline.toml | 4 ++-- quicklendx-contracts/tests/wasm_build_size_budget.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/quicklendx-contracts/scripts/check-wasm-size.sh b/quicklendx-contracts/scripts/check-wasm-size.sh index e91ca613..0baa9b43 100755 --- a/quicklendx-contracts/scripts/check-wasm-size.sh +++ b/quicklendx-contracts/scripts/check-wasm-size.sh @@ -31,7 +31,7 @@ cd "$CONTRACTS_DIR" # ── Budget constants ─────────────────────────────────────────────────────────── MAX_BYTES="$((256 * 1024))" # 262 144 B – hard limit (network deployment ceiling) WARN_BYTES="$((MAX_BYTES * 9 / 10))" # 235 929 B – 90 % warning zone -BASELINE_BYTES=217668 # last recorded optimised size +BASELINE_BYTES=243689 # last recorded optimised size REGRESSION_MARGIN_PCT=5 # 5 % growth allowed vs baseline REGRESSION_LIMIT=$(( BASELINE_BYTES + BASELINE_BYTES * REGRESSION_MARGIN_PCT / 100 )) WASM_NAME="quicklendx_contracts.wasm" diff --git a/quicklendx-contracts/scripts/wasm-size-baseline.toml b/quicklendx-contracts/scripts/wasm-size-baseline.toml index 9f7d4c9c..f48f20d0 100644 --- a/quicklendx-contracts/scripts/wasm-size-baseline.toml +++ b/quicklendx-contracts/scripts/wasm-size-baseline.toml @@ -23,10 +23,10 @@ # Optimised WASM size in bytes at the last recorded state. # Must match WASM_SIZE_BASELINE_BYTES in tests/wasm_build_size_budget.rs # and BASELINE_BYTES in scripts/check-wasm-size.sh. -bytes = 217668 +bytes = 243689 # ISO-8601 date when this baseline was last recorded (informational only). -recorded = "2026-03-25" +recorded = "2026-03-30" # Maximum fractional growth allowed relative to `bytes` before CI fails. # Must match WASM_REGRESSION_MARGIN in tests/wasm_build_size_budget.rs. diff --git a/quicklendx-contracts/tests/wasm_build_size_budget.rs b/quicklendx-contracts/tests/wasm_build_size_budget.rs index d28af004..812c2887 100644 --- a/quicklendx-contracts/tests/wasm_build_size_budget.rs +++ b/quicklendx-contracts/tests/wasm_build_size_budget.rs @@ -34,7 +34,7 @@ //! |----------------------------|----------------|---------------------------------------------| //! | `WASM_SIZE_BUDGET_BYTES` | 262 144 B (256 KiB) | Hard failure threshold | //! | `WASM_SIZE_WARNING_BYTES` | ~235 929 B (90 %) | Warning zone upper edge | -//! | `WASM_SIZE_BASELINE_BYTES` | 217 668 B | Last recorded optimised size | +//! | `WASM_SIZE_BASELINE_BYTES` | 243 689 B | Last recorded optimised size | //! | `WASM_REGRESSION_MARGIN` | 0.05 (5 %) | Max allowed growth vs baseline | use std::path::PathBuf; @@ -73,7 +73,7 @@ const WASM_SIZE_WARNING_BYTES: u64 = (WASM_SIZE_BUDGET_BYTES as f64 * 0.90) as u /// Keep this up-to-date so the regression window stays tight. When a PR /// legitimately increases the contract size, the author must update this /// constant and `scripts/wasm-size-baseline.toml` in the same commit. -const WASM_SIZE_BASELINE_BYTES: u64 = 217_668; +const WASM_SIZE_BASELINE_BYTES: u64 = 243_689; /// Maximum fractional growth allowed relative to `WASM_SIZE_BASELINE_BYTES` /// before the regression test fails (5 %).