This document defines the architecture and conventions for managing storage schema changes and versioning across contract upgrades in Lafiya's Soroban smart contracts.
Soroban contract storage relies on keys serialized via XDR. For DataKey enums, we enforce the following rules to prevent key collision or deserialization failure when contract bytecode is upgraded:
- Rule: New keys/variants must be added to the end of the
DataKeyenum. - Rule: Existing variants must never be reordered, deleted, or have their types modified.
- Reason: Soroban serializes enums based on their variant discriminants (index order). Changing the order or deleting a variant will shift the discriminants of subsequent variants, leading to silent collisions or failure to deserialize previously stored keys.
- If a variant stores dynamic parameters (e.g.,
Attestation(BytesN<32>)), ensure the inner types are fully versioned or structured if their fields are subject to change.
Every contract must maintain an explicit schema version in its instance storage.
- Schema Version Variant: A
SchemaVersionvariant is included in theDataKeyenum. - Current Version Constant: A
const SCHEMA_VERSION: u32defines the schema version supported by the current bytecode. - Initialization: During
initialize(), the contract setsSchemaVersionin instance storage:env.storage().instance().set(&DataKey::SchemaVersion, &SCHEMA_VERSION);
- View Interface: Every contract must expose a read-only view function to query the current version:
Note: Defaulting to 1 ensures backwards compatibility with contracts deployed before schema versioning was introduced.
pub fn get_schema_version(env: Env) -> u32 { env.storage().instance().get(&DataKey::SchemaVersion).unwrap_or(1) }
When upgrading a contract to a new bytecode version with schema changes, a migration pattern must be followed.
Upgradable contracts should expose an upgrade(env: Env, new_wasm_hash: BytesN<32>) function restricted to the Admin.
Within the upgrade or post-upgrade logic:
- Read the old version from instance storage:
let old_version: u32 = env.storage().instance().get(&DataKey::SchemaVersion).unwrap_or(1);
- If
old_version < SCHEMA_VERSION, execute conditional migration logic:if old_version < 2 { // Run migration from version 1 to 2 migrate_v1_to_v2(&env); }
- Update the stored schema version to the new
SCHEMA_VERSION:env.storage().instance().set(&DataKey::SchemaVersion, &SCHEMA_VERSION);
- Keep migrations safe by using
instanceortemporarystorage where appropriate. - For
persistentstorage migration (which can be large and exceed budget limits), prefer lazy/on-demand migration during normal reads/writes, or paginated/chunked migrations triggered by administrative calls.