From 41db5bca37fc30dfd0743328d59a0a674337c1ec Mon Sep 17 00:00:00 2001 From: Jopsan-gm Date: Tue, 24 Feb 2026 19:14:23 -0600 Subject: [PATCH 1/4] feat: configurable minimum deposit amount - Add tests for minimum deposit enforcement (issue #43) - deposit_below_minimum_panics: verifies deposits below min_deposit panic - deposit_equal_to_minimum_succeeds: verifies exact minimum is accepted - deposit_with_zero_minimum_always_succeeds: verifies min=0 disables restriction - Fix init_none_balance test to use correct init() signature --- contracts/vault/src/test.rs | 65 +++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/contracts/vault/src/test.rs b/contracts/vault/src/test.rs index c458a63..58c1380 100644 --- a/contracts/vault/src/test.rs +++ b/contracts/vault/src/test.rs @@ -477,9 +477,11 @@ fn init_none_balance() { let owner = Address::generate(&env); let contract_id = env.register(CalloraVault {}, ()); let client = CalloraVaultClient::new(&env, &contract_id); + let (usdc_address, _, _) = create_usdc(&env, &owner); - // Call init with None - client.init(&owner, &None); + env.mock_all_auths(); + // Call init with None balance and None min_deposit + client.init(&owner, &usdc_address, &None, &None); // Assert balance is 0 assert_eq!(client.balance(), 0); @@ -490,6 +492,65 @@ fn init_none_balance() { assert_eq!(meta.balance, 0); } +/// Verifies that a deposit below the configured minimum deposit panics. +/// Issue #43: Enforce Minimum Deposit Amount (Configurable) +#[test] +#[should_panic(expected = "deposit below minimum")] +fn deposit_below_minimum_panics() { + let env = Env::default(); + env.mock_all_auths(); + + let owner = Address::generate(&env); + let (_, vault) = create_vault(&env); + let (usdc_address, _, _) = create_usdc(&env, &owner); + + // Initialize vault with a minimum deposit of 100 + vault.init(&owner, &usdc_address, &None, &Some(100)); + + // Attempt a deposit of 99, which is below the minimum — must panic + vault.deposit(&99); +} + +/// Verifies that a deposit exactly equal to the minimum deposit succeeds. +/// Issue #43: Enforce Minimum Deposit Amount (Configurable) +#[test] +fn deposit_equal_to_minimum_succeeds() { + let env = Env::default(); + env.mock_all_auths(); + + let owner = Address::generate(&env); + let (_, vault) = create_vault(&env); + let (usdc_address, _, _) = create_usdc(&env, &owner); + + // Initialize vault with a minimum deposit of 50 + vault.init(&owner, &usdc_address, &None, &Some(50)); + + // A deposit exactly at the minimum must succeed + let new_balance = vault.deposit(&50); + assert_eq!(new_balance, 50); + assert_eq!(vault.balance(), 50); +} + +/// Verifies that when min_deposit is 0 (disabled), any positive deposit is accepted. +/// Issue #43: Enforce Minimum Deposit Amount (Configurable) +#[test] +fn deposit_with_zero_minimum_always_succeeds() { + let env = Env::default(); + env.mock_all_auths(); + + let owner = Address::generate(&env); + let (_, vault) = create_vault(&env); + let (usdc_address, _, _) = create_usdc(&env, &owner); + + // min_deposit = 0 means no restriction + vault.init(&owner, &usdc_address, &None, &Some(0)); + + // Even a deposit of 1 must work + let new_balance = vault.deposit(&1); + assert_eq!(new_balance, 1); + assert_eq!(vault.balance(), 1); +} + #[test] fn batch_deduct_success() { let env = Env::default(); From 372903822fb3c9e8689ff75c692563bd5330e274 Mon Sep 17 00:00:00 2001 From: Jopsan-gm Date: Wed, 25 Feb 2026 01:03:57 -0600 Subject: [PATCH 2/4] fix: update test signatures to match new API after upstream merge --- contracts/vault/src/test.rs | 86 ++++++++++++------------------------- 1 file changed, 27 insertions(+), 59 deletions(-) diff --git a/contracts/vault/src/test.rs b/contracts/vault/src/test.rs index 070a5fa..55c09e6 100644 --- a/contracts/vault/src/test.rs +++ b/contracts/vault/src/test.rs @@ -518,60 +518,22 @@ fn test_multiple_depositors() { let owner = Address::generate(&env); let contract_id = env.register(CalloraVault {}, ()); let client = CalloraVaultClient::new(&env, &contract_id); - let (usdc_address, _, _) = create_usdc(&env, &owner); - + let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &owner); env.mock_all_auths(); - // Call init with None balance and None min_deposit - client.init(&owner, &usdc_address, &None, &None); - let all_events = env.as_contract(&contract_id, || { - CalloraVault::init( - env.clone(), - owner.clone(), - usdc_address.clone(), - None, - None, - None, - None, - ); - CalloraVault::deposit(env.clone(), dep1.clone(), 100); - CalloraVault::deposit(env.clone(), dep2.clone(), 200); + let dep1 = Address::generate(&env); + let dep2 = Address::generate(&env); + fund_user(&usdc_admin, &dep1, 100); + fund_user(&usdc_admin, &dep2, 200); + approve_spend(&env, &usdc_client, &dep1, &contract_id, 100); + approve_spend(&env, &usdc_client, &dep2, &contract_id, 200); - env.events().all() - }); - let contract_events: std::vec::Vec<_> = - all_events.iter().filter(|e| e.0 == contract_id).collect(); + client.init(&owner, &usdc_address, &None, &None, &None, &None); + client.deposit(&dep1, &100); + client.deposit(&dep2, &200); + // Both depositors contributed: total balance must equal 300 assert_eq!(client.balance(), 300); - - assert_eq!( - contract_events.len(), - 3, - "vault should emit init + 2 deposits" - ); - - // Event 1: Init event - let event0 = contract_events.first().unwrap(); - let topic0_0: Symbol = event0.1.get(0).unwrap().into_val(&env); - assert_eq!(topic0_0, Symbol::new(&env, "init")); - - // Event 2: deposit from dep1 - let event1 = contract_events.get(1).unwrap(); - let topic1_0: Symbol = event1.1.get(0).unwrap().into_val(&env); - let topic1_1: Address = event1.1.get(1).unwrap().into_val(&env); - let data1: i128 = event1.2.into_val(&env); - assert_eq!(topic1_0, Symbol::new(&env, "deposit")); - assert_eq!(topic1_1, dep1); - assert_eq!(data1, 100); - - // Event 3: deposit from dep2 - let event2 = contract_events.get(2).unwrap(); - let topic2_0: Symbol = event2.1.get(0).unwrap().into_val(&env); - let topic2_1: Address = event2.1.get(1).unwrap().into_val(&env); - let data2: i128 = event2.2.into_val(&env); - assert_eq!(topic2_0, Symbol::new(&env, "deposit")); - assert_eq!(topic2_1, dep2); - assert_eq!(data2, 200); } /// Verifies that a deposit below the configured minimum deposit panics. @@ -587,10 +549,10 @@ fn deposit_below_minimum_panics() { let (usdc_address, _, _) = create_usdc(&env, &owner); // Initialize vault with a minimum deposit of 100 - vault.init(&owner, &usdc_address, &None, &Some(100)); + vault.init(&owner, &usdc_address, &None, &Some(100), &None, &None); // Attempt a deposit of 99, which is below the minimum — must panic - vault.deposit(&99); + vault.deposit(&owner, &99); } /// Verifies that a deposit exactly equal to the minimum deposit succeeds. @@ -601,14 +563,17 @@ fn deposit_equal_to_minimum_succeeds() { env.mock_all_auths(); let owner = Address::generate(&env); - let (_, vault) = create_vault(&env); - let (usdc_address, _, _) = create_usdc(&env, &owner); + let (vault_address, vault) = create_vault(&env); + let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &owner); + + fund_user(&usdc_admin, &owner, 100); + approve_spend(&env, &usdc_client, &owner, &vault_address, 100); // Initialize vault with a minimum deposit of 50 - vault.init(&owner, &usdc_address, &None, &Some(50)); + vault.init(&owner, &usdc_address, &None, &Some(50), &None, &None); // A deposit exactly at the minimum must succeed - let new_balance = vault.deposit(&50); + let new_balance = vault.deposit(&owner, &50); assert_eq!(new_balance, 50); assert_eq!(vault.balance(), 50); } @@ -621,14 +586,17 @@ fn deposit_with_zero_minimum_always_succeeds() { env.mock_all_auths(); let owner = Address::generate(&env); - let (_, vault) = create_vault(&env); - let (usdc_address, _, _) = create_usdc(&env, &owner); + let (vault_address, vault) = create_vault(&env); + let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &owner); + + fund_user(&usdc_admin, &owner, 100); + approve_spend(&env, &usdc_client, &owner, &vault_address, 100); // min_deposit = 0 means no restriction - vault.init(&owner, &usdc_address, &None, &Some(0)); + vault.init(&owner, &usdc_address, &None, &Some(0), &None, &None); // Even a deposit of 1 must work - let new_balance = vault.deposit(&1); + let new_balance = vault.deposit(&owner, &1); assert_eq!(new_balance, 1); assert_eq!(vault.balance(), 1); } From 379521049479cf393c2e426b13241fe3f899d59f Mon Sep 17 00:00:00 2001 From: Jopsan-gm Date: Wed, 25 Feb 2026 01:10:59 -0600 Subject: [PATCH 3/4] fix: resolve duplicate fuzz test and fix 4-arg init calls in new upstream tests --- contracts/vault/src/test.rs | 98 ++++++++++++------------------------- 1 file changed, 30 insertions(+), 68 deletions(-) diff --git a/contracts/vault/src/test.rs b/contracts/vault/src/test.rs index 4053432..e333b35 100644 --- a/contracts/vault/src/test.rs +++ b/contracts/vault/src/test.rs @@ -782,27 +782,33 @@ fn init_already_initialized_panics() { /// Run with: cargo test --package callora-vault fuzz_deposit_and_deduct -- --nocapture #[test] fn fuzz_deposit_and_deduct() { - use rand::Rng; + use rand::rngs::StdRng; + use rand::{Rng, SeedableRng}; let env = Env::default(); env.mock_all_auths(); let owner = Address::generate(&env); - let (_, vault) = create_vault(&env); - let (usdc_address, _, _) = create_usdc(&env, &owner); + let (vault_address, vault) = create_vault(&env); + let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &owner); - let initial_balance: i128 = 1_000; - vault.init(&owner, &usdc_address, &Some(initial_balance), &None); - let mut expected = initial_balance; - let mut rng = rand::thread_rng(); + // Pre-fund vault and user so deposits/deducts work + fund_vault(&usdc_admin, &vault_address, 1_000); + vault.init(&owner, &usdc_address, &Some(1_000), &None, &None, &None); + let mut expected: i128 = 1_000; + let mut rng = StdRng::seed_from_u64(42); for _ in 0..500 { - if rng.gen_bool(0.5) { - let amount = rng.gen_range(1..=500); + let action: u8 = rng.gen_range(0..2); + + if action == 0 { + let amount: i128 = rng.gen_range(1..=500); + fund_user(&usdc_admin, &owner, amount); + approve_spend(&env, &usdc_client, &owner, &vault_address, amount); vault.deposit(&owner, &amount); expected += amount; } else if expected > 0 { - let amount = rng.gen_range(1..=expected.min(500)); + let amount: i128 = rng.gen_range(1..=expected.min(500)); vault.deduct(&owner, &amount, &None); expected -= amount; } @@ -819,66 +825,17 @@ fn fuzz_deposit_and_deduct() { assert_eq!(vault.balance(), expected); } -#[test] -fn deduct_returns_new_balance() { - let env = Env::default(); - env.mock_all_auths(); - - let owner = Address::generate(&env); - let (_, vault) = create_vault(&env); - let (usdc_address, _, _) = create_usdc(&env, &owner); - - vault.init(&owner, &usdc_address, &Some(100), &None); - let new_balance = vault.deduct(&owner, &30, &None); - assert_eq!(new_balance, 70); - assert_eq!(vault.balance(), 70); -} - -/// Fuzz test: random deposit/deduct sequence asserting balance >= 0 and matches expected. -#[test] -fn fuzz_deposit_and_deduct() { - use rand::rngs::StdRng; - use rand::{Rng, SeedableRng}; - - let env = Env::default(); - env.mock_all_auths(); - - let owner = Address::generate(&env); - let (_, vault) = create_vault(&env); - let (usdc_address, _, _) = create_usdc(&env, &owner); - - vault.init(&owner, &usdc_address, &Some(0), &None); - let mut expected: i128 = 0; - let mut rng = StdRng::seed_from_u64(42); - - for _ in 0..500 { - let action: u8 = rng.gen_range(0..2); - - if action == 0 { - let amount: i128 = rng.gen_range(1..=10_000); - vault.deposit(&owner, &amount); - expected += amount; - } else if expected > 0 { - let amount: i128 = rng.gen_range(1..=expected); - vault.deduct(&owner, &amount, &None); - expected -= amount; - } - - assert!(expected >= 0, "balance went negative"); - assert_eq!(vault.balance(), expected, "balance mismatch at iteration"); - } -} - #[test] fn batch_deduct_all_succeed() { let env = Env::default(); let owner = Address::generate(&env); let contract_id = env.register(CalloraVault {}, ()); let client = CalloraVaultClient::new(&env, &contract_id); - let (usdc_address, _, _) = create_usdc(&env, &owner); + let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); env.mock_all_auths(); - client.init(&owner, &usdc_address, &Some(60), &None); + fund_vault(&usdc_admin, &contract_id, 60); + client.init(&owner, &usdc_address, &Some(60), &None, &None, &None); let items = vec![ &env, DeductItem { @@ -908,10 +865,11 @@ fn batch_deduct_all_revert() { let owner = Address::generate(&env); let contract_id = env.register(CalloraVault {}, ()); let client = CalloraVaultClient::new(&env, &contract_id); - let (usdc_address, _, _) = create_usdc(&env, &owner); + let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); env.mock_all_auths(); - client.init(&owner, &usdc_address, &Some(25), &None); + fund_vault(&usdc_admin, &contract_id, 25); + client.init(&owner, &usdc_address, &Some(25), &None, &None, &None); assert_eq!(client.balance(), 25); let items = vec![ &env, @@ -939,10 +897,11 @@ fn batch_deduct_revert_preserves_balance() { let owner = Address::generate(&env); let contract_id = env.register(CalloraVault {}, ()); let client = CalloraVaultClient::new(&env, &contract_id); - let (usdc_address, _, _) = create_usdc(&env, &owner); + let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); env.mock_all_auths(); - client.init(&owner, &usdc_address, &Some(25), &None); + fund_vault(&usdc_admin, &contract_id, 25); + client.init(&owner, &usdc_address, &Some(25), &None, &None, &None); assert_eq!(client.balance(), 25); let items = vec![ &env, @@ -976,10 +935,13 @@ fn owner_unchanged_after_deposit_and_deduct() { let owner = Address::generate(&env); let contract_id = env.register(CalloraVault {}, ()); let client = CalloraVaultClient::new(&env, &contract_id); - let (usdc_address, _, _) = create_usdc(&env, &owner); + let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &owner); env.mock_all_auths(); - client.init(&owner, &usdc_address, &Some(100), &None); + fund_vault(&usdc_admin, &contract_id, 100); + client.init(&owner, &usdc_address, &Some(100), &None, &None, &None); + fund_user(&usdc_admin, &owner, 50); + approve_spend(&env, &usdc_client, &owner, &contract_id, 50); client.deposit(&owner, &50); client.deduct(&owner, &30, &None); From bfc82436076619f11af58522764e498651abd100 Mon Sep 17 00:00:00 2001 From: Jopsan-gm Date: Wed, 25 Feb 2026 19:03:17 -0600 Subject: [PATCH 4/4] sync: adopt upstream/main vault API rewrite (simplified init/deposit/deduct) --- contracts/vault/src/lib.rs | 451 +++++------------- contracts/vault/src/test.rs | 924 +++++++++++------------------------- 2 files changed, 395 insertions(+), 980 deletions(-) diff --git a/contracts/vault/src/lib.rs b/contracts/vault/src/lib.rs index a1a1965..0dc2220 100644 --- a/contracts/vault/src/lib.rs +++ b/contracts/vault/src/lib.rs @@ -1,38 +1,48 @@ -#![no_std] +//! # Callora Vault Contract +//! +//! ## Access Control +//! +//! The vault implements role-based access control for deposits: +//! +//! - **Owner**: Set at initialization, immutable. Always permitted to deposit. +//! - **Allowed Depositors**: Optional addresses (e.g., backend service) that can be +//! explicitly approved by the owner. Can be set, changed, or cleared at any time. +//! - **Other addresses**: Rejected with an authorization error. +//! +//! ### Production Usage +//! +//! In production, the owner typically represents the end user's account, while the +//! allowed depositors are backend services that handle automated deposits on behalf +//! of the user. +//! +//! ### Managing the Allowed Depositors +//! +//! - Add or update: `set_allowed_depositor(Some(address))` adds the address if not present +//! - Clear (revoke all access): `set_allowed_depositor(None)` +//! - Only the owner can call `set_allowed_depositor` +//! +//! ### Security Model +//! +//! - The owner has full control over who can deposit +//! - The allowed depositors are trusted addresses (typically backend services) +//! - Access can be revoked at any time by the owner +//! - All deposit attempts are authenticated against the caller's address -use soroban_sdk::{contract, contractimpl, contracttype, token, Address, Env, Symbol, Vec}; +#![no_std] -/// Single item for batch deduct: amount and optional request id for idempotency/tracking. -#[contracttype] -#[derive(Clone)] -pub struct DeductItem { - pub amount: i128, - pub request_id: Option, -} +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, Symbol, Vec}; #[contracttype] #[derive(Clone)] pub struct VaultMeta { pub owner: Address, pub balance: i128, - /// Minimum amount required per deposit; deposits below this panic. - pub min_deposit: i128, } -const META_KEY: &str = "meta"; -const USDC_KEY: &str = "usdc"; -const ADMIN_KEY: &str = "admin"; -const REVENUE_POOL_KEY: &str = "revenue_pool"; -const MAX_DEDUCT_KEY: &str = "max_deduct"; - -/// Default maximum single deduct amount when not set at init (no cap). -pub const DEFAULT_MAX_DEDUCT: i128 = i128::MAX; - #[contracttype] -#[derive(Clone, Debug, PartialEq)] -pub struct DistributeEvent { - pub to: Address, - pub amount: i128, +pub enum StorageKey { + Meta, + AllowedDepositors, } #[contract] @@ -40,374 +50,153 @@ pub struct CalloraVault; #[contractimpl] impl CalloraVault { - /// Initialize vault for an owner with optional initial balance and minimum deposit. - /// If initial_balance > 0, the contract must already hold at least that much USDC (e.g. deployer transferred in first). + /// Initialize vault for an owner with optional initial balance. /// Emits an "init" event with the owner address and initial balance. /// - /// # Arguments - /// * `revenue_pool` – Optional address to receive USDC on each deduct (e.g. settlement contract). If None, USDC stays in vault. - /// * `max_deduct` – Optional cap per single deduct; if None, uses DEFAULT_MAX_DEDUCT (no cap). - pub fn init( - env: Env, - owner: Address, - usdc_token: Address, - initial_balance: Option, - min_deposit: Option, - revenue_pool: Option
, - max_deduct: Option, - ) -> VaultMeta { - owner.require_auth(); - if env.storage().instance().has(&Symbol::new(&env, META_KEY)) { + /// # Panics + /// - If the vault is already initialized + /// - If `initial_balance` is negative + pub fn init(env: Env, owner: Address, initial_balance: Option) -> VaultMeta { + if env.storage().instance().has(&StorageKey::Meta) { panic!("vault already initialized"); } let balance = initial_balance.unwrap_or(0); - if balance > 0 { - let usdc = token::Client::new(&env, &usdc_token); - let contract_balance = usdc.balance(&env.current_contract_address()); - if contract_balance < balance { - panic!("insufficient USDC in contract for initial_balance"); - } - } - let min_deposit_val = min_deposit.unwrap_or(0); - let max_deduct_val = max_deduct.unwrap_or(DEFAULT_MAX_DEDUCT); - if max_deduct_val <= 0 { - panic!("max_deduct must be positive"); - } + assert!(balance >= 0, "initial balance must be non-negative"); let meta = VaultMeta { owner: owner.clone(), balance, - min_deposit: min_deposit_val, }; - env.storage() - .instance() - .set(&Symbol::new(&env, META_KEY), &meta); - env.storage() - .instance() - .set(&Symbol::new(&env, USDC_KEY), &usdc_token); - env.storage() - .instance() - .set(&Symbol::new(&env, ADMIN_KEY), &owner); - env.storage() - .instance() - .set(&Symbol::new(&env, REVENUE_POOL_KEY), &revenue_pool); - env.storage() - .instance() - .set(&Symbol::new(&env, MAX_DEDUCT_KEY), &max_deduct_val); + env.storage().instance().set(&StorageKey::Meta, &meta); + // Emit event: topics = (init, owner), data = balance env.events() - .publish((Symbol::new(&env, "init"), owner), balance); - + .publish((Symbol::new(&env, "init"), owner.clone()), balance); meta } - /// Return the current admin address. - pub fn get_admin(env: Env) -> Address { - env.storage() - .instance() - .get(&Symbol::new(&env, ADMIN_KEY)) - .unwrap_or_else(|| panic!("vault not initialized")) - } - - /// Replace the current admin. Only the existing admin may call this. - pub fn set_admin(env: Env, caller: Address, new_admin: Address) { - caller.require_auth(); - let current_admin = Self::get_admin(env.clone()); - if caller != current_admin { - panic!("unauthorized: caller is not admin"); + /// Check if the caller is authorized to deposit (owner or allowed depositor). + pub fn is_authorized_depositor(env: Env, caller: Address) -> bool { + let meta = Self::get_meta(env.clone()); + // Owner is always authorized + if caller == meta.owner { + return true; } - env.storage() - .instance() - .set(&Symbol::new(&env, ADMIN_KEY), &new_admin); - } - /// Return the maximum allowed amount for a single deduct (configurable at init). - pub fn get_max_deduct(env: Env) -> i128 { - env.storage() + // Check if caller is in the allowed depositors + let allowed: Vec
= env + .storage() .instance() - .get(&Symbol::new(&env, MAX_DEDUCT_KEY)) - .unwrap_or_else(|| panic!("vault not initialized")) + .get(&StorageKey::AllowedDepositors) + .unwrap_or(Vec::new(&env)); + allowed.contains(&caller) } - /// Return the revenue pool address if set (receives USDC on deduct). - pub fn get_revenue_pool(env: Env) -> Option
{ - env.storage() - .instance() - .get(&Symbol::new(&env, REVENUE_POOL_KEY)) - .unwrap_or(None) + /// Require that the caller is the owner, panic otherwise. + pub fn require_owner(env: Env, caller: Address) { + let meta = Self::get_meta(env.clone()); + assert!(caller == meta.owner, "unauthorized: owner only"); } - /// Distribute accumulated USDC to a single developer address. - /// - /// # Access control - /// Only the admin (backend / multisig) may call this. - /// - /// # Arguments - /// * `caller` – Must be the current admin address. - /// * `to` – Developer wallet to receive the USDC. - /// * `amount` – Amount in USDC micro-units (must be > 0 and ≤ vault balance). + /// Get vault metadata (owner and balance). /// /// # Panics - /// * `"unauthorized: caller is not admin"` – caller is not the admin. - /// * `"amount must be positive"` – amount is zero or negative. - /// * `"insufficient USDC balance"` – vault holds less than amount. - /// - /// # Events - /// Emits topic `("distribute", to)` with data `amount` on success. - pub fn distribute(env: Env, caller: Address, to: Address, amount: i128) { - // 1. Require on-chain signature from caller. - caller.require_auth(); - - // 2. Only the registered admin may distribute. - let admin = Self::get_admin(env.clone()); - if caller != admin { - panic!("unauthorized: caller is not admin"); - } - - // 3. Amount must be positive. - if amount <= 0 { - panic!("amount must be positive"); - } - - // 4. Load the USDC token address. - let usdc_address: Address = env - .storage() - .instance() - .get(&Symbol::new(&env, USDC_KEY)) - .unwrap_or_else(|| panic!("vault not initialized")); - - let usdc = token::Client::new(&env, &usdc_address); - - // 5. Check vault has enough USDC. - let vault_balance = usdc.balance(&env.current_contract_address()); - if vault_balance < amount { - panic!("insufficient USDC balance"); - } - - // 6. Transfer USDC from vault to developer. - usdc.transfer(&env.current_contract_address(), &to, &amount); - - // 7. Emit distribute event. - env.events() - .publish((Symbol::new(&env, "distribute"), to), amount); - } - - /// Get vault metadata (owner and balance). + /// - If the vault has not been initialized pub fn get_meta(env: Env) -> VaultMeta { env.storage() .instance() - .get(&Symbol::new(&env, META_KEY)) + .get(&StorageKey::Meta) .unwrap_or_else(|| panic!("vault not initialized")) } - /// Deposit: user transfers USDC to the contract; contract increases internal balance. - /// Caller must have authorized the transfer (token transfer_from). Supports multiple depositors. + /// Add or clear allowed depositors. Owner-only. + /// Pass `None` to clear all allowed depositors, `Some(address)` to add the address if not already present. + pub fn set_allowed_depositor(env: Env, caller: Address, depositor: Option
) { + caller.require_auth(); + Self::require_owner(env.clone(), caller.clone()); + + match depositor { + Some(addr) => { + let mut allowed: Vec
= env + .storage() + .instance() + .get(&StorageKey::AllowedDepositors) + .unwrap_or(Vec::new(&env)); + if !allowed.contains(&addr) { + allowed.push_back(addr); + } + env.storage() + .instance() + .set(&StorageKey::AllowedDepositors, &allowed); + } + None => { + env.storage() + .instance() + .remove(&StorageKey::AllowedDepositors); + } + } + } + + /// Deposit increases balance. Callable by owner or designated depositor. /// Emits a "deposit" event with the depositor address and amount. - pub fn deposit(env: Env, from: Address, amount: i128) -> i128 { - from.require_auth(); + pub fn deposit(env: Env, caller: Address, amount: i128) -> i128 { + caller.require_auth(); + assert!(amount > 0, "amount must be positive"); - let mut meta = Self::get_meta(env.clone()); assert!( - amount >= meta.min_deposit, - "deposit below minimum: {} < {}", - amount, - meta.min_deposit - ); - - let usdc_address: Address = env - .storage() - .instance() - .get(&Symbol::new(&env, USDC_KEY)) - .unwrap_or_else(|| panic!("vault not initialized")); - let usdc = token::Client::new(&env, &usdc_address); - usdc.transfer_from( - &env.current_contract_address(), - &from, - &env.current_contract_address(), - &amount, + Self::is_authorized_depositor(env.clone(), caller.clone()), + "unauthorized: only owner or allowed depositor can deposit" ); + let mut meta = Self::get_meta(env.clone()); meta.balance += amount; - env.storage() - .instance() - .set(&Symbol::new(&env, META_KEY), &meta); + env.storage().instance().set(&StorageKey::Meta, &meta); env.events() - .publish((Symbol::new(&env, "deposit"), from), amount); - + .publish((Symbol::new(&env, "deposit"), caller), amount); meta.balance } - /// Deduct balance for an API call. Callable by authorized caller (e.g. backend). - /// Amount must not exceed max single deduct (see init / get_max_deduct). - /// If revenue pool is set, USDC is transferred to it; otherwise it remains in the vault. - /// Emits a "deduct" event with caller, optional request_id, amount, and new balance. - pub fn deduct(env: Env, caller: Address, amount: i128, request_id: Option) -> i128 { + /// Deduct balance for an API call. Only owner/authorized caller in production. + pub fn deduct(env: Env, caller: Address, amount: i128) -> i128 { caller.require_auth(); - let max_deduct = Self::get_max_deduct(env.clone()); - assert!(amount > 0, "amount must be positive"); - assert!(amount <= max_deduct, "deduct amount exceeds max_deduct"); + Self::require_owner(env.clone(), caller); let mut meta = Self::get_meta(env.clone()); + assert!(amount > 0, "amount must be positive"); assert!(meta.balance >= amount, "insufficient balance"); - - let usdc_address: Address = env - .storage() - .instance() - .get(&Symbol::new(&env, USDC_KEY)) - .unwrap_or_else(|| panic!("vault not initialized")); - let revenue_pool: Option
= env - .storage() - .instance() - .get(&Symbol::new(&env, REVENUE_POOL_KEY)) - .unwrap_or(None); - meta.balance -= amount; - env.storage() - .instance() - .set(&Symbol::new(&env, META_KEY), &meta); - - if let Some(to) = revenue_pool { - let usdc = token::Client::new(&env, &usdc_address); - usdc.transfer(&env.current_contract_address(), &to, &amount); - } - - let topics = match &request_id { - Some(rid) => (Symbol::new(&env, "deduct"), caller.clone(), rid.clone()), - None => ( - Symbol::new(&env, "deduct"), - caller.clone(), - Symbol::new(&env, ""), - ), - }; - env.events().publish(topics, (amount, meta.balance)); + env.storage().instance().set(&StorageKey::Meta, &meta); meta.balance } - /// Batch deduct: multiple (amount, optional request_id) in one transaction. - /// Each amount must not exceed max_deduct. Reverts entire batch if any check fails. - /// If revenue pool is set, total deducted USDC is transferred to it once. - /// Emits one "deduct" event per item. - pub fn batch_deduct(env: Env, caller: Address, items: Vec) -> i128 { - caller.require_auth(); - let max_deduct = Self::get_max_deduct(env.clone()); - let mut meta = Self::get_meta(env.clone()); - let n = items.len(); - assert!(n > 0, "batch_deduct requires at least one item"); - - let mut total_deduct = 0i128; - let mut running = meta.balance; - for item in items.iter() { - assert!(item.amount > 0, "amount must be positive"); - assert!( - item.amount <= max_deduct, - "deduct amount exceeds max_deduct" - ); - assert!(running >= item.amount, "insufficient balance"); - running -= item.amount; - total_deduct += item.amount; - } - - let usdc_address: Address = env - .storage() - .instance() - .get(&Symbol::new(&env, USDC_KEY)) - .unwrap_or_else(|| panic!("vault not initialized")); - let revenue_pool: Option
= env - .storage() - .instance() - .get(&Symbol::new(&env, REVENUE_POOL_KEY)) - .unwrap_or(None); - - let mut balance = meta.balance; - for item in items.iter() { - balance -= item.amount; - let topics = match &item.request_id { - Some(rid) => (Symbol::new(&env, "deduct"), caller.clone(), rid.clone()), - None => ( - Symbol::new(&env, "deduct"), - caller.clone(), - Symbol::new(&env, ""), - ), - }; - env.events().publish(topics, (item.amount, balance)); - } - - meta.balance = balance; - env.storage() - .instance() - .set(&Symbol::new(&env, META_KEY), &meta); - - if total_deduct > 0 { - if let Some(to) = revenue_pool { - let usdc = token::Client::new(&env, &usdc_address); - usdc.transfer(&env.current_contract_address(), &to, &total_deduct); - } - } - - meta.balance + /// Return current balance. + pub fn balance(env: Env) -> i128 { + Self::get_meta(env).balance } - /// Withdraw from vault. Callable only by the vault owner; reduces balance and transfers USDC to owner. - pub fn withdraw(env: Env, amount: i128) -> i128 { + pub fn transfer_ownership(env: Env, new_owner: Address) { let mut meta = Self::get_meta(env.clone()); meta.owner.require_auth(); - assert!(amount > 0, "amount must be positive"); - assert!(meta.balance >= amount, "insufficient balance"); - - let usdc_address: Address = env - .storage() - .instance() - .get(&Symbol::new(&env, USDC_KEY)) - .unwrap_or_else(|| panic!("vault not initialized")); - let usdc = token::Client::new(&env, &usdc_address); - usdc.transfer(&env.current_contract_address(), &meta.owner, &amount); - - meta.balance -= amount; - env.storage() - .instance() - .set(&Symbol::new(&env, META_KEY), &meta); - env.events().publish( - (Symbol::new(&env, "withdraw"), meta.owner.clone()), - (amount, meta.balance), + // Validate new_owner is not the same as current owner + assert!( + new_owner != meta.owner, + "new_owner must be different from current owner" ); - meta.balance - } - - /// Withdraw from vault to a designated address. Owner-only; transfers USDC to `to`. - pub fn withdraw_to(env: Env, to: Address, amount: i128) -> i128 { - let mut meta = Self::get_meta(env.clone()); - meta.owner.require_auth(); - assert!(amount > 0, "amount must be positive"); - assert!(meta.balance >= amount, "insufficient balance"); - - let usdc_address: Address = env - .storage() - .instance() - .get(&Symbol::new(&env, USDC_KEY)) - .unwrap_or_else(|| panic!("vault not initialized")); - let usdc = token::Client::new(&env, &usdc_address); - usdc.transfer(&env.current_contract_address(), &to, &amount); - - meta.balance -= amount; - env.storage() - .instance() - .set(&Symbol::new(&env, META_KEY), &meta); + // Emit event before changing the owner, so we have the old owner + // topics = (transfer_ownership, old_owner, new_owner) env.events().publish( ( - Symbol::new(&env, "withdraw_to"), + Symbol::new(&env, "transfer_ownership"), meta.owner.clone(), - to.clone(), + new_owner.clone(), ), - (amount, meta.balance), + (), ); - meta.balance - } - /// Return current balance. - pub fn balance(env: Env) -> i128 { - Self::get_meta(env).balance + meta.owner = new_owner; + env.storage().instance().set(&StorageKey::Meta, &meta); } } diff --git a/contracts/vault/src/test.rs b/contracts/vault/src/test.rs index e333b35..646f453 100644 --- a/contracts/vault/src/test.rs +++ b/contracts/vault/src/test.rs @@ -1,150 +1,85 @@ extern crate std; use super::*; -use soroban_sdk::testutils::{Address as _, Events as _}; -use soroban_sdk::{token, vec, IntoVal, Symbol}; - -fn create_usdc<'a>( - env: &'a Env, - admin: &Address, -) -> (Address, token::Client<'a>, token::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); - (address, client, admin_client) -} - -fn create_vault(env: &Env) -> (Address, CalloraVaultClient<'_>) { - let address = env.register(CalloraVault, ()); - let client = CalloraVaultClient::new(env, &address); - (address, client) -} - -fn fund_vault( - usdc_admin_client: &token::StellarAssetClient, - vault_address: &Address, - amount: i128, -) { - usdc_admin_client.mint(vault_address, &amount); -} - -fn fund_user(usdc_admin_client: &token::StellarAssetClient, user: &Address, amount: i128) { - usdc_admin_client.mint(user, &amount); -} +use soroban_sdk::testutils::Address as _; +use soroban_sdk::testutils::Events as _; +use soroban_sdk::Env; +use soroban_sdk::{IntoVal, Symbol}; -/// Approve spender to transfer amount from from (for deposit tests; from must have auth). -fn approve_spend( - _env: &Env, - usdc_client: &token::Client, - from: &Address, - spender: &Address, - amount: i128, -) { - // expiration_ledger 0 = no expiration in Stellar Asset Contract - usdc_client.approve(from, spender, &amount, &0u32); -} - -/// Logs approximate CPU/instruction and fee for init, deposit, deduct, and balance. -/// Run with: cargo test --ignored vault_operation_costs -- --nocapture -/// Requires invocation cost metering; may panic on default test env. #[test] -#[ignore] -fn vault_operation_costs() { +fn init_and_balance() { let env = Env::default(); let owner = Address::generate(&env); - // Register contract instance with a unique salt (owner) to avoid address reuse - let contract_id = env.register(CalloraVault {}, (owner.clone(),)); - let client = CalloraVaultClient::new(&env, &contract_id); - let (usdc, _, _) = create_usdc(&env, &owner); + let contract_id = env.register(CalloraVault, ()); - env.mock_all_auths(); + // Call init directly inside as_contract so events are captured + let events = env.as_contract(&contract_id, || { + CalloraVault::init(env.clone(), owner.clone(), Some(1000)); + env.events().all() + }); - client.init(&owner, &usdc, &Some(0), &None, &None, &None); - let res = env.cost_estimate().resources(); - let fee = env.cost_estimate().fee(); - std::println!( - "init: instructions={} fee_total={}", - res.instructions, - fee.total - ); + // Verify balance through client + let client = CalloraVaultClient::new(&env, &contract_id); + assert_eq!(client.balance(), 1000); - client.deposit(&owner, &100); - let res = env.cost_estimate().resources(); - let fee = env.cost_estimate().fee(); - std::println!( - "deposit: instructions={} fee_total={}", - res.instructions, - fee.total - ); + // Verify "init" event was emitted + let last_event = events.last().expect("expected at least one event"); - client.deduct(&owner, &50, &None); - let res = env.cost_estimate().resources(); - let fee = env.cost_estimate().fee(); - std::println!( - "deduct: instructions={} fee_total={}", - res.instructions, - fee.total - ); + // Contract ID matches + assert_eq!(last_event.0, contract_id); - let _ = client.balance(); - let res = env.cost_estimate().resources(); - let fee = env.cost_estimate().fee(); - std::println!( - "balance: instructions={} fee_total={}", - res.instructions, - fee.total - ); + // Topic 0 = Symbol("init"), Topic 1 = owner address + let topics = &last_event.1; + assert_eq!(topics.len(), 2); + let topic0: Symbol = topics.get(0).unwrap().into_val(&env); + let topic1: Address = topics.get(1).unwrap().into_val(&env); + assert_eq!(topic0, Symbol::new(&env, "init")); + assert_eq!(topic1, owner); + + // Data = initial balance as i128 + let data: i128 = last_event.2.into_val(&env); + assert_eq!(data, 1000); } #[test] -fn init_and_balance() { +fn init_default_zero_balance() { let env = Env::default(); let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); - + let contract_id = env.register(CalloraVault, ()); let client = CalloraVaultClient::new(&env, &contract_id); - let (usdc, _, usdc_admin) = create_usdc(&env, &owner); - env.mock_all_auths(); - fund_vault(&usdc_admin, &contract_id, 1000); - client.init(&owner, &usdc, &Some(1000), &None, &None, &None); - let _events = env.events().all(); - assert_eq!(client.balance(), 1000); + env.mock_all_auths(); + client.init(&owner, &None); + assert_eq!(client.balance(), 0); } #[test] fn deposit_and_deduct() { let env = Env::default(); let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); + let contract_id = env.register(CalloraVault, ()); let client = CalloraVaultClient::new(&env, &contract_id); - let (usdc, usdc_client, usdc_admin) = create_usdc(&env, &owner); + client.init(&owner, &Some(100)); + env.mock_all_auths(); - fund_vault(&usdc_admin, &contract_id, 100); - client.init(&owner, &usdc, &Some(100), &None, &None, &None); - fund_user(&usdc_admin, &owner, 200); - approve_spend(&env, &usdc_client, &owner, &contract_id, 200); client.deposit(&owner, &200); assert_eq!(client.balance(), 300); - client.deduct(&owner, &50, &None); + + client.deduct(&owner, &50); assert_eq!(client.balance(), 250); } -/// Test that verifies consistency between balance() and get_meta() after init, deposit, and deduct. -/// This ensures that both methods return the same balance value and that the owner remains unchanged. #[test] -fn balance_and_meta_consistency() { +fn owner_can_deposit() { let env = Env::default(); let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); + let contract_id = env.register(CalloraVault, ()); let client = CalloraVaultClient::new(&env, &contract_id); + // Initialize vault with initial balance env.mock_all_auths(); - let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &owner); - fund_vault(&usdc_admin, &contract_id, 500); - client.init(&owner, &usdc_address, &Some(500), &None, &None, &None); + client.init(&owner, &Some(500)); let meta = client.get_meta(); let balance = client.balance(); @@ -152,24 +87,26 @@ fn balance_and_meta_consistency() { assert_eq!(meta.owner, owner, "owner changed after init"); assert_eq!(balance, 500, "incorrect balance after init"); - fund_user(&usdc_admin, &owner, 425); - approve_spend(&env, &usdc_client, &owner, &contract_id, 425); client.deposit(&owner, &300); let meta = client.get_meta(); let balance = client.balance(); assert_eq!(meta.balance, balance, "balance mismatch after deposit"); assert_eq!(balance, 800, "incorrect balance after deposit"); - client.deduct(&owner, &150, &None); + // Deduct and verify consistency + client.deduct(&owner, &150); + client.deduct(&owner, &150); let meta = client.get_meta(); let balance = client.balance(); assert_eq!(meta.balance, balance, "balance mismatch after deduct"); - assert_eq!(balance, 650, "incorrect balance after deduct"); + assert_eq!(balance, 500, "incorrect balance after deduct"); - fund_user(&usdc_admin, &owner, 125); - approve_spend(&env, &usdc_client, &owner, &contract_id, 125); + // Perform multiple operations and verify final state client.deposit(&owner, &100); - client.deduct(&owner, &50, &None); + client.deduct(&owner, &50); + client.deposit(&owner, &25); + client.deposit(&owner, &100); + client.deduct(&owner, &50); client.deposit(&owner, &25); let meta = client.get_meta(); let balance = client.balance(); @@ -177,7 +114,7 @@ fn balance_and_meta_consistency() { meta.balance, balance, "balance mismatch after multiple operations" ); - assert_eq!(balance, 725, "incorrect final balance"); + assert_eq!(balance, 650, "incorrect final balance"); } #[test] @@ -185,582 +122,329 @@ fn balance_and_meta_consistency() { fn deduct_exact_balance_and_panic() { let env = Env::default(); let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); + let contract_id = env.register(CalloraVault, ()); let client = CalloraVaultClient::new(&env, &contract_id); - let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); env.mock_all_auths(); - fund_vault(&usdc_admin, &contract_id, 100); - client.init(&owner, &usdc_address, &Some(100), &None, &None, &None); + client.init(&owner, &Some(100)); assert_eq!(client.balance(), 100); - client.deduct(&owner, &100, &None); + // Deduct exact balance + client.deduct(&owner, &100); assert_eq!(client.balance(), 0); - client.deduct(&owner, &1, &None); + // Further deduct should panic + client.deduct(&owner, &1); } #[test] -fn deduct_event_emission() { +fn allowed_depositor_can_deposit() { let env = Env::default(); let owner = Address::generate(&env); - let caller = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); + let depositor = Address::generate(&env); + let contract_id = env.register(CalloraVault, ()); let client = CalloraVaultClient::new(&env, &contract_id); - let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); - env.mock_all_auths(); - fund_vault(&usdc_admin, &contract_id, 1000); - client.init(&owner, &usdc_address, &Some(1000), &None, &None, &None); - let req_id = Symbol::new(&env, "req123"); - - // Call client directly to avoid re-entry panic inside as_contract - client.deduct(&caller, &200, &Some(req_id.clone())); + client.init(&owner, &Some(100)); - let events = env.events().all(); - - let last_event = events.last().unwrap(); - assert_eq!(last_event.0, contract_id); + // Owner sets the allowed depositor + env.mock_all_auths(); + client.set_allowed_depositor(&owner, &Some(depositor.clone())); - let topics = &last_event.1; - assert_eq!(topics.len(), 3); - let topic0: Symbol = topics.get(0).unwrap().into_val(&env); - assert_eq!(topic0, Symbol::new(&env, "deduct")); - let topic_caller: Address = topics.get(1).unwrap().into_val(&env); - assert_eq!(topic_caller, caller); - let topic_req_id: Symbol = topics.get(2).unwrap().into_val(&env); - assert_eq!(topic_req_id, req_id); - - let data: (i128, i128) = last_event.2.into_val(&env); - assert_eq!(data, (200, 800)); + // Depositor can now deposit + client.deposit(&depositor, &50); + assert_eq!(client.balance(), 150); } #[test] -fn test_init_success() { +#[should_panic(expected = "unauthorized: only owner or allowed depositor can deposit")] +fn unauthorized_address_cannot_deposit() { let env = Env::default(); - env.mock_all_auths(); - let owner = Address::generate(&env); - let (_, vault) = create_vault(&env); - let (usdc_address, _, _) = create_usdc(&env, &owner); + let contract_id = env.register(CalloraVault, ()); + let client = CalloraVaultClient::new(&env, &contract_id); - let meta = vault.init(&owner, &usdc_address, &None, &None, &None, &None); + client.init(&owner, &Some(100)); - assert_eq!(meta.owner, owner); - assert_eq!(meta.balance, 0); + // Try to deposit as unauthorized address (should panic) + env.mock_all_auths(); + let unauthorized_addr = Address::generate(&env); + client.deposit(&unauthorized_addr, &50); } #[test] -#[should_panic(expected = "vault already initialized")] -fn test_init_double_panics() { +fn owner_can_set_allowed_depositor() { let env = Env::default(); - env.mock_all_auths(); - let owner = Address::generate(&env); - let (_, vault) = create_vault(&env); - let (usdc_address, _, _) = create_usdc(&env, &owner); + let depositor = Address::generate(&env); + let contract_id = env.register(CalloraVault, ()); + let client = CalloraVaultClient::new(&env, &contract_id); - vault.init(&owner, &usdc_address, &None, &None, &None, &None); - vault.init(&owner, &usdc_address, &None, &None, &None, &None); -} + client.init(&owner, &Some(100)); -#[test] -fn test_distribute_success() { - let env = Env::default(); + // Owner sets allowed depositor env.mock_all_auths(); + client.set_allowed_depositor(&owner, &Some(depositor.clone())); - let admin = Address::generate(&env); - let developer = Address::generate(&env); - let (vault_address, vault) = create_vault(&env); - let (usdc_address, usdc_client, usdc_admin_client) = create_usdc(&env, &admin); - - fund_vault(&usdc_admin_client, &vault_address, 1_000); - vault.init(&admin, &usdc_address, &None, &None, &None, &None); - vault.distribute(&admin, &developer, &400); - - assert_eq!(usdc_client.balance(&vault_address), 600); - assert_eq!(usdc_client.balance(&developer), 400); + // Depositor can deposit + client.deposit(&depositor, &25); + assert_eq!(client.balance(), 125); } #[test] -#[should_panic(expected = "insufficient USDC balance")] -fn test_distribute_excess_panics() { +fn owner_can_clear_allowed_depositor() { let env = Env::default(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - let developer = Address::generate(&env); - let (vault_address, vault) = create_vault(&env); - let (usdc_address, _, usdc_admin_client) = create_usdc(&env, &admin); + let owner = Address::generate(&env); + let depositor = Address::generate(&env); + let contract_id = env.register(CalloraVault, ()); + let client = CalloraVaultClient::new(&env, &contract_id); - fund_vault(&usdc_admin_client, &vault_address, 100); - vault.init(&admin, &usdc_address, &None, &None, &None, &None); - vault.distribute(&admin, &developer, &101); -} + client.init(&owner, &Some(100)); -#[test] -#[should_panic(expected = "amount must be positive")] -fn test_distribute_zero_panics() { - let env = Env::default(); env.mock_all_auths(); - let admin = Address::generate(&env); - let developer = Address::generate(&env); - let (_, vault) = create_vault(&env); - let (usdc_address, _, _) = create_usdc(&env, &admin); + // Set depositor + client.set_allowed_depositor(&owner, &Some(depositor.clone())); + client.deposit(&depositor, &50); + assert_eq!(client.balance(), 150); - vault.init(&admin, &usdc_address, &None, &None, &None, &None); - vault.distribute(&admin, &developer, &0); -} + // Clear depositor + client.set_allowed_depositor(&owner, &None); -#[test] -#[should_panic(expected = "amount must be positive")] -fn test_distribute_negative_panics() { - let env = Env::default(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - let developer = Address::generate(&env); - let (_, vault) = create_vault(&env); - let (usdc_address, _, _) = create_usdc(&env, &admin); - - vault.init(&admin, &usdc_address, &None, &None, &None, &None); - vault.distribute(&admin, &developer, &-1); + // Owner can still deposit + client.deposit(&owner, &25); + assert_eq!(client.balance(), 175); } #[test] -#[should_panic(expected = "unauthorized: caller is not admin")] -fn test_distribute_unauthorized_panics() { +#[should_panic(expected = "unauthorized: owner only")] +fn non_owner_cannot_set_allowed_depositor() { let env = Env::default(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - let attacker = Address::generate(&env); - let developer = Address::generate(&env); - let (vault_address, vault) = create_vault(&env); - let (usdc_address, _, usdc_admin_client) = create_usdc(&env, &admin); + let owner = Address::generate(&env); + let depositor = Address::generate(&env); + let contract_id = env.register(CalloraVault, ()); + let client = CalloraVaultClient::new(&env, &contract_id); - fund_vault(&usdc_admin_client, &vault_address, 1_000); - vault.init(&admin, &usdc_address, &None, &None, &None, &None); - vault.distribute(&attacker, &developer, &500); -} + client.init(&owner, &Some(100)); -#[test] -fn test_distribute_full_balance() { - let env = Env::default(); + // Try to set allowed depositor as non-owner (should panic) env.mock_all_auths(); - - let admin = Address::generate(&env); - let developer = Address::generate(&env); - let (vault_address, vault) = create_vault(&env); - let (usdc_address, usdc_client, usdc_admin_client) = create_usdc(&env, &admin); - - fund_vault(&usdc_admin_client, &vault_address, 777); - vault.init(&admin, &usdc_address, &None, &None, &None, &None); - vault.distribute(&admin, &developer, &777); - - assert_eq!(usdc_client.balance(&vault_address), 0); - assert_eq!(usdc_client.balance(&developer), 777); + let non_owner_addr = Address::generate(&env); + client.set_allowed_depositor(&non_owner_addr, &Some(depositor)); } #[test] -fn test_distribute_multiple_times() { +#[should_panic(expected = "unauthorized: only owner or allowed depositor can deposit")] +fn deposit_after_depositor_cleared_is_rejected() { let env = Env::default(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - let dev_a = Address::generate(&env); - let dev_b = Address::generate(&env); - let (vault_address, vault) = create_vault(&env); - let (usdc_address, usdc_client, usdc_admin_client) = create_usdc(&env, &admin); + let owner = Address::generate(&env); + let depositor = Address::generate(&env); + let contract_id = env.register(CalloraVault, ()); + let client = CalloraVaultClient::new(&env, &contract_id); - fund_vault(&usdc_admin_client, &vault_address, 1_000); - vault.init(&admin, &usdc_address, &None, &None, &None, &None); - vault.distribute(&admin, &dev_a, &300); - vault.distribute(&admin, &dev_b, &200); + client.init(&owner, &Some(100)); - assert_eq!(usdc_client.balance(&vault_address), 500); - assert_eq!(usdc_client.balance(&dev_a), 300); - assert_eq!(usdc_client.balance(&dev_b), 200); -} - -#[test] -fn test_set_admin_transfers_control() { - let env = Env::default(); env.mock_all_auths(); - let original_admin = Address::generate(&env); - let new_admin = Address::generate(&env); - let developer = Address::generate(&env); - let (vault_address, vault) = create_vault(&env); - let (usdc_address, usdc_client, usdc_admin_client) = create_usdc(&env, &original_admin); - - fund_vault(&usdc_admin_client, &vault_address, 500); - vault.init(&original_admin, &usdc_address, &None, &None, &None, &None); - vault.set_admin(&original_admin, &new_admin); + // Set and then clear depositor + client.set_allowed_depositor(&owner, &Some(depositor.clone())); + client.set_allowed_depositor(&owner, &None); - assert_eq!(vault.get_admin(), new_admin); - - vault.distribute(&new_admin, &developer, &100); - assert_eq!(usdc_client.balance(&developer), 100); + // Depositor should no longer be able to deposit + client.deposit(&depositor, &50); } #[test] -#[should_panic(expected = "unauthorized: caller is not admin")] -fn test_old_admin_cannot_distribute_after_transfer() { +#[should_panic(expected = "amount must be positive")] +fn deposit_zero_panics() { let env = Env::default(); - env.mock_all_auths(); - - let original_admin = Address::generate(&env); - let new_admin = Address::generate(&env); - let developer = Address::generate(&env); - let (vault_address, vault) = create_vault(&env); - let (usdc_address, _, usdc_admin_client) = create_usdc(&env, &original_admin); - - fund_vault(&usdc_admin_client, &vault_address, 500); - vault.init(&original_admin, &usdc_address, &None, &None, &None, &None); - vault.set_admin(&original_admin, &new_admin); - vault.distribute(&original_admin, &developer, &100); -} + let owner = Address::generate(&env); + let contract_id = env.register(CalloraVault, ()); + let client = CalloraVaultClient::new(&env, &contract_id); -#[test] -fn test_deposit_and_balance() { - let env = Env::default(); env.mock_all_auths(); - - let owner = Address::generate(&env); - let (vault_address, vault) = create_vault(&env); - let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &owner); - - vault.init(&owner, &usdc_address, &Some(0), &None, &None, &None); - fund_user(&usdc_admin, &owner, 250); - approve_spend(&env, &usdc_client, &owner, &vault_address, 250); - vault.deposit(&owner, &200); - assert_eq!(vault.balance(), 200); - vault.deposit(&owner, &50); - assert_eq!(vault.balance(), 250); + client.init(&owner, &Some(1000)); + client.deposit(&owner, &0); } #[test] -fn test_deduct_success() { +#[should_panic(expected = "amount must be positive")] +fn deposit_negative_panics() { let env = Env::default(); - env.mock_all_auths(); - let owner = Address::generate(&env); - let (vault_address, vault) = create_vault(&env); - let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); - - fund_vault(&usdc_admin, &vault_address, 300); - vault.init(&owner, &usdc_address, &Some(300), &None, &None, &None); - vault.deduct(&owner, &100, &None); - assert_eq!(vault.balance(), 200); -} + let contract_id = env.register(CalloraVault, ()); + let client = CalloraVaultClient::new(&env, &contract_id); -#[test] -#[should_panic(expected = "deduct amount exceeds max_deduct")] -fn test_deduct_above_max_deduct_panics() { - let env = Env::default(); env.mock_all_auths(); - - let owner = Address::generate(&env); - let (vault_address, vault) = create_vault(&env); - let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); - - fund_vault(&usdc_admin, &vault_address, 10_000); - vault.init( - &owner, - &usdc_address, - &Some(10_000), - &None, - &None, - &Some(100), - ); - assert_eq!(vault.get_max_deduct(), 100); - vault.deduct(&owner, &100, &None); - assert_eq!(vault.balance(), 9_900); - vault.deduct(&owner, &101, &None); + client.init(&owner, &Some(100)); + client.deposit(&owner, &-100); } #[test] -#[should_panic(expected = "insufficient balance")] -fn test_deduct_excess_panics() { +#[should_panic(expected = "amount must be positive")] +fn deduct_zero_panics() { let env = Env::default(); - env.mock_all_auths(); - let owner = Address::generate(&env); - let (vault_address, vault) = create_vault(&env); - let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); - - fund_vault(&usdc_admin, &vault_address, 50); - vault.init(&owner, &usdc_address, &Some(50), &None, &None, &None); - vault.deduct(&owner, &100, &None); -} + let contract_id = env.register(CalloraVault, ()); + let client = CalloraVaultClient::new(&env, &contract_id); -#[test] -fn test_get_meta_returns_correct_values() { - let env = Env::default(); env.mock_all_auths(); - - let owner = Address::generate(&env); - let (vault_address, vault) = create_vault(&env); - let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); - - fund_vault(&usdc_admin, &vault_address, 999); - vault.init(&owner, &usdc_address, &Some(999), &None, &None, &None); - let meta = vault.get_meta(); - assert_eq!(meta.owner, owner); - assert_eq!(meta.balance, 999); + client.init(&owner, &Some(500)); + client.deduct(&owner, &0); } #[test] -fn test_multiple_depositors() { +#[should_panic(expected = "amount must be positive")] +fn deduct_negative_panics() { let env = Env::default(); - env.mock_all_auths(); - let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); + let contract_id = env.register(CalloraVault, ()); let client = CalloraVaultClient::new(&env, &contract_id); - let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &owner); - env.mock_all_auths(); - - let dep1 = Address::generate(&env); - let dep2 = Address::generate(&env); - fund_user(&usdc_admin, &dep1, 100); - fund_user(&usdc_admin, &dep2, 200); - approve_spend(&env, &usdc_client, &dep1, &contract_id, 100); - approve_spend(&env, &usdc_client, &dep2, &contract_id, 200); - - client.init(&owner, &usdc_address, &None, &None, &None, &None); - client.deposit(&dep1, &100); - client.deposit(&dep2, &200); - - // Both depositors contributed: total balance must equal 300 - assert_eq!(client.balance(), 300); -} -/// Verifies that a deposit below the configured minimum deposit panics. -/// Issue #43: Enforce Minimum Deposit Amount (Configurable) -#[test] -#[should_panic(expected = "deposit below minimum")] -fn deposit_below_minimum_panics() { - let env = Env::default(); env.mock_all_auths(); - - let owner = Address::generate(&env); - let (_, vault) = create_vault(&env); - let (usdc_address, _, _) = create_usdc(&env, &owner); - - // Initialize vault with a minimum deposit of 100 - vault.init(&owner, &usdc_address, &None, &Some(100), &None, &None); - - // Attempt a deposit of 99, which is below the minimum — must panic - vault.deposit(&owner, &99); + client.init(&owner, &Some(100)); + client.deduct(&owner, &-50); } -/// Verifies that a deposit exactly equal to the minimum deposit succeeds. -/// Issue #43: Enforce Minimum Deposit Amount (Configurable) #[test] -fn deposit_equal_to_minimum_succeeds() { +#[should_panic(expected = "insufficient balance")] +fn deduct_exceeds_balance_panics() { let env = Env::default(); - env.mock_all_auths(); - let owner = Address::generate(&env); - let (vault_address, vault) = create_vault(&env); - let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &owner); - - fund_user(&usdc_admin, &owner, 100); - approve_spend(&env, &usdc_client, &owner, &vault_address, 100); - - // Initialize vault with a minimum deposit of 50 - vault.init(&owner, &usdc_address, &None, &Some(50), &None, &None); + let contract_id = env.register(CalloraVault, ()); + let client = CalloraVaultClient::new(&env, &contract_id); - // A deposit exactly at the minimum must succeed - let new_balance = vault.deposit(&owner, &50); - assert_eq!(new_balance, 50); - assert_eq!(vault.balance(), 50); + env.mock_all_auths(); + client.init(&owner, &Some(50)); + client.deduct(&owner, &100); } -/// Verifies that when min_deposit is 0 (disabled), any positive deposit is accepted. -/// Issue #43: Enforce Minimum Deposit Amount (Configurable) #[test] -fn deposit_with_zero_minimum_always_succeeds() { +fn test_transfer_ownership() { let env = Env::default(); env.mock_all_auths(); let owner = Address::generate(&env); - let (vault_address, vault) = create_vault(&env); - let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &owner); + let new_owner = Address::generate(&env); + let contract_id = env.register(CalloraVault, ()); + let client = CalloraVaultClient::new(&env, &contract_id); - fund_user(&usdc_admin, &owner, 100); - approve_spend(&env, &usdc_client, &owner, &vault_address, 100); + client.init(&owner, &Some(100)); - // min_deposit = 0 means no restriction - vault.init(&owner, &usdc_address, &None, &Some(0), &None, &None); + // transfer ownership via client + client.transfer_ownership(&new_owner); - // Even a deposit of 1 must work - let new_balance = vault.deposit(&owner, &1); - assert_eq!(new_balance, 1); - assert_eq!(vault.balance(), 1); -} + let transfer_event = env + .events() + .all() + .into_iter() + .find(|e| { + e.0 == contract_id && { + let topics = &e.1; + if !topics.is_empty() { + let topic_name: Symbol = topics.get(0).unwrap().into_val(&env); + topic_name == Symbol::new(&env, "transfer_ownership") + } else { + false + } + } + }) + .expect("expected transfer event"); -#[test] -fn batch_deduct_success() { - let env = Env::default(); - let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); - let client = CalloraVaultClient::new(&env, &contract_id); - let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); + let topics = &transfer_event.1; + let topic_old_owner: Address = topics.get(1).unwrap().into_val(&env); + assert_eq!(topic_old_owner, owner); - env.mock_all_auths(); - fund_vault(&usdc_admin, &contract_id, 1000); - client.init(&owner, &usdc_address, &Some(1000), &None, &None, &None); - let req1 = Symbol::new(&env, "req1"); - let req2 = Symbol::new(&env, "req2"); - let items = vec![ - &env, - DeductItem { - amount: 100, - request_id: Some(req1.clone()), - }, - DeductItem { - amount: 200, - request_id: Some(req2.clone()), - }, - DeductItem { - amount: 50, - request_id: None, - }, - ]; - let caller = Address::generate(&env); - env.mock_all_auths(); - let new_balance = client.batch_deduct(&caller, &items); - assert_eq!(new_balance, 650); - assert_eq!(client.balance(), 650); + let topic_new_owner: Address = topics.get(2).unwrap().into_val(&env); + assert_eq!(topic_new_owner, new_owner); } #[test] -#[should_panic(expected = "insufficient balance")] -fn batch_deduct_reverts_entire_batch() { +#[should_panic(expected = "new_owner must be different from current owner")] +fn test_transfer_ownership_same_address_fails() { let env = Env::default(); - let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); - let client = CalloraVaultClient::new(&env, &contract_id); - let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); - - env.mock_all_auths(); - fund_vault(&usdc_admin, &contract_id, 100); - client.init(&owner, &usdc_address, &Some(100), &None, &None, &None); - let items = vec![ - &env, - DeductItem { - amount: 60, - request_id: None, - }, - DeductItem { - amount: 60, - request_id: None, - }, // total 120 > 100 - ]; - let caller = Address::generate(&env); env.mock_all_auths(); - client.batch_deduct(&caller, &items); -} -#[test] -fn withdraw_owner_success() { - let env = Env::default(); let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); + let contract_id = env.register(CalloraVault, ()); let client = CalloraVaultClient::new(&env, &contract_id); - let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); - - env.mock_all_auths(); - fund_vault(&usdc_admin, &contract_id, 500); - client.init(&owner, &usdc_address, &Some(500), &None, &None, &None); - let new_balance = client.withdraw(&200); - assert_eq!(new_balance, 300); - assert_eq!(client.balance(), 300); -} -#[test] -fn withdraw_exact_balance() { - let env = Env::default(); - let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); - let client = CalloraVaultClient::new(&env, &contract_id); - let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); + client.init(&owner, &Some(100)); - env.mock_all_auths(); - fund_vault(&usdc_admin, &contract_id, 100); - client.init(&owner, &usdc_address, &Some(100), &None, &None, &None); - let new_balance = client.withdraw(&100); - assert_eq!(new_balance, 0); - assert_eq!(client.balance(), 0); + // This should panic because new_owner is the same as current owner + client.transfer_ownership(&owner); } #[test] #[should_panic(expected = "insufficient balance")] -fn withdraw_exceeds_balance_fails() { +fn deduct_greater_than_balance_panics() { let env = Env::default(); let owner = Address::generate(&env); let contract_id = env.register(CalloraVault {}, ()); let client = CalloraVaultClient::new(&env, &contract_id); - let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); + client.init(&owner, &Some(100)); + + // Mock the owner as the invoker env.mock_all_auths(); - fund_vault(&usdc_admin, &contract_id, 50); - client.init(&owner, &usdc_address, &Some(50), &None, &None, &None); - client.withdraw(&100); + + // This should panic with "insufficient balance" + client.deduct(&owner, &101); } #[test] -fn withdraw_to_success() { +fn balance_unchanged_after_failed_deduct() { let env = Env::default(); let owner = Address::generate(&env); - let to = Address::generate(&env); let contract_id = env.register(CalloraVault {}, ()); let client = CalloraVaultClient::new(&env, &contract_id); - let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); + // Initialize with balance of 100 + client.init(&owner, &Some(100)); + assert_eq!(client.balance(), 100); + + // Mock the owner as the invoker env.mock_all_auths(); - fund_vault(&usdc_admin, &contract_id, 500); - client.init(&owner, &usdc_address, &Some(500), &None, &None, &None); - let new_balance = client.withdraw_to(&to, &150); - assert_eq!(new_balance, 350); - assert_eq!(client.balance(), 350); + + // Attempt to deduct more than balance, which should panic + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + client.deduct(&owner, &101); + })); + + // Verify the operation panicked + assert!(result.is_err()); + + // Verify balance is still 100 (unchanged after the failed deduct) + assert_eq!(client.balance(), 100); } #[test] #[should_panic] -fn withdraw_without_auth_fails() { +fn test_transfer_ownership_not_owner() { let env = Env::default(); + let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); + let new_owner = Address::generate(&env); + let contract_id = env.register(CalloraVault, ()); let client = CalloraVaultClient::new(&env, &contract_id); - let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); - fund_vault(&usdc_admin, &contract_id, 100); + // Mock auth for init env.mock_auths(&[soroban_sdk::testutils::MockAuth { address: &owner, invoke: &soroban_sdk::testutils::MockAuthInvoke { contract: &contract_id, fn_name: "init", - args: ( - &owner, - &usdc_address, - Some(100i128), - Option::::None, - Option::
::None, - Option::::None, - ) - .into_val(&env), + args: (&owner, &Some(100i128)).into_val(&env), sub_invokes: &[], }, }]); - client.init(&owner, &usdc_address, &Some(100), &None, &None, &None); + client.init(&owner, &Some(100)); + + env.mock_auths(&[]); // Clear mock auths so subsequent calls require explicit valid signatures - client.withdraw(&50); + // This should panic because neither `owner` nor `not_owner` has provided a valid mock signature. + client.transfer_ownership(&new_owner); } #[test] @@ -768,52 +452,45 @@ fn withdraw_without_auth_fails() { fn init_already_initialized_panics() { let env = Env::default(); let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); + let contract_id = env.register(CalloraVault, ()); let client = CalloraVaultClient::new(&env, &contract_id); env.mock_all_auths(); - let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); - fund_vault(&usdc_admin, &contract_id, 100); - client.init(&owner, &usdc_address, &Some(100), &None, &None, &None); - client.init(&owner, &usdc_address, &Some(200), &None, &None, &None); + client.init(&owner, &Some(100)); + client.init(&owner, &Some(200)); // Should panic } /// Fuzz test: random deposit/deduct sequence asserting balance >= 0 and matches expected. /// Run with: cargo test --package callora-vault fuzz_deposit_and_deduct -- --nocapture #[test] fn fuzz_deposit_and_deduct() { - use rand::rngs::StdRng; - use rand::{Rng, SeedableRng}; + use rand::Rng; let env = Env::default(); env.mock_all_auths(); let owner = Address::generate(&env); - let (vault_address, vault) = create_vault(&env); - let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &owner); + let contract_id = env.register(CalloraVault, ()); + let client = CalloraVaultClient::new(&env, &contract_id); - // Pre-fund vault and user so deposits/deducts work - fund_vault(&usdc_admin, &vault_address, 1_000); - vault.init(&owner, &usdc_address, &Some(1_000), &None, &None, &None); - let mut expected: i128 = 1_000; - let mut rng = StdRng::seed_from_u64(42); + let initial_balance: i128 = 1_000; + client.init(&owner, &Some(initial_balance)); - for _ in 0..500 { - let action: u8 = rng.gen_range(0..2); + let mut expected = initial_balance; + let mut rng = rand::thread_rng(); - if action == 0 { - let amount: i128 = rng.gen_range(1..=500); - fund_user(&usdc_admin, &owner, amount); - approve_spend(&env, &usdc_client, &owner, &vault_address, amount); - vault.deposit(&owner, &amount); + for _ in 0..500 { + if rng.gen_bool(0.5) { + let amount = rng.gen_range(1..=500); + client.deposit(&owner, &amount); expected += amount; } else if expected > 0 { - let amount: i128 = rng.gen_range(1..=expected.min(500)); - vault.deduct(&owner, &amount, &None); + let amount = rng.gen_range(1..=expected.min(500)); + client.deduct(&owner, &amount); expected -= amount; } - let balance = vault.balance(); + let balance = client.balance(); assert!(balance >= 0, "balance went negative: {}", balance); assert_eq!( balance, expected, @@ -822,107 +499,61 @@ fn fuzz_deposit_and_deduct() { ); } - assert_eq!(vault.balance(), expected); + assert_eq!(client.balance(), expected); } #[test] -fn batch_deduct_all_succeed() { +fn deduct_returns_new_balance() { let env = Env::default(); + env.mock_all_auths(); + let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); + let contract_id = env.register(CalloraVault, ()); let client = CalloraVaultClient::new(&env, &contract_id); - let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); - env.mock_all_auths(); - fund_vault(&usdc_admin, &contract_id, 60); - client.init(&owner, &usdc_address, &Some(60), &None, &None, &None); - let items = vec![ - &env, - DeductItem { - amount: 10, - request_id: None, - }, - DeductItem { - amount: 20, - request_id: None, - }, - DeductItem { - amount: 30, - request_id: None, - }, - ]; - let caller = Address::generate(&env); - env.mock_all_auths(); - let new_balance = client.batch_deduct(&caller, &items); - assert_eq!(new_balance, 0); - assert_eq!(client.balance(), 0); + client.init(&owner, &Some(100)); + let new_balance = client.deduct(&owner, &30); + assert_eq!(new_balance, 70); + assert_eq!(client.balance(), 70); } #[test] -#[should_panic(expected = "insufficient balance")] -fn batch_deduct_all_revert() { +fn test_concurrent_deposits() { let env = Env::default(); + env.mock_all_auths(); + let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); + let contract_id = env.register(CalloraVault, ()); let client = CalloraVaultClient::new(&env, &contract_id); - let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); - env.mock_all_auths(); - fund_vault(&usdc_admin, &contract_id, 25); - client.init(&owner, &usdc_address, &Some(25), &None, &None, &None); - assert_eq!(client.balance(), 25); - let items = vec![ - &env, - DeductItem { - amount: 10, - request_id: None, - }, - DeductItem { - amount: 20, - request_id: None, - }, - DeductItem { - amount: 30, - request_id: None, - }, - ]; - let caller = Address::generate(&env); - env.mock_all_auths(); - client.batch_deduct(&caller, &items); + client.init(&owner, &Some(100)); + + let dep1 = Address::generate(&env); + let dep2 = Address::generate(&env); + + client.set_allowed_depositor(&owner, &Some(dep1.clone())); + client.set_allowed_depositor(&owner, &Some(dep2.clone())); + + // Concurrent deposits + client.deposit(&dep1, &200); + client.deposit(&dep2, &300); + + assert_eq!(client.balance(), 600); } #[test] -fn batch_deduct_revert_preserves_balance() { +fn init_twice_panics_on_reinit() { let env = Env::default(); let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); + let contract_id = env.register(CalloraVault, ()); let client = CalloraVaultClient::new(&env, &contract_id); - let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); env.mock_all_auths(); - fund_vault(&usdc_admin, &contract_id, 25); - client.init(&owner, &usdc_address, &Some(25), &None, &None, &None); + client.init(&owner, &Some(25)); assert_eq!(client.balance(), 25); - let items = vec![ - &env, - DeductItem { - amount: 10, - request_id: None, - }, - DeductItem { - amount: 20, - request_id: None, - }, - DeductItem { - amount: 30, - request_id: None, - }, - ]; - let caller = Address::generate(&env); - env.mock_all_auths(); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - client.batch_deduct(&caller, &items); + client.init(&owner, &Some(50)); })); assert!(result.is_err()); @@ -933,17 +564,12 @@ fn batch_deduct_revert_preserves_balance() { fn owner_unchanged_after_deposit_and_deduct() { let env = Env::default(); let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); + let contract_id = env.register(CalloraVault, ()); let client = CalloraVaultClient::new(&env, &contract_id); - let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &owner); env.mock_all_auths(); - fund_vault(&usdc_admin, &contract_id, 100); - client.init(&owner, &usdc_address, &Some(100), &None, &None, &None); - fund_user(&usdc_admin, &owner, 50); - approve_spend(&env, &usdc_client, &owner, &contract_id, 50); + client.init(&owner, &Some(100)); client.deposit(&owner, &50); - client.deduct(&owner, &30, &None); - + client.deduct(&owner, &30); assert_eq!(client.get_meta().owner, owner); }