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 8923d501..e0ca1278 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) @@ -241,6 +258,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() @@ -478,6 +501,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/test_init.rs b/quicklendx-contracts/src/test_init.rs index 6d214fde..b472ac90 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" + ); + } }