Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions contracts/settlement/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ fn require_admin(env: &Env, caller: &Address) {
}
}

/// Retrieve the USDC token address from instance storage, panicking with
/// [`SettlementError::UsdcTokenNotConfigured`] if it has not been set.
fn get_usdc(env: &Env) -> Address {
env.storage()
.instance()
.get(&StorageKey::Usdc)
.unwrap_or_else(|| env.panic_with_error(SettlementError::UsdcTokenNotConfigured))
}

pub(crate) fn propose_balance_migration(env: &Env, caller: &Address, from: &Address, to: &Address) {
require_admin(env, caller);
if from == to {
Expand All @@ -23,10 +32,13 @@ pub(crate) fn propose_balance_migration(env: &Env, caller: &Address, from: &Addr
env.panic_with_error(SettlementError::InvalidMigrationTarget);
}

let usdc_token = get_usdc(env);

// Read the source developer's V2 per-token balance.
let amount: i128 = env
.storage()
.persistent()
.get(&StorageKey::DeveloperBalance(from.clone()))
.get(&StorageKey::DeveloperBalance(from.clone(), usdc_token.clone()))
.unwrap_or(0);
if amount <= 0 {
env.panic_with_error(SettlementError::NoDeveloperBalance);
Expand Down Expand Up @@ -60,26 +72,28 @@ pub(crate) fn execute_balance_migration(env: &Env, caller: &Address, from: &Addr
env.panic_with_error(SettlementError::TimelockNotExpired);
}

let usdc_token = get_usdc(env);
let source_key = StorageKey::DeveloperBalance(from.clone(), usdc_token.clone());
let destination_key = StorageKey::DeveloperBalance(migration.to.clone(), usdc_token.clone());

let source_balance: i128 = env
.storage()
.persistent()
.get(&StorageKey::DeveloperBalance(from.clone()))
.get(&source_key)
.unwrap_or(0);
let new_source_balance = source_balance
.checked_sub(migration.amount)
.filter(|balance| *balance >= 0)
.filter(|b| *b >= 0)
.unwrap_or_else(|| env.panic_with_error(SettlementError::MigrationBalanceChanged));
let destination_balance: i128 = env
.storage()
.persistent()
.get(&StorageKey::DeveloperBalance(migration.to.clone()))
.get(&destination_key)
.unwrap_or(0);
let new_destination_balance = destination_balance
.checked_add(migration.amount)
.unwrap_or_else(|| env.panic_with_error(SettlementError::DeveloperOverflow));

let source_key = StorageKey::DeveloperBalance(from.clone());
let destination_key = StorageKey::DeveloperBalance(migration.to.clone());
env.storage()
.persistent()
.set(&source_key, &new_source_balance);
Expand Down
54 changes: 54 additions & 0 deletions contracts/settlement/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1188,9 +1188,63 @@ impl CalloraSettlement {
}
let _ = env; // env available for future use
}

/// One-shot V1 -> V2 storage migration (admin only).
///
/// Converts all `DeveloperBalanceV1(addr)` persistent slots to per-token
/// `DeveloperBalance(addr, usdc_token)` slots in a single transaction.
/// For deployments with more than [`MAX_BATCH_SIZE`] developers use
/// [`migrate_v1_to_v2_page`] to spread the work across multiple ledgers.
///
/// # Access Control
/// Only the current admin may call this function.
///
/// # Idempotency
/// Safe to call multiple times; re-running after `StorageVersion == 2`
/// returns immediately without modifying any state.
///
/// # Panics
/// - [`SettlementError::NotInitialized`] if the contract is not initialised.
/// - [`SettlementError::Unauthorized`] if the caller is not the admin.
/// - [`SettlementError::UsdcTokenNotConfigured`] if USDC is not configured.
pub fn migrate_v1_to_v2(env: Env, caller: Address) {
migrate::migrate_v1_to_v2(&env, &caller);
}

/// Paginated V1 -> V2 storage migration (admin only).
///
/// Processes up to `batch_size` (capped at [`MAX_BATCH_SIZE`]) developer
/// accounts per call, starting from index position `offset`.
///
/// # Returns
/// `(next_offset, is_complete)`. When `is_complete` is `true` all developer
/// slots have been converted and `StorageVersion` is set to `2`.
///
/// # Access Control
/// Only the current admin may call this function.
///
/// # Idempotency
/// Returns `(0, true)` immediately when migration is already complete.
pub fn migrate_v1_to_v2_page(
env: Env,
caller: Address,
offset: u32,
batch_size: u32,
) -> (u32, bool) {
migrate::migrate_v1_to_v2_page(&env, &caller, offset, batch_size)
}

/// Return the current storage-layout version.
///
/// `1` = V1 layout (pre-migration or key absent).
/// `2` = V2 per-token layout (migration complete).
pub fn migration_storage_version(env: Env) -> u32 {
migrate::storage_version(&env)
}
}

mod events;
pub mod migrate;

#[cfg(test)]
mod test;
Expand Down
Loading
Loading