diff --git a/contracts/revenue_pool/src/lib.rs b/contracts/revenue_pool/src/lib.rs index 598cd9c3..623719e3 100644 --- a/contracts/revenue_pool/src/lib.rs +++ b/contracts/revenue_pool/src/lib.rs @@ -1,35 +1,52 @@ #![no_std] +//! Revenue settlement contract: receives USDC from vault deducts and distributes to developers. +//! +//! Flow: vault deduct → vault transfers USDC to this contract → admin calls distribute(to, amount). +//! +//! # Security Assumptions +//! - **Admin Key**: The admin has full control over fund distribution. Must be a secure multisig. +//! - **USDC Asset**: The token address is permanently set on initialization. Must be carefully verified. +//! - **Balances / Griefing**: The contract does not rely on strict balance invariants. External transfers +//! increase balance without breaking logic. +//! +//! For detailed threat models and mitigations, see [`SECURITY.md`](../../SECURITY.md). + use soroban_sdk::{ contract, contracterror, contractimpl, contracttype, token, Address, BytesN, Env, Map, String, Symbol, Vec, }; -/// Revenue settlement contract: receives USDC from vault deducts and distributes to developers. -/// -/// Flow: vault deduct → vault transfers USDC to this contract → admin calls distribute(to, amount). -/// -/// # Security Assumptions -/// - **Admin Key**: The admin has full control over fund distribution. Must be a secure multisig. -/// - **USDC Asset**: The token address is permanently set on initialization. Must be carefully verified. -/// - **Balances / Griefing**: The contract does not rely on strict balance invariants. External transfers -/// increase balance without breaking logic. -/// -/// For detailed threat models and mitigations, see [`SECURITY.md`](../../SECURITY.md). +/// Storage key for the admin address. const ADMIN_KEY: &str = "admin"; +/// Storage key for the pending admin address during admin transfer. const PENDING_ADMIN_KEY: &str = "pending_admin"; +/// Storage key for the pause guardian address. const PAUSE_GUARDIAN_KEY: &str = "pause_guardian"; +/// Storage key for the USDC token contract address. const USDC_KEY: &str = "usdc"; +/// Storage key for the maximum distribute amount limit. const MAX_DISTRIBUTE_KEY: &str = "max_distribute"; +/// Storage key for tracking cumulative yield deposited. const CUMULATIVE_YIELD_DEPOSITED_KEY: &str = "cumulative_yield_deposited"; +/// Error message: distribution amount must be positive. const ERR_AMOUNT_NOT_POSITIVE: &str = "amount must be positive"; +/// Error message: distribution amount exceeds the configured max_distribute limit. const ERR_AMOUNT_EXCEEDS_MAX_DISTRIBUTE: &str = "amount exceeds max_distribute"; +/// Error message: caller is not the admin. const ERR_UNAUTHORIZED: &str = "unauthorized: caller is not admin"; +/// Error message: caller is not admin or pause guardian. const ERR_UNAUTHORIZED_PAUSE: &str = "unauthorized: caller is not admin or pause guardian"; +/// Error message: contract USDC balance is insufficient for the requested operation. const ERR_INSUFFICIENT_BALANCE: &str = "insufficient USDC balance"; +/// Error message: revenue pool has not been initialized. const ERR_NOT_INITIALIZED: &str = "revenue pool not initialized"; +/// Error message: duplicate recipient address in batch distribute. const ERR_DUPLICATE_RECIPIENT: &str = "duplicate recipient in batch"; +/// Storage key for the paused state flag. const PAUSED_KEY: &str = "paused"; +/// Error message: operation rejected because the contract is paused. const ERR_PAUSED: &str = "revenue pool paused"; +/// Storage key for the current contract version hash. const VERSION_KEY: &str = "version"; /// Typed contract errors for the revenue pool. @@ -48,20 +65,25 @@ pub enum RevenuePoolError { BatchTooLarge = 2, } +/// Default maximum distribute amount per call (unlimited). pub const DEFAULT_MAX_DISTRIBUTE: i128 = i128::MAX; /// Maximum number of payments allowed in a single `batch_distribute` call. /// Caps CPU/memory usage well within Soroban resource limits and aligns with /// the vault's `MAX_BATCH_SIZE` for `batch_deduct`. pub const MAX_BATCH_SIZE: u32 = 50; +/// Maximum allowed length (in bytes) for admin broadcast messages. pub const MAX_MESSAGE_LEN: u32 = 256; /// Severity levels for admin broadcast messages. #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub enum Severity { + /// Informational message — no action required. Info, + /// Warning message — potential issue detected. Warn, + /// Critical message — immediate attention required. Crit, } @@ -69,7 +91,9 @@ pub enum Severity { #[contracttype] #[derive(Clone, Debug)] pub struct AdminBroadcast { + /// Severity level of the broadcast. pub severity: Severity, + /// Broadcast message content. pub message: String, } @@ -77,11 +101,17 @@ pub struct AdminBroadcast { #[contracttype] #[derive(Clone, Debug, PartialEq)] pub struct StorageEntryTtl { + /// Storage category name (e.g. "Instance", "Persistent", "Temporary"). pub category: String, + /// Human-readable description of the storage key. pub key_desc: String, + /// Type of storage (e.g. "Instance", "Persistent"). pub storage_type: String, + /// Current remaining TTL in ledgers. pub ttl: u32, + /// Threshold below which TTL bumping triggers. pub threshold: u32, + /// Amount to extend TTL by when bumping. pub bump_amount: u32, } @@ -94,11 +124,14 @@ pub struct StorageEntryTtl { /// - `BUMP_AMOUNT`: Number of ledgers to extend TTL by (10000 ledgers ≈ 16 days) /// - `LIFETIME_THRESHOLD`: Minimum TTL before triggering a bump (1000 ledgers ≈ 1.5 days) pub const BUMP_AMOUNT: u32 = 10000; +/// Lifetime threshold (in ledgers) below which TTL bumping triggers (~1.5 days). pub const LIFETIME_THRESHOLD: u32 = 1000; +/// The Revenue Pool contract implementation. #[contract] pub struct RevenuePool; +/// Contract implementation block for [`RevenuePool`]. #[contractimpl] impl RevenuePool { /// Initialize the revenue pool with an admin and the USDC token address. @@ -534,7 +567,11 @@ impl RevenuePool { } /// Get the current per-leg distribution cap. - /// Defaults to `i128::MAX` when unset. + /// + /// The cap limits the maximum USDC amount that can be distributed in a single + /// [`Self::distribute`] call. Admins raise/lower it via [`Self::set_max_distribute`]. + /// + /// Defaults to `i128::MAX` when unset (effectively no cap). pub fn get_max_distribute(env: Env) -> i128 { env.storage() .instance() diff --git a/contracts/settlement/src/admin.rs b/contracts/settlement/src/admin.rs index c62c0a28..27ddae0b 100644 --- a/contracts/settlement/src/admin.rs +++ b/contracts/settlement/src/admin.rs @@ -38,7 +38,10 @@ pub(crate) fn propose_balance_migration(env: &Env, caller: &Address, from: &Addr let amount: i128 = env .storage() .persistent() - .get(&StorageKey::DeveloperBalance(from.clone(), usdc_token.clone())) + .get(&StorageKey::DeveloperBalance( + from.clone(), + usdc_token.clone(), + )) .unwrap_or(0); if amount <= 0 { env.panic_with_error(SettlementError::NoDeveloperBalance); @@ -76,11 +79,7 @@ pub(crate) fn execute_balance_migration(env: &Env, caller: &Address, from: &Addr 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(&source_key) - .unwrap_or(0); + let source_balance: i128 = env.storage().persistent().get(&source_key).unwrap_or(0); let new_source_balance = source_balance .checked_sub(migration.amount) .filter(|b| *b >= 0) diff --git a/contracts/settlement/src/errors.rs b/contracts/settlement/src/errors.rs index 3d38e507..44b37eba 100644 --- a/contracts/settlement/src/errors.rs +++ b/contracts/settlement/src/errors.rs @@ -57,5 +57,7 @@ pub enum SettlementError { MigrationNotFound = 20, TimelockNotExpired = 21, MigrationBalanceChanged = 22, - MinimumBalanceRequired = 23, + OverDraft = 23, + InvalidClaimWindow = 24, + ClaimWindowClosed = 25, } diff --git a/contracts/settlement/src/events.rs b/contracts/settlement/src/events.rs index f3cad070..45927739 100644 --- a/contracts/settlement/src/events.rs +++ b/contracts/settlement/src/events.rs @@ -209,7 +209,10 @@ mod tests { #[test] fn test_event_admin_broadcast_bytes() { let env = Env::default(); - assert_eq!(event_admin_broadcast(&env), Symbol::new(&env, "admin_broadcast")); + assert_eq!( + event_admin_broadcast(&env), + Symbol::new(&env, "admin_broadcast") + ); } #[test] diff --git a/contracts/settlement/src/lib.rs b/contracts/settlement/src/lib.rs index 8b39bfc5..8c431a64 100644 --- a/contracts/settlement/src/lib.rs +++ b/contracts/settlement/src/lib.rs @@ -10,94 +10,16 @@ pub const MAX_BATCH_SIZE: u32 = 50; /// Maximum number of developer balances returned per page in paginated queries. pub const MAX_DEVELOPER_BALANCES_PAGE_SIZE: u32 = 100; -/// Typed errors for the settlement contract. -/// -/// Using `#[contracterror]` encodes each variant as a stable `u32` code. -/// Callers and indexers can match on the code rather than parsing raw panic strings, -/// and the WASM binary shrinks because no error string literals are embedded. -/// -/// | Code | Variant | When | -/// |------|------------------------------|---------------------------------------------------| -/// | 1 | NotInitialized | A function is called before `init` | -/// | 2 | AlreadyInitialized | `init` is called more than once | -/// | 3 | Unauthorized | Caller is not the vault or admin | -/// | 4 | AmountNotPositive | `amount` is zero or negative | -/// | 5 | DeveloperRequired | `to_pool=false` but no developer address supplied | -/// | 6 | DeveloperMustBeNone | `to_pool=true` but a developer address was given | -/// | 7 | PoolOverflow | Global pool `i128` addition would overflow | -/// | 8 | DeveloperOverflow | Developer balance `i128` addition would overflow | -/// | 9 | UsdcTokenNotConfigured | USDC token address not configured for withdrawals | -/// | 10 | InsufficientDeveloperBalance | Developer balance is less than withdrawal amount | -/// | 11 | DeveloperBalanceUnderflow | Developer balance subtraction would overflow | -/// | 12 | InsufficientContractBalance | Settlement contract lacks on-ledger USDC | -/// | 13 | DailyWithdrawCapExceeded | Developer's daily withdrawal cap would be exceeded| -/// | 14 | GasExhaustionRisk | Index too large for safe full scan; use pagination| -/// | 15 | ReasonTooLong | Reason Symbol exceeds maximum allowed length | -/// | 16 | InvalidClaimWindow | Claim window end is before start | -/// | 17 | ClaimWindowClosed | Claim attempted outside developer's claim window | -#[contracterror] -#[derive(Clone, Copy, Debug, PartialEq)] -#[repr(u32)] -pub enum SettlementError { - NotInitialized = 1, - AlreadyInitialized = 2, - Unauthorized = 3, - AmountNotPositive = 4, - DeveloperRequired = 5, - DeveloperMustBeNone = 6, - PoolOverflow = 7, - DeveloperOverflow = 8, - UsdcTokenNotConfigured = 9, - InsufficientDeveloperBalance = 10, - DeveloperBalanceUnderflow = 11, - InsufficientContractBalance = 12, - DailyWithdrawCapExceeded = 13, - GasExhaustionRisk = 14, - ReasonTooLong = 15, - InvalidClaimWindow = 16, - ClaimWindowClosed = 17, -} +mod admin; +mod errors; +mod limits; +mod pagination; +mod timelock; +mod types; -/// Persistent storage keys for settlement contract -#[contracttype] -#[derive(Clone, Debug, PartialEq)] -pub enum StorageKey { - Admin, - Vault, - PendingAdmin, - PendingVault, - DeveloperIndex, - DeveloperBalance(Address), - GlobalPool, - Usdc, - DailyWithdrawCap(Address), - WithdrawalToday(Address), - DeveloperClaimWindow(Address), - ContractVersion, -} - -/// Developer balance record in settlement contract -#[contracttype] -#[derive(Clone, Debug, PartialEq)] -pub struct DeveloperBalance { - pub address: Address, - pub balance: i128, -} - -/// Global pool balance tracking. -/// -/// `last_updated` is set to `env.ledger().timestamp()` on every -/// `receive_payment` call that credits the pool (`to_pool = true`). -/// It is also set at `init` time. It is **not** updated when payments -/// are routed to individual developer balances. -#[contracttype] -#[derive(Clone, Debug, PartialEq)] -pub struct GlobalPool { - pub total_balance: i128, - /// Ledger timestamp of the last pool credit. Useful for analytics - /// and staleness checks. - pub last_updated: u64, -} +pub use errors::SettlementError; +pub use timelock::PendingDeveloperMigration; +pub use types::*; /// Tracks a developer's cumulative withdrawal amount for a given epoch day. /// @@ -130,6 +52,7 @@ pub struct PaymentReceivedEvent { pub amount: i128, pub to_pool: bool, // true if credited to global pool, false if to specific developer pub developer: Option
, // developer address if credited to specific developer + pub token: Address, } /// Balance credited event @@ -139,6 +62,7 @@ pub struct BalanceCreditedEvent { pub developer: Address, pub amount: i128, pub new_balance: i128, + pub token: Address, } /// Emitted when a new vault address is proposed via `propose_vault()`. @@ -417,12 +341,7 @@ impl CalloraSettlement { env.storage().persistent().set(&balance_key, &new_balance); env.storage() .persistent() - .set(&StorageKey::DeveloperBalance(dev.clone()), &new_balance); - env.storage().persistent().extend_ttl( - &StorageKey::DeveloperBalance(dev.clone()), - 50000, - 50000, - ); + .extend_ttl(&balance_key, 50000, 50000); // Add to index in sorted order if not already present let mut index: Vec
= inst .get(&StorageKey::DeveloperIndex) @@ -588,10 +507,14 @@ impl CalloraSettlement { Self::require_claim_window_open(&env, &developer)?; + let usdc_address = Self::get_usdc_token(env.clone())?; let current_balance: i128 = env .storage() .persistent() - .get(&StorageKey::DeveloperBalance(developer.clone())) + .get(&StorageKey::DeveloperBalance( + developer.clone(), + usdc_address.clone(), + )) .unwrap_or(0); if amount > current_balance { return Err(SettlementError::InsufficientDeveloperBalance); @@ -625,7 +548,6 @@ impl CalloraSettlement { .checked_sub(amount) .ok_or(SettlementError::DeveloperBalanceUnderflow)?; - let usdc_address = Self::get_usdc_token(env.clone())?; let usdc = token::Client::new(&env, &usdc_address); if usdc.balance(&contract_address) < amount { @@ -635,11 +557,11 @@ impl CalloraSettlement { usdc.transfer(&contract_address, &recipient, &amount); env.storage().persistent().set( - &StorageKey::DeveloperBalance(developer.clone()), + &StorageKey::DeveloperBalance(developer.clone(), usdc_address.clone()), &new_balance, ); env.storage().persistent().extend_ttl( - &StorageKey::DeveloperBalance(developer.clone()), + &StorageKey::DeveloperBalance(developer.clone(), usdc_address.clone()), 50000, 50000, ); @@ -902,11 +824,11 @@ impl CalloraSettlement { .unwrap_or_else(|| env.panic_with_error(SettlementError::DeveloperOverflow)); env.storage().persistent().set( - &StorageKey::DeveloperBalance(developer.clone()), + &StorageKey::DeveloperBalance(developer.clone(), token.clone()), &new_balance, ); env.storage().persistent().extend_ttl( - &StorageKey::DeveloperBalance(developer.clone()), + &StorageKey::DeveloperBalance(developer.clone(), token.clone()), 50000, 50000, ); @@ -1124,223 +1046,17 @@ impl CalloraSettlement { .get(&StorageKey::DeveloperIndex) .unwrap_or_else(|| Vec::new(&env)); - pagination::get_page(&env, &index, cursor, limit) + pagination::get_page(&env, &index, cursor, limit, &token) } /// Return the remaining TTL for each storage key category. /// /// # Parameters /// - `developer_addresses` — optional list of developers to check. If empty, the index is used. - pub fn get_storage_ttl(env: Env, developer_addresses: Vec
) -> Vec { - let mut result = Vec::new(&env); - - // 1. Instance Storage - let instance_ttl = { - #[cfg(any(test, feature = "testutils"))] - { - env.storage().instance().get_ttl() - } - #[cfg(not(any(test, feature = "testutils")))] - { - 17_280 * 60 - } - }; - result.push_back(StorageEntryTtl { - category: String::from_str(&env, "Instance"), - key_desc: String::from_str(&env, "Instance"), - storage_type: String::from_str(&env, "Instance"), - ttl: instance_ttl, - threshold: 17_280 * 30, - bump_amount: 17_280 * 60, - }); - - // Determine which developer addresses to inspect - let devs = if developer_addresses.len() > 0 { - developer_addresses - } else { - env.storage() - .instance() - .get(&StorageKey::DeveloperIndex) - .unwrap_or_else(|| Vec::new(&env)) - }; - - for dev in devs.iter() { - // Check DeveloperBalance (Persistent) - let bal_key = StorageKey::DeveloperBalance(dev.clone()); - if env.storage().persistent().has(&bal_key) { - let ttl = { - #[cfg(any(test, feature = "testutils"))] - { - env.storage().persistent().get_ttl(&bal_key) - } - #[cfg(not(any(test, feature = "testutils")))] - { - 50000 - } - }; - result.push_back(StorageEntryTtl { - category: String::from_str(&env, "DeveloperBalance"), - key_desc: String::from_str(&env, "DeveloperBalance"), - storage_type: String::from_str(&env, "Persistent"), - ttl, - threshold: 50000, - bump_amount: 50000, - }); - } - - let balance: i128 = env - .storage() - .persistent() - .get(&StorageKey::DeveloperBalance( - address.clone(), - token.clone(), - )) - .unwrap_or(0i128); - result.push_back(DeveloperBalance { - address: address.clone(), - token: token.clone(), - balance, - }); - last_address = Some(address.clone()); - - // Check DailyWithdrawCap (Persistent) - let cap_key = StorageKey::DailyWithdrawCap(dev.clone()); - if env.storage().persistent().has(&cap_key) { - let ttl = { - #[cfg(any(test, feature = "testutils"))] - { - env.storage().persistent().get_ttl(&cap_key) - } - #[cfg(not(any(test, feature = "testutils")))] - { - 50000 - } - }; - result.push_back(StorageEntryTtl { - category: String::from_str(&env, "DailyWithdrawCap"), - key_desc: String::from_str(&env, "DailyWithdrawCap"), - storage_type: String::from_str(&env, "Persistent"), - ttl, - threshold: 50000, - bump_amount: 50000, - }); - } - } - - result - } - /// Return the remaining TTL for each storage key category. /// /// # Parameters /// - `developer_addresses` — optional list of developers to check. If empty, the index is used. - pub fn get_storage_ttl(env: Env, developer_addresses: Vec
) -> Vec { - let mut result = Vec::new(&env); - - // 1. Instance Storage - let instance_ttl = { - #[cfg(any(test, feature = "testutils"))] - { - env.storage().instance().get_ttl() - } - #[cfg(not(any(test, feature = "testutils")))] - { - 17_280 * 60 - } - }; - result.push_back(StorageEntryTtl { - category: String::from_str(&env, "Instance"), - key_desc: String::from_str(&env, "Instance"), - storage_type: String::from_str(&env, "Instance"), - ttl: instance_ttl, - threshold: 17_280 * 30, - bump_amount: 17_280 * 60, - }); - - // Determine which developer addresses to inspect - let devs = if developer_addresses.len() > 0 { - developer_addresses - } else { - env.storage() - .instance() - .get(&StorageKey::DeveloperIndex) - .unwrap_or_else(|| Vec::new(&env)) - }; - - for dev in devs.iter() { - // Check DeveloperBalance (Persistent) - let bal_key = StorageKey::DeveloperBalance(dev.clone()); - if env.storage().persistent().has(&bal_key) { - let ttl = { - #[cfg(any(test, feature = "testutils"))] - { - env.storage().persistent().get_ttl(&bal_key) - } - #[cfg(not(any(test, feature = "testutils")))] - { - 50000 - } - }; - result.push_back(StorageEntryTtl { - category: String::from_str(&env, "DeveloperBalance"), - key_desc: String::from_str(&env, "DeveloperBalance"), - storage_type: String::from_str(&env, "Persistent"), - ttl, - threshold: 50000, - bump_amount: 50000, - }); - } - - // Check WithdrawalToday (Persistent) - let today_key = StorageKey::WithdrawalToday(dev.clone()); - if env.storage().persistent().has(&today_key) { - let ttl = { - #[cfg(any(test, feature = "testutils"))] - { - env.storage().persistent().get_ttl(&today_key) - } - #[cfg(not(any(test, feature = "testutils")))] - { - 50000 - } - }; - result.push_back(StorageEntryTtl { - category: String::from_str(&env, "WithdrawalToday"), - key_desc: String::from_str(&env, "WithdrawalToday"), - storage_type: String::from_str(&env, "Persistent"), - ttl, - threshold: 50000, - bump_amount: 50000, - }); - } - - // Check DailyWithdrawCap (Persistent) - let cap_key = StorageKey::DailyWithdrawCap(dev.clone()); - if env.storage().persistent().has(&cap_key) { - let ttl = { - #[cfg(any(test, feature = "testutils"))] - { - env.storage().persistent().get_ttl(&cap_key) - } - #[cfg(not(any(test, feature = "testutils")))] - { - 50000 - } - }; - result.push_back(StorageEntryTtl { - category: String::from_str(&env, "DailyWithdrawCap"), - key_desc: String::from_str(&env, "DailyWithdrawCap"), - storage_type: String::from_str(&env, "Persistent"), - ttl, - threshold: 50000, - bump_amount: 50000, - }); - } - } - - result - } - /// Return the pending admin address, or `None` if no two-step admin transfer is in progress. /// /// Integrators can poll this to detect an in-flight admin handover @@ -1576,7 +1292,7 @@ impl CalloraSettlement { /// Only the current admin may call. This will instruct the host to update /// the current contract WASM to `new_wasm_hash` and persist the version marker. /// Emits an `upgraded` event with the admin as topic and the new version as data. - pub fn broadcast(env: Env, caller: Address, severity: Severity, message: String) { + pub fn broadcast(env: Env, caller: Address, severity: Severity, message: soroban_sdk::String) { caller.require_auth(); let admin = Self::get_admin(env.clone()); if caller != admin { @@ -1703,6 +1419,15 @@ impl CalloraSettlement { pub fn migration_storage_version(env: Env) -> u32 { migrate::storage_version(&env) } + + /// Migrate a single developer's V1 balance to V2 (admin only). + pub fn migrate_single_dev_v2( + env: Env, + caller: Address, + developer: Address, + ) -> Result<(), SettlementError> { + migrate::migrate_single_developer(&env, &caller, &developer) + } } mod events; diff --git a/contracts/settlement/src/limits.rs b/contracts/settlement/src/limits.rs index 3f79cd71..54159c1d 100644 --- a/contracts/settlement/src/limits.rs +++ b/contracts/settlement/src/limits.rs @@ -1,8 +1,8 @@ //! Limits module for per-developer minimum balance. -use soroban_sdk::{Env, Address, Symbol, contracterror, contracttype}; use crate::errors::SettlementError; use crate::types::StorageKey; +use soroban_sdk::{contracterror, contracttype, Address, Env, Symbol}; /// Set the minimum balance for a developer. /// @@ -14,7 +14,7 @@ use crate::types::StorageKey; pub fn set_developer_min_balance(env: Env, caller: Address, developer: Address, min_balance: i128) { // Auth check – admin only. caller.require_auth(); - let admin = crate::lib::CalloraSettlement::get_admin(env.clone()); + let admin = crate::CalloraSettlement::get_admin(env.clone()); if caller != admin { env.panic_with_error(SettlementError::Unauthorized); } @@ -22,9 +22,16 @@ pub fn set_developer_min_balance(env: Env, caller: Address, developer: Address, panic!("minimum balance must be non‑negative"); } // Store the value. - env.storage().persistent().set(&StorageKey::DeveloperMinBalance(developer.clone()), &min_balance); + env.storage().persistent().set( + &StorageKey::DeveloperMinBalance(developer.clone()), + &min_balance, + ); // Optional TTL similar to other persistent entries. - env.storage().persistent().extend_ttl(&StorageKey::DeveloperMinBalance(developer), 50000, 50000); + env.storage().persistent().extend_ttl( + &StorageKey::DeveloperMinBalance(developer), + 50000, + 50000, + ); } /// Retrieve the minimum balance for a developer. Returns `0` if not set. diff --git a/contracts/settlement/src/migrate.rs b/contracts/settlement/src/migrate.rs index 5d711672..a50f4344 100644 --- a/contracts/settlement/src/migrate.rs +++ b/contracts/settlement/src/migrate.rs @@ -178,7 +178,11 @@ pub fn migrate_v1_to_v2_page( .unwrap_or_else(|| Vec::new(env)); let total = index.len(); - let effective = if batch_size == 0 { 1 } else { batch_size.min(MAX_BATCH_SIZE) }; + let effective = if batch_size == 0 { + 1 + } else { + batch_size.min(MAX_BATCH_SIZE) + }; let end = offset.saturating_add(effective).min(total); let mut i = 0u32; @@ -226,11 +230,7 @@ fn migrate_developer_slot(env: &Env, addr: &Address, usdc_token: &Address) { let v1_balance: Option = env.storage().persistent().get(&v1_key); if let Some(v1) = v1_balance { let v2_key = StorageKey::DeveloperBalance(addr.clone(), usdc_token.clone()); - let existing_v2: i128 = env - .storage() - .persistent() - .get(&v2_key) - .unwrap_or(0i128); + let existing_v2: i128 = env.storage().persistent().get(&v2_key).unwrap_or(0i128); let merged = v1 .checked_add(existing_v2) .unwrap_or_else(|| env.panic_with_error(SettlementError::DeveloperOverflow)); @@ -242,6 +242,21 @@ fn migrate_developer_slot(env: &Env, addr: &Address, usdc_token: &Address) { } } +/// Migrate a single developer's V1 balance to V2 (admin only). +pub fn migrate_single_developer( + env: &Env, + caller: &Address, + developer: &Address, +) -> Result<(), crate::SettlementError> { + require_admin(env, caller); + let inst = env.storage().instance(); + let usdc_token: Address = inst + .get(&crate::StorageKey::Usdc) + .ok_or(crate::SettlementError::UsdcTokenNotConfigured)?; + migrate_developer_slot(env, developer, &usdc_token); + Ok(()) +} + // ─── Tests ──────────────────────────────────────────────────────────────────── #[cfg(test)] diff --git a/contracts/settlement/src/pagination.rs b/contracts/settlement/src/pagination.rs index b12e01af..7251629d 100644 --- a/contracts/settlement/src/pagination.rs +++ b/contracts/settlement/src/pagination.rs @@ -15,7 +15,7 @@ use soroban_sdk::{Address, Env, Vec}; /// /// # Ordering Guarantees /// The index is maintained in deterministic sorted ascending order by address bytes, guaranteeing -/// stable, deterministic pagination across repeated calls. The output is sorted, meaning pages +/// stable, deterministic pagination across repeated calls. The output is sorted, meaning pages /// are stable even if interleaved credits happen for developers that sort after the cursor. /// /// # Page-size Configuration @@ -23,7 +23,7 @@ use soroban_sdk::{Address, Env, Vec}; /// and prevent transaction size limits from being exceeded. /// /// # Intended Use -/// This function is designed for batch reconciliation, indexing, and reporting dashboards +/// This function is designed for batch reconciliation, indexing, and reporting dashboards /// where developer balances must be safely and incrementally sync'd. /// /// # State Mutation @@ -33,6 +33,7 @@ pub fn get_page( index: &Vec
, cursor: Option
, limit: u32, + token: &Address, ) -> (Vec, Option
) { let effective_limit = if limit == 0 { return (Vec::new(env), None); @@ -57,12 +58,16 @@ pub fn get_page( let balance: i128 = env .storage() .persistent() - .get(&StorageKey::DeveloperBalance(address.clone())) + .get(&StorageKey::DeveloperBalance( + address.clone(), + token.clone(), + )) .unwrap_or(0); result.push_back(DeveloperBalance { address: address.clone(), balance, + token: token.clone(), }); last_address = Some(address.clone()); diff --git a/contracts/settlement/src/test.rs b/contracts/settlement/src/test.rs index 162d3998..960d615f 100644 --- a/contracts/settlement/src/test.rs +++ b/contracts/settlement/src/test.rs @@ -4,7 +4,8 @@ mod settlement_tests { use crate::{CalloraSettlement, CalloraSettlementClient, SettlementError, StorageKey}; use soroban_sdk::testutils::{Address as _, Events as _, Ledger as _}; - use soroban_sdk::{token, Address, BytesN, Env, Error, InvokeError, Symbol, TryFromVal}; + use soroban_sdk::{Address, BytesN, Env, Error, InvokeError, Symbol, TryFromVal}; + use soroban_sdk::token as token_mod; fn setup_contract() -> (Env, Address, Address, Address, Address, Address) { let env = Env::default(); @@ -33,11 +34,11 @@ mod settlement_tests { fn create_usdc<'a>( env: &'a Env, admin: &Address, - ) -> (Address, token::Client<'a>, token::StellarAssetClient<'a>) { + ) -> (Address, token_mod::Client<'a>, token_mod::StellarAssetClient<'a>) { let contract_address = env.register_stellar_asset_contract_v2(admin.clone()); let address = contract_address.address(); - let client = token::Client::new(env, &address); - let admin_client = token::StellarAssetClient::new(env, &address); + let client = token_mod::Client::new(env, &address); + let admin_client = token_mod::StellarAssetClient::new(env, &address); (address, client, admin_client) } @@ -290,18 +291,28 @@ mod settlement_tests { client.init(&admin, &vault); client.set_usdc_token(&admin, &usdc_address); - client.receive_payment(&vault, &100i128, &false, &Some(developer.clone()), &usdc_address); + client.receive_payment( + &vault, + &100i128, + &false, + &Some(developer.clone()), + &usdc_address, + ); usdc_admin_client.mint(&addr, &100i128); - let result = client.try_withdraw_developer_balance(&developer, &100i128, &None, &usdc_address); + let result = + client.try_withdraw_developer_balance(&developer, &100i128, &None); assert!(result.is_ok()); - assert_eq!(client.get_developer_balance(&developer, &usdc_address), 0i128); assert_eq!( - token::Client::new(&env, &usdc_address).balance(&addr), + client.get_developer_balance(&developer, &usdc_address), 0i128 ); assert_eq!( - token::Client::new(&env, &usdc_address).balance(&developer), + token_mod::Client::new(&env, &usdc_address).balance(&addr), + 0i128 + ); + assert_eq!( + token_mod::Client::new(&env, &usdc_address).balance(&developer), 100i128 ); } @@ -319,12 +330,22 @@ mod settlement_tests { client.init(&admin, &vault); client.set_usdc_token(&admin, &usdc_address); - client.receive_payment(&vault, &100i128, &false, &Some(developer.clone()), &usdc_address); + client.receive_payment( + &vault, + &100i128, + &false, + &Some(developer.clone()), + &usdc_address, + ); usdc_admin_client.mint(&addr, &100i128); - let result = client.try_withdraw_developer_balance(&developer, &101i128, &None, &usdc_address); + let result = + client.try_withdraw_developer_balance(&developer, &101i128, &None); assert!(result.is_err()); - assert_eq!(client.get_developer_balance(&developer, &usdc_address), 100i128); + assert_eq!( + client.get_developer_balance(&developer, &usdc_address), + 100i128 + ); } #[test] @@ -339,9 +360,11 @@ mod settlement_tests { let token = Address::generate(&env); client.init(&admin, &vault); + let token = Address::generate(&env); - let zero_result = client.try_withdraw_developer_balance(&developer, &0i128, &None, &token); - let negative_result = client.try_withdraw_developer_balance(&developer, &-1i128, &None, &token); + let zero_result = client.try_withdraw_developer_balance(&developer, &0i128, &None); + let negative_result = + client.try_withdraw_developer_balance(&developer, &-1i128, &None); assert!(zero_result.is_err()); assert!(negative_result.is_err()); @@ -363,10 +386,17 @@ mod settlement_tests { client.init(&admin, &vault); client.set_usdc_token(&admin, &usdc_address); - client.receive_payment(&vault, &200i128, &false, &Some(developer.clone()), &usdc_address); + client.receive_payment( + &vault, + &200i128, + &false, + &Some(developer.clone()), + &usdc_address, + ); usdc_admin_client.mint(&addr, &200i128); - let result = client.try_withdraw_developer_balance(&developer, &200i128, &None, &usdc_address); + let result = + client.try_withdraw_developer_balance(&developer, &200i128, &None); assert!(result.is_ok()); let events = env.events().all(); @@ -404,19 +434,31 @@ mod settlement_tests { client.init(&admin, &vault); client.set_usdc_token(&admin, &usdc_address); - client.receive_payment(&vault, &150i128, &false, &Some(developer.clone()), &usdc_address); + client.receive_payment( + &vault, + &150i128, + &false, + &Some(developer.clone()), + &usdc_address, + ); usdc_admin_client.mint(&addr, &150i128); - let result = - client.try_withdraw_developer_balance(&developer, &150i128, &Some(custodial.clone())); + let result = client.try_withdraw_developer_balance( + &developer, + &150i128, + &Some(custodial.clone()), + ); assert!(result.is_ok()); - assert_eq!(client.get_developer_balance(&developer, &usdc_address), 0i128); assert_eq!( - token::Client::new(&env, &usdc_address).balance(&addr), + client.get_developer_balance(&developer, &usdc_address), 0i128 ); assert_eq!( - token::Client::new(&env, &usdc_address).balance(&custodial), + token_mod::Client::new(&env, &usdc_address).balance(&addr), + 0i128 + ); + assert_eq!( + token_mod::Client::new(&env, &usdc_address).balance(&custodial), 150i128 ); } @@ -438,11 +480,20 @@ mod settlement_tests { client.init(&admin, &vault); client.set_usdc_token(&admin, &usdc_address); - client.receive_payment(&vault, &200i128, &false, &Some(developer.clone()), &usdc_address); + client.receive_payment( + &vault, + &200i128, + &false, + &Some(developer.clone()), + &usdc_address, + ); usdc_admin_client.mint(&addr, &200i128); - let result = - client.try_withdraw_developer_balance(&developer, &200i128, &Some(custodial.clone())); + let result = client.try_withdraw_developer_balance( + &developer, + &200i128, + &Some(custodial.clone()), + ); assert!(result.is_ok()); let events = env.events().all(); @@ -477,10 +528,16 @@ mod settlement_tests { client.init(&admin, &vault); client.set_usdc_token(&admin, &usdc_address); - client.receive_payment(&vault, &100i128, &false, &Some(developer.clone()), &usdc_address); + client.receive_payment( + &vault, + &100i128, + &false, + &Some(developer.clone()), + &usdc_address, + ); usdc_admin_client.mint(&addr, &100i128); - client.withdraw_developer_balance(&developer, &100i128, &Some(addr.clone()), &usdc_address); + client.withdraw_developer_balance(&developer, &100i128, &Some(addr.clone())); } #[test] @@ -530,7 +587,7 @@ mod settlement_tests { client.init(&admin, &vault); let token = Address::generate(&env); - let all = client.get_all_developer_balances(&admin, &token); + let _all = client.get_all_developer_balances(&admin, &token); let env = Env::default(); env.mock_all_auths(); let admin = Address::generate(&env); @@ -977,7 +1034,10 @@ mod settlement_tests { client.accept_admin(); // State preserved - assert_eq!(client.get_developer_balance(&developer, &token), dev_balance_before); + assert_eq!( + client.get_developer_balance(&developer, &token), + dev_balance_before + ); assert_eq!( client.get_global_pool().total_balance, pool_before.total_balance @@ -1290,8 +1350,9 @@ mod settlement_tests { let addr = env.register(CalloraSettlement, ()); let client = CalloraSettlementClient::new(&env, &addr); client.init(&admin, &vault); + let token = Address::generate(&env); - client.receive_payment(&vault, &750i128, &true, &None); + client.receive_payment(&vault, &750i128, &true, &None, &token); let events = env.events().all(); let ev = events @@ -1311,6 +1372,7 @@ mod settlement_tests { amount: 750i128, to_pool: true, developer: None, + token: token.clone(), }; assert_eq!(data, expected); @@ -1331,8 +1393,9 @@ mod settlement_tests { let addr = env.register(CalloraSettlement, ()); let client = CalloraSettlementClient::new(&env, &addr); client.init(&admin, &vault); + let token = Address::generate(&env); - client.receive_payment(&vault, &321i128, &false, &Some(developer.clone())); + client.receive_payment(&vault, &321i128, &false, &Some(developer.clone()), &token); let events = env.events().all(); let ev = events @@ -1352,6 +1415,8 @@ mod settlement_tests { amount: 321i128, to_pool: false, developer: Some(developer.clone()), + + token: token.clone(), }; assert_eq!(data, expected); @@ -1545,8 +1610,20 @@ mod settlement_tests { client.accept_vault(&new_vault); // More payments from new vault - client.receive_payment(&new_vault, &150i128, &false, &Some(developer.clone()), &token); - client.receive_payment(&new_vault, &200i128, &false, &Some(developer.clone()), &token); + client.receive_payment( + &new_vault, + &150i128, + &false, + &Some(developer.clone()), + &token, + ); + client.receive_payment( + &new_vault, + &200i128, + &false, + &Some(developer.clone()), + &token, + ); // Total should accumulate correctly assert_eq!(client.get_developer_balance(&developer, &token), 450i128); @@ -1598,6 +1675,7 @@ mod settlement_tests { env.ledger().set_timestamp(1_000); client.init(&admin, &vault); + let token = Address::generate(&env); assert_eq!(client.get_global_pool().last_updated, 1_000); // Advance time and credit pool � last_updated must change @@ -1630,6 +1708,7 @@ mod settlement_tests { env.ledger().set_timestamp(1_000); client.init(&admin, &vault); + let token = Address::generate(&env); env.ledger().set_timestamp(5_000); client.receive_payment(&vault, &200i128, &false, &Some(developer.clone()), &token); @@ -1909,8 +1988,20 @@ mod settlement_tests { let client = CalloraSettlementClient::new(&env, &addr); let developer = Address::generate(&env); - client.force_credit_developer(&admin, &developer, &500i128, &Symbol::new(&env, "first")); - client.force_credit_developer(&admin, &developer, &300i128, &Symbol::new(&env, "second")); + client.force_credit_developer( + &admin, + &developer, + &500i128, + &token, + &Symbol::new(&env, "first"), + ); + client.force_credit_developer( + &admin, + &developer, + &300i128, + &token, + &Symbol::new(&env, "second"), + ); assert_eq!(client.get_developer_balance(&developer, &token), 800i128); } @@ -1922,7 +2013,8 @@ mod settlement_tests { let developer = Address::generate(&env); let reason = Symbol::new(&env, "unauthorized_test"); - let vault_result = client.try_force_credit_developer(&vault, &developer, &100i128, &reason); + let vault_result = + client.try_force_credit_developer(&vault, &developer, &100i128, &token, &reason); assert!(is_error(vault_result, SettlementError::Unauthorized)); let third_party_result = @@ -2018,7 +2110,7 @@ mod settlement_tests { env.as_contract(&addr, || { env.storage().persistent().set( - &crate::StorageKey::DeveloperBalance(developer.clone()), + &crate::StorageKey::DeveloperBalance(developer.clone(), token.clone()), &i128::MAX, ); }); @@ -2066,7 +2158,13 @@ mod settlement_tests { } else { let dev_idx = (next_rand() % 10) as usize; if let Some(developer) = developers.get(dev_idx) { - client.receive_payment(&vault, &amount, &false, &Some(developer.clone()), &token); + client.receive_payment( + &vault, + &amount, + &false, + &Some(developer.clone()), + &token, + ); } } total_credited += amount; @@ -2086,7 +2184,13 @@ mod settlement_tests { // Large credit to a developer if let Some(developer) = developers.get(0) { - client.receive_payment(&vault, &half_remaining, &false, &Some(developer.clone()), &token); + client.receive_payment( + &vault, + &half_remaining, + &false, + &Some(developer.clone()), + &token, + ); total_credited += half_remaining; } } @@ -2141,18 +2245,32 @@ mod settlement_tests { client.init(&admin, &vault); client.set_usdc_token(&admin, &usdc_address); client.set_daily_withdraw_cap(&admin, &developer, &500i128); - client.receive_payment(&vault, &1000i128, &false, &Some(developer.clone()), &usdc_address); + client.receive_payment( + &vault, + &1000i128, + &false, + &Some(developer.clone()), + &usdc_address, + ); usdc_admin_client.mint(&addr, &1000i128); // First withdrawal of 300 should succeed (under 500 cap) - let result = client.try_withdraw_developer_balance(&developer, &300i128, &None); + let result = + client.try_withdraw_developer_balance(&developer, &300i128, &None); assert!(result.is_ok()); - assert_eq!(client.get_developer_balance(&developer, &usdc_address), 700i128); + assert_eq!( + client.get_developer_balance(&developer, &usdc_address), + 700i128 + ); // Second withdrawal of 300 would push total to 600 (over 500 cap) - let result = client.try_withdraw_developer_balance(&developer, &300i128, &None); + let result = + client.try_withdraw_developer_balance(&developer, &300i128, &None); assert!(is_error(result, SettlementError::DailyWithdrawCapExceeded)); - assert_eq!(client.get_developer_balance(&developer, &usdc_address), 700i128); + assert_eq!( + client.get_developer_balance(&developer, &usdc_address), + 700i128 + ); } #[test] @@ -2169,7 +2287,13 @@ mod settlement_tests { client.init(&admin, &vault); client.set_usdc_token(&admin, &usdc_address); client.set_daily_withdraw_cap(&admin, &developer, &500i128); - client.receive_payment(&vault, &1000i128, &false, &Some(developer.clone()), &usdc_address); + client.receive_payment( + &vault, + &1000i128, + &false, + &Some(developer.clone()), + &usdc_address, + ); usdc_admin_client.mint(&addr, &1000i128); // Withdraw 200 + 200 = 400, still under 500 @@ -2179,16 +2303,23 @@ mod settlement_tests { assert!(client .try_withdraw_developer_balance(&developer, &200i128, &None) .is_ok()); - assert_eq!(client.get_developer_balance(&developer), 600i128); + assert_eq!( + client.get_developer_balance(&developer, &usdc_address), + 600i128 + ); // Third withdrawal of 100 would push to 500 (exact cap — allowed) assert!(client .try_withdraw_developer_balance(&developer, &100i128, &None) .is_ok()); - assert_eq!(client.get_developer_balance(&developer), 500i128); + assert_eq!( + client.get_developer_balance(&developer, &usdc_address), + 500i128 + ); // Fourth withdrawal of 1 would exceed cap - let result = client.try_withdraw_developer_balance(&developer, &1i128, &None); + let result = + client.try_withdraw_developer_balance(&developer, &1i128, &None); assert!(is_error(result, SettlementError::DailyWithdrawCapExceeded)); } @@ -2207,13 +2338,22 @@ mod settlement_tests { client.set_usdc_token(&admin, &usdc_address); // Cap = 0 explicitly means unlimited client.set_daily_withdraw_cap(&admin, &developer, &0i128); - client.receive_payment(&vault, &1000i128, &false, &Some(developer.clone()), &usdc_address); + client.receive_payment( + &vault, + &1000i128, + &false, + &Some(developer.clone()), + &usdc_address, + ); usdc_admin_client.mint(&addr, &1000i128); assert!(client .try_withdraw_developer_balance(&developer, &1000i128, &None) .is_ok()); - assert_eq!(client.get_developer_balance(&developer), 0i128); + assert_eq!( + client.get_developer_balance(&developer, &usdc_address), + 0i128 + ); } #[test] @@ -2230,13 +2370,22 @@ mod settlement_tests { client.init(&admin, &vault); client.set_usdc_token(&admin, &usdc_address); // No cap set at all — should be unlimited - client.receive_payment(&vault, &1000i128, &false, &Some(developer.clone()), &usdc_address); + client.receive_payment( + &vault, + &1000i128, + &false, + &Some(developer.clone()), + &usdc_address, + ); usdc_admin_client.mint(&addr, &1000i128); assert!(client .try_withdraw_developer_balance(&developer, &1000i128, &None) .is_ok()); - assert_eq!(client.get_developer_balance(&developer), 0i128); + assert_eq!( + client.get_developer_balance(&developer, &usdc_address), + 0i128 + ); } #[test] @@ -2255,17 +2404,27 @@ mod settlement_tests { client.init(&admin, &vault); client.set_usdc_token(&admin, &usdc_address); client.set_daily_withdraw_cap(&admin, &developer, &500i128); - client.receive_payment(&vault, &1000i128, &false, &Some(developer.clone()), &usdc_address); + client.receive_payment( + &vault, + &1000i128, + &false, + &Some(developer.clone()), + &usdc_address, + ); usdc_admin_client.mint(&addr, &1000i128); // Withdraw 400 on day 0 assert!(client .try_withdraw_developer_balance(&developer, &400i128, &None) .is_ok()); - assert_eq!(client.get_developer_balance(&developer), 600i128); + assert_eq!( + client.get_developer_balance(&developer, &usdc_address), + 600i128 + ); // Another 200 would exceed the 500 cap - let result = client.try_withdraw_developer_balance(&developer, &200i128, &None); + let result = + client.try_withdraw_developer_balance(&developer, &200i128, &None); assert!(is_error(result, SettlementError::DailyWithdrawCapExceeded)); // Advance to day 1 @@ -2277,7 +2436,10 @@ mod settlement_tests { assert!(client .try_withdraw_developer_balance(&developer, &500i128, &None) .is_ok()); - assert_eq!(client.get_developer_balance(&developer), 100i128); + assert_eq!( + client.get_developer_balance(&developer, &usdc_address), + 100i128 + ); } #[test] @@ -2364,7 +2526,13 @@ mod settlement_tests { client.init(&admin, &vault); client.set_usdc_token(&admin, &usdc_address); client.set_daily_withdraw_cap(&admin, &developer, &1000i128); - client.receive_payment(&vault, &1000i128, &false, &Some(developer.clone()), &usdc_address); + client.receive_payment( + &vault, + &1000i128, + &false, + &Some(developer.clone()), + &usdc_address, + ); usdc_admin_client.mint(&addr, &1000i128); assert_eq!(client.get_withdrawal_today(&developer), 0i128); @@ -2393,7 +2561,13 @@ mod settlement_tests { client.set_usdc_token(&admin, &usdc_address); client.set_daily_withdraw_cap(&admin, &dev1, &500i128); // dev2 has no cap (unlimited) - client.receive_payment(&vault, &1000i128, &false, &Some(dev1.clone()), &usdc_address); + client.receive_payment( + &vault, + &1000i128, + &false, + &Some(dev1.clone()), + &usdc_address, + ); client.receive_payment(&vault, &500i128, &false, &Some(dev2.clone()), &usdc_address); usdc_admin_client.mint(&addr, &1500i128); @@ -2415,236 +2589,6 @@ mod settlement_tests { .is_ok()); } - // ── developer claim window tests ──────────────────────────────────────── - - #[test] - fn test_claim_window_blocks_withdraw_before_start() { - let env = Env::default(); - env.mock_all_auths(); - env.ledger().set_timestamp(99); - let admin = Address::generate(&env); - let vault = Address::generate(&env); - let developer = Address::generate(&env); - let addr = env.register(CalloraSettlement, ()); - let client = CalloraSettlementClient::new(&env, &addr); - let (usdc_address, _, usdc_admin_client) = create_usdc(&env, &admin); - - client.init(&admin, &vault); - client.set_usdc_token(&admin, &usdc_address); - client - .try_set_developer_claim_window(&admin, &developer, &100u64, &200u64) - .unwrap() - .unwrap(); - client.receive_payment(&vault, &100i128, &false, &Some(developer.clone())); - usdc_admin_client.mint(&addr, &100i128); - - let result = client.try_withdraw_developer_balance(&developer, &100i128, &None); - assert!(is_error(result, SettlementError::ClaimWindowClosed)); - assert_eq!(client.get_developer_balance(&developer), 100i128); - assert_eq!( - token::Client::new(&env, &usdc_address).balance(&developer), - 0i128 - ); - } - - #[test] - fn test_claim_window_allows_withdraw_at_start_and_end() { - let env = Env::default(); - env.mock_all_auths(); - env.ledger().set_timestamp(100); - let admin = Address::generate(&env); - let vault = Address::generate(&env); - let developer = Address::generate(&env); - let addr = env.register(CalloraSettlement, ()); - let client = CalloraSettlementClient::new(&env, &addr); - let (usdc_address, _, usdc_admin_client) = create_usdc(&env, &admin); - - client.init(&admin, &vault); - client.set_usdc_token(&admin, &usdc_address); - client - .try_set_developer_claim_window(&admin, &developer, &100u64, &200u64) - .unwrap() - .unwrap(); - client.receive_payment(&vault, &200i128, &false, &Some(developer.clone())); - usdc_admin_client.mint(&addr, &200i128); - - assert!(client - .try_withdraw_developer_balance(&developer, &50i128, &None) - .is_ok()); - assert_eq!(client.get_developer_balance(&developer), 150i128); - - env.ledger().set_timestamp(200); - assert!(client - .try_withdraw_developer_balance(&developer, &150i128, &None) - .is_ok()); - assert_eq!(client.get_developer_balance(&developer), 0i128); - assert_eq!( - token::Client::new(&env, &usdc_address).balance(&developer), - 200i128 - ); - } - - #[test] - fn test_claim_window_blocks_withdraw_after_end() { - let env = Env::default(); - env.mock_all_auths(); - env.ledger().set_timestamp(201); - let admin = Address::generate(&env); - let vault = Address::generate(&env); - let developer = Address::generate(&env); - let addr = env.register(CalloraSettlement, ()); - let client = CalloraSettlementClient::new(&env, &addr); - let (usdc_address, _, usdc_admin_client) = create_usdc(&env, &admin); - - client.init(&admin, &vault); - client.set_usdc_token(&admin, &usdc_address); - client - .try_set_developer_claim_window(&admin, &developer, &100u64, &200u64) - .unwrap() - .unwrap(); - client.receive_payment(&vault, &100i128, &false, &Some(developer.clone())); - usdc_admin_client.mint(&addr, &100i128); - - let result = client.try_withdraw_developer_balance(&developer, &100i128, &None); - assert!(is_error(result, SettlementError::ClaimWindowClosed)); - assert_eq!(client.get_developer_balance(&developer), 100i128); - } - - #[test] - fn test_claim_window_clear_restores_unrestricted_withdraw() { - let env = Env::default(); - env.mock_all_auths(); - env.ledger().set_timestamp(201); - let admin = Address::generate(&env); - let vault = Address::generate(&env); - let developer = Address::generate(&env); - let addr = env.register(CalloraSettlement, ()); - let client = CalloraSettlementClient::new(&env, &addr); - let (usdc_address, _, usdc_admin_client) = create_usdc(&env, &admin); - - client.init(&admin, &vault); - client.set_usdc_token(&admin, &usdc_address); - client - .try_set_developer_claim_window(&admin, &developer, &100u64, &200u64) - .unwrap() - .unwrap(); - assert!(client.get_developer_claim_window(&developer).is_some()); - - client - .try_clear_developer_claim_window(&admin, &developer) - .unwrap() - .unwrap(); - assert!(client.get_developer_claim_window(&developer).is_none()); - - client.receive_payment(&vault, &100i128, &false, &Some(developer.clone())); - usdc_admin_client.mint(&addr, &100i128); - - assert!(client - .try_withdraw_developer_balance(&developer, &100i128, &None) - .is_ok()); - assert_eq!(client.get_developer_balance(&developer), 0i128); - } - - #[test] - fn test_set_claim_window_rejects_invalid_range() { - let (env, addr, admin, _vault, _third_party) = setup_contract(); - let client = CalloraSettlementClient::new(&env, &addr); - let developer = Address::generate(&env); - - let result = client.try_set_developer_claim_window(&admin, &developer, &200u64, &100u64); - - assert!(is_error(result, SettlementError::InvalidClaimWindow)); - assert!(client.get_developer_claim_window(&developer).is_none()); - } - - #[test] - fn test_set_and_clear_claim_window_unauthorized() { - let (env, addr, _admin, vault, third_party) = setup_contract(); - let client = CalloraSettlementClient::new(&env, &addr); - let developer = Address::generate(&env); - - let result = client.try_set_developer_claim_window(&vault, &developer, &100u64, &200u64); - assert!(is_error(result, SettlementError::Unauthorized)); - - let result = - client.try_set_developer_claim_window(&third_party, &developer, &100u64, &200u64); - assert!(is_error(result, SettlementError::Unauthorized)); - - let result = client.try_clear_developer_claim_window(&third_party, &developer); - assert!(is_error(result, SettlementError::Unauthorized)); - } - - #[test] - fn test_claim_window_is_per_developer() { - let env = Env::default(); - env.mock_all_auths(); - env.ledger().set_timestamp(201); - let admin = Address::generate(&env); - let vault = Address::generate(&env); - let restricted = Address::generate(&env); - let unrestricted = Address::generate(&env); - let addr = env.register(CalloraSettlement, ()); - let client = CalloraSettlementClient::new(&env, &addr); - let (usdc_address, _, usdc_admin_client) = create_usdc(&env, &admin); - - client.init(&admin, &vault); - client.set_usdc_token(&admin, &usdc_address); - client - .try_set_developer_claim_window(&admin, &restricted, &100u64, &200u64) - .unwrap() - .unwrap(); - client.receive_payment(&vault, &100i128, &false, &Some(restricted.clone())); - client.receive_payment(&vault, &100i128, &false, &Some(unrestricted.clone())); - usdc_admin_client.mint(&addr, &200i128); - - let result = client.try_withdraw_developer_balance(&restricted, &100i128, &None); - assert!(is_error(result, SettlementError::ClaimWindowClosed)); - assert!(client - .try_withdraw_developer_balance(&unrestricted, &100i128, &None) - .is_ok()); - assert_eq!(client.get_developer_balance(&restricted), 100i128); - assert_eq!(client.get_developer_balance(&unrestricted), 0i128); - } - - #[test] - fn test_set_claim_window_emits_event_and_getter_returns_window() { - use soroban_sdk::testutils::Events as _; - use soroban_sdk::{IntoVal, Symbol}; - - let (env, addr, admin, _vault, _third_party) = setup_contract(); - let client = CalloraSettlementClient::new(&env, &addr); - let developer = Address::generate(&env); - - client - .try_set_developer_claim_window(&admin, &developer, &100u64, &200u64) - .unwrap() - .unwrap(); - - let window = client.get_developer_claim_window(&developer).unwrap(); - assert_eq!(window.start_ts, 100u64); - assert_eq!(window.end_ts, 200u64); - - let events = env.events().all(); - let ev = events - .iter() - .find(|e| { - !e.1.is_empty() && { - let t: Symbol = e.1.get(0).unwrap().into_val(&env); - t == Symbol::new(&env, "claim_window_changed") - } - }) - .expect("expected claim_window_changed event"); - - let topic1: Address = ev.1.get(1).unwrap().into_val(&env); - assert_eq!(topic1, developer); - - let data: crate::DeveloperClaimWindowChanged = ev.2.into_val(&env); - assert_eq!(data.developer, developer); - assert_eq!(data.start_ts, 100u64); - assert_eq!(data.end_ts, 200u64); - assert!(data.enabled); - } - // ── cursor-based pagination tests ──────────────────────────────────────── /// First page with cursor=None returns up to `limit` records from the @@ -2713,7 +2657,7 @@ mod settlement_tests { assert!(next1.is_some()); // Page 2 — use next_cursor from page 1 - let (page2, next2) = client.get_developer_balances_cursor(&admin, &next1, &2u32); + let (page2, next2) = client.get_developer_balances_cursor(&admin, &next1, &2u32, &token); assert_eq!( page2.len(), 1, @@ -2754,7 +2698,8 @@ mod settlement_tests { client.receive_payment(&vault, &2i128, &false, &Some(dev2.clone()), &token); // Exhaust the index with a large limit to find the last cursor. - let (full_page, last_cursor) = client.get_developer_balances_cursor(&admin, &None, &100u32, &token); + let (full_page, last_cursor) = + client.get_developer_balances_cursor(&admin, &None, &100u32, &token); assert_eq!(full_page.len(), 2); assert!(last_cursor.is_none()); @@ -2797,7 +2742,8 @@ mod settlement_tests { client.receive_payment(&vault, &999i128, &false, &Some(first_addr.clone()), &token); // Continue pagination from the saved cursor. - let (page2, _) = client.get_developer_balances_cursor(&admin, &cursor_after_first, &10u32); + let (page2, _) = + client.get_developer_balances_cursor(&admin, &cursor_after_first, &10u32, &token); assert_eq!(page2.len(), 2, "two records must remain after the cursor"); // The first developer must not appear again in page2. @@ -2834,6 +2780,7 @@ mod settlement_tests { &admin, &None, &(MAX_DEVELOPER_BALANCES_PAGE_SIZE + 50), + &token, ); assert_eq!( page.len(), diff --git a/contracts/settlement/src/test_admin_migration.rs b/contracts/settlement/src/test_admin_migration.rs index 00e6f355..533c9188 100644 --- a/contracts/settlement/src/test_admin_migration.rs +++ b/contracts/settlement/src/test_admin_migration.rs @@ -13,12 +13,13 @@ fn setup() -> (Env, Address, Address, Address, Address, Address) { env.ledger().set_timestamp(1_700_000_000); let admin = Address::generate(&env); let vault = Address::generate(&env); + let token = Address::generate(&env); let from = Address::generate(&env); let to = Address::generate(&env); let contract = env.register(CalloraSettlement, ()); let client = CalloraSettlementClient::new(&env, &contract); client.init(&admin, &vault); - client.receive_payment(&vault, &500, &false, &Some(from.clone())); + client.receive_payment(&vault, &500, &false, &Some(from.clone()), &token); (env, contract, admin, vault, from, to) } @@ -74,9 +75,9 @@ fn execution_requires_timelock_and_succeeds_at_boundary() { fn execution_adds_to_destination_and_leaves_later_source_credits() { let (env, contract, admin, vault, from, to) = setup(); let client = CalloraSettlementClient::new(&env, &contract); - client.receive_payment(&vault, &40, &false, &Some(to.clone())); + client.receive_payment(&vault, &40, &false, &Some(to.clone()), &token); client.propose_balance_migration(&admin, &from, &to); - client.receive_payment(&vault, &25, &false, &Some(from.clone())); + client.receive_payment(&vault, &25, &false, &Some(from.clone()), &token); env.ledger() .set_timestamp(1_700_000_000 + DEVELOPER_MIGRATION_TIMELOCK_SECONDS); diff --git a/contracts/settlement/src/test_invariant.rs b/contracts/settlement/src/test_invariant.rs index 28588282..fa47aca9 100644 --- a/contracts/settlement/src/test_invariant.rs +++ b/contracts/settlement/src/test_invariant.rs @@ -37,7 +37,8 @@ use std::boxed::Box; use proptest::prelude::*; use soroban_sdk::testutils::Address as _; -use soroban_sdk::{token, Address, Env, Vec}; +use soroban_sdk::{Address, Env, Vec}; +use soroban_sdk::token as token_mod; use crate::{CalloraSettlement, CalloraSettlementClient}; @@ -163,7 +164,9 @@ impl Trace { for s in &self.steps { msg.push_str(&std::format!( " [{:>3}] {:30} {}\n", - s.index, s.op, s.detail + s.index, + s.op, + s.detail )); } msg.push_str("==========================\n"); @@ -179,12 +182,12 @@ fn make_usdc<'a>( env: &'a Env, mint_to: &Address, amount: i128, -) -> (Address, token::Client<'a>, token::StellarAssetClient<'a>) { +) -> (Address, token_mod::Client<'a>, token_mod::StellarAssetClient<'a>) { let admin = Address::generate(env); let ca = env.register_stellar_asset_contract_v2(admin.clone()); let addr = ca.address(); - let client = token::Client::new(env, &addr); - let sac = token::StellarAssetClient::new(env, &addr); + let client = token_mod::Client::new(env, &addr); + let sac = token_mod::StellarAssetClient::new(env, &addr); // Pre-fund the settlement contract so withdrawals can succeed. sac.mint(mint_to, &amount); (addr, client, sac) @@ -192,12 +195,12 @@ fn make_usdc<'a>( fn setup_env() -> ( &'static Env, - Address, // contract address + Address, // contract address CalloraSettlementClient<'static>, - Address, // admin - Address, // vault - Address, // usdc token - token::StellarAssetClient<'static>, // usdc SAC (for minting) + Address, // admin + Address, // vault + Address, // usdc token + token_mod::StellarAssetClient<'static>, // usdc SAC (for minting) ) { // SAFETY: We immediately tie the 'static lifetime to `env` via Box::leak. // The Env is leaked so the client can borrow it for the duration of the test. @@ -215,11 +218,11 @@ fn setup_env() -> ( client.init(&admin, &vault); client.set_usdc_token(&admin, &usdc_addr); - let usdc_sac_static: token::StellarAssetClient<'static> = - token::StellarAssetClient::new(env, &usdc_addr); + let usdc_sac_static: token_mod::StellarAssetClient<'static> = + token_mod::StellarAssetClient::new(env, &usdc_addr); ( - (*env).clone(), + env, contract, client, admin, @@ -254,7 +257,12 @@ fn check_invariant( // Developer balance sum must equal our running tally. if dev_sum != expected_dev_total || pool != expected_pool_total { - trace.panic_invariant(step, expected_dev_total + expected_pool_total, dev_sum, pool); + trace.panic_invariant( + step, + expected_dev_total + expected_pool_total, + dev_sum, + pool, + ); } } @@ -301,16 +309,17 @@ fn run_trace(seed: u64) { let usdc_admin = Address::generate(env); let usdc_ca = env.register_stellar_asset_contract_v2(usdc_admin.clone()); let usdc_addr = usdc_ca.address(); - let usdc_sac = token::StellarAssetClient::new(env, &usdc_addr); - usdc_sac.mint(&contract, &(AMOUNT_CAP * TRACE_LENGTH as i128 * MAX_BATCH as i128 * 2)); + let usdc_sac = token_mod::StellarAssetClient::new(env, &usdc_addr); + usdc_sac.mint( + &contract, + &(AMOUNT_CAP * TRACE_LENGTH as i128 * MAX_BATCH as i128 * 2), + ); client.init(&admin, &vault); client.set_usdc_token(&admin, &usdc_addr); // Pre-generate a small pool of developer addresses to encourage balance accumulation. - let devs: std::vec::Vec
= (0..DEV_POOL_SIZE) - .map(|_| Address::generate(env)) - .collect(); + let devs: std::vec::Vec
= (0..DEV_POOL_SIZE).map(|_| Address::generate(env)).collect(); // Running tallies — our "expected" state that must match contract storage. let mut expected_dev_total: i128 = 0; @@ -343,7 +352,11 @@ fn run_trace(seed: u64) { expected_pool_total = expected_pool_total .checked_add(amount) .expect("test tally overflow"); - trace.push(step, "receive_payment(pool)", std::format!("amount={amount}")); + trace.push( + step, + "receive_payment(pool)", + std::format!("amount={amount}"), + ); } x if x == Op::BatchReceiveDev as u8 => { @@ -354,7 +367,9 @@ fn run_trace(seed: u64) { let dev = devs[rng.gen_usize(0, DEV_POOL_SIZE - 1)].clone(); let amount = rng.gen_i128(1, AMOUNT_CAP); items.push_back((dev, amount)); - batch_total = batch_total.checked_add(amount).expect("batch tally overflow"); + batch_total = batch_total + .checked_add(amount) + .expect("batch tally overflow"); } client.batch_receive_payment(&vault, &items, &usdc_addr); expected_dev_total = expected_dev_total @@ -373,7 +388,8 @@ fn run_trace(seed: u64) { let current: i128 = client.get_developer_balance(&dev, &usdc_addr); if current > 0 { let amount = rng.gen_i128(1, current.min(AMOUNT_CAP)); - let result = client.try_withdraw_developer_balance(&dev, &amount, &None, &usdc_addr); + let result = + client.try_withdraw_developer_balance(&dev, &amount, &None); if result.is_ok() { expected_dev_total = expected_dev_total .checked_sub(amount) @@ -381,7 +397,10 @@ fn run_trace(seed: u64) { trace.push( step, "withdraw(ok)", - std::format!("dev={dev:?} amount={amount} remaining={}", current - amount), + std::format!( + "dev={dev:?} amount={amount} remaining={}", + current - amount + ), ); } else { trace.push( @@ -391,11 +410,7 @@ fn run_trace(seed: u64) { ); } } else { - trace.push( - step, - "withdraw(skip-zero)", - std::format!("dev={dev:?}"), - ); + trace.push(step, "withdraw(skip-zero)", std::format!("dev={dev:?}")); } } @@ -444,7 +459,7 @@ fn test_invariant_pool_only() { let usdc_admin = Address::generate(env); let ca = env.register_stellar_asset_contract_v2(usdc_admin.clone()); let usdc_addr = ca.address(); - let sac = token::StellarAssetClient::new(env, &usdc_addr); + let sac = token_mod::StellarAssetClient::new(env, &usdc_addr); sac.mint(&contract, &1_000_000); client.init(&admin, &vault); @@ -460,7 +475,11 @@ fn test_invariant_pool_only() { pool, expected_pool, "pool invariant failed at step {i}: expected {expected_pool}, got {pool}" ); - let dev_sum: i128 = client.get_all_developer_balances(&admin, &usdc_addr).iter().map(|b| b.balance).sum(); + let dev_sum: i128 = client + .get_all_developer_balances(&admin, &usdc_addr) + .iter() + .map(|b| b.balance) + .sum(); assert_eq!(dev_sum, 0, "no developer should have a balance (step {i})"); } } @@ -480,7 +499,7 @@ fn test_invariant_single_dev_full_withdraw() { let usdc_admin = Address::generate(env); let ca = env.register_stellar_asset_contract_v2(usdc_admin.clone()); let usdc_addr = ca.address(); - let sac = token::StellarAssetClient::new(env, &usdc_addr); + let sac = token_mod::StellarAssetClient::new(env, &usdc_addr); sac.mint(&contract, &10_000); client.init(&admin, &vault); @@ -494,11 +513,15 @@ fn test_invariant_single_dev_full_withdraw() { let balance = client.get_developer_balance(&dev, &usdc_addr); assert_eq!(balance, 3_500); - let dev_sum: i128 = client.get_all_developer_balances(&admin, &usdc_addr).iter().map(|b| b.balance).sum(); + let dev_sum: i128 = client + .get_all_developer_balances(&admin, &usdc_addr) + .iter() + .map(|b| b.balance) + .sum(); assert_eq!(dev_sum, 3_500, "dev sum before withdraw"); // Full withdraw. - client.withdraw_developer_balance(&dev, &3_500, &None, &usdc_addr); + client.withdraw_developer_balance(&dev, &3_500, &None); let dev_sum_after: i128 = client .get_all_developer_balances(&admin, &usdc_addr) @@ -506,7 +529,11 @@ fn test_invariant_single_dev_full_withdraw() { .map(|b| b.balance) .sum(); assert_eq!(dev_sum_after, 0, "dev sum must be 0 after full withdraw"); - assert_eq!(client.get_global_pool().total_balance, 0, "pool must stay 0"); + assert_eq!( + client.get_global_pool().total_balance, + 0, + "pool must stay 0" + ); } /// Edge case: batch payments with duplicated developer in same batch accumulate correctly. @@ -524,7 +551,7 @@ fn test_invariant_batch_duplicate_dev() { let usdc_admin = Address::generate(env); let ca = env.register_stellar_asset_contract_v2(usdc_admin.clone()); let usdc_addr = ca.address(); - let sac = token::StellarAssetClient::new(env, &usdc_addr); + let sac = token_mod::StellarAssetClient::new(env, &usdc_addr); sac.mint(&contract, &1_000_000); client.init(&admin, &vault); @@ -536,8 +563,15 @@ fn test_invariant_batch_duplicate_dev() { items.push_back((dev.clone(), 200)); client.batch_receive_payment(&vault, &items, &usdc_addr); - let dev_sum: i128 = client.get_all_developer_balances(&admin, &usdc_addr).iter().map(|b| b.balance).sum(); - assert_eq!(dev_sum, 300, "batch duplicate dev: expected 300, got {dev_sum}"); + let dev_sum: i128 = client + .get_all_developer_balances(&admin, &usdc_addr) + .iter() + .map(|b| b.balance) + .sum(); + assert_eq!( + dev_sum, 300, + "batch duplicate dev: expected 300, got {dev_sum}" + ); assert_eq!(client.get_developer_balance(&dev, &usdc_addr), 300); } @@ -557,7 +591,7 @@ fn test_invariant_interleaved_dev_and_pool() { let usdc_admin = Address::generate(env); let ca = env.register_stellar_asset_contract_v2(usdc_admin.clone()); let usdc_addr = ca.address(); - let sac = token::StellarAssetClient::new(env, &usdc_addr); + let sac = token_mod::StellarAssetClient::new(env, &usdc_addr); sac.mint(&contract, &1_000_000); client.init(&admin, &vault); @@ -584,7 +618,11 @@ fn test_invariant_interleaved_dev_and_pool() { client.receive_payment(&vault, &amount, &false, &Some(dev), &usdc_addr); exp_dev += amount; } - let dev_sum: i128 = client.get_all_developer_balances(&admin, &usdc_addr).iter().map(|b| b.balance).sum(); + let dev_sum: i128 = client + .get_all_developer_balances(&admin, &usdc_addr) + .iter() + .map(|b| b.balance) + .sum(); let pool = client.get_global_pool().total_balance; assert_eq!(dev_sum, exp_dev, "dev sum mismatch"); assert_eq!(pool, exp_pool, "pool mismatch"); diff --git a/contracts/settlement/src/test_multi_asset.rs b/contracts/settlement/src/test_multi_asset.rs index 813dc389..89698d19 100644 --- a/contracts/settlement/src/test_multi_asset.rs +++ b/contracts/settlement/src/test_multi_asset.rs @@ -2,16 +2,17 @@ extern crate std; use crate::{CalloraSettlement, CalloraSettlementClient, SettlementError, StorageKey}; use soroban_sdk::testutils::{Address as _, Ledger as _}; -use soroban_sdk::{token, Address, Env, Symbol}; +use soroban_sdk::{Address, Env, Symbol}; +use soroban_sdk::token as token_mod; fn create_token<'a>( env: &'a Env, admin: &Address, -) -> (Address, token::Client<'a>, token::StellarAssetClient<'a>) { +) -> (Address, token_mod::Client<'a>, token_mod::StellarAssetClient<'a>) { let contract_address = env.register_stellar_asset_contract_v2(admin.clone()); let address = contract_address.address(); - let client = token::Client::new(env, &address); - let admin_client = token::StellarAssetClient::new(env, &address); + let client = token_mod::Client::new(env, &address); + let admin_client = token_mod::StellarAssetClient::new(env, &address); (address, client, admin_client) } @@ -34,19 +35,25 @@ fn test_two_tokens_independent_balances() { client.init(&admin, &vault); // Credit token_a to developer - client.receive_payment(&vault, &1000i128, &false, &Some(developer.clone()), &token_a); + client.receive_payment( + &vault, + &1000i128, + &false, + &Some(developer.clone()), + &token_a, + ); // Credit token_b to developer - client.receive_payment(&vault, &2000i128, &false, &Some(developer.clone()), &token_b); + client.receive_payment( + &vault, + &2000i128, + &false, + &Some(developer.clone()), + &token_b, + ); // Balances are independent per token - assert_eq!( - client.get_developer_balance(&developer, &token_a), - 1000i128 - ); - assert_eq!( - client.get_developer_balance(&developer, &token_b), - 2000i128 - ); + assert_eq!(client.get_developer_balance(&developer, &token_a), 1000i128); + assert_eq!(client.get_developer_balance(&developer, &token_b), 2000i128); // get_all_developer_balances filters by token let all_a = client.get_all_developer_balances(&admin, &token_a); @@ -122,8 +129,7 @@ fn test_withdraw_asserts_token() { &developer, &200i128, &Some(recipient.clone()), - &token_a, - ); + ); assert!(result.is_ok()); assert_eq!(client.get_developer_balance(&developer, &token_a), 300i128); assert_eq!(token_b_client.balance(&recipient), 0i128); // token_b not touched @@ -133,8 +139,7 @@ fn test_withdraw_asserts_token() { &developer, &100i128, &Some(recipient.clone()), - &token_b, - ); + ); assert!(result.is_ok()); assert_eq!(client.get_developer_balance(&developer, &token_b), 200i128); @@ -145,8 +150,7 @@ fn test_withdraw_asserts_token() { &developer, &300i128, &Some(recipient.clone()), - &token_b, - ); + ); assert!(result.is_err()); // InsufficientDeveloperBalance for token_b // Cannot withdraw token_b balance when passing token_a (301 > token_a's 300 balance) @@ -154,8 +158,7 @@ fn test_withdraw_asserts_token() { &developer, &301i128, &Some(recipient.clone()), - &token_a, - ); + ); assert!(result.is_err()); // InsufficientDeveloperBalance for token_a } @@ -194,7 +197,7 @@ fn test_migrate_developer_balance() { ); // Run migration - let result = client.try_migrate_developer_balance(&admin, &developer); + let result = client.try_migrate_single_dev_v2(&admin, &developer); assert!(result.is_ok(), "migration should succeed"); // After migration, new per-token read returns the migrated value @@ -236,17 +239,23 @@ fn test_migrate_developer_balance_idempotent() { env.storage() .persistent() .set(&StorageKey::DeveloperBalanceV1(developer.clone()), &555i128); - env.storage() - .persistent() - .extend_ttl(&StorageKey::DeveloperBalanceV1(developer.clone()), 50000, 50000); + env.storage().persistent().extend_ttl( + &StorageKey::DeveloperBalanceV1(developer.clone()), + 50000, + 50000, + ); }); // First migration - assert!(client.try_migrate_developer_balance(&admin, &developer).is_ok()); + assert!(client + .try_migrate_single_dev_v2(&admin, &developer) + .is_ok()); assert_eq!(client.get_developer_balance(&developer, &usdc), 555i128); // Second migration — idempotent, no error, balance unchanged - assert!(client.try_migrate_developer_balance(&admin, &developer).is_ok()); + assert!(client + .try_migrate_single_dev_v2(&admin, &developer) + .is_ok()); assert_eq!(client.get_developer_balance(&developer, &usdc), 555i128); } @@ -270,7 +279,7 @@ fn test_migrate_developer_balance_unauthorized() { // Non-admin tries to migrate let attacker = Address::generate(&env); - let result = client.try_migrate_developer_balance(&attacker, &developer); + let result = client.try_migrate_single_dev_v2(&attacker, &developer); assert!(result.is_err()); } @@ -290,7 +299,7 @@ fn test_migrate_developer_balance_no_usdc() { client.init(&admin, &vault); // Do NOT set USDC token - let result = client.try_migrate_developer_balance(&admin, &developer); + let result = client.try_migrate_single_dev_v2(&admin, &developer); assert!(result.is_err()); } diff --git a/contracts/settlement/src/test_views.rs b/contracts/settlement/src/test_views.rs index abe46cfc..e1a0c571 100644 --- a/contracts/settlement/src/test_views.rs +++ b/contracts/settlement/src/test_views.rs @@ -80,6 +80,7 @@ fn test_get_developer_balance_returns_zero_when_not_stored() { let client = CalloraSettlementClient::new(&env, &addr); client.init(&admin, &vault); + let token = Address::generate(&env); let balance = client.get_developer_balance(&dev, &token); assert_eq!(balance, 0); @@ -112,15 +113,16 @@ fn test_pagination_fewer_than_limit() { let addr = env.register(CalloraSettlement, ()); let client = CalloraSettlementClient::new(&env, &addr); client.init(&admin, &vault); + let token = Address::generate(&env); // 5 developers for _ in 0..5 { let dev = Address::generate(&env); - client.receive_payment(&admin, &1000i128, &false, &Some(dev)); + client.receive_payment(&admin, &1000i128, &false, &Some(dev), &token); } // limit 10 - let (page, next_cursor) = client.get_developer_balances_cursor(&admin, &None, &10u32); + let (page, next_cursor) = client.get_developer_balances_cursor(&admin, &None, &10u32, &token); assert_eq!(page.len(), 5); assert!(next_cursor.is_none()); } @@ -134,22 +136,24 @@ fn test_pagination_exactly_limit() { let addr = env.register(CalloraSettlement, ()); let client = CalloraSettlementClient::new(&env, &addr); client.init(&admin, &vault); + let token = Address::generate(&env); // 10 developers let mut devs = soroban_sdk::Vec::new(&env); for _ in 0..10 { let dev = Address::generate(&env); - client.receive_payment(&admin, &1000i128, &false, &Some(dev.clone())); + client.receive_payment(&admin, &1000i128, &false, &Some(dev.clone()), &token); devs.push_back(dev); } // limit 10 - let (page, next_cursor) = client.get_developer_balances_cursor(&admin, &None, &10u32); + let (page, next_cursor) = client.get_developer_balances_cursor(&admin, &None, &10u32, &token); assert_eq!(page.len(), 10); assert!(next_cursor.is_some()); // Page 2 using next_cursor - let (page2, next_cursor2) = client.get_developer_balances_cursor(&admin, &next_cursor, &10u32); + let (page2, next_cursor2) = + client.get_developer_balances_cursor(&admin, &next_cursor, &10u32, &token); assert_eq!(page2.len(), 0); assert!(next_cursor2.is_none()); } @@ -163,20 +167,21 @@ fn test_pagination_more_than_limit() { let addr = env.register(CalloraSettlement, ()); let client = CalloraSettlementClient::new(&env, &addr); client.init(&admin, &vault); + let token = Address::generate(&env); // 15 developers for _ in 0..15 { let dev = Address::generate(&env); - client.receive_payment(&admin, &1000i128, &false, &Some(dev)); + client.receive_payment(&admin, &1000i128, &false, &Some(dev), &token); } // Page 1: limit 10 - let (page1, cursor1) = client.get_developer_balances_cursor(&admin, &None, &10u32); + let (page1, cursor1) = client.get_developer_balances_cursor(&admin, &None, &10u32, &token); assert_eq!(page1.len(), 10); assert!(cursor1.is_some()); // Page 2: limit 10 - let (page2, cursor2) = client.get_developer_balances_cursor(&admin, &cursor1, &10u32); + let (page2, cursor2) = client.get_developer_balances_cursor(&admin, &cursor1, &10u32, &token); assert_eq!(page2.len(), 5); assert!(cursor2.is_none()); } @@ -190,21 +195,26 @@ fn test_pagination_stable_ordering() { let addr = env.register(CalloraSettlement, ()); let client = CalloraSettlementClient::new(&env, &addr); client.init(&admin, &vault); + let token = Address::generate(&env); for _ in 0..8 { let dev = Address::generate(&env); - client.receive_payment(&admin, &1000i128, &false, &Some(dev)); + client.receive_payment(&admin, &1000i128, &false, &Some(dev), &token); } - let (p1_run1, cursor1_run1) = client.get_developer_balances_cursor(&admin, &None, &5u32); - let (p1_run2, cursor1_run2) = client.get_developer_balances_cursor(&admin, &None, &5u32); + let (p1_run1, cursor1_run1) = + client.get_developer_balances_cursor(&admin, &None, &5u32, &token); + let (p1_run2, cursor1_run2) = + client.get_developer_balances_cursor(&admin, &None, &5u32, &token); assert_eq!(p1_run1.len(), 5); assert_eq!(p1_run1, p1_run2); assert_eq!(cursor1_run1, cursor1_run2); - let (p2_run1, cursor2_run1) = client.get_developer_balances_cursor(&admin, &cursor1_run1, &5u32); - let (p2_run2, cursor2_run2) = client.get_developer_balances_cursor(&admin, &cursor1_run2, &5u32); + let (p2_run1, cursor2_run1) = + client.get_developer_balances_cursor(&admin, &cursor1_run1, &5u32, &token); + let (p2_run2, cursor2_run2) = + client.get_developer_balances_cursor(&admin, &cursor1_run2, &5u32, &token); assert_eq!(p2_run1.len(), 3); assert_eq!(p2_run1, p2_run2); @@ -220,8 +230,9 @@ fn test_pagination_empty() { let addr = env.register(CalloraSettlement, ()); let client = CalloraSettlementClient::new(&env, &addr); client.init(&admin, &vault); + let token = Address::generate(&env); - let (page, next_cursor) = client.get_developer_balances_cursor(&admin, &None, &10u32); + let (page, next_cursor) = client.get_developer_balances_cursor(&admin, &None, &10u32, &token); assert_eq!(page.len(), 0); assert!(next_cursor.is_none()); } @@ -235,15 +246,16 @@ fn test_pagination_invalid_cursor() { let addr = env.register(CalloraSettlement, ()); let client = CalloraSettlementClient::new(&env, &addr); client.init(&admin, &vault); + let token = Address::generate(&env); for _ in 0..5 { let dev = Address::generate(&env); - client.receive_payment(&admin, &1000i128, &false, &Some(dev)); + client.receive_payment(&admin, &1000i128, &false, &Some(dev), &token); } let invalid_cursor = Some(Address::generate(&env)); - let (page, next_cursor) = client.get_developer_balances_cursor(&admin, &invalid_cursor, &10u32); + let (page, next_cursor) = + client.get_developer_balances_cursor(&admin, &invalid_cursor, &10u32, &token); assert_eq!(page.len(), 0); assert!(next_cursor.is_none()); } - diff --git a/contracts/settlement/src/types.rs b/contracts/settlement/src/types.rs index d5fc1c10..ef6ef2e2 100644 --- a/contracts/settlement/src/types.rs +++ b/contracts/settlement/src/types.rs @@ -43,6 +43,24 @@ pub enum StorageKey { /// Absent → V1 (pre-migration, no version tracking). /// Value 2 → V2 (single-token → per-token migration complete). StorageVersion, + Checkpoint, + CheckpointCounter, + /// Developer claim window `(developer)`. + DeveloperClaimWindow(Address), +} + +/// Checkpoint snapshot for bounded storage growth. +/// +/// Captures a consistent point-in-time view of the global pool and developer +/// index so that future archival / pruning logic can bound on-chain state. +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct Checkpoint { + pub checkpoint_id: u32, + pub total_pool_balance: i128, + pub developer_count: u32, + pub ledger_timestamp: u64, + pub timestamp: u64, } /// Severity levels for admin broadcast messages. diff --git a/contracts/vault/src/lib.rs b/contracts/vault/src/lib.rs index 754a1fa1..309e7307 100644 --- a/contracts/vault/src/lib.rs +++ b/contracts/vault/src/lib.rs @@ -31,8 +31,8 @@ /// persistent, they do not silently archive. To prevent state bloat, an owner /// can explicitly prune old markers using `prune_processed_requests`. use soroban_sdk::{ - contract, contractclient, contractimpl, contracttype, token, Address, BytesN, Env, String, - Symbol, Vec, + contract, contractclient, contracterror, contractimpl, contracttype, token, Address, BytesN, Env, + String, Symbol, Vec, }; /// Typed error codes for the Callora Vault contract. @@ -326,7 +326,7 @@ impl CalloraVault { authorized_caller, min_deposit: min_d, }; - inst.set(&StorageKey::Meta, &meta); + inst.set(&StorageKey::MetaKey, &meta); inst.set(&StorageKey::UsdcToken, &usdc_token); inst.set(&StorageKey::Admin, &owner); if let Some(p) = revenue_pool { @@ -838,7 +838,7 @@ impl CalloraVault { /// If `calculated_fee_bps > max_fee_bps` the call reverts with /// `VaultError::Slippage` **before** any state is mutated. /// - /// Pass `u16::MAX` (65535) to disable the guard and preserve the existing + /// Pass `u32::MAX` (4294967295) to disable the guard and preserve the existing /// unrestricted behaviour — this is the default for backward compatibility. /// /// # Idempotency @@ -858,7 +858,7 @@ impl CalloraVault { caller: Address, amount: i128, request_id: Option, - max_fee_bps: u16, + max_fee_bps: u32, developer: Address, ) -> Result { Self::require_not_paused(env.clone())?; @@ -885,8 +885,8 @@ impl CalloraVault { } // Slippage guard: reject if the deducted amount exceeds max_fee_bps of the // current balance. Calculated before any state mutation or external call. - // Uses u16::MAX as the sentinel for "no limit" (backward-compatible default). - if max_fee_bps < u16::MAX && meta.balance > 0 { + // Uses u32::MAX as the sentinel for "no limit" (backward-compatible default). + if max_fee_bps < u32::MAX && meta.balance > 0 { let calculated_fee_bps = amount .checked_mul(10_000) .ok_or(VaultError::Overflow)? @@ -916,6 +916,7 @@ impl CalloraVault { &amount, &true, // to_pool = true: credit global pool &Some(developer.clone()), // developer is passed down + &ut, // token: USDC token address ); // Now that external operations succeeded, update internal state @@ -1027,6 +1028,7 @@ impl CalloraVault { &total, &true, // to_pool = true: credit global pool &None, // developers are tracked per-item, not passed for whole batch + &ut, // token: USDC token address ); // Now that external operations succeeded, update internal state @@ -1083,7 +1085,7 @@ impl CalloraVault { let mut meta = Self::get_meta(env.clone())?; let old = meta.owner.clone(); meta.owner = pending; - env.storage().instance().set(&StorageKey::Meta, &meta); + env.storage().instance().set(&StorageKey::MetaKey, &meta); env.storage().instance().remove(&StorageKey::PendingOwner); env.events().publish( (events::event_ownership_accepted(&env), old, meta.owner), @@ -1302,13 +1304,6 @@ impl CalloraVault { Ok(()) } - pub fn get_max_deduct(env: Env) -> i128 { - env.storage() - .instance() - .get(&StorageKey::MaxDeduct) - .unwrap_or(DEFAULT_MAX_DEDUCT) - } - /// Store the settlement contract address (admin only). /// /// `deduct` and `batch_deduct` return error until this is called. @@ -1698,40 +1693,6 @@ impl CalloraVault { } Ok(()) } - - /// Broadcast an emergency message from the admin. - /// - /// Only the current admin may call this function. - /// The message length is capped at 256 characters. - /// - /// # Arguments - /// * `env` - The environment running the contract. - /// * `caller` - Must be the current admin; must authorize. - /// * `severity` - Severity level of the broadcast (Info/Warn/Crit). - /// * `message` - The broadcast message, capped at 256 characters. - /// - /// # Errors - /// * `VaultError::Unauthorized` - If the caller is not the current admin. - /// * `VaultError::MetadataTooLong` - If the message length exceeds 256 characters. - pub fn broadcast(env: Env, caller: Address, severity: Severity, message: String) -> Result<(), VaultError> { - caller.require_auth(); - let admin = Self::get_admin(env.clone())?; - if caller != admin { - return Err(VaultError::Unauthorized); - } - let len = message.len(); - if len == 0 { - return Err(VaultError::MetadataTooLong); // Reusing existing error for message too long/empty - } - if len > MAX_MESSAGE_LEN { - return Err(VaultError::MetadataTooLong); - } - env.events().publish( - (events::event_admin_broadcast(&env), caller), - AdminBroadcast { severity, message }, - ); - Ok(()) - } } // Allowlist aliases — convenience wrappers used by tests and external callers. @@ -1795,6 +1756,7 @@ impl CalloraVault { mod events; pub mod rate_limit; +mod validators; // --------------------------------------------------------------------------- // Test modules diff --git a/contracts/vault/src/test.rs b/contracts/vault/src/test.rs index d2c8ca8f..5259429e 100644 --- a/contracts/vault/src/test.rs +++ b/contracts/vault/src/test.rs @@ -908,7 +908,7 @@ fn set_authorized_caller_sets_and_emits_event() { assert_eq!(now, Some(new_caller.clone())); assert_eq!(nonce, 0u64); - let remaining = client.deduct(&new_caller, &50, &None, &u16::MAX, &Address::generate(&env)); + let remaining = client.deduct(&new_caller, &50, &None, &u32::MAX, &Address::generate(&env)); assert_eq!(remaining, 150); } @@ -925,7 +925,7 @@ fn deduct_reduces_balance() { let settlement = create_settlement(&env, &owner, &vault_address); client.set_settlement(&owner, &settlement); - let returned = client.deduct(&owner, &50, &None, &u16::MAX, &Address::generate(&env)); + let returned = client.deduct(&owner, &50, &None, &u32::MAX, &Address::generate(&env)); assert_eq!(returned, 250); assert_eq!(client.balance(), 250); } @@ -943,7 +943,7 @@ fn deduct_with_request_id() { let settlement = create_settlement(&env, &owner, &vault_address); client.set_settlement(&owner, &settlement); - let remaining = client.deduct(&owner, &100, &Some(Symbol::new(&env, "req123")), &u16::MAX, &Address::generate(&env)); + let remaining = client.deduct(&owner, &100, &Some(Symbol::new(&env, "req123")), &u32::MAX, &Address::generate(&env)); assert_eq!(remaining, 900); } @@ -958,7 +958,7 @@ fn deduct_insufficient_balance_fails() { fund_vault(&usdc_admin, &vault_address, 10); client.init(&owner, &usdc, &Some(10), &None, &None, &None, &None); - let result = client.try_deduct(&owner, &100, &None, &u16::MAX); + let result = client.try_deduct(&owner, &100, &None, &u32::MAX); assert!(result.is_err(), "expected error for insufficient balance"); } @@ -975,7 +975,7 @@ fn deduct_exact_balance_succeeds() { let settlement = create_settlement(&env, &owner, &vault_address); client.set_settlement(&owner, &settlement); - let remaining = client.deduct(&owner, &75, &None, &u16::MAX, &Address::generate(&env)); + let remaining = client.deduct(&owner, &75, &None, &u32::MAX, &Address::generate(&env)); assert_eq!(remaining, 0); assert_eq!(client.balance(), 0); } @@ -994,7 +994,7 @@ fn deduct_event_contains_request_id() { client.set_settlement(&owner, &settlement); let request_id = Symbol::new(&env, "api_call_42"); - client.deduct(&owner, &150, &Some(request_id.clone(), &Address::generate(&env)), &u16::MAX); + client.deduct(&owner, &150, &Some(request_id.clone(), &Address::generate(&env)), &u32::MAX); let events = env.events().all(); let ev = events.last().expect("expected deduct event"); @@ -1023,7 +1023,7 @@ fn deduct_zero_amount_fails() { env.mock_all_auths(); fund_vault(&usdc_admin, &client.address, 100); client.init(&owner, &usdc, &Some(100), &None, &None, &None, &None); - client.deduct(&owner, &0, &None, &u16::MAX); + client.deduct(&owner, &0, &None, &u32::MAX); } #[test] @@ -1037,7 +1037,7 @@ fn deduct_exceeding_max_fails() { fund_vault(&usdc_admin, &client.address, 1000); // Set max_deduct to 500 client.init(&owner, &usdc, &Some(1000), &None, &None, &None, &Some(500)); - client.deduct(&owner, &501, &None, &u16::MAX); + client.deduct(&owner, &501, &None, &u32::MAX); } #[test] @@ -1060,7 +1060,7 @@ fn deduct_authorized_caller_succeeds() { ); let settlement = create_settlement(&env, &owner, &vault_address); client.set_settlement(&owner, &settlement); - let remaining = client.deduct(&authorized, &100, &None, &u16::MAX, &Address::generate(&env)); + let remaining = client.deduct(&authorized, &100, &None, &u32::MAX, &Address::generate(&env)); assert_eq!(remaining, 900); } @@ -1075,7 +1075,7 @@ fn deduct_paused_fails() { fund_vault(&usdc_admin, &client.address, 1000); client.init(&owner, &usdc, &Some(1000), &None, &None, &None, &None); client.pause(&owner); - client.deduct(&owner, &100, &None, &u16::MAX); + client.deduct(&owner, &100, &None, &u32::MAX); } #[test] @@ -1090,7 +1090,7 @@ fn deduct_event_no_request_id_uses_empty_symbol() { client.init(&owner, &usdc, &Some(300), &None, &None, &None, &None); let settlement = create_settlement(&env, &owner, &vault_address); client.set_settlement(&owner, &settlement); - client.deduct(&owner, &100, &None, &u16::MAX); + client.deduct(&owner, &100, &None, &u32::MAX); let events = env.events().all(); let ev = events.last().expect("expected deduct event"); @@ -1119,7 +1119,7 @@ fn deduct_zero_panics() { env.mock_all_auths(); fund_vault(&usdc_admin, &vault_address, 500); client.init(&owner, &usdc, &Some(500), &None, &None, &None, &None); - client.deduct(&owner, &0, &None, &u16::MAX); + client.deduct(&owner, &0, &None, &u32::MAX); } #[test] @@ -1133,7 +1133,7 @@ fn deduct_negative_panics() { env.mock_all_auths(); fund_vault(&usdc_admin, &vault_address, 100); client.init(&owner, &usdc, &Some(100), &None, &None, &None, &None); - client.deduct(&owner, &-50, &None, &u16::MAX); + client.deduct(&owner, &-50, &None, &u32::MAX); } #[test] @@ -1147,7 +1147,7 @@ fn deduct_exceeds_balance_panics() { env.mock_all_auths(); fund_vault(&usdc_admin, &vault_address, 50); client.init(&owner, &usdc, &Some(50), &None, &None, &None, &None); - client.deduct(&owner, &100, &None, &u16::MAX); + client.deduct(&owner, &100, &None, &u32::MAX); } #[test] @@ -1161,7 +1161,7 @@ fn balance_unchanged_after_failed_deduct() { fund_vault(&usdc_admin, &vault_address, 100); client.init(&owner, &usdc, &Some(100), &None, &None, &None, &None); - let _ = client.try_deduct(&owner, &200, &None, &u16::MAX); + let _ = client.try_deduct(&owner, &200, &None, &u32::MAX); assert_eq!(client.balance(), 100); } @@ -1399,10 +1399,10 @@ fn get_revenue_pool_consistent_after_deduct_operations() { assert_eq!(before, Some(revenue_pool.clone())); // Perform deduct operation (routes to settlement, not revenue_pool) - client.deduct(&caller, &200, &None, &u16::MAX); + client.deduct(&caller, &200, &None, &u32::MAX); // Query revenue pool after deduct - should be unchanged - let after = client.get_revenue_pool(, &Address::generate(&env)); + let after = client.get_revenue_pool(&env); assert_eq!(after, Some(revenue_pool.clone())); assert_eq!(before, after); @@ -2248,15 +2248,18 @@ fn vault_full_lifecycle() { &env, DeductItem { amount: 100, - request_id: Some(Symbol::new(&env, "r1")) + request_id: Some(Symbol::new(&env, "r1")), + developer: Address::generate(&env), }, DeductItem { amount: 50, - request_id: None - , developer: Address::generate(&env) }, + request_id: None, + developer: Address::generate(&env), + }, DeductItem { amount: 25, - request_id: Some(Symbol::new(&env, "r3")) + request_id: Some(Symbol::new(&env, "r3")), + developer: Address::generate(&env), }, ]; let after_batch = client.batch_deduct(&owner, &items); @@ -2264,7 +2267,7 @@ fn vault_full_lifecycle() { assert_eq!(client.balance(), 525); // Single deduct - let after_deduct = client.deduct(&owner, &25, &Some(Symbol::new(&env, "r4")), &u16::MAX, &Address::generate(&env)); + let after_deduct = client.deduct(&owner, &25, &Some(Symbol::new(&env, "r4")), &u32::MAX, &Address::generate(&env)); assert_eq!(after_deduct, 500); // Admin change @@ -2338,7 +2341,7 @@ fn deduct_with_only_revenue_pool_panics() { &None, ); - client.deduct(&caller, &300, &None, &u16::MAX); + client.deduct(&caller, &300, &None, &u32::MAX); } #[test] @@ -2363,7 +2366,7 @@ fn deduct_with_settlement_transfers_usdc() { ); client.set_settlement(&owner, &settlement); - client.deduct(&caller, &250, &None, &u16::MAX); + client.deduct(&caller, &250, &None, &u32::MAX); assert_eq!(client.balance(, &Address::generate(&env)), 550); assert_eq!(usdc_client.balance(&settlement), 250); @@ -2396,15 +2399,17 @@ fn batch_deduct_with_only_revenue_pool_panics() { &env, DeductItem { amount: 200, - request_id: None - , developer: Address::generate(&env) }, + request_id: None, + developer: Address::generate(&env), + }, DeductItem { amount: 150, - request_id: None - , developer: Address::generate(&env) }, + request_id: None, + developer: Address::generate(&env), + }, ]; client.batch_deduct(&caller, &items); -} + } #[test] fn batch_deduct_with_settlement_transfers_total_usdc() { @@ -2432,18 +2437,20 @@ fn batch_deduct_with_settlement_transfers_total_usdc() { &env, DeductItem { amount: 200, - request_id: None - , developer: Address::generate(&env) }, + request_id: None, + developer: Address::generate(&env), + }, DeductItem { amount: 150, - request_id: None - , developer: Address::generate(&env) }, + request_id: None, + developer: Address::generate(&env), + }, ]; client.batch_deduct(&caller, &items); assert_eq!(client.balance(), 650); assert_eq!(usdc_client.balance(&settlement), 350); -} + } // --------------------------------------------------------------------------- // set_revenue_pool / get_revenue_pool tests @@ -2608,10 +2615,10 @@ fn get_revenue_pool_consistent_after_deduct_operations() { assert_eq!(before, Some(revenue_pool.clone())); // Perform deduct operation (routes to settlement, not revenue_pool) - client.deduct(&caller, &200, &None, &u16::MAX); + client.deduct(&caller, &200, &None, &u32::MAX); // Query revenue pool after deduct - should be unchanged - let after = client.get_revenue_pool(, &Address::generate(&env)); + let after = client.get_revenue_pool(&env); assert_eq!(after, Some(revenue_pool.clone())); assert_eq!(before, after); @@ -2758,7 +2765,7 @@ fn deduct_routes_to_settlement_when_both_configured() { ); client.set_settlement(&owner, &settlement); - client.deduct(&caller, &400, &None, &u16::MAX, &Address::generate(&env)); + client.deduct(&caller, &400, &None, &u32::MAX, &Address::generate(&env)); // settlement gets the funds, revenue_pool gets nothing assert_eq!(usdc_client.balance(&settlement), 400); @@ -2931,10 +2938,10 @@ fn get_settlement_consistent_after_deduct_operations() { assert_eq!(before, settlement); // Perform deduct operation - client.deduct(&caller, &200, &None, &u16::MAX); + client.deduct(&caller, &200, &None, &u32::MAX); // Query settlement after deduct - should be unchanged - let after = client.get_settlement(, &Address::generate(&env)); + let after = client.get_settlement(&env); assert_eq!(after, settlement); assert_eq!(before, after); @@ -3254,7 +3261,7 @@ fn test_deduct_with_settlement_success() { ); client.set_settlement(&owner, &settlement); - client.deduct(&owner, &300, &None, &u16::MAX); + client.deduct(&owner, &300, &None, &u32::MAX); assert_eq!(client.balance(, &Address::generate(&env)), 700); assert_eq!(usdc_client.balance(&settlement), 300); @@ -3316,7 +3323,7 @@ fn deduct_to_zero_succeeds() { let settlement = create_settlement(&env, &owner, &vault_address); client.set_settlement(&owner, &settlement); - assert_eq!(client.deduct(&owner, &500, &None, &u16::MAX, &Address::generate(&env)), 0); + assert_eq!(client.deduct(&owner, &500, &None, &u32::MAX, &Address::generate(&env)), 0); } #[test] @@ -3369,19 +3376,22 @@ fn batch_deduct_to_zero_succeeds() { &env, DeductItem { amount: 200, - request_id: None - , developer: Address::generate(&env) }, + request_id: None, + developer: Address::generate(&env), + }, DeductItem { amount: 200, - request_id: None - , developer: Address::generate(&env) }, + request_id: None, + developer: Address::generate(&env), + }, DeductItem { amount: 200, - request_id: None - , developer: Address::generate(&env) }, + request_id: None, + developer: Address::generate(&env), + }, ]; assert_eq!(client.batch_deduct(&owner, &items), 0); -} + } // --------------------------------------------------------------------------- // Issue #108 — set_allowed_depositor: duplicate add, clear, unauthorized @@ -3548,7 +3558,7 @@ fn deduct_while_paused_fails() { let settlement = create_settlement(&env, &owner, &vault_address); client.set_settlement(&owner, &settlement); client.pause(&owner); - client.deduct(&owner, &100, &None, &u16::MAX); + client.deduct(&owner, &100, &None, &u32::MAX); } #[test] @@ -3568,11 +3578,12 @@ fn batch_deduct_while_paused_fails() { &env, DeductItem { amount: 100, - request_id: None - , developer: Address::generate(&env) } + request_id: None, + developer: Address::generate(&env), + }, ]; client.batch_deduct(&owner, &items); // must panic with "vault is paused" -} + } #[test] #[should_panic(expected = "unauthorized caller")] @@ -3587,7 +3598,7 @@ fn deduct_unauthorized_caller_fails() { // init with an authorized_caller so the None branch is not taken let auth = Address::generate(&env); client.init(&owner, &usdc, &Some(500), &Some(auth), &None, &None, &None); - client.deduct(&attacker, &100, &None, &u16::MAX); + client.deduct(&attacker, &100, &None, &u32::MAX); } #[test] @@ -3607,10 +3618,11 @@ fn batch_deduct_unauthorized_caller_fails() { DeductItem { amount: 100, request_id: None, + developer: Address::generate(&env), }, ]; client.batch_deduct(&attacker, &items); -} + } #[test] #[should_panic(expected = "deduct amount exceeds max_deduct")] @@ -3622,7 +3634,7 @@ fn deduct_exceeds_max_deduct_fails() { env.mock_all_auths(); fund_vault(&usdc_admin, &vault_address, 1000); client.init(&owner, &usdc, &Some(1000), &None, &None, &None, &Some(50)); - client.deduct(&owner, &100, &None, &u16::MAX); // 100 > max_deduct(50) + client.deduct(&owner, &100, &None, &u32::MAX); // 100 > max_deduct(50) } #[test] @@ -3640,10 +3652,11 @@ fn batch_deduct_item_exceeds_max_deduct_fails() { DeductItem { amount: 100, request_id: None, + developer: Address::generate(&env), }, ]; client.batch_deduct(&owner, &items); -} + } #[test] #[should_panic(expected = "amount must be positive")] @@ -3776,11 +3789,11 @@ fn deduct_without_settlement_panics() { env.mock_all_auths(); fund_vault(&usdc_admin, &vault_address, 500); client.init(&owner, &usdc, &Some(500), &None, &None, &None, &None); - client.deduct(&owner, &200, &None, &u16::MAX); + client.deduct(&owner, &200, &None, &u32::MAX); } #[test] -fn deduct_without_settlement_does_not_mutate_state(, &Address::generate(&env)) { +fn deduct_without_settlement_does_not_mutate_state() { // When deduct panics due to missing settlement, vault state must be unchanged. let env = Env::default(); let owner = Address::generate(&env); @@ -3790,7 +3803,7 @@ fn deduct_without_settlement_does_not_mutate_state(, &Address::generate(&env)) { fund_vault(&usdc_admin, &vault_address, 500); client.init(&owner, &usdc, &Some(500), &None, &None, &None, &None); - let result = client.try_deduct(&owner, &200, &None, &u16::MAX); + let result = client.try_deduct(&owner, &200, &None, &u32::MAX); assert!(result.is_err(), "expected panic for missing settlement"); assert_eq!(client.balance(), 500); assert_eq!(usdc_client.balance(&vault_address), 500); @@ -3811,14 +3824,16 @@ fn batch_deduct_without_settlement_panics() { DeductItem { amount: 100, request_id: None, + developer: Address::generate(&env), }, DeductItem { amount: 50, request_id: None, + developer: Address::generate(&env), }, ]; client.batch_deduct(&owner, &items); -} + } #[test] fn batch_deduct_without_settlement_does_not_mutate_state() { @@ -3834,10 +3849,12 @@ fn batch_deduct_without_settlement_does_not_mutate_state() { DeductItem { amount: 100, request_id: None, + developer: Address::generate(&env), }, DeductItem { amount: 50, request_id: None, + developer: Address::generate(&env), }, ]; let result = client.try_batch_deduct(&owner, &items); @@ -4057,13 +4074,13 @@ mod fuzz { let amount: i128 = rng.gen_range(1..=op_cap); if paused { // deduct must fail while paused - assert!(client.try_deduct(&caller, &amount, &None, &u16::MAX).is_err()); + assert!(client.try_deduct(&caller, &amount, &None, &u32::MAX).is_err()); } else if sim >= amount { sim -= amount; - client.deduct(&caller, &amount, &None, &u16::MAX); + client.deduct(&caller, &amount, &None, &u32::MAX); } else { // must fail — balance unchanged (insufficient, &Address::generate(&env)) - assert!(client.try_deduct(&caller, &amount, &None, &u16::MAX).is_err()); + assert!(client.try_deduct(&caller, &amount, &None, &u32::MAX).is_err()); } } @@ -4207,8 +4224,9 @@ mod fuzz { items.push_back(DeductItem { amount: rng.gen_range(1..=200_i128), request_id: None, - }); - } + developer: Address::generate(&env), +}); + } let total: i128 = items.iter().map(|i| i.amount).sum(); if before >= total { client.batch_deduct(&owner, &items); @@ -4260,6 +4278,7 @@ mod fuzz { DeductItem { amount: amt, request_id: None, + developer: Address::generate(&env), }, ]; if exceed { @@ -4341,11 +4360,11 @@ mod fuzz { let amount: i128 = rng.gen_range(1..=max_d); if sim >= amount { sim -= amount; - client.deduct(&caller, &amount, &None, &u16::MAX, &Address::generate(&env)); + client.deduct(&caller, &amount, &None, &u32::MAX, &Address::generate(&env)); } else { // Must be rejected; balance and sim are unchanged. assert!( - client.try_deduct(&caller, &amount, &None, &u16::MAX).is_err(), + client.try_deduct(&caller, &amount, &None, &u32::MAX).is_err(), "deduct exceeding balance must fail at step {step}" ); } @@ -4410,7 +4429,8 @@ mod fuzz { items.push_back(DeductItem { amount: amt, request_id: None, - }); + developer: Address::generate(&env), +}); batch_total = match batch_total.checked_add(amt) { Some(v) => v, None => { @@ -4522,15 +4542,15 @@ mod fuzz { let amount: i128 = rng.gen_range(1..=max_d); if paused { assert!( - client.try_deduct(&caller, &amount, &None, &u16::MAX).is_err(), + client.try_deduct(&caller, &amount, &None, &u32::MAX).is_err(), "deduct must fail while paused at step {step}" ); } else if sim >= amount { sim -= amount; - client.deduct(&caller, &amount, &None, &u16::MAX, &Address::generate(&env)); + client.deduct(&caller, &amount, &None, &u32::MAX, &Address::generate(&env)); } else { assert!( - client.try_deduct(&caller, &amount, &None, &u16::MAX).is_err(), + client.try_deduct(&caller, &amount, &None, &u32::MAX).is_err(), "insufficient deduct must fail at step {step}" ); } @@ -4608,9 +4628,10 @@ mod fuzz { items.push_back(DeductItem { amount: amt, request_id: None, - }); + developer: Address::generate(&env), +}); batch_total = batch_total.saturating_add(amt); - } + } let before = client.balance(); if has_over { @@ -4684,11 +4705,11 @@ mod fuzz { client.deposit(&owner, &1); } else if sim >= 1 { sim -= 1; - client.deduct(&caller, &1, &None, &u16::MAX, &Address::generate(&env)); + client.deduct(&caller, &1, &None, &u32::MAX, &Address::generate(&env)); } else { // Balance exhausted: deduct must fail. assert!( - client.try_deduct(&caller, &1, &None, &u16::MAX).is_err(), + client.try_deduct(&caller, &1, &None, &u32::MAX).is_err(), "deduct must fail when balance=0 at step {step}" ); } @@ -4750,10 +4771,10 @@ mod fuzz { let amount: i128 = rng.gen_range(1..=max_d); if sim >= amount { sim -= amount; - client.deduct(&owner, &amount, &None, &u16::MAX, &Address::generate(&env)); + client.deduct(&owner, &amount, &None, &u32::MAX, &Address::generate(&env)); } else { assert!( - client.try_deduct(&owner, &amount, &None, &u16::MAX).is_err(), + client.try_deduct(&owner, &amount, &None, &u32::MAX).is_err(), "owner deduct must fail when balance insufficient at step {step}" ); } @@ -4762,10 +4783,10 @@ mod fuzz { let amount: i128 = rng.gen_range(1..=max_d); if sim >= amount { sim -= amount; - client.deduct(&caller_b, &amount, &None, &u16::MAX, &Address::generate(&env)); + client.deduct(&caller_b, &amount, &None, &u32::MAX, &Address::generate(&env)); } else { assert!( - client.try_deduct(&caller_b, &amount, &None, &u16::MAX).is_err(), + client.try_deduct(&caller_b, &amount, &None, &u32::MAX).is_err(), "caller_b deduct must fail when balance insufficient at step {step}" ); } @@ -4936,7 +4957,7 @@ fn deduct_equal_to_max_deduct_succeeds() { usdc_client.approve(&owner, &vault_address, &200, &1000); client.deposit(&owner, &200); // deduct exactly equal to max_deduct — must succeed - let balance = client.deduct(&owner, &100, &None, &u16::MAX, &Address::generate(&env)); + let balance = client.deduct(&owner, &100, &None, &u32::MAX, &Address::generate(&env)); assert_eq!(balance, 600); } @@ -4954,7 +4975,7 @@ fn deduct_above_max_deduct_panics() { usdc_client.approve(&owner, &vault_address, &200, &1000); client.deposit(&owner, &200); // deduct 101 > max_deduct 100 — must panic - client.deduct(&owner, &101, &None, &u16::MAX); + client.deduct(&owner, &101, &None, &u32::MAX); } #[test] @@ -4973,7 +4994,7 @@ fn deduct_default_cap_is_i128_max() { usdc_client.approve(&owner, &vault_address, &1_000_000, &1000); client.deposit(&owner, &1_000_000); // large deduct well below i128::MAX should succeed - let balance = client.deduct(&owner, &999_999, &None, &u16::MAX, &Address::generate(&env)); + let balance = client.deduct(&owner, &999_999, &None, &u32::MAX, &Address::generate(&env)); assert_eq!(balance, 1); } @@ -4997,20 +5018,23 @@ fn batch_deduct_each_item_constrained_by_max_deduct() { &env, DeductItem { amount: 50, - request_id: None - , developer: Address::generate(&env) }, + request_id: None, + developer: Address::generate(&env), + }, DeductItem { amount: 50, - request_id: None - , developer: Address::generate(&env) }, + request_id: None, + developer: Address::generate(&env), + }, DeductItem { amount: 50, - request_id: None - , developer: Address::generate(&env) }, + request_id: None, + developer: Address::generate(&env), + }, ]; let balance = client.batch_deduct(&owner, &items); assert_eq!(balance, 150); -} + } #[test] #[should_panic(expected = "deduct amount exceeds max_deduct")] @@ -5030,15 +5054,17 @@ fn batch_deduct_one_item_above_max_deduct_panics() { &env, DeductItem { amount: 50, - request_id: None - , developer: Address::generate(&env) }, + request_id: None, + developer: Address::generate(&env), + }, DeductItem { amount: 51, - request_id: None - , developer: Address::generate(&env) }, + request_id: None, + developer: Address::generate(&env), + }, ]; client.batch_deduct(&owner, &items); -} + } // --------------------------------------------------------------------------- // get_contract_addresses tests (Issue #257) @@ -5266,7 +5292,7 @@ fn instance_ttl_extended_on_deduct_and_batch_deduct() { client.deposit(&owner, &500); // deduct — bumps TTL - client.deduct(&owner, &100, &None, &u16::MAX); + client.deduct(&owner, &100, &None, &u32::MAX); let seq = env.ledger().sequence(); env.ledger() .set_sequence_number(seq + INSTANCE_BUMP_THRESHOLD - 1); @@ -5281,12 +5307,14 @@ fn instance_ttl_extended_on_deduct_and_batch_deduct() { &env, DeductItem { amount: 50, - request_id: None - , developer: Address::generate(&env) }, + request_id: None, + developer: Address::generate(&env), + }, DeductItem { amount: 50, - request_id: None - , developer: Address::generate(&env) }, + request_id: None, + developer: Address::generate(&env), + }, ]; client.batch_deduct(&owner, &items); let seq = env.ledger().sequence(); @@ -5375,7 +5403,7 @@ mod malicious_token { let vault_client = CalloraVaultClient::new(&env, &vault_addr); // 😈 ATTACK: Call back into the vault - vault_client.deduct(&caller, &attack_amount, &Some(Symbol::new(&env, "reentry")), &u16::MAX, &Address::generate(&env), &Address::generate(&env)); + vault_client.deduct(&caller, &attack_amount, &Some(Symbol::new(&env, "reentry")), &u32::MAX, &Address::generate(&env), &Address::generate(&env)); } } @@ -5449,7 +5477,7 @@ fn test_reentry_protection_single_deduct() { // Call 2 (Re-entrant): deduct(600) -> sees balance 500 -> PANIC "insufficient balance". malicious_client.set_attack_config(&vault_address, &owner, &600, &1); - let result = vault_client.try_deduct(&owner, &500, &None, &u16::MAX); + let result = vault_client.try_deduct(&owner, &500, &None, &u32::MAX); // Acceptable Outcome A: Re-entrant call fails due to state guard (insufficient balance). assert!( @@ -5510,12 +5538,14 @@ fn test_reentry_protection_batch_deduct() { &env, DeductItem { amount: 300, - request_id: None - , developer: Address::generate(&env) }, + request_id: None, + developer: Address::generate(&env), + }, DeductItem { amount: 200, - request_id: None - , developer: Address::generate(&env) }, + request_id: None, + developer: Address::generate(&env), + }, ]; let result = vault_client.try_batch_deduct(&owner, &items); @@ -5571,7 +5601,7 @@ fn test_reentry_success_preserves_accounting() { // Call 1 resumes. malicious_client.set_attack_config(&vault_address, &owner, &100, &1); - vault_client.deduct(&owner, &200, &None, &u16::MAX, &Address::generate(&env)); + vault_client.deduct(&owner, &200, &None, &u32::MAX, &Address::generate(&env)); // Final balance must be exactly 700. assert_eq!( @@ -5611,7 +5641,7 @@ fn test_nested_reentry_protection() { // Total should be: 100 (original) + 3 * 100 (re-entries) = 400. token_client.set_attack_config(&vault_address, &owner, &100, &3); - vault_client.deduct(&owner, &100, &None, &u16::MAX, &Address::generate(&env)); + vault_client.deduct(&owner, &100, &None, &u32::MAX, &Address::generate(&env)); assert_eq!(vault_client.balance(, &Address::generate(&env)), 600); } @@ -5655,7 +5685,7 @@ fn test_reentry_exact_balance_exhaustion() { // re-entry deduct(500) -> balance 0. (Success) malicious_client.set_attack_config(&vault_address, &owner, &500, &1); - vault_client.deduct(&owner, &500, &None, &u16::MAX, &Address::generate(&env)); + vault_client.deduct(&owner, &500, &None, &u32::MAX, &Address::generate(&env)); assert_eq!(vault_client.balance(, &Address::generate(&env)), 0); // Try again with over-exhaustion @@ -5663,7 +5693,7 @@ fn test_reentry_exact_balance_exhaustion() { // deduct(600) -> balance 400. // re-entry deduct(401) -> Fail. malicious_client.set_attack_config(&vault_address, &owner, &401, &1); - let result = vault_client.try_deduct(&owner, &600, &None, &u16::MAX); + let result = vault_client.try_deduct(&owner, &600, &None, &u32::MAX); assert!(result.is_err()); assert_eq!(vault_client.balance(), 1000); } @@ -5703,7 +5733,7 @@ fn test_reentry_near_zero_balance() { // Call 2 (Re-entrant): deduct(1) -> sees balance 0 -> PANIC "insufficient balance" malicious_client.set_attack_config(&vault_address, &owner, &1, &1); - let result = vault_client.try_deduct(&owner, &1, &None, &u16::MAX); + let result = vault_client.try_deduct(&owner, &1, &None, &u32::MAX); // Must fail due to insufficient balance in re-entrant call assert!(result.is_err()); @@ -5755,16 +5785,19 @@ fn test_reentry_multiple_recipients_batch() { &env, DeductItem { amount: 200, - request_id: None - , developer: Address::generate(&env) }, + request_id: None, + developer: Address::generate(&env), + }, DeductItem { amount: 200, - request_id: None - , developer: Address::generate(&env) }, + request_id: None, + developer: Address::generate(&env), + }, DeductItem { amount: 200, - request_id: None - , developer: Address::generate(&env) }, + request_id: None, + developer: Address::generate(&env), + }, ]; vault_client.batch_deduct(&owner, &items); @@ -5817,12 +5850,14 @@ fn test_reentry_callback_after_partial_batch() { &env, DeductItem { amount: 300, - request_id: None - , developer: Address::generate(&env) }, + request_id: None, + developer: Address::generate(&env), + }, DeductItem { amount: 400, - request_id: None - , developer: Address::generate(&env) }, + request_id: None, + developer: Address::generate(&env), + }, ]; vault_client.batch_deduct(&owner, &items); @@ -5869,7 +5904,7 @@ fn test_reentry_repeated_attempts() { // This tests that the vault's balance validation prevents over-deduction malicious_client.set_attack_config(&vault_address, &owner, &100, &5); - vault_client.deduct(&owner, &100, &None, &u16::MAX, &Address::generate(&env), &Address::generate(&env)); + vault_client.deduct(&owner, &100, &None, &u32::MAX, &Address::generate(&env), &Address::generate(&env)); // With 5 re-entries of 100 each, plus original 100, total should be 600 // So final balance should be 1000 - 600 = 400 @@ -6103,7 +6138,7 @@ fn budget_measure_single_deduct() { let (owner, client) = setup_vault_for_deduct(&env, 100_000_000); let before = BudgetSnapshot::capture(&env); - client.deduct(&owner, &1_000_000, &None, &u16::MAX); + client.deduct(&owner, &1_000_000, &None, &u32::MAX); let after = BudgetSnapshot::capture(&env); let delta = after.delta(&before, &Address::generate(&env)); @@ -6127,8 +6162,9 @@ fn budget_measure_batch_deduct_size_1() { &env, DeductItem { amount: 1_000_000, - request_id: None - , developer: Address::generate(&env) } + request_id: None, + developer: Address::generate(&env), + }, ]; let before = BudgetSnapshot::capture(&env); @@ -6156,8 +6192,9 @@ fn budget_measure_batch_deduct_size_10() { items.push_back(DeductItem { amount: 1_000_000, request_id: Some(Symbol::new(&env, "req")), - }); - } + developer: Address::generate(&env), +}); + } let before = BudgetSnapshot::capture(&env); client.batch_deduct(&owner, &items); @@ -6184,8 +6221,9 @@ fn budget_measure_batch_deduct_size_25() { items.push_back(DeductItem { amount: 500_000, request_id: Some(Symbol::new(&env, "req")), - }); - } + developer: Address::generate(&env), +}); + } let before = BudgetSnapshot::capture(&env); client.batch_deduct(&owner, &items); @@ -6212,8 +6250,9 @@ fn budget_measure_batch_deduct_size_50() { items.push_back(DeductItem { amount: 300_000, request_id: Some(Symbol::new(&env, "req")), - }); - } + developer: Address::generate(&env), +}); + } let before = BudgetSnapshot::capture(&env); client.batch_deduct(&owner, &items); @@ -6293,14 +6332,14 @@ fn slippage_fee_above_limit_returns_slippage_error() { ); } -/// Passing u16::MAX behaves like the old unrestricted deduct. +/// Passing u32::MAX behaves like the old unrestricted deduct. #[test] fn slippage_max_u16_is_unrestricted() { let env = Env::default(); let (owner, client) = setup_slippage_vault(&env, 1000); env.mock_all_auths(); - // Deduct 99% of balance — would fail any real limit, but u16::MAX = no limit - let remaining = client.deduct(&owner, &999, &None, &u16::MAX, &Address::generate(&env)); + // Deduct 99% of balance — would fail any real limit, but u32::MAX = no limit + let remaining = client.deduct(&owner, &999, &None, &u32::MAX, &Address::generate(&env)); assert_eq!(remaining, 1); } @@ -6342,12 +6381,12 @@ fn slippage_check_before_state_mutation() { assert_eq!(client.balance(), balance_before, "balance must be unchanged after slippage revert"); } -/// Existing deductions (u16::MAX) continue to work — no regression. +/// Existing deductions (u32::MAX) continue to work — no regression. #[test] fn slippage_no_regression_existing_deductions() { let env = Env::default(); let (owner, client) = setup_slippage_vault(&env, 500); env.mock_all_auths(); - assert_eq!(client.deduct(&owner, &200, &None, &u16::MAX, &Address::generate(&env)), 300); - assert_eq!(client.deduct(&owner, &300, &None, &u16::MAX, &Address::generate(&env)), 0); + assert_eq!(client.deduct(&owner, &200, &None, &u32::MAX, &Address::generate(&env)), 300); + assert_eq!(client.deduct(&owner, &300, &None, &u32::MAX, &Address::generate(&env)), 0); } \ No newline at end of file diff --git a/contracts/vault/src/test_balance_property.rs b/contracts/vault/src/test_balance_property.rs index 4a7e1717..fdf5dd2b 100644 --- a/contracts/vault/src/test_balance_property.rs +++ b/contracts/vault/src/test_balance_property.rs @@ -304,7 +304,7 @@ fn run_property_trace(seed: u64) { None }; if paused { - let result = client.try_deduct(&owner, &amount, &rid, &u16::MAX); + let result = client.try_deduct(&owner, &amount, &rid, &u32::MAX); trace.push( step, "deduct (paused, expect fail)", @@ -312,7 +312,7 @@ fn run_property_trace(seed: u64) { ); assert!(result.is_err()); } else if balance_before >= amount { - client.deduct(&owner, &amount, &rid, &u16::MAX); + client.deduct(&owner, &amount, &rid, &u32::MAX); if let Some(ref id) = rid { used_request_ids.push(id.clone(), &Address::generate(&env)); } @@ -322,7 +322,7 @@ fn run_property_trace(seed: u64) { std::format!("amount={amount} rid={with_id:?}"), ); } else { - let result = client.try_deduct(&owner, &amount, &rid, &u16::MAX); + let result = client.try_deduct(&owner, &amount, &rid, &u32::MAX); trace.push( step, "deduct (insufficient, expect fail)", @@ -464,8 +464,8 @@ fn run_property_trace(seed: u64) { if !paused && balance_before >= amount { let rid = make_request_id(&env, rid_counter); rid_counter += 1; - client.deduct(&owner, &amount, &Some(rid.clone(), &Address::generate(&env)), &u16::MAX); - let retry = client.try_deduct(&owner, &amount, &Some(rid.clone()), &u16::MAX); + client.deduct(&owner, &amount, &Some(rid.clone(), &Address::generate(&env)), &u32::MAX); + let retry = client.try_deduct(&owner, &amount, &Some(rid.clone()), &u32::MAX); trace.push( step, "request_id_reuse", @@ -480,7 +480,7 @@ fn run_property_trace(seed: u64) { let idx = rng.gen_range_usize(0, used_request_ids.len()); let rid = used_request_ids[idx].clone(); let amount = rng.gen_range_i128(1, AMOUNT_CAP); - let retry = client.try_deduct(&owner, &amount, &Some(rid.clone()), &u16::MAX); + let retry = client.try_deduct(&owner, &amount, &Some(rid.clone()), &u32::MAX); trace.push( step, "request_id_reuse", @@ -546,7 +546,7 @@ fn test_balance_property_pause_mid_sequence() { client.pause(&owner); assert!(client.try_deposit(&owner, &50).is_err()); - assert!(client.try_deduct(&owner, &10, &None, &u16::MAX).is_err()); + assert!(client.try_deduct(&owner, &10, &None, &u32::MAX).is_err()); assert_balance_in_sync(&client, &usdc_client, &vault_addr, &Trace::new(42), 2); // Withdraw is allowed while paused. @@ -554,7 +554,7 @@ fn test_balance_property_pause_mid_sequence() { assert_balance_in_sync(&client, &usdc_client, &vault_addr, &Trace::new(42), 3); client.unpause(&owner); - client.deduct(&owner, &25, &None, &u16::MAX, &Address::generate(&env)); + client.deduct(&owner, &25, &None, &u32::MAX, &Address::generate(&env)); assert_balance_in_sync(&client, &usdc_client, &vault_addr, &Trace::new(42), 4); } @@ -620,10 +620,10 @@ fn test_balance_property_request_id_reuse() { client.set_settlement(&owner, &settlement); let rid = Symbol::new(&env, "reuse_test_id"); - client.deduct(&owner, &100, &Some(rid.clone(), &Address::generate(&env)), &u16::MAX); + client.deduct(&owner, &100, &Some(rid.clone(), &Address::generate(&env)), &u32::MAX); assert_balance_in_sync(&client, &usdc_client, &vault_addr, &Trace::new(13), 1); - let retry = client.try_deduct(&owner, &50, &Some(rid.clone()), &u16::MAX); + let retry = client.try_deduct(&owner, &50, &Some(rid.clone()), &u32::MAX); assert!(retry.is_err()); assert_balance_in_sync(&client, &usdc_client, &vault_addr, &Trace::new(13), 2); assert_eq!(client.balance(), INITIAL_BALANCE - 100); diff --git a/contracts/vault/src/test_idempotency.rs b/contracts/vault/src/test_idempotency.rs index c22c5c5f..94e1c374 100644 --- a/contracts/vault/src/test_idempotency.rs +++ b/contracts/vault/src/test_idempotency.rs @@ -150,11 +150,11 @@ fn deduct_duplicate_request_id_rejected() { let rid = Symbol::new(&env, "req_001"); // First call — must succeed. - let remaining = client.deduct(&owner, &100, &Some(rid.clone(), &Address::generate(&env)), &u16::MAX); + let remaining = client.deduct(&owner, &100, &Some(rid.clone(), &Address::generate(&env)), &u32::MAX); assert_eq!(remaining, 900); // Second call with same request_id — must be rejected. - let result = client.try_deduct(&owner, &100, &Some(rid.clone()), &u16::MAX); + let result = client.try_deduct(&owner, &100, &Some(rid.clone()), &u32::MAX); assert!(result.is_err(), "duplicate request_id must be rejected"); // Balance must be unchanged after the rejected retry. @@ -174,10 +174,10 @@ fn deduct_distinct_request_ids_both_succeed() { let rid_a = Symbol::new(&env, "req_a"); let rid_b = Symbol::new(&env, "req_b"); - let after_a = client.deduct(&owner, &100, &Some(rid_a.clone(), &Address::generate(&env)), &u16::MAX); + let after_a = client.deduct(&owner, &100, &Some(rid_a.clone(), &Address::generate(&env)), &u32::MAX); assert_eq!(after_a, 900); - let after_b = client.deduct(&owner, &200, &Some(rid_b.clone(), &Address::generate(&env)), &u16::MAX); + let after_b = client.deduct(&owner, &200, &Some(rid_b.clone(), &Address::generate(&env)), &u32::MAX); assert_eq!(after_b, 700); assert_eq!(client.balance(), 700); @@ -190,9 +190,9 @@ fn deduct_none_request_id_not_deduplicated() { let (_, client, _, owner) = setup_vault(&env, 1_000); // Three calls with None — all must succeed. - assert_eq!(client.deduct(&owner, &100, &None, &u16::MAX, &Address::generate(&env)), 900); - assert_eq!(client.deduct(&owner, &100, &None, &u16::MAX, &Address::generate(&env)), 800); - assert_eq!(client.deduct(&owner, &100, &None, &u16::MAX, &Address::generate(&env)), 700); + assert_eq!(client.deduct(&owner, &100, &None, &u32::MAX, &Address::generate(&env)), 900); + assert_eq!(client.deduct(&owner, &100, &None, &u32::MAX, &Address::generate(&env)), 800); + assert_eq!(client.deduct(&owner, &100, &None, &u32::MAX, &Address::generate(&env)), 700); assert_eq!(client.balance(), 700); } @@ -205,7 +205,7 @@ fn deduct_failed_due_to_insufficient_balance_does_not_mark_id() { let rid = Symbol::new(&env, "req_fail"); // Attempt to deduct more than the balance — must fail. - let result = client.try_deduct(&owner, &100, &Some(rid.clone()), &u16::MAX); + let result = client.try_deduct(&owner, &100, &Some(rid.clone()), &u32::MAX); assert!(result.is_err(), "expected insufficient balance error"); // The id must NOT be marked — a retry with sufficient balance should succeed. @@ -226,7 +226,7 @@ fn deduct_failed_due_to_paused_does_not_mark_id() { let rid = Symbol::new(&env, "req_paused"); client.pause(&owner); - let result = client.try_deduct(&owner, &100, &Some(rid.clone()), &u16::MAX); + let result = client.try_deduct(&owner, &100, &Some(rid.clone()), &u32::MAX); assert!(result.is_err(), "expected paused error"); assert!( @@ -256,7 +256,7 @@ fn is_request_processed_true_after_successful_deduct() { let (_, client, _, owner) = setup_vault(&env, 500); let rid = Symbol::new(&env, "seen"); - client.deduct(&owner, &50, &Some(rid.clone(), &Address::generate(&env)), &u16::MAX); + client.deduct(&owner, &50, &Some(rid.clone(), &Address::generate(&env)), &u32::MAX); assert!( client.is_request_processed(&rid), @@ -273,7 +273,7 @@ fn is_request_processed_false_for_different_id() { let rid_a = Symbol::new(&env, "id_a"); let rid_b = Symbol::new(&env, "id_b"); - client.deduct(&owner, &50, &Some(rid_a.clone(), &Address::generate(&env)), &u16::MAX); + client.deduct(&owner, &50, &Some(rid_a.clone(), &Address::generate(&env)), &u32::MAX); assert!(client.is_request_processed(&rid_a)); assert!(!client.is_request_processed(&rid_b)); @@ -292,7 +292,7 @@ fn batch_deduct_duplicate_request_id_rejected_atomically() { let rid = Symbol::new(&env, "batch_dup"); // First single deduct marks the id. - client.deduct(&owner, &100, &Some(rid.clone(), &Address::generate(&env)), &u16::MAX); + client.deduct(&owner, &100, &Some(rid.clone(), &Address::generate(&env)), &u32::MAX); assert_eq!(client.balance(), 900); // Batch that reuses the same id — must be rejected atomically. @@ -478,10 +478,10 @@ fn deduct_retry_with_different_amount_still_rejected() { let rid = Symbol::new(&env, "retry_amt"); - client.deduct(&owner, &100, &Some(rid.clone(), &Address::generate(&env)), &u16::MAX); + client.deduct(&owner, &100, &Some(rid.clone(), &Address::generate(&env)), &u32::MAX); // Retry with a different amount — still rejected. - let result = client.try_deduct(&owner, &50, &Some(rid.clone()), &u16::MAX); + let result = client.try_deduct(&owner, &50, &Some(rid.clone()), &u32::MAX); assert!( result.is_err(), "retry with different amount must be rejected" @@ -521,11 +521,11 @@ fn batch_deduct_mixed_ids_marks_only_some_ids() { assert!(client.is_request_processed(&rid_z)); // Retrying either Some id must fail. - assert!(client.try_deduct(&owner, &10, &Some(rid_x)).is_err(), &u16::MAX); - assert!(client.try_deduct(&owner, &10, &Some(rid_z)).is_err(), &u16::MAX); + assert!(client.try_deduct(&owner, &10, &Some(rid_x)).is_err(), &u32::MAX); + assert!(client.try_deduct(&owner, &10, &Some(rid_z)).is_err(), &u32::MAX); // None deducts still go through. - assert_eq!(client.deduct(&owner, &10, &None, &u16::MAX, &Address::generate(&env)), 765); + assert_eq!(client.deduct(&owner, &10, &None, &u32::MAX, &Address::generate(&env)), 765); } #[test] @@ -536,7 +536,7 @@ fn replay_across_long_window_rejected() { let rid = Symbol::new(&env, "req_long_win"); // First call succeeds - client.deduct(&owner, &100, &Some(rid.clone(), &Address::generate(&env)), &u16::MAX); + client.deduct(&owner, &100, &Some(rid.clone(), &Address::generate(&env)), &u32::MAX); // Fast-forward ledger 6 months (approx 6 * 30 days) let new_timestamp = env.ledger().timestamp() + 180 * 24 * 60 * 60; @@ -552,7 +552,7 @@ fn replay_across_long_window_rejected() { }); // Retry should still be rejected because it's persistent and hasn't been explicitly pruned. - let res = client.try_deduct(&owner, &100, &Some(rid.clone()), &u16::MAX); + let res = client.try_deduct(&owner, &100, &Some(rid.clone()), &u32::MAX); assert!(res.is_err(), "should still reject after multi-month window"); } @@ -564,8 +564,8 @@ fn gc_entrypoint_prunes_and_emits_event() { let rid1 = Symbol::new(&env, "req_gc_1"); let rid2 = Symbol::new(&env, "req_gc_2"); - client.deduct(&owner, &100, &Some(rid1.clone(), &Address::generate(&env)), &u16::MAX); - client.deduct(&owner, &100, &Some(rid2.clone(), &Address::generate(&env)), &u16::MAX); + client.deduct(&owner, &100, &Some(rid1.clone(), &Address::generate(&env)), &u32::MAX); + client.deduct(&owner, &100, &Some(rid2.clone(), &Address::generate(&env)), &u32::MAX); let mut ids_to_prune = soroban_sdk::Vec::new(&env); ids_to_prune.push_back(rid1.clone()); @@ -588,7 +588,7 @@ fn gc_entrypoint_prunes_and_emits_event() { assert!(has_event, "Should emit request_id_pruned event"); // Should now be able to replay rid1 - client.deduct(&owner, &100, &Some(rid1, &Address::generate(&env)), &u16::MAX); + client.deduct(&owner, &100, &Some(rid1, &Address::generate(&env)), &u32::MAX); } #[test] @@ -611,7 +611,7 @@ fn gc_allowed_during_pause() { let (_, client, _, owner) = setup_vault(&env, 1_000); let rid1 = Symbol::new(&env, "req_gc_pause"); - client.deduct(&owner, &100, &Some(rid1.clone(), &Address::generate(&env)), &u16::MAX); + client.deduct(&owner, &100, &Some(rid1.clone(), &Address::generate(&env)), &u32::MAX); client.pause(&owner); assert!(client.is_paused()); diff --git a/contracts/vault/src/test_rate_limit.rs b/contracts/vault/src/test_rate_limit.rs index c5813aa2..9e952025 100644 --- a/contracts/vault/src/test_rate_limit.rs +++ b/contracts/vault/src/test_rate_limit.rs @@ -30,7 +30,7 @@ fn rate_limit_bucket_enforcement() { client.set_developer_rate_limit(&owner, &developer, &100, &10); // Try to deduct more than capacity -> fails - let res = client.try_deduct(&caller, &150, &None, &u16::MAX, &developer); + let res = client.try_deduct(&caller, &150, &None, &u32::MAX, &developer); assert_eq!(res.unwrap_err().unwrap(), VaultError::RateLimited); // We cannot deduct immediately if we don't have balance in vault, but since usdc isn't mocked properly, diff --git a/contracts/vault/src/test_reentrancy.rs b/contracts/vault/src/test_reentrancy.rs index 45091889..adc0f811 100644 --- a/contracts/vault/src/test_reentrancy.rs +++ b/contracts/vault/src/test_reentrancy.rs @@ -41,7 +41,7 @@ impl MaliciousToken { let client = CalloraVaultClient::new(&env, &vault); // Attempt re-entry into deduct - let _ = client.try_deduct(&caller, &1, &Some(Symbol::new(&env, "reentry_token")), &u16::MAX); + let _ = client.try_deduct(&caller, &1, &Some(Symbol::new(&env, "reentry_token")), &u32::MAX); } } } @@ -103,7 +103,7 @@ impl MaliciousSettlement { let client = CalloraVaultClient::new(&env, &vault); // Attempt re-entry into deduct - let _ = client.try_deduct(&caller, &1, &Some(Symbol::new(&env, "reentry_settle")), &u16::MAX); + let _ = client.try_deduct(&caller, &1, &Some(Symbol::new(&env, "reentry_settle")), &u32::MAX); } } } @@ -155,7 +155,7 @@ fn test_reentrancy_via_token_transfer_is_blocked_by_auth() { assert_eq!(initial_balance, 1000); // Trigger deduct -> calls token.transfer -> calls vault.deduct (re-entry) - let result = vault_client.try_deduct(&owner, &100, &Some(Symbol::new(&env, "first_call")), &u16::MAX); + let result = vault_client.try_deduct(&owner, &100, &Some(Symbol::new(&env, "first_call")), &u32::MAX); assert!(result.is_ok(), "First deduct should succeed"); assert_eq!( @@ -197,7 +197,7 @@ fn test_reentrancy_via_settlement_callback_is_blocked() { assert_eq!(initial_balance, 1000); // Trigger deduct -> calls settlement.receive_payment -> calls vault.deduct (re-entry) - let result = vault_client.try_deduct(&owner, &100, &Some(Symbol::new(&env, "first_call")), &u16::MAX); + let result = vault_client.try_deduct(&owner, &100, &Some(Symbol::new(&env, "first_call")), &u32::MAX); assert!(result.is_ok(), "First deduct should succeed"); assert_eq!( @@ -298,7 +298,7 @@ fn test_reentrancy_by_authorized_attacker() { assert_eq!(initial_balance, 1000); // Attacker calls deduct -> token.transfer -> attacker calls vault.deduct (re-entry) - let result = vault_client.try_deduct(&attacker, &100, &Some(Symbol::new(&env, "first_call")), &u16::MAX); + let result = vault_client.try_deduct(&attacker, &100, &Some(Symbol::new(&env, "first_call")), &u32::MAX); assert!(result.is_ok(), "First deduct should succeed"); assert_eq!(