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!(