From 95601bafbc000c8dbd8bb57bf1479c5991f24a5c Mon Sep 17 00:00:00 2001 From: Harbduls Date: Fri, 29 May 2026 15:39:15 +0100 Subject: [PATCH 1/2] Implement versioned storage schema with migration functionality - Add STORAGE_VERSION constant (v2) in storage.rs - Add StorageVersion key to CounterKey enum - Create migration.rs module with comprehensive migration logic - Add migrate() function to handle v1 to v2 migration - Update contract initialization to store storage version - Add migrate_storage() and get_storage_version() public functions - Implement comprehensive test suite for migration scenarios - Migration safely adds new v2 fields without breaking existing data - Supports incremental migrations for future schema changes --- TICK_IMPLEMENTATION_SUMMARY.md | 168 ++++++++++++ contracts/stellar-save/src/lib.rs | 274 ++++++++++++++++++- contracts/stellar-save/src/migration.rs | 338 ++++++++++++++++++++++++ contracts/stellar-save/src/storage.rs | 32 +++ 4 files changed, 811 insertions(+), 1 deletion(-) create mode 100644 TICK_IMPLEMENTATION_SUMMARY.md create mode 100644 contracts/stellar-save/src/migration.rs diff --git a/TICK_IMPLEMENTATION_SUMMARY.md b/TICK_IMPLEMENTATION_SUMMARY.md new file mode 100644 index 00000000..25e5ead7 --- /dev/null +++ b/TICK_IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,168 @@ +# Tick Function Implementation Summary + +## Overview +Successfully implemented the `tick(group_id)` function that enables trustless automation for cycle advancement in the Stellar Save contract. + +## Key Features Implemented + +### 1. Core tick() Function +- **Location**: `contracts/stellar-save/src/lib.rs` +- **Function**: `pub fn tick(env: Env, group_id: u64) -> Result<(), StellarSaveError>` +- **Purpose**: Anyone can call this function to advance a group's cycle when the deadline is reached + +### 2. Deadline Checking +- Uses `env.ledger().timestamp()` to get current blockchain time +- Leverages existing `is_cycle_deadline_passed()` helper function +- Compares against cycle deadline: `started_at + (cycle_duration * (current_cycle + 1))` + +### 3. Automatic Payout Execution +- Checks if cycle is complete using `is_cycle_complete()` +- If complete: executes payout via `execute_payout()` +- If payout fails: marks cycle as defaulted but still advances + +### 4. Cycle Defaulting +- If contributions are missing when deadline passes, marks cycle as defaulted +- Still advances to next cycle to maintain group progression +- Tracks defaulted status in event emission + +### 5. New Event: CycleAdvanced +- **Location**: `contracts/stellar-save/src/events.rs` +- **Fields**: + - `group_id`: The group being advanced + - `old_cycle`: Previous cycle number + - `new_cycle`: New cycle number + - `payout_executed`: Whether payout was successfully executed + - `defaulted`: Whether cycle was marked as defaulted + - `advanced_at`: Timestamp of advancement + +### 6. New Error Type: DeadlineNotReached +- **Location**: `contracts/stellar-save/src/error.rs` +- **Code**: 3005 (Contribution category) +- **Purpose**: Prevents premature tick calls before deadline +- **Retryable**: Yes (timing-based error) + +## Function Behavior + +### Input Validation +1. Verifies group exists (`GroupNotFound`) +2. Checks group is active and not complete (`InvalidState`) +3. Ensures deadline has passed (`DeadlineNotReached`) + +### Execution Logic +1. **Load group** from storage +2. **Check deadline** using current timestamp +3. **Determine cycle status** (complete vs incomplete) +4. **Execute payout** if cycle complete, or mark as defaulted +5. **Advance cycle** using existing `group.advance_cycle()` +6. **Update storage** with new group state +7. **Emit events** for cycle advancement and completion (if applicable) + +### Event Emission +- Always emits `CycleAdvanced` event with execution details +- Emits `GroupCompleted` event if group finishes all cycles +- Provides full transparency for off-chain monitoring + +## Test Coverage + +### Comprehensive Test Suite +- **test_tick_group_not_found**: Handles non-existent groups +- **test_tick_group_not_active**: Prevents ticking inactive groups +- **test_tick_deadline_not_reached**: Enforces deadline requirement +- **test_tick_cycle_complete_with_payout**: Successful payout execution +- **test_tick_cycle_incomplete_defaulted**: Handles missing contributions +- **test_tick_completes_group**: Verifies group completion logic +- **test_tick_already_complete_group**: Prevents ticking completed groups +- **test_tick_emits_cycle_advanced_event**: Validates event emission + +## Integration Points + +### Existing Functions Used +- `is_cycle_deadline_passed()`: Deadline validation +- `is_cycle_complete()`: Contribution status checking +- `execute_payout()`: Payout processing +- `group.advance_cycle()`: Cycle progression +- `get_total_paid_out()`: Completion event data + +### Storage Operations +- Reads group data from persistent storage +- Updates group state after cycle advancement +- Maintains data consistency throughout process + +## Trustless Automation Benefits + +### Anyone Can Call +- No authorization required (unlike creator-only functions) +- Enables automated bots and external services +- Prevents groups from getting stuck due to inactive creators + +### Transparent Execution +- All actions logged via events +- Clear distinction between successful payouts and defaults +- Maintains audit trail for all cycle advancements + +### Robust Error Handling +- Graceful handling of payout failures +- Continues group progression even with issues +- Provides clear error messages for debugging + +## Usage Examples + +### Successful Cycle Advancement +```rust +// After deadline passes and all members contributed +let result = contract.tick(group_id); +// Result: Ok(()), cycle advanced, payout executed, CycleAdvanced event emitted +``` + +### Defaulted Cycle +```rust +// After deadline passes but some contributions missing +let result = contract.tick(group_id); +// Result: Ok(()), cycle advanced, no payout, CycleAdvanced event with defaulted=true +``` + +### Premature Call +```rust +// Before deadline passes +let result = contract.tick(group_id); +// Result: Err(DeadlineNotReached), no state changes +``` + +## Future Enhancements + +### Potential Improvements +1. **Incentive Mechanism**: Reward addresses that call tick() +2. **Batch Processing**: Allow ticking multiple groups in one call +3. **Deadline Extensions**: Allow groups to extend deadlines under certain conditions +4. **Default Penalties**: Implement penalties for members who cause defaults + +### Monitoring Integration +- Events enable easy off-chain monitoring +- Can trigger notifications for defaults or completions +- Supports analytics on group performance and automation usage + +## Files Modified + +1. **contracts/stellar-save/src/lib.rs** + - Added `tick()` function with full implementation + - Added comprehensive test suite + +2. **contracts/stellar-save/src/events.rs** + - Added `CycleAdvanced` event struct + - Added `emit_cycle_advanced()` function + - Added event tests + +3. **contracts/stellar-save/src/error.rs** + - Added `DeadlineNotReached` error variant + - Added error message and recovery guidance + - Updated retryable error classification + +4. **Cargo.toml** (root) + - Fixed workspace configuration + +5. **client/Cargo.toml** + - Fixed dependency version conflicts + +## Conclusion + +The tick function implementation successfully provides trustless automation for the Stellar Save contract, enabling reliable cycle advancement without requiring creator intervention. The implementation is robust, well-tested, and maintains full compatibility with existing contract functionality. \ No newline at end of file diff --git a/contracts/stellar-save/src/lib.rs b/contracts/stellar-save/src/lib.rs index b034cd5e..10688e0b 100644 --- a/contracts/stellar-save/src/lib.rs +++ b/contracts/stellar-save/src/lib.rs @@ -23,6 +23,7 @@ pub mod contribution; pub mod error; pub mod events; pub mod group; +pub mod migration; pub mod payout; pub mod payout_executor; pub mod pool; @@ -36,6 +37,7 @@ pub use error::{ContractResult, ErrorCategory, StellarSaveError}; pub use events::EventEmitter; pub use events::*; pub use group::{Group, GroupStatus}; +use migration::{initialize_storage_version, migrate}; pub use payout::PayoutRecord; pub use pool::{PoolCalculator, PoolInfo}; #[cfg(test)] @@ -360,6 +362,9 @@ impl StellarSaveContract { } /// Initializes or updates the global contract configuration. + /// + /// This function also handles storage migration when needed and initializes + /// the storage version on first deployment. /// Only the current admin can perform this update. pub fn update_config(env: Env, new_config: ContractConfig) -> Result<(), StellarSaveError> { // 1. Validation Logic @@ -375,13 +380,53 @@ impl StellarSaveContract { } else { // First time initialization: caller becomes admin new_config.admin.require_auth(); + + // Initialize storage version on first deployment + initialize_storage_version(&env); } - // 3. Save Configuration + // 3. Perform migration if needed + migrate(&env)?; + + // 4. Save Configuration env.storage().persistent().set(&key, &new_config); Ok(()) } + /// Performs storage migration to the latest schema version. + /// + /// This function can be called by the admin to manually trigger migration + /// without updating the contract configuration. + /// + /// # Returns + /// * `Ok(())` - If migration completed successfully or no migration needed + /// * `Err(StellarSaveError)` - If migration failed or caller is not admin + pub fn migrate_storage(env: Env, caller: Address) -> Result<(), StellarSaveError> { + // Require admin authorization + let config_key = StorageKeyBuilder::contract_config(); + if let Some(config) = env.storage().persistent().get::<_, ContractConfig>(&config_key) { + if config.admin != caller { + return Err(StellarSaveError::Unauthorized); + } + caller.require_auth(); + } else { + return Err(StellarSaveError::InvalidState); // No config means contract not initialized + } + + // Perform migration + migrate(&env)?; + + Ok(()) + } + + /// Gets the current storage schema version. + /// + /// Returns the version number of the storage schema currently in use. + /// This can be used to check if migration is needed. + pub fn get_storage_version(env: Env) -> u32 { + migration::get_storage_version(&env) + } + /// Creates a new savings group (ROSCA). /// Tasks: Validate parameters, Generate ID, Initialize Struct, Store Data, Emit Event. pub fn create_group( @@ -9462,4 +9507,231 @@ mod tests { assert_eq!(cycle_advanced_events.len(), 1); } + + // Migration Tests + #[test] + fn test_get_storage_version_new_contract() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(StellarSaveContract, ()); + let client = StellarSaveContractClient::new(&env, &contract_id); + + // New contract should default to v1 until initialized + let version = client.get_storage_version(); + assert_eq!(version, 1); + } + + #[test] + fn test_storage_version_initialized_on_config_update() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(StellarSaveContract, ()); + let client = StellarSaveContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let config = ContractConfig { + admin: admin.clone(), + min_contribution: 1_000_000, + max_contribution: 1_000_000_000, + min_members: 2, + max_members: 10, + min_cycle_duration: 86400, + max_cycle_duration: 2_592_000, + }; + + // Initialize config (first time) + client.update_config(&config); + + // Storage version should now be current + let version = client.get_storage_version(); + assert_eq!(version, storage::STORAGE_VERSION); + } + + #[test] + fn test_migrate_storage_requires_admin() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(StellarSaveContract, ()); + let client = StellarSaveContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let non_admin = Address::generate(&env); + + let config = ContractConfig { + admin: admin.clone(), + min_contribution: 1_000_000, + max_contribution: 1_000_000_000, + min_members: 2, + max_members: 10, + min_cycle_duration: 86400, + max_cycle_duration: 2_592_000, + }; + + // Initialize config + client.update_config(&config); + + // Non-admin should not be able to migrate + let result = client.try_migrate_storage(&non_admin); + assert_eq!(result, Err(Ok(StellarSaveError::Unauthorized))); + + // Admin should be able to migrate + let result = client.try_migrate_storage(&admin); + assert!(result.is_ok()); + } + + #[test] + fn test_migrate_storage_uninitialized_contract() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(StellarSaveContract, ()); + let client = StellarSaveContractClient::new(&env, &contract_id); + + let caller = Address::generate(&env); + + // Should fail on uninitialized contract + let result = client.try_migrate_storage(&caller); + assert_eq!(result, Err(Ok(StellarSaveError::InvalidState))); + } + + #[test] + fn test_migration_from_v1_to_v2() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(StellarSaveContract, ()); + let client = StellarSaveContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let creator = Address::generate(&env); + + // Simulate v1 contract state + // 1. Set up config without version + let config = ContractConfig { + admin: admin.clone(), + min_contribution: 1_000_000, + max_contribution: 1_000_000_000, + min_members: 2, + max_members: 10, + min_cycle_duration: 86400, + max_cycle_duration: 2_592_000, + }; + + // Store config directly to simulate v1 state + let config_key = StorageKeyBuilder::contract_config(); + env.storage().persistent().set(&config_key, &config); + + // Create a group in v1 format (without balance tracking) + let group_id = client.create_group(&creator, &10_000_000, &604800, &5); + + // Manually set storage version to v1 + let version_key = StorageKeyBuilder::storage_version(); + env.storage().persistent().set(&version_key, &1u32); + + // Verify we're at v1 + assert_eq!(client.get_storage_version(), 1); + + // Run migration + client.migrate_storage(&admin); + + // Verify migration to v2 + assert_eq!(client.get_storage_version(), storage::STORAGE_VERSION); + + // Verify v2 fields were initialized + let pause_key = StorageKeyBuilder::emergency_pause(); + let guard_key = StorageKeyBuilder::reentrancy_guard(); + let balance_key = StorageKeyBuilder::group_balance(group_id); + let paid_out_key = StorageKeyBuilder::group_total_paid_out(group_id); + + assert_eq!(env.storage().persistent().get::(&pause_key).unwrap(), false); + assert_eq!(env.storage().persistent().get::(&guard_key).unwrap(), false); + assert_eq!(env.storage().persistent().get::(&balance_key).unwrap(), 0); + assert_eq!(env.storage().persistent().get::(&paid_out_key).unwrap(), 0); + + // Verify original group data is preserved + let group = client.get_group(&group_id); + assert_eq!(group.creator, creator); + assert_eq!(group.contribution_amount, 10_000_000); + } + + #[test] + fn test_migration_idempotent() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(StellarSaveContract, ()); + let client = StellarSaveContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let config = ContractConfig { + admin: admin.clone(), + min_contribution: 1_000_000, + max_contribution: 1_000_000_000, + min_members: 2, + max_members: 10, + min_cycle_duration: 86400, + max_cycle_duration: 2_592_000, + }; + + // Initialize config (triggers migration) + client.update_config(&config); + let version_after_init = client.get_storage_version(); + + // Run migration again + client.migrate_storage(&admin); + let version_after_migrate = client.get_storage_version(); + + // Should be the same + assert_eq!(version_after_init, version_after_migrate); + assert_eq!(version_after_migrate, storage::STORAGE_VERSION); + } + + #[test] + fn test_migration_preserves_existing_data() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(StellarSaveContract, ()); + let client = StellarSaveContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let creator = Address::generate(&env); + let member = Address::generate(&env); + + // Set up v1 contract with data + let config = ContractConfig { + admin: admin.clone(), + min_contribution: 1_000_000, + max_contribution: 1_000_000_000, + min_members: 2, + max_members: 10, + min_cycle_duration: 86400, + max_cycle_duration: 2_592_000, + }; + + // Store config and create group + let config_key = StorageKeyBuilder::contract_config(); + env.storage().persistent().set(&config_key, &config); + + let group_id = client.create_group(&creator, &10_000_000, &604800, &5); + client.join_group(&group_id, &creator); + client.join_group(&group_id, &member); + + // Set to v1 and add some contributions + let version_key = StorageKeyBuilder::storage_version(); + env.storage().persistent().set(&version_key, &1u32); + + // Store some v1 data + let total_groups_key = StorageKeyBuilder::total_groups(); + env.storage().persistent().set(&total_groups_key, &1u64); + + // Run migration + client.migrate_storage(&admin); + + // Verify data preservation + assert_eq!(client.get_storage_version(), storage::STORAGE_VERSION); + + let group = client.get_group(&group_id); + assert_eq!(group.creator, creator); + assert_eq!(group.contribution_amount, 10_000_000); + + let total_groups: u64 = env.storage().persistent().get(&total_groups_key).unwrap(); + assert_eq!(total_groups, 1); + } } diff --git a/contracts/stellar-save/src/migration.rs b/contracts/stellar-save/src/migration.rs new file mode 100644 index 00000000..84108110 --- /dev/null +++ b/contracts/stellar-save/src/migration.rs @@ -0,0 +1,338 @@ +use soroban_sdk::{Env, Vec}; +use crate::{ + error::StellarSaveError, + storage::{StorageKeyBuilder, STORAGE_VERSION}, + group::Group, + status::GroupStatus, +}; + +/// Migration utilities for handling storage schema upgrades. +/// +/// This module provides functionality to migrate data between different +/// storage schema versions, ensuring backward compatibility during upgrades. + +/// Performs migration from older storage layouts to the current version. +/// +/// This function checks the current storage version and applies necessary +/// migrations to bring the data up to the latest schema version. +/// +/// # Arguments +/// * `env` - The contract environment +/// +/// # Returns +/// * `Ok(())` - If migration completed successfully or no migration needed +/// * `Err(StellarSaveError)` - If migration failed +/// +/// # Migration Process +/// 1. Check current storage version +/// 2. Apply incremental migrations if needed +/// 3. Update storage version to current +pub fn migrate(env: &Env) -> Result<(), StellarSaveError> { + let version_key = StorageKeyBuilder::storage_version(); + let current_version: u32 = env.storage().persistent().get(&version_key).unwrap_or(1); + + // If already at current version, no migration needed + if current_version >= STORAGE_VERSION { + return Ok(()); + } + + // Apply migrations incrementally + if current_version < 2 { + migrate_v1_to_v2(env)?; + } + + // Future migrations would be added here: + // if current_version < 3 { + // migrate_v2_to_v3(env)?; + // } + + // Update storage version to current + env.storage().persistent().set(&version_key, &STORAGE_VERSION); + + Ok(()) +} + +/// Migrates storage from version 1 to version 2. +/// +/// Version 2 changes: +/// - Adds storage version tracking +/// - Adds emergency pause functionality +/// - Adds reentrancy guard protection +/// +/// This migration is safe as it only adds new fields without modifying existing data. +fn migrate_v1_to_v2(env: &Env) -> Result<(), StellarSaveError> { + // Initialize new v2 fields with default values + + // Set emergency pause to false (not paused) + let pause_key = StorageKeyBuilder::emergency_pause(); + if !env.storage().persistent().has(&pause_key) { + env.storage().persistent().set(&pause_key, &false); + } + + // Initialize reentrancy guard to false (not locked) + let guard_key = StorageKeyBuilder::reentrancy_guard(); + if !env.storage().persistent().has(&guard_key) { + env.storage().persistent().set(&guard_key, &false); + } + + // Migrate existing groups to ensure they have all required v2 fields + let total_groups_key = StorageKeyBuilder::total_groups(); + let total_groups: u64 = env.storage().persistent().get(&total_groups_key).unwrap_or(0); + + for group_id in 1..=total_groups { + migrate_group_v1_to_v2(env, group_id)?; + } + + Ok(()) +} + +/// Migrates a single group from v1 to v2 format. +/// +/// Ensures the group has all required v2 fields and initializes +/// any missing balance tracking. +fn migrate_group_v1_to_v2(env: &Env, group_id: u64) -> Result<(), StellarSaveError> { + let group_key = StorageKeyBuilder::group_data(group_id); + + // Check if group exists + if !env.storage().persistent().has(&group_key) { + return Ok(()); // Skip non-existent groups + } + + // Initialize group balance if not present + let balance_key = StorageKeyBuilder::group_balance(group_id); + if !env.storage().persistent().has(&balance_key) { + env.storage().persistent().set(&balance_key, &0i128); + } + + // Initialize total paid out if not present + let paid_out_key = StorageKeyBuilder::group_total_paid_out(group_id); + if !env.storage().persistent().has(&paid_out_key) { + env.storage().persistent().set(&paid_out_key, &0i128); + } + + Ok(()) +} + +/// Initializes storage version on first contract deployment. +/// +/// This should be called during contract initialization to set up +/// the storage version tracking. +pub fn initialize_storage_version(env: &Env) { + let version_key = StorageKeyBuilder::storage_version(); + + // Only set if not already present (avoid overwriting during upgrades) + if !env.storage().persistent().has(&version_key) { + env.storage().persistent().set(&version_key, &STORAGE_VERSION); + } +} + +/// Gets the current storage version from storage. +/// +/// Returns 1 if no version is stored (indicating v1 schema). +pub fn get_storage_version(env: &Env) -> u32 { + let version_key = StorageKeyBuilder::storage_version(); + env.storage().persistent().get(&version_key).unwrap_or(1) +} + +/// Checks if migration is needed by comparing stored version with current version. +pub fn is_migration_needed(env: &Env) -> bool { + get_storage_version(env) < STORAGE_VERSION +} + +#[cfg(test)] +mod tests { + use super::*; + use soroban_sdk::{testutils::Address as _, Address, Env}; + use crate::group::Group; + use crate::status::GroupStatus; + + #[test] + fn test_initialize_storage_version() { + let env = Env::default(); + + // Initially no version should be stored + assert_eq!(get_storage_version(&env), 1); + + // Initialize version + initialize_storage_version(&env); + + // Should now have current version + assert_eq!(get_storage_version(&env), STORAGE_VERSION); + } + + #[test] + fn test_initialize_storage_version_idempotent() { + let env = Env::default(); + + // Initialize version + initialize_storage_version(&env); + let first_version = get_storage_version(&env); + + // Initialize again + initialize_storage_version(&env); + let second_version = get_storage_version(&env); + + // Should be the same + assert_eq!(first_version, second_version); + } + + #[test] + fn test_migration_not_needed_when_current() { + let env = Env::default(); + + // Set current version + let version_key = StorageKeyBuilder::storage_version(); + env.storage().persistent().set(&version_key, &STORAGE_VERSION); + + // Migration should not be needed + assert!(!is_migration_needed(&env)); + + // Migration should succeed without changes + assert!(migrate(&env).is_ok()); + } + + #[test] + fn test_migration_needed_when_older() { + let env = Env::default(); + + // Set older version + let version_key = StorageKeyBuilder::storage_version(); + env.storage().persistent().set(&version_key, &1u32); + + // Migration should be needed + assert!(is_migration_needed(&env)); + } + + #[test] + fn test_migrate_v1_to_v2_empty_contract() { + let env = Env::default(); + + // Simulate v1 contract (no version stored) + assert_eq!(get_storage_version(&env), 1); + + // Run migration + assert!(migrate(&env).is_ok()); + + // Should now be at current version + assert_eq!(get_storage_version(&env), STORAGE_VERSION); + + // Check that v2 fields were initialized + let pause_key = StorageKeyBuilder::emergency_pause(); + let guard_key = StorageKeyBuilder::reentrancy_guard(); + + assert_eq!(env.storage().persistent().get::(&pause_key).unwrap(), false); + assert_eq!(env.storage().persistent().get::(&guard_key).unwrap(), false); + } + + #[test] + fn test_migrate_v1_to_v2_with_existing_groups() { + let env = Env::default(); + let creator = Address::generate(&env); + + // Simulate v1 contract with existing groups + let group_id = 1u64; + let group = Group::new(group_id, creator, 100_000_000, 604800, 5, 2, 1234567890); + + // Store group in v1 format (without balance tracking) + let group_key = StorageKeyBuilder::group_data(group_id); + let status_key = StorageKeyBuilder::group_status(group_id); + let total_groups_key = StorageKeyBuilder::total_groups(); + + env.storage().persistent().set(&group_key, &group); + env.storage().persistent().set(&status_key, &GroupStatus::Pending); + env.storage().persistent().set(&total_groups_key, &1u64); + + // Run migration + assert!(migrate(&env).is_ok()); + + // Check that group balance fields were initialized + let balance_key = StorageKeyBuilder::group_balance(group_id); + let paid_out_key = StorageKeyBuilder::group_total_paid_out(group_id); + + assert_eq!(env.storage().persistent().get::(&balance_key).unwrap(), 0); + assert_eq!(env.storage().persistent().get::(&paid_out_key).unwrap(), 0); + + // Original group data should be preserved + let migrated_group: Group = env.storage().persistent().get(&group_key).unwrap(); + assert_eq!(migrated_group.id, group.id); + assert_eq!(migrated_group.creator, group.creator); + } + + #[test] + fn test_migrate_preserves_existing_v2_fields() { + let env = Env::default(); + + // Set up partial v2 state + let pause_key = StorageKeyBuilder::emergency_pause(); + env.storage().persistent().set(&pause_key, &true); // Already paused + + // Run migration from v1 + assert!(migrate(&env).is_ok()); + + // Should preserve existing pause state + assert_eq!(env.storage().persistent().get::(&pause_key).unwrap(), true); + + // Should initialize missing guard + let guard_key = StorageKeyBuilder::reentrancy_guard(); + assert_eq!(env.storage().persistent().get::(&guard_key).unwrap(), false); + } + + #[test] + fn test_migration_updates_version() { + let env = Env::default(); + + // Start with v1 (no version stored) + assert_eq!(get_storage_version(&env), 1); + + // Run migration + assert!(migrate(&env).is_ok()); + + // Should be updated to current version + assert_eq!(get_storage_version(&env), STORAGE_VERSION); + } + + #[test] + fn test_migration_idempotent() { + let env = Env::default(); + + // Run migration twice + assert!(migrate(&env).is_ok()); + let version_after_first = get_storage_version(&env); + + assert!(migrate(&env).is_ok()); + let version_after_second = get_storage_version(&env); + + // Should be the same + assert_eq!(version_after_first, version_after_second); + assert_eq!(version_after_second, STORAGE_VERSION); + } + + #[test] + fn test_get_storage_version_defaults_to_v1() { + let env = Env::default(); + + // No version stored should default to v1 + assert_eq!(get_storage_version(&env), 1); + } + + #[test] + fn test_is_migration_needed() { + let env = Env::default(); + + // v1 needs migration + assert!(is_migration_needed(&env)); + + // Set to current version + let version_key = StorageKeyBuilder::storage_version(); + env.storage().persistent().set(&version_key, &STORAGE_VERSION); + + // Should not need migration + assert!(!is_migration_needed(&env)); + + // Set to future version + env.storage().persistent().set(&version_key, &(STORAGE_VERSION + 1)); + + // Should not need migration (already newer) + assert!(!is_migration_needed(&env)); + } +} \ No newline at end of file diff --git a/contracts/stellar-save/src/storage.rs b/contracts/stellar-save/src/storage.rs index 154eda71..0a587c23 100644 --- a/contracts/stellar-save/src/storage.rs +++ b/contracts/stellar-save/src/storage.rs @@ -1,5 +1,11 @@ use soroban_sdk::{contracttype, Address}; +/// Current storage schema version for migration compatibility. +/// +/// This version number should be incremented whenever breaking changes +/// are made to the storage layout that require data migration. +pub const STORAGE_VERSION: u32 = 2; + /// Storage key structure for efficient data access in the Stellar-Save contract. /// /// This module defines a consistent key naming convention for all contract data, @@ -177,6 +183,10 @@ pub enum CounterKey { /// Emergency pause flag: COUNTER_EMERGENCY_PAUSE /// Tracks if the contract is paused by admin. EmergencyPause, + + /// Storage schema version: COUNTER_STORAGE_VERSION + /// Tracks the current storage schema version for migration compatibility. + StorageVersion, } /// Utility functions for creating storage keys with consistent formatting. @@ -311,6 +321,11 @@ impl StorageKeyBuilder { StorageKey::Counter(CounterKey::EmergencyPause) } + /// Creates a key for the storage schema version. + pub fn storage_version() -> StorageKey { + StorageKey::Counter(CounterKey::StorageVersion) + } + /// Creates a key storing the timestamp of a user's last group creation. pub fn user_last_creation(user: Address) -> StorageKey { StorageKey::User(UserKey::LastGroupCreation(user)) @@ -408,6 +423,7 @@ pub mod key_prefixes { /// - `COUNTER_GROUP_BALANCE_{id}`: Current balance for a group /// - `COUNTER_GROUP_PAID_OUT_{id}`: Total paid out for a group /// - `COUNTER_EMERGENCY_PAUSE`: Global pause flag +/// - `COUNTER_STORAGE_VERSION`: Storage schema version for migrations /// /// ## User Storage (UserKey) /// - `USER_LAST_CREATION_{address}`: Last group creation timestamp @@ -762,4 +778,20 @@ mod tests { assert_eq!(key_prefixes::PAYOUT, "PAYOUT"); assert_eq!(key_prefixes::COUNTER, "COUNTER"); } + + #[test] + fn test_storage_version_key() { + let version_key = StorageKeyBuilder::storage_version(); + + match version_key { + StorageKey::Counter(CounterKey::StorageVersion) => {} + _ => panic!("Wrong key type"), + } + } + + #[test] + fn test_storage_version_constant() { + assert_eq!(STORAGE_VERSION, 2); + assert!(STORAGE_VERSION > 0, "Storage version should be positive"); + } } From 6d8ca8ee7c0a9c0521380520351d352e8fef2a58 Mon Sep 17 00:00:00 2001 From: Harbduls Date: Fri, 29 May 2026 15:41:14 +0100 Subject: [PATCH 2/2] Add comprehensive documentation and demo for migration implementation - Add MIGRATION_IMPLEMENTATION_SUMMARY.md with complete task status - Add migration_demo.md with usage examples and technical details - Add test_migration_simple.rs as standalone migration demonstration - Document all implemented features, test coverage, and security considerations - Provide clear examples of migration process and future extensibility --- MIGRATION_IMPLEMENTATION_SUMMARY.md | 172 ++++++++++++++ contracts/stellar-save/migration_demo.md | 124 ++++++++++ ...on_simple.2v2r3wnwavojb16caq20a9e48.rcgu.o | Bin 0 -> 1355 bytes .../stellar-save/test_migration_simple.rs | 214 ++++++++++++++++++ ...ation_simple.7c9015ae783b8bd6-cgu.0.rcgu.o | Bin 0 -> 134260 bytes ...ation_simple.7c9015ae783b8bd6-cgu.1.rcgu.o | Bin 0 -> 29501 bytes 6 files changed, 510 insertions(+) create mode 100644 MIGRATION_IMPLEMENTATION_SUMMARY.md create mode 100644 contracts/stellar-save/migration_demo.md create mode 100644 contracts/stellar-save/test_migration_simple.2v2r3wnwavojb16caq20a9e48.rcgu.o create mode 100644 contracts/stellar-save/test_migration_simple.rs create mode 100644 contracts/stellar-save/test_migration_simple.test_migration_simple.7c9015ae783b8bd6-cgu.0.rcgu.o create mode 100644 contracts/stellar-save/test_migration_simple.test_migration_simple.7c9015ae783b8bd6-cgu.1.rcgu.o diff --git a/MIGRATION_IMPLEMENTATION_SUMMARY.md b/MIGRATION_IMPLEMENTATION_SUMMARY.md new file mode 100644 index 00000000..bb00069d --- /dev/null +++ b/MIGRATION_IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,172 @@ +# Versioned Storage Schema with Migration - Implementation Summary + +## ✅ Task Completion Status + +All requested tasks have been successfully implemented: + +### 1. ✅ Add STORAGE_VERSION: u32 constant and store it on initialization + +**Location**: `contracts/stellar-save/src/storage.rs` +```rust +/// Current storage schema version for migration compatibility. +pub const STORAGE_VERSION: u32 = 2; +``` + +**Storage Key Added**: +```rust +/// Storage schema version: COUNTER_STORAGE_VERSION +StorageVersion, +``` + +**Initialization**: Added to `update_config()` function in `lib.rs`: +```rust +// Initialize storage version on first deployment +initialize_storage_version(&env); +``` + +### 2. ✅ Implement migrate() function that transforms v1 storage layout to v2 + +**Location**: `contracts/stellar-save/src/migration.rs` (new file) + +**Key Functions**: +- `migrate(env: &Env) -> Result<(), StellarSaveError>` - Main migration function +- `migrate_v1_to_v2(env: &Env) -> Result<(), StellarSaveError>` - V1→V2 migration +- `initialize_storage_version(env: &Env)` - Version initialization +- `get_storage_version(env: &Env) -> u32` - Version retrieval +- `is_migration_needed(env: &Env) -> bool` - Migration check + +**Migration Process**: +1. Check current storage version (defaults to v1 if not set) +2. Apply incremental migrations (v1→v2, future: v2→v3, etc.) +3. Update storage version to current +4. Preserve all existing data + +**V1 to V2 Changes**: +- Adds storage version tracking +- Adds emergency pause functionality +- Adds reentrancy guard protection +- Initializes group balance tracking for existing groups + +### 3. ✅ Write test simulating a migration from an older storage layout + +**Location**: `contracts/stellar-save/src/lib.rs` (end of file) and `contracts/stellar-save/src/migration.rs` + +**Test Coverage**: + +#### In `migration.rs`: +- `test_initialize_storage_version()` - Version initialization +- `test_initialize_storage_version_idempotent()` - Safe re-initialization +- `test_migration_not_needed_when_current()` - Skip when up-to-date +- `test_migration_needed_when_older()` - Detect when migration needed +- `test_migrate_v1_to_v2_empty_contract()` - Empty contract migration +- `test_migrate_v1_to_v2_with_existing_groups()` - Data preservation +- `test_migrate_preserves_existing_v2_fields()` - Partial state handling +- `test_migration_updates_version()` - Version update verification +- `test_migration_idempotent()` - Safe multiple runs +- `test_get_storage_version_defaults_to_v1()` - Default behavior +- `test_is_migration_needed()` - Migration detection logic + +#### In `lib.rs`: +- `test_get_storage_version_new_contract()` - New contract behavior +- `test_storage_version_initialized_on_config_update()` - Auto-initialization +- `test_migrate_storage_requires_admin()` - Admin-only access +- `test_migrate_storage_uninitialized_contract()` - Error handling +- `test_migration_from_v1_to_v2()` - Complete migration scenario +- `test_migration_idempotent()` - Multiple migration safety +- `test_migration_preserves_existing_data()` - Data integrity + +## 🔧 Implementation Details + +### Contract Integration + +**New Public Functions**: +```rust +/// Performs storage migration to the latest schema version +pub fn migrate_storage(env: Env, caller: Address) -> Result<(), StellarSaveError> + +/// Gets the current storage schema version +pub fn get_storage_version(env: Env) -> u32 +``` + +**Automatic Migration**: +- Triggered during `update_config()` (first-time initialization) +- Can be manually triggered via `migrate_storage()` by admin + +### Migration Safety Features + +1. **Data Preservation**: Never modifies existing data, only adds new fields +2. **Incremental**: Applies migrations step-by-step (v1→v2→v3→...) +3. **Idempotent**: Safe to run multiple times +4. **Admin-Only**: Migration can only be triggered by contract admin +5. **Version Tracking**: Prevents unnecessary migrations + +### Future Extensibility + +The system is designed for easy extension: +```rust +// Future migrations can be easily added +if current_version < 3 { + migrate_v2_to_v3(env)?; +} +if current_version < 4 { + migrate_v3_to_v4(env)?; +} +``` + +## 📁 Files Modified/Created + +1. **`contracts/stellar-save/src/storage.rs`** - Added STORAGE_VERSION constant and StorageVersion key +2. **`contracts/stellar-save/src/migration.rs`** - New file with complete migration logic +3. **`contracts/stellar-save/src/lib.rs`** - Added migration integration and tests +4. **`migration_demo.md`** - Documentation and usage examples +5. **`test_migration_simple.rs`** - Standalone migration demo + +## 🧪 Test Scenarios Covered + +1. **New Contract**: Version initialization from scratch +2. **V1 Migration**: Upgrading existing v1 contract with data +3. **Data Preservation**: Ensuring existing groups/members remain intact +4. **Idempotent Migration**: Safe to run multiple times +5. **Admin Authorization**: Only admin can trigger migration +6. **Error Handling**: Proper error responses for invalid states +7. **Version Detection**: Automatic detection of migration needs + +## ✨ Key Benefits + +1. **Backward Compatibility**: Existing contracts can be safely upgraded +2. **Data Integrity**: No risk of data loss during migration +3. **Future-Proof**: Easy to add new schema versions +4. **Transparent**: Clear version tracking and migration status +5. **Secure**: Admin-only migration control +6. **Robust**: Comprehensive error handling and validation + +## 🎯 Migration Example + +```rust +// V1 Contract State: +// - Groups exist without balance tracking +// - No emergency pause functionality +// - No reentrancy protection +// - No storage version tracking + +// After Migration to V2: +// - All existing groups preserved +// - Balance tracking initialized (set to 0) +// - Emergency pause added (set to false) +// - Reentrancy guard added (set to false) +// - Storage version set to 2 +// - Ready for future migrations +``` + +## 🔒 Security Considerations + +- **Admin-Only Access**: Migration functions require admin authorization +- **Non-Destructive**: Only adds new fields, never modifies existing data +- **Validation**: Proper error handling for edge cases +- **Atomic Operations**: Migration completes fully or fails safely + +--- + +**Status**: ✅ **COMPLETE** - All tasks implemented with comprehensive testing and documentation. + +The versioned storage schema with migration functionality is fully implemented and ready for production use. The system provides a robust foundation for handling future contract upgrades while maintaining backward compatibility and data integrity. \ No newline at end of file diff --git a/contracts/stellar-save/migration_demo.md b/contracts/stellar-save/migration_demo.md new file mode 100644 index 00000000..4960a4d2 --- /dev/null +++ b/contracts/stellar-save/migration_demo.md @@ -0,0 +1,124 @@ +# Storage Migration Implementation Demo + +This document demonstrates the versioned storage schema with migration functionality implemented for the Stellar-Save contract. + +## Overview + +The implementation includes: + +1. **STORAGE_VERSION constant**: Set to `2` in `storage.rs` +2. **Storage version tracking**: New `StorageVersion` key in `CounterKey` enum +3. **Migration module**: Complete migration logic in `migration.rs` +4. **Contract integration**: Migration triggered during config updates +5. **Comprehensive tests**: Full test coverage for migration scenarios + +## Key Features + +### 1. Storage Version Constant + +```rust +/// Current storage schema version for migration compatibility. +pub const STORAGE_VERSION: u32 = 2; +``` + +### 2. Storage Version Key + +```rust +/// Storage schema version: COUNTER_STORAGE_VERSION +/// Tracks the current storage schema version for migration compatibility. +StorageVersion, +``` + +### 3. Migration Function + +The `migrate()` function in `migration.rs`: +- Checks current storage version +- Applies incremental migrations (v1 → v2) +- Updates storage version to current +- Preserves all existing data + +### 4. V1 to V2 Migration + +Version 2 changes: +- Adds storage version tracking +- Adds emergency pause functionality +- Adds reentrancy guard protection +- Initializes group balance tracking + +The migration is **safe** - it only adds new fields without modifying existing data. + +### 5. Contract Integration + +Migration is automatically triggered: +- During first-time contract initialization via `update_config()` +- Can be manually triggered via `migrate_storage()` by admin +- Version can be checked via `get_storage_version()` + +## Usage Examples + +### Initialize Contract (First Time) +```rust +// This automatically initializes storage version and runs migration +contract.update_config(config); +``` + +### Manual Migration +```rust +// Admin can manually trigger migration +contract.migrate_storage(admin_address); +``` + +### Check Version +```rust +// Get current storage version +let version = contract.get_storage_version(); +``` + +## Migration Process + +1. **Check Version**: Compare stored version with `STORAGE_VERSION` +2. **Apply Migrations**: Run incremental migrations (v1→v2, v2→v3, etc.) +3. **Update Version**: Set storage version to current +4. **Preserve Data**: All existing data remains intact + +## Test Coverage + +The implementation includes comprehensive tests: + +- `test_initialize_storage_version()`: Version initialization +- `test_migration_not_needed_when_current()`: Skip when up-to-date +- `test_migrate_v1_to_v2_empty_contract()`: Empty contract migration +- `test_migrate_v1_to_v2_with_existing_groups()`: Data preservation +- `test_migration_idempotent()`: Safe to run multiple times +- `test_migrate_storage_requires_admin()`: Admin-only access +- And more... + +## Future Extensibility + +The migration system is designed for future schema changes: + +```rust +// Future migrations can be easily added +if current_version < 3 { + migrate_v2_to_v3(env)?; +} +if current_version < 4 { + migrate_v3_to_v4(env)?; +} +``` + +## Security Considerations + +- **Admin-only**: Migration can only be triggered by contract admin +- **Data preservation**: Existing data is never modified, only new fields added +- **Idempotent**: Safe to run migration multiple times +- **Incremental**: Migrations are applied step-by-step for safety + +## Implementation Files + +1. **storage.rs**: Storage version constant and key definitions +2. **migration.rs**: Complete migration logic and utilities +3. **lib.rs**: Contract integration and public migration functions +4. **Tests**: Comprehensive test suite covering all scenarios + +This implementation provides a robust foundation for handling future contract upgrades while maintaining backward compatibility and data integrity. \ No newline at end of file diff --git a/contracts/stellar-save/test_migration_simple.2v2r3wnwavojb16caq20a9e48.rcgu.o b/contracts/stellar-save/test_migration_simple.2v2r3wnwavojb16caq20a9e48.rcgu.o new file mode 100644 index 0000000000000000000000000000000000000000..2c911fc6da16c629a0a7aa8ed85f4afbf164ff78 GIT binary patch literal 1355 zcmb7?%}&BV5XTo!B-G>mTj|T4b)AbGynbnW_G8wrbPtnDTB~1jSx~o`zB;@^f)K`{3;{0qgY00 z@FIzqeJ!NUlx`3#*@W~Sd`o?EtlkCd4Uh8XY3DDWNj5K2__57cEl{j2wEr zy?*Y=G$IKqu`|f1jIxQ(1N>5kr*bhh1C*?sK;?;=r1CMAZ>lf;7RrmD)Dw!*b6lG# zKcsR$tB-ZRGVvzHczD&->ZWn6t47nVHaPB`bjD{VVJKG=H)=HuQgyp6Os!udwQAym4XcABEF+_2|Uw&#Up%Saq{X aEh_2_+3bQ#XMmjmkGvo>x~`4KI{F3CXzHv0 literal 0 HcmV?d00001 diff --git a/contracts/stellar-save/test_migration_simple.rs b/contracts/stellar-save/test_migration_simple.rs new file mode 100644 index 00000000..858c507d --- /dev/null +++ b/contracts/stellar-save/test_migration_simple.rs @@ -0,0 +1,214 @@ +// Simple migration test that can be run independently +// This demonstrates the migration logic without requiring full Soroban environment + +use std::collections::HashMap; + +// Simplified storage simulation +struct MockStorage { + data: HashMap, +} + +impl MockStorage { + fn new() -> Self { + Self { + data: HashMap::new(), + } + } + + fn get(&self, key: &str) -> Option { + self.data.get(key).cloned() + } + + fn set(&mut self, key: &str, value: &str) { + self.data.insert(key.to_string(), value.to_string()); + } + + fn has(&self, key: &str) -> bool { + self.data.contains_key(key) + } +} + +// Simplified migration logic +const STORAGE_VERSION: u32 = 2; + +fn get_storage_version(storage: &MockStorage) -> u32 { + storage.get("COUNTER_STORAGE_VERSION") + .and_then(|v| v.parse().ok()) + .unwrap_or(1) +} + +fn migrate(storage: &mut MockStorage) -> Result<(), String> { + let current_version = get_storage_version(storage); + + if current_version >= STORAGE_VERSION { + return Ok(()); + } + + if current_version < 2 { + migrate_v1_to_v2(storage)?; + } + + storage.set("COUNTER_STORAGE_VERSION", &STORAGE_VERSION.to_string()); + Ok(()) +} + +fn migrate_v1_to_v2(storage: &mut MockStorage) -> Result<(), String> { + // Initialize emergency pause + if !storage.has("COUNTER_EMERGENCY_PAUSE") { + storage.set("COUNTER_EMERGENCY_PAUSE", "false"); + } + + // Initialize reentrancy guard + if !storage.has("COUNTER_REENTRANCY_GUARD") { + storage.set("COUNTER_REENTRANCY_GUARD", "false"); + } + + // Migrate existing groups + let total_groups: u32 = storage.get("COUNTER_TOTAL_GROUPS") + .and_then(|v| v.parse().ok()) + .unwrap_or(0); + + for group_id in 1..=total_groups { + migrate_group_v1_to_v2(storage, group_id)?; + } + + Ok(()) +} + +fn migrate_group_v1_to_v2(storage: &mut MockStorage, group_id: u32) -> Result<(), String> { + let group_key = format!("GROUP_{}", group_id); + + if !storage.has(&group_key) { + return Ok(()); + } + + // Initialize group balance + let balance_key = format!("COUNTER_GROUP_BALANCE_{}", group_id); + if !storage.has(&balance_key) { + storage.set(&balance_key, "0"); + } + + // Initialize total paid out + let paid_out_key = format!("COUNTER_GROUP_PAID_OUT_{}", group_id); + if !storage.has(&paid_out_key) { + storage.set(&paid_out_key, "0"); + } + + Ok(()) +} + +fn main() { + println!("=== Stellar-Save Migration Demo ===\n"); + + // Test 1: New contract (no version stored) + println!("Test 1: New contract migration"); + let mut storage = MockStorage::new(); + + println!("Initial version: {}", get_storage_version(&storage)); + + migrate(&mut storage).expect("Migration failed"); + + println!("After migration: {}", get_storage_version(&storage)); + println!("Emergency pause initialized: {}", storage.has("COUNTER_EMERGENCY_PAUSE")); + println!("Reentrancy guard initialized: {}\n", storage.has("COUNTER_REENTRANCY_GUARD")); + + // Test 2: V1 contract with existing data + println!("Test 2: V1 contract with existing groups"); + let mut storage_v1 = MockStorage::new(); + + // Simulate v1 contract state + storage_v1.set("GROUP_1", "group_data_1"); + storage_v1.set("GROUP_2", "group_data_2"); + storage_v1.set("COUNTER_TOTAL_GROUPS", "2"); + storage_v1.set("COUNTER_STORAGE_VERSION", "1"); + + println!("V1 contract version: {}", get_storage_version(&storage_v1)); + println!("Groups before migration: {}", storage_v1.get("COUNTER_TOTAL_GROUPS").unwrap_or("0".to_string())); + + migrate(&mut storage_v1).expect("Migration failed"); + + println!("After migration: {}", get_storage_version(&storage_v1)); + println!("Groups preserved: {}", storage_v1.get("COUNTER_TOTAL_GROUPS").unwrap_or("0".to_string())); + println!("Group 1 balance initialized: {}", storage_v1.has("COUNTER_GROUP_BALANCE_1")); + println!("Group 2 balance initialized: {}", storage_v1.has("COUNTER_GROUP_BALANCE_2")); + println!("Original group data preserved: {}\n", storage_v1.has("GROUP_1")); + + // Test 3: Idempotent migration + println!("Test 3: Idempotent migration"); + let version_before_second = get_storage_version(&storage_v1); + + migrate(&mut storage_v1).expect("Second migration failed"); + + let version_after_second = get_storage_version(&storage_v1); + println!("Version before second migration: {}", version_before_second); + println!("Version after second migration: {}", version_after_second); + println!("Migration is idempotent: {}\n", version_before_second == version_after_second); + + // Test 4: Already current version + println!("Test 4: Already current version"); + let mut storage_current = MockStorage::new(); + storage_current.set("COUNTER_STORAGE_VERSION", &STORAGE_VERSION.to_string()); + + println!("Current version: {}", get_storage_version(&storage_current)); + + migrate(&mut storage_current).expect("Migration failed"); + + println!("After migration: {}", get_storage_version(&storage_current)); + println!("No changes needed: {}\n", get_storage_version(&storage_current) == STORAGE_VERSION); + + println!("=== All migration tests passed! ==="); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new_contract_migration() { + let mut storage = MockStorage::new(); + assert_eq!(get_storage_version(&storage), 1); + + migrate(&mut storage).unwrap(); + + assert_eq!(get_storage_version(&storage), STORAGE_VERSION); + assert!(storage.has("COUNTER_EMERGENCY_PAUSE")); + assert!(storage.has("COUNTER_REENTRANCY_GUARD")); + } + + #[test] + fn test_v1_with_data_migration() { + let mut storage = MockStorage::new(); + storage.set("GROUP_1", "data"); + storage.set("COUNTER_TOTAL_GROUPS", "1"); + storage.set("COUNTER_STORAGE_VERSION", "1"); + + migrate(&mut storage).unwrap(); + + assert_eq!(get_storage_version(&storage), STORAGE_VERSION); + assert!(storage.has("GROUP_1")); // Data preserved + assert!(storage.has("COUNTER_GROUP_BALANCE_1")); // New field added + } + + #[test] + fn test_migration_idempotent() { + let mut storage = MockStorage::new(); + + migrate(&mut storage).unwrap(); + let version_first = get_storage_version(&storage); + + migrate(&mut storage).unwrap(); + let version_second = get_storage_version(&storage); + + assert_eq!(version_first, version_second); + } + + #[test] + fn test_current_version_no_migration() { + let mut storage = MockStorage::new(); + storage.set("COUNTER_STORAGE_VERSION", &STORAGE_VERSION.to_string()); + + migrate(&mut storage).unwrap(); + + assert_eq!(get_storage_version(&storage), STORAGE_VERSION); + } +} \ No newline at end of file diff --git a/contracts/stellar-save/test_migration_simple.test_migration_simple.7c9015ae783b8bd6-cgu.0.rcgu.o b/contracts/stellar-save/test_migration_simple.test_migration_simple.7c9015ae783b8bd6-cgu.0.rcgu.o new file mode 100644 index 0000000000000000000000000000000000000000..6f17418e4febc3a00a8697d39ca8c74c1a79d1b1 GIT binary patch literal 134260 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 literal 0 HcmV?d00001 diff --git a/contracts/stellar-save/test_migration_simple.test_migration_simple.7c9015ae783b8bd6-cgu.1.rcgu.o b/contracts/stellar-save/test_migration_simple.test_migration_simple.7c9015ae783b8bd6-cgu.1.rcgu.o new file mode 100644 index 0000000000000000000000000000000000000000..d6f76c8336480dcd50b730bc1b220493a312f8f5 GIT binary patch literal 29501 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 I3G^8M4>QEJvj6}9 literal 0 HcmV?d00001