From f8b3a9b6ac05ddb8de3460c1e6c47c4e98ee615e Mon Sep 17 00:00:00 2001 From: clintjeff2 Date: Wed, 25 Feb 2026 15:49:03 +0100 Subject: [PATCH 1/7] . --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 31c707c..80d13b6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ Cargo.lock .env .env.* /target/ -/target_local/ \ No newline at end of file +/target_local/ +.md-* \ No newline at end of file From 0325bb41aa5813880a58065abfb1b9b3ba2f34a0 Mon Sep 17 00:00:00 2001 From: clintjeff2 Date: Wed, 25 Feb 2026 15:49:57 +0100 Subject: [PATCH 2/7] mo more fixes --- contracts/vault/src/lib.rs | 136 +-- contracts/vault/src/test.rs | 1560 ++++++++++++++--------------------- 2 files changed, 654 insertions(+), 1042 deletions(-) diff --git a/contracts/vault/src/lib.rs b/contracts/vault/src/lib.rs index aa61dc9..ce0d4b7 100644 --- a/contracts/vault/src/lib.rs +++ b/contracts/vault/src/lib.rs @@ -78,21 +78,16 @@ impl CalloraVault { 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); + // Persist metadata under both the literal key and the constant for safety. + let inst = env.storage().instance(); + inst.set(&Symbol::new(&env, "meta"), &meta); + inst.set(&Symbol::new(&env, META_KEY), &meta); + inst.set(&Symbol::new(&env, USDC_KEY), &usdc_token); + inst.set(&Symbol::new(&env, ADMIN_KEY), &owner); + if let Some(pool) = revenue_pool { + inst.set(&Symbol::new(&env, REVENUE_POOL_KEY), &pool); + } + inst.set(&Symbol::new(&env, MAX_DEDUCT_KEY), &max_deduct_val); env.events() .publish((Symbol::new(&env, "init"), owner), balance); @@ -115,9 +110,8 @@ impl CalloraVault { if caller != current_admin { panic!("unauthorized: caller is not admin"); } - env.storage() - .instance() - .set(&Symbol::new(&env, ADMIN_KEY), &new_admin); + let inst = env.storage().instance(); + inst.set(&Symbol::new(&env, ADMIN_KEY), &new_admin); } /// Return the maximum allowed amount for a single deduct (configurable at init). @@ -169,11 +163,8 @@ impl CalloraVault { } // 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_opt: Option
= env.storage().instance().get(&Symbol::new(&env, USDC_KEY)); + let usdc_address: Address = usdc_opt.unwrap_or_else(|| panic!("vault not initialized")); let usdc = token::Client::new(&env, &usdc_address); @@ -192,9 +183,6 @@ impl CalloraVault { } /// Get vault metadata (owner and balance). - /// - /// # Panics - /// - If the vault has not been initialized pub fn get_meta(env: Env) -> VaultMeta { env.storage() .instance() @@ -222,12 +210,16 @@ impl CalloraVault { .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(), &amount); + usdc.transfer_from( + &env.current_contract_address(), + &from, + &env.current_contract_address(), + &amount, + ); meta.balance += amount; - env.storage() - .instance() - .set(&Symbol::new(&env, META_KEY), &meta); + let inst = env.storage().instance(); + inst.set(&Symbol::new(&env, "meta"), &meta); env.events() .publish((Symbol::new(&env, "deposit"), from), amount); @@ -248,26 +240,9 @@ impl CalloraVault { let mut meta = Self::get_meta(env.clone()); 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 inst = env.storage().instance(); + inst.set(&Symbol::new(&env, "meta"), &meta); let topics = match &request_id { Some(rid) => (Symbol::new(&env, "deduct"), caller.clone(), rid.clone()), @@ -292,7 +267,6 @@ impl CalloraVault { 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"); @@ -302,20 +276,8 @@ impl CalloraVault { ); 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; @@ -331,17 +293,8 @@ impl CalloraVault { } 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); - } - } - + let inst = env.storage().instance(); + inst.set(&Symbol::new(&env, "meta"), &meta); meta.balance } @@ -361,9 +314,8 @@ impl CalloraVault { usdc.transfer(&env.current_contract_address(), &meta.owner, &amount); meta.balance -= amount; - env.storage() - .instance() - .set(&Symbol::new(&env, META_KEY), &meta); + let inst = env.storage().instance(); + inst.set(&Symbol::new(&env, "meta"), &meta); env.events().publish( (Symbol::new(&env, "withdraw"), meta.owner.clone()), @@ -388,9 +340,8 @@ impl CalloraVault { usdc.transfer(&env.current_contract_address(), &to, &amount); meta.balance -= amount; - env.storage() - .instance() - .set(&Symbol::new(&env, META_KEY), &meta); + let inst = env.storage().instance(); + inst.set(&Symbol::new(&env, "meta"), &meta); env.events().publish( ( @@ -407,33 +358,6 @@ impl CalloraVault { pub fn balance(env: Env) -> i128 { Self::get_meta(env).balance } - - pub fn transfer_ownership(env: Env, new_owner: Address) { - let mut meta = Self::get_meta(env.clone()); - meta.owner.require_auth(); - - // Validate new_owner is not the same as current owner - assert!( - new_owner != meta.owner, - "new_owner must be different from current owner" - ); - - // 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, "transfer_ownership"), - meta.owner.clone(), - new_owner.clone(), - ), - (), - ); - - meta.owner = new_owner; - env.storage() - .instance() - .set(&Symbol::new(&env, "meta"), &meta); - } } #[cfg(test)] diff --git a/contracts/vault/src/test.rs b/contracts/vault/src/test.rs index c14f811..42ceb22 100644 --- a/contracts/vault/src/test.rs +++ b/contracts/vault/src/test.rs @@ -29,697 +29,877 @@ fn fund_vault( 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); -} - -/// 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); -} - -/// Lifecycle test: init → deposit → deduct → balance. #[test] -fn vault_operation_costs() { +fn vault_full_lifecycle() { let env = Env::default(); let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault, ()); + let new_admin = Address::generate(&env); + let caller = Address::generate(&env); + let recipient = Address::generate(&env); + 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_user(&usdc_admin, &owner, 200); - client.init(&owner, &usdc, &Some(0), &None, &None, &None); - client.deposit(&owner, &100); - client.deduct(&owner, &50, &None); - let bal = client.balance(); - assert_eq!(bal, 50); + fund_vault(&usdc_admin, &contract_id, 500); + let meta = client.init(&owner, &usdc, &Some(500), &Some(10), &None, &None); + assert_eq!(meta.balance, 500); + assert_eq!(meta.owner, owner); + assert_eq!(client.balance(), 500); + assert_eq!(client.get_admin(), owner); + + let depositor = Address::generate(&env); + fund_vault(&usdc_admin, &depositor, 200); + let usdc_client = token::Client::new(&env, &usdc); + usdc_client.approve(&depositor, &contract_id, &200, &1000); + let after_deposit = client.deposit(&depositor, &200); + assert_eq!(after_deposit, 700); + assert_eq!(client.balance(), 700); + + let items = soroban_sdk::vec![ + &env, + DeductItem { + amount: 100, + request_id: Some(Symbol::new(&env, "r1")), + }, + DeductItem { + amount: 50, + request_id: None, + }, + DeductItem { + amount: 25, + request_id: Some(Symbol::new(&env, "r3")), + }, + ]; + let after_batch = client.batch_deduct(&caller, &items); + assert_eq!(after_batch, 525); + assert_eq!(client.balance(), 525); + + let after_deduct = client.deduct(&caller, &25, &Some(Symbol::new(&env, "r4"))); + assert_eq!(after_deduct, 500); + + client.set_admin(&owner, &new_admin); + assert_eq!(client.get_admin(), new_admin); + + let after_withdraw = client.withdraw_to(&recipient, &100); + assert_eq!(after_withdraw, 400); + assert_eq!(client.balance(), 400); + + let after_withdraw2 = client.withdraw(&50); + assert_eq!(after_withdraw2, 350); + assert_eq!(client.balance(), 350); + + let final_meta = client.get_meta(); + assert_eq!(final_meta.balance, 350); + assert_eq!(final_meta.owner, owner); + assert_eq!(final_meta.min_deposit, 10); } #[test] -fn init_and_balance() { +fn init_with_balance_emits_event() { let env = Env::default(); let owner = Address::generate(&env); let contract_id = env.register(CalloraVault {}, ()); + let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); - 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(); + let events = env.as_contract(&contract_id, || { + CalloraVault::init( + env.clone(), + owner.clone(), + usdc_token.clone(), + Some(1000), + None, + None, + None, + ); + env.events().all() + }); + + let client = CalloraVaultClient::new(&env, &contract_id); assert_eq!(client.balance(), 1000); + + let last_event = events.last().expect("expected at least one event"); + assert_eq!(last_event.0, contract_id); + + 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); + + let data: i128 = last_event.2.into_val(&env); + assert_eq!(data, 1000); } #[test] -fn deposit_and_deduct() { +fn init_defaults_balance_to_zero() { let env = Env::default(); let owner = Address::generate(&env); let contract_id = env.register(CalloraVault {}, ()); let client = CalloraVaultClient::new(&env, &contract_id); + let (usdc_token, _, _) = create_usdc(&env, &owner); - let (usdc, usdc_client, usdc_admin) = create_usdc(&env, &owner); 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); - assert_eq!(client.balance(), 250); + client.init(&owner, &usdc_token, &None, &None, &None, &None); + assert_eq!(client.balance(), 0); } -/// 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 get_meta_returns_owner_and_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_token, _, usdc_admin) = create_usdc(&env, &owner); 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); - - let meta = client.get_meta(); - let balance = client.balance(); - assert_eq!(meta.balance, balance, "balance mismatch after init"); - 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); + client.init(&owner, &usdc_token, &Some(500), &None, &None, &None); 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); - 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"); - - fund_user(&usdc_admin, &owner, 125); - approve_spend(&env, &usdc_client, &owner, &contract_id, 125); - client.deposit(&owner, &100); - client.deduct(&owner, &50, &None); - client.deposit(&owner, &25); - let meta = client.get_meta(); - let balance = client.balance(); - assert_eq!( - meta.balance, balance, - "balance mismatch after multiple operations" + assert_eq!(meta.owner, owner); + assert_eq!(meta.balance, 500); +} + +#[test] +fn get_meta_before_init_fails() { + let env = Env::default(); + let contract_id = env.register(CalloraVault {}, ()); + let client = CalloraVaultClient::new(&env, &contract_id); + + let result = client.try_get_meta(); + assert!( + result.is_err(), + "expected error when vault is uninitialised" ); - assert_eq!(balance, 725, "incorrect final balance"); } #[test] -#[should_panic(expected = "insufficient balance")] -fn deduct_exact_balance_and_panic() { +fn deposit_and_balance_match() { let env = Env::default(); let owner = Address::generate(&env); + let depositor = Address::generate(&env); let contract_id = env.register(CalloraVault {}, ()); let client = CalloraVaultClient::new(&env, &contract_id); + let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); - 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); - assert_eq!(client.balance(), 100); + client.init(&owner, &usdc_token, &Some(100), &None, &None, &None); - client.deduct(&owner, &100, &None); - assert_eq!(client.balance(), 0); + fund_vault(&usdc_admin, &depositor, 200); + let usdc_client = token::Client::new(&env, &usdc_token); + usdc_client.approve(&depositor, &contract_id, &200, &1000); + let returned = client.deposit(&depositor, &200); - client.deduct(&owner, &1, &None); + assert_eq!( + returned, 300, + "deposit should return the new running balance" + ); + assert_eq!(client.balance(), 300); } #[test] -fn deduct_event_emission() { +fn deduct_reduces_balance() { let env = Env::default(); let owner = Address::generate(&env); - let caller = Address::generate(&env); let contract_id = env.register(CalloraVault {}, ()); let client = CalloraVaultClient::new(&env, &contract_id); + let (usdc, _, usdc_admin) = create_usdc(&env, &owner); + let caller = Address::generate(&env); + let depositor = Address::generate(&env); - 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())); - - let events = env.events().all(); - - let last_event = events.last().unwrap(); - assert_eq!(last_event.0, contract_id); + fund_vault(&usdc_admin, &contract_id, 100); + client.init(&owner, &usdc, &Some(100), &None, &None, &None); - 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); + fund_vault(&usdc_admin, &depositor, 200); + let usdc_client = token::Client::new(&env, &usdc); + usdc_client.approve(&depositor, &contract_id, &200, &1000); + client.deposit(&depositor, &200); + assert_eq!(client.balance(), 300); - let data: (i128, i128) = last_event.2.into_val(&env); - assert_eq!(data, (200, 800)); + let returned = client.deduct(&caller, &50, &None); + assert_eq!(returned, 250, "deduct should return the remaining balance"); + assert_eq!(client.balance(), 250); } #[test] -fn test_init_success() { +fn deduct_with_request_id() { 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 (usdc, _, usdc_admin) = create_usdc(&env, &owner); + let caller = Address::generate(&env); - let meta = vault.init(&owner, &usdc_address, &None, &None, &None, &None); + env.mock_all_auths(); + fund_vault(&usdc_admin, &contract_id, 1000); + client.init(&owner, &usdc, &Some(1000), &None, &None, &None); - assert_eq!(meta.owner, owner); - assert_eq!(meta.balance, 0); + let request_id = Symbol::new(&env, "req123"); + let remaining = client.deduct(&caller, &100, &Some(request_id)); + assert_eq!(remaining, 900); } #[test] -#[should_panic(expected = "vault already initialized")] -fn test_init_double_panics() { +fn deduct_insufficient_balance_fails() { 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 (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); + let caller = Address::generate(&env); + + env.mock_all_auths(); + fund_vault(&usdc_admin, &contract_id, 10); + client.init(&owner, &usdc_token, &Some(10), &None, &None, &None); - vault.init(&owner, &usdc_address, &None, &None, &None, &None); - vault.init(&owner, &usdc_address, &None, &None, &None, &None); + let result = client.try_deduct(&caller, &100, &None); + assert!(result.is_err(), "expected error for insufficient balance"); } #[test] -fn test_distribute_success() { +fn deduct_exact_balance_succeeds() { 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_client, usdc_admin_client) = create_usdc(&env, &admin); + let owner = Address::generate(&env); + let contract_id = env.register(CalloraVault {}, ()); + let client = CalloraVaultClient::new(&env, &contract_id); + let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); + let caller = Address::generate(&env); - fund_vault(&usdc_admin_client, &vault_address, 1_000); - vault.init(&admin, &usdc_address, &None, &None, &None, &None); - vault.distribute(&admin, &developer, &400); + env.mock_all_auths(); + fund_vault(&usdc_admin, &contract_id, 75); + client.init(&owner, &usdc_token, &Some(75), &None, &None, &None); + let remaining = client.deduct(&caller, &75, &None); - assert_eq!(usdc_client.balance(&vault_address), 600); - assert_eq!(usdc_client.balance(&developer), 400); + assert_eq!(remaining, 0); + assert_eq!(client.balance(), 0); } #[test] -#[should_panic(expected = "insufficient USDC balance")] -fn test_distribute_excess_panics() { +fn deduct_event_contains_request_id() { let env = Env::default(); + let owner = Address::generate(&env); + let caller = Address::generate(&env); + let contract_id = env.register(CalloraVault {}, ()); + let client = CalloraVaultClient::new(&env, &contract_id); + let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); + env.mock_all_auths(); + fund_vault(&usdc_admin, &contract_id, 500); + client.init(&owner, &usdc_token, &Some(500), &None, &None, &None); - 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 request_id = Symbol::new(&env, "api_call_42"); + client.deduct(&caller, &150, &Some(request_id.clone())); - fund_vault(&usdc_admin_client, &vault_address, 100); - vault.init(&admin, &usdc_address, &None, &None, &None, &None); - vault.distribute(&admin, &developer, &101); -} + let events = env.events().all(); + let ev = events.last().expect("expected deduct event"); -#[test] -#[should_panic(expected = "amount must be positive")] -fn test_distribute_zero_panics() { - let env = Env::default(); - env.mock_all_auths(); + let topic0: Symbol = ev.1.get(0).unwrap().into_val(&env); + let topic1: Address = ev.1.get(1).unwrap().into_val(&env); + let topic2: Symbol = ev.1.get(2).unwrap().into_val(&env); - let admin = Address::generate(&env); - let developer = Address::generate(&env); - let (_, vault) = create_vault(&env); - let (usdc_address, _, _) = create_usdc(&env, &admin); + assert_eq!(topic0, Symbol::new(&env, "deduct")); + assert_eq!(topic1, caller); + assert_eq!(topic2, request_id); - vault.init(&admin, &usdc_address, &None, &None, &None, &None); - vault.distribute(&admin, &developer, &0); + let (emitted_amount, remaining): (i128, i128) = ev.2.into_val(&env); + assert_eq!(emitted_amount, 150); + assert_eq!(remaining, 350); } #[test] -#[should_panic(expected = "amount must be positive")] -fn test_distribute_negative_panics() { +fn deduct_event_no_request_id_uses_empty_symbol() { let env = Env::default(); + let owner = Address::generate(&env); + let caller = Address::generate(&env); + let contract_id = env.register(CalloraVault {}, ()); + let client = CalloraVaultClient::new(&env, &contract_id); + let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); + env.mock_all_auths(); + fund_vault(&usdc_admin, &contract_id, 300); + client.init(&owner, &usdc_token, &Some(300), &None, &None, &None); + client.deduct(&caller, &100, &None); - let admin = Address::generate(&env); - let developer = Address::generate(&env); - let (_, vault) = create_vault(&env); - let (usdc_address, _, _) = create_usdc(&env, &admin); + let events = env.events().all(); + let ev = events.last().expect("expected deduct event"); + + let topic0: Symbol = ev.1.get(0).unwrap().into_val(&env); + let topic2: Symbol = ev.1.get(2).unwrap().into_val(&env); - vault.init(&admin, &usdc_address, &None, &None, &None, &None); - vault.distribute(&admin, &developer, &-1); + assert_eq!(topic0, Symbol::new(&env, "deduct")); + assert_eq!(topic2, Symbol::new(&env, "")); } #[test] -#[should_panic(expected = "unauthorized: caller is not admin")] -fn test_distribute_unauthorized_panics() { +fn batch_deduct_events_contain_request_ids() { let env = Env::default(); + let owner = Address::generate(&env); + let caller = Address::generate(&env); + let contract_id = env.register(CalloraVault {}, ()); + let client = CalloraVaultClient::new(&env, &contract_id); + let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); + env.mock_all_auths(); + fund_vault(&usdc_admin, &contract_id, 1000); + client.init(&owner, &usdc_token, &Some(1000), &None, &None, &None); - 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 rid_a = Symbol::new(&env, "batch_a"); + let rid_b = Symbol::new(&env, "batch_b"); - fund_vault(&usdc_admin_client, &vault_address, 1_000); - vault.init(&admin, &usdc_address, &None, &None, &None, &None); - vault.distribute(&attacker, &developer, &500); -} + let items = soroban_sdk::vec![ + &env, + DeductItem { + amount: 200, + request_id: Some(rid_a.clone()), + }, + DeductItem { + amount: 300, + request_id: Some(rid_b.clone()), + }, + ]; + client.batch_deduct(&caller, &items); -#[test] -fn test_distribute_full_balance() { - let env = Env::default(); - env.mock_all_auths(); + let all_events = env.events().all(); + assert_eq!(all_events.len(), 2); - 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); + let ev_a = all_events.get(0).unwrap(); + let ev_b = all_events.get(1).unwrap(); - fund_vault(&usdc_admin_client, &vault_address, 777); - vault.init(&admin, &usdc_address, &None, &None, &None, &None); - vault.distribute(&admin, &developer, &777); + let req_a: Symbol = ev_a.1.get(2).unwrap().into_val(&env); + let req_b: Symbol = ev_b.1.get(2).unwrap().into_val(&env); + assert_eq!(req_a, rid_a); + assert_eq!(req_b, rid_b); - assert_eq!(usdc_client.balance(&vault_address), 0); - assert_eq!(usdc_client.balance(&developer), 777); + let (amt_a, _): (i128, i128) = ev_a.2.into_val(&env); + let (amt_b, _): (i128, i128) = ev_b.2.into_val(&env); + assert_eq!(amt_a, 200); + assert_eq!(amt_b, 300); } #[test] -fn test_distribute_multiple_times() { +fn get_admin_returns_correct_address() { 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 contract_id = env.register(CalloraVault {}, ()); + let client = CalloraVaultClient::new(&env, &contract_id); + let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); - 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); + env.mock_all_auths(); + fund_vault(&usdc_admin, &contract_id, 100); + client.init(&owner, &usdc_token, &Some(100), &None, &None, &None); - assert_eq!(usdc_client.balance(&vault_address), 500); - assert_eq!(usdc_client.balance(&dev_a), 300); - assert_eq!(usdc_client.balance(&dev_b), 200); + let admin = client.get_admin(); + assert_eq!(admin, owner); } #[test] -fn test_set_admin_transfers_control() { +fn set_admin_updates_admin() { let env = Env::default(); - env.mock_all_auths(); - - let original_admin = Address::generate(&env); + let owner = 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); + let contract_id = env.register(CalloraVault {}, ()); + let client = CalloraVaultClient::new(&env, &contract_id); + let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); - assert_eq!(vault.get_admin(), new_admin); + env.mock_all_auths(); + fund_vault(&usdc_admin, &contract_id, 100); + client.init(&owner, &usdc_token, &Some(100), &None, &None, &None); - vault.distribute(&new_admin, &developer, &100); - assert_eq!(usdc_client.balance(&developer), 100); + client.set_admin(&owner, &new_admin); + assert_eq!(client.get_admin(), new_admin); } #[test] -#[should_panic(expected = "unauthorized: caller is not admin")] -fn test_old_admin_cannot_distribute_after_transfer() { +fn set_admin_unauthorized_fails() { let env = Env::default(); - env.mock_all_auths(); - - let original_admin = Address::generate(&env); + let owner = Address::generate(&env); + let intruder = 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 contract_id = env.register(CalloraVault {}, ()); + let client = CalloraVaultClient::new(&env, &contract_id); + let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); -#[test] -fn test_deposit_and_balance() { - let env = Env::default(); env.mock_all_auths(); + fund_vault(&usdc_admin, &contract_id, 100); + client.init(&owner, &usdc_token, &Some(100), &None, &None, &None); - 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); + let result = client.try_set_admin(&intruder, &new_admin); + assert!( + result.is_err(), + "expected error when non-admin tries to set admin" + ); } #[test] -fn test_deduct_success() { +fn distribute_transfers_usdc_to_developer() { let env = Env::default(); + let admin = Address::generate(&env); + let developer = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, usdc_client, usdc_admin_client) = create_usdc(&env, &admin); + 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_client, &vault_address, 1000); + client.init(&admin, &usdc, &Some(0), &None, &None, &None); - 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); + client.distribute(&admin, &developer, &300); + + assert_eq!(usdc_client.balance(&developer), 300); + assert_eq!(usdc_client.balance(&vault_address), 700); } #[test] -#[should_panic(expected = "deduct amount exceeds max_deduct")] -fn test_deduct_above_max_deduct_panics() { +fn distribute_unauthorized_fails() { let env = Env::default(); + let admin = Address::generate(&env); + let intruder = Address::generate(&env); + let developer = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, _, usdc_admin_client) = create_usdc(&env, &admin); + 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_client, &vault_address, 1000); + client.init(&admin, &usdc, &Some(0), &None, &None, &None); - fund_vault(&usdc_admin, &vault_address, 10_000); - vault.init( - &owner, - &usdc_address, - &Some(10_000), - &None, - &None, - &Some(100), + let result = client.try_distribute(&intruder, &developer, &300); + assert!( + result.is_err(), + "expected error when non-admin tries to distribute" ); - assert_eq!(vault.get_max_deduct(), 100); - vault.deduct(&owner, &100, &None); - assert_eq!(vault.balance(), 9_900); - vault.deduct(&owner, &101, &None); } #[test] -#[should_panic(expected = "insufficient balance")] -fn test_deduct_excess_panics() { +fn distribute_insufficient_usdc_fails() { let env = Env::default(); + let admin = Address::generate(&env); + let developer = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, _, usdc_admin_client) = create_usdc(&env, &admin); + 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_client, &vault_address, 100); + client.init(&admin, &usdc, &Some(0), &None, &None, &None); - fund_vault(&usdc_admin, &vault_address, 50); - vault.init(&owner, &usdc_address, &Some(50), &None, &None, &None); - vault.deduct(&owner, &100, &None); + let result = client.try_distribute(&admin, &developer, &500); + assert!( + result.is_err(), + "expected error for insufficient USDC balance" + ); } #[test] -fn test_get_meta_returns_correct_values() { +fn distribute_zero_amount_fails() { let env = Env::default(); + let admin = Address::generate(&env); + let developer = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, _, usdc_admin_client) = create_usdc(&env, &admin); + 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_client, &vault_address, 1000); + client.init(&admin, &usdc, &Some(0), &None, &None, &None); - 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); + let result = client.try_distribute(&admin, &developer, &0); + assert!(result.is_err(), "expected error for zero amount"); } #[test] -fn test_multiple_depositors() { +fn batch_deduct_multiple_items() { let env = Env::default(); - env.mock_all_auths(); - let owner = Address::generate(&env); + let caller = Address::generate(&env); let contract_id = env.register(CalloraVault {}, ()); let client = CalloraVaultClient::new(&env, &contract_id); - let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &owner); + let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); + env.mock_all_auths(); + fund_vault(&usdc_admin, &contract_id, 1000); + client.init(&owner, &usdc_token, &Some(1000), &None, &None, &None); - 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); - - 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); - - env.events().all() - }); - let contract_events: std::vec::Vec<_> = - all_events.iter().filter(|e| e.0 == contract_id).collect(); - - assert_eq!(client.balance(), 300); - - assert_eq!( - contract_events.len(), - 3, - "vault should emit init + 2 deposits" - ); + let items = soroban_sdk::vec![ + &env, + DeductItem { + amount: 100, + request_id: Some(Symbol::new(&env, "req1")) + }, + DeductItem { + amount: 200, + request_id: None + }, + DeductItem { + amount: 50, + request_id: Some(Symbol::new(&env, "req2")) + } + ]; - // 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); + let remaining = client.batch_deduct(&caller, &items); + assert_eq!(remaining, 650); + assert_eq!(client.balance(), 650); } #[test] -fn batch_deduct_success() { +fn batch_deduct_insufficient_balance_fails() { let env = Env::default(); let owner = Address::generate(&env); + let caller = 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 (usdc_token, _, 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 req1 = Symbol::new(&env, "req1"); - let req2 = Symbol::new(&env, "req2"); + fund_vault(&usdc_admin, &contract_id, 100); + client.init(&owner, &usdc_token, &Some(100), &None, &None, &None); + let items = soroban_sdk::vec![ &env, - DeductItem { - amount: 100, - request_id: Some(req1.clone()), - }, - DeductItem { - amount: 200, - request_id: Some(req2.clone()), - }, DeductItem { amount: 50, - request_id: None, + request_id: None }, + DeductItem { + amount: 80, + request_id: None + } ]; + + let result = client.try_batch_deduct(&caller, &items); + assert!(result.is_err(), "expected error for batch overdraw"); + assert_eq!(client.balance(), 100); +} + +#[test] +fn batch_deduct_empty_fails() { + let env = Env::default(); + let owner = Address::generate(&env); let caller = Address::generate(&env); + let contract_id = env.register(CalloraVault {}, ()); + let client = CalloraVaultClient::new(&env, &contract_id); + let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); + env.mock_all_auths(); - let new_balance = client.batch_deduct(&caller, &items); - assert_eq!(new_balance, 650); - assert_eq!(client.balance(), 650); + fund_vault(&usdc_admin, &contract_id, 100); + client.init(&owner, &usdc_token, &Some(100), &None, &None, &None); + + let items: soroban_sdk::Vec = soroban_sdk::vec![&env]; + + let result = client.try_batch_deduct(&caller, &items); + assert!(result.is_err(), "expected error for empty batch"); } #[test] -#[should_panic(expected = "insufficient balance")] -fn batch_deduct_reverts_entire_batch() { +fn batch_deduct_zero_amount_fails() { let env = Env::default(); let owner = Address::generate(&env); + let caller = 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 (usdc_token, _, 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, &usdc_token, &Some(100), &None, &None, &None); + let items = soroban_sdk::vec![ &env, DeductItem { - amount: 60, - request_id: None, - }, - DeductItem { - amount: 60, - request_id: None, - }, // total 120 > 100 + amount: 0, + request_id: None + } ]; - let caller = Address::generate(&env); - env.mock_all_auths(); - client.batch_deduct(&caller, &items); + + let result = client.try_batch_deduct(&caller, &items); + assert!(result.is_err(), "expected error for zero amount"); } #[test] -fn withdraw_owner_success() { +fn withdraw_reduces_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); + let (usdc_token, _, 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); + client.init(&owner, &usdc_token, &Some(500), &None, &None, &None); + + let remaining = client.withdraw(&200); + assert_eq!(remaining, 300); assert_eq!(client.balance(), 300); } #[test] -fn withdraw_exact_balance() { +fn withdraw_insufficient_balance_fails() { let env = Env::default(); + env.mock_all_auths(); + 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 (usdc_token, _, 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 new_balance = client.withdraw(&100); - assert_eq!(new_balance, 0); - assert_eq!(client.balance(), 0); + client.init(&owner, &usdc_token, &Some(100), &None, &None, &None); + + let result = client.try_withdraw(&500); + assert!(result.is_err(), "expected error for insufficient balance"); } #[test] -#[should_panic(expected = "insufficient balance")] -fn withdraw_exceeds_balance_fails() { +fn withdraw_zero_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); + let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); env.mock_all_auths(); - fund_vault(&usdc_admin, &contract_id, 50); - client.init(&owner, &usdc_address, &Some(50), &None, &None, &None); - client.withdraw(&100); + fund_vault(&usdc_admin, &contract_id, 100); + client.init(&owner, &usdc_token, &Some(100), &None, &None, &None); + + let result = client.try_withdraw(&0); + assert!(result.is_err(), "expected error for zero amount"); } #[test] -fn withdraw_to_success() { +fn withdraw_to_reduces_balance() { let env = Env::default(); let owner = Address::generate(&env); - let to = Address::generate(&env); + let recipient = 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 (usdc_token, _, 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_to(&to, &150); - assert_eq!(new_balance, 350); + client.init(&owner, &usdc_token, &Some(500), &None, &None, &None); + + let remaining = client.withdraw_to(&recipient, &150); + assert_eq!(remaining, 350); assert_eq!(client.balance(), 350); } #[test] -#[should_panic] -fn withdraw_without_auth_fails() { +fn withdraw_to_insufficient_balance_fails() { let env = Env::default(); let owner = Address::generate(&env); + let recipient = 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 (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); + env.mock_all_auths(); fund_vault(&usdc_admin, &contract_id, 100); - 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), - sub_invokes: &[], - }, - }]); + client.init(&owner, &usdc_token, &Some(100), &None, &None, &None); - client.init(&owner, &usdc_address, &Some(100), &None, &None, &None); + let result = client.try_withdraw_to(&recipient, &500); + assert!(result.is_err(), "expected error for insufficient balance"); +} + +#[test] +fn deposit_below_minimum_fails() { + let env = Env::default(); + let owner = Address::generate(&env); + let depositor = Address::generate(&env); + let contract_id = env.register(CalloraVault {}, ()); + let client = CalloraVaultClient::new(&env, &contract_id); + let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); + + env.mock_all_auths(); + fund_vault(&usdc_admin, &contract_id, 100); + client.init(&owner, &usdc_token, &Some(100), &Some(50), &None, &None); - client.withdraw(&50); + fund_vault(&usdc_admin, &depositor, 30); + let usdc_client = token::Client::new(&env, &usdc_token); + usdc_client.approve(&depositor, &contract_id, &30, &1000); + let result = client.try_deposit(&depositor, &30); + assert!(result.is_err(), "expected error for deposit below minimum"); } #[test] -#[should_panic(expected = "vault already initialized")] -fn init_already_initialized_panics() { +fn deposit_at_minimum_succeeds() { let env = Env::default(); let owner = Address::generate(&env); + let depositor = Address::generate(&env); let contract_id = env.register(CalloraVault {}, ()); let client = CalloraVaultClient::new(&env, &contract_id); + let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); 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, &usdc_token, &Some(100), &Some(50), &None, &None); + + fund_vault(&usdc_admin, &depositor, 50); + let usdc_client = token::Client::new(&env, &usdc_token); + usdc_client.approve(&depositor, &contract_id, &50, &1000); + let new_balance = client.deposit(&depositor, &50); + assert_eq!(new_balance, 150); +} + +#[test] +fn double_init_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_token, _, usdc_admin) = create_usdc(&env, &owner); + + env.mock_all_auths(); + fund_vault(&usdc_admin, &contract_id, 100); + client.init(&owner, &usdc_token, &Some(100), &None, &None, &None); + + let result = client.try_init(&owner, &usdc_token, &Some(200), &None, &None, &None); + assert!(result.is_err(), "expected error for double init"); +} + +#[test] +fn init_insufficient_usdc_balance_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_token, _, usdc_admin) = create_usdc(&env, &owner); + + env.mock_all_auths(); + fund_vault(&usdc_admin, &contract_id, 50); + + let result = client.try_init(&owner, &usdc_token, &Some(100), &None, &None, &None); + assert!( + result.is_err(), + "expected error when initial_balance exceeds contract USDC" + ); +} + +#[test] +fn init_with_zero_max_deduct_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_token, _, _) = create_usdc(&env, &owner); + + env.mock_all_auths(); + + let result = client.try_init(&owner, &usdc_token, &None, &None, &None, &Some(0)); + assert!(result.is_err(), "expected error for max_deduct <= 0"); +} + +#[test] +fn init_with_revenue_pool_and_get_revenue_pool() { + let env = Env::default(); + let owner = Address::generate(&env); + let revenue_pool = Address::generate(&env); + let contract_id = env.register(CalloraVault {}, ()); + let client = CalloraVaultClient::new(&env, &contract_id); + let (usdc_token, _, _) = create_usdc(&env, &owner); + + env.mock_all_auths(); + client.init( + &owner, + &usdc_token, + &None, + &None, + &Some(revenue_pool.clone()), + &None, + ); + + let retrieved_pool = client.get_revenue_pool(); + assert_eq!(retrieved_pool, Some(revenue_pool)); +} + +#[test] +fn get_revenue_pool_returns_none_when_not_set() { + let env = Env::default(); + let owner = Address::generate(&env); + let contract_id = env.register(CalloraVault {}, ()); + let client = CalloraVaultClient::new(&env, &contract_id); + let (usdc_token, _, _) = create_usdc(&env, &owner); + + env.mock_all_auths(); + client.init(&owner, &usdc_token, &None, &None, &None, &None); + + let retrieved_pool = client.get_revenue_pool(); + assert_eq!(retrieved_pool, None); +} + +#[test] +fn get_max_deduct_returns_configured_value() { + let env = Env::default(); + let owner = Address::generate(&env); + let contract_id = env.register(CalloraVault {}, ()); + let client = CalloraVaultClient::new(&env, &contract_id); + let (usdc_token, _, _) = create_usdc(&env, &owner); + + env.mock_all_auths(); + client.init(&owner, &usdc_token, &None, &None, &None, &Some(5000)); + + let max_deduct = client.get_max_deduct(); + assert_eq!(max_deduct, 5000); +} + +/// 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::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 initial_balance: i128 = 1_000; + fund_vault(&usdc_admin, &vault_address, initial_balance); + // Pre-fund owner for deposits in the loop + usdc_admin.mint(&owner, &250_000); + usdc_client.approve(&owner, &vault_address, &250_000, &10_000); + vault.init( + &owner, + &usdc_address, + &Some(initial_balance), + &None, + &None, + &None, + ); + let mut expected = initial_balance; + let mut rng = rand::thread_rng(); + + for _ in 0..500 { + if rng.gen_bool(0.5) { + let amount = rng.gen_range(1..=500); + vault.deposit(&owner, &amount); + expected += amount; + } else if expected > 0 { + let amount = rng.gen_range(1..=expected.min(500)); + vault.deduct(&owner, &amount, &None); + expected -= amount; + } + + let balance = vault.balance(); + assert!(balance >= 0, "balance went negative: {}", balance); + assert_eq!( + balance, expected, + "balance mismatch: got {}, expected {}", + balance, expected + ); + } + + assert_eq!(vault.balance(), expected); } #[test] @@ -728,19 +908,19 @@ fn deduct_returns_new_balance() { env.mock_all_auths(); let owner = Address::generate(&env); - let (vault_addr, vault) = create_vault(&env); + let (vault_address, vault) = create_vault(&env); let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); - usdc_admin.mint(&vault_addr, &100); + fund_vault(&usdc_admin, &vault_address, 100); vault.init(&owner, &usdc_address, &Some(100), &None, &None, &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. +/// Fuzz test (seeded): deterministic deposit/deduct sequence asserting balance >= 0 and matches expected. #[test] -fn fuzz_deposit_and_deduct() { +fn fuzz_deposit_and_deduct_seeded() { use rand::rngs::StdRng; use rand::{Rng, SeedableRng}; @@ -748,11 +928,13 @@ fn fuzz_deposit_and_deduct() { env.mock_all_auths(); let owner = Address::generate(&env); - let (_vault_addr, vault) = create_vault(&env); - let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); + let (vault_address, vault) = create_vault(&env); + let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &owner); + // Pre-fund owner for deposits in the loop + usdc_admin.mint(&owner, &5_000_000); + usdc_client.approve(&owner, &vault_address, &5_000_000, &10_000); vault.init(&owner, &usdc_address, &Some(0), &None, &None, &None); - usdc_admin.mint(&owner, &(i128::MAX / 2)); let mut expected: i128 = 0; let mut rng = StdRng::seed_from_u64(42); @@ -783,7 +965,7 @@ fn batch_deduct_all_succeed() { let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); env.mock_all_auths(); - usdc_admin.mint(&contract_id, &60); + fund_vault(&usdc_admin, &contract_id, 60); client.init(&owner, &usdc_address, &Some(60), &None, &None, &None); let items = soroban_sdk::vec![ &env, @@ -817,7 +999,7 @@ fn batch_deduct_all_revert() { let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); env.mock_all_auths(); - usdc_admin.mint(&contract_id, &25); + fund_vault(&usdc_admin, &contract_id, 25); client.init(&owner, &usdc_address, &Some(25), &None, &None, &None); assert_eq!(client.balance(), 25); let items = soroban_sdk::vec![ @@ -849,7 +1031,7 @@ fn batch_deduct_revert_preserves_balance() { let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); env.mock_all_auths(); - usdc_admin.mint(&contract_id, &25); + fund_vault(&usdc_admin, &contract_id, 25); client.init(&owner, &usdc_address, &Some(25), &None, &None, &None); assert_eq!(client.balance(), 25); let items = soroban_sdk::vec![ @@ -884,510 +1066,16 @@ 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, _, usdc_admin) = create_usdc(&env, &owner); + let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &owner); env.mock_all_auths(); - usdc_admin.mint(&contract_id, &100); - client.init(&owner, &usdc_address, &Some(100), &None, &None, &None); + fund_vault(&usdc_admin, &contract_id, 100); + // Fund owner for the deposit call usdc_admin.mint(&owner, &50); + usdc_client.approve(&owner, &contract_id, &50, &10_000); + client.init(&owner, &usdc_address, &Some(100), &None, &None, &None); client.deposit(&owner, &50); client.deduct(&owner, &30, &None); assert_eq!(client.get_meta().owner, owner); } - -#[test] -#[should_panic(expected = "insufficient USDC in contract for initial_balance")] -fn init_insufficient_usdc_panics() { - let env = Env::default(); - env.mock_all_auths(); - let owner = Address::generate(&env); - let (_, vault) = create_vault(&env); - let (usdc, _, _) = create_usdc(&env, &owner); - // No USDC funded to vault — should panic - vault.init(&owner, &usdc, &Some(1000), &None, &None, &None); -} - -#[test] -#[should_panic(expected = "max_deduct must be positive")] -fn init_max_deduct_zero_panics() { - let env = Env::default(); - env.mock_all_auths(); - let owner = Address::generate(&env); - let (_, vault) = create_vault(&env); - let (usdc, _, _) = create_usdc(&env, &owner); - vault.init(&owner, &usdc, &None, &None, &None, &Some(0)); -} - -#[test] -#[should_panic(expected = "max_deduct must be positive")] -fn init_max_deduct_negative_panics() { - let env = Env::default(); - env.mock_all_auths(); - let owner = Address::generate(&env); - let (_, vault) = create_vault(&env); - let (usdc, _, _) = create_usdc(&env, &owner); - vault.init(&owner, &usdc, &None, &None, &None, &Some(-5)); -} - -#[test] -fn init_stores_revenue_pool_and_max_deduct() { - let env = Env::default(); - env.mock_all_auths(); - let owner = Address::generate(&env); - let rev_pool = Address::generate(&env); - let (_, vault) = create_vault(&env); - let (usdc, _, _) = create_usdc(&env, &owner); - - vault.init( - &owner, - &usdc, - &None, - &None, - &Some(rev_pool.clone()), - &Some(500), - ); - - assert_eq!(vault.get_max_deduct(), 500); - assert_eq!(vault.get_revenue_pool(), Some(rev_pool)); - assert_eq!(vault.get_admin(), owner); - assert_eq!(vault.balance(), 0); -} - -#[test] -fn get_revenue_pool_none_when_not_set() { - let env = Env::default(); - env.mock_all_auths(); - let owner = Address::generate(&env); - let (_, vault) = create_vault(&env); - let (usdc, _, _) = create_usdc(&env, &owner); - - vault.init(&owner, &usdc, &None, &None, &None, &None); - assert_eq!(vault.get_revenue_pool(), None); -} - -#[test] -#[should_panic(expected = "unauthorized: caller is not admin")] -fn set_admin_unauthorized_panics() { - let env = Env::default(); - env.mock_all_auths(); - let owner = Address::generate(&env); - let attacker = Address::generate(&env); - let new_admin = Address::generate(&env); - let (_, vault) = create_vault(&env); - let (usdc, _, _) = create_usdc(&env, &owner); - - vault.init(&owner, &usdc, &None, &None, &None, &None); - vault.set_admin(&attacker, &new_admin); -} - -#[test] -fn deduct_with_revenue_pool_transfers_usdc() { - let env = Env::default(); - env.mock_all_auths(); - let owner = Address::generate(&env); - let rev_pool = Address::generate(&env); - let (vault_addr, vault) = create_vault(&env); - let (usdc, usdc_client, usdc_admin) = create_usdc(&env, &owner); - - fund_vault(&usdc_admin, &vault_addr, 1000); - vault.init( - &owner, - &usdc, - &Some(1000), - &None, - &Some(rev_pool.clone()), - &None, - ); - - let caller = Address::generate(&env); - vault.deduct(&caller, &200, &Some(Symbol::new(&env, "req_pool"))); - - assert_eq!(vault.balance(), 800); - assert_eq!(usdc_client.balance(&rev_pool), 200); - assert_eq!(usdc_client.balance(&vault_addr), 800); -} - -#[test] -fn deduct_without_request_id_event() { - let env = Env::default(); - let owner = Address::generate(&env); - let caller = Address::generate(&env); - 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, 500); - client.init(&owner, &usdc, &Some(500), &None, &None, &None); - - // Deduct without request_id - client.deduct(&caller, &100, &None); - - let events = env.events().all(); - let last_event = events.last().unwrap(); - assert_eq!(last_event.0, contract_id); - - 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")); - // topic[2] should be empty symbol when no request_id - let topic2: Symbol = topics.get(2).unwrap().into_val(&env); - assert_eq!(topic2, Symbol::new(&env, "")); - - // Verify data: (amount, new_balance) - let data: (i128, i128) = last_event.2.into_val(&env); - assert_eq!(data, (100, 400)); -} - -#[test] -fn batch_deduct_with_revenue_pool_transfers_usdc() { - let env = Env::default(); - env.mock_all_auths(); - let owner = Address::generate(&env); - let rev_pool = Address::generate(&env); - let (vault_addr, vault) = create_vault(&env); - let (usdc, usdc_client, usdc_admin) = create_usdc(&env, &owner); - - fund_vault(&usdc_admin, &vault_addr, 1000); - vault.init( - &owner, - &usdc, - &Some(1000), - &None, - &Some(rev_pool.clone()), - &None, - ); - - let caller = Address::generate(&env); - let items = soroban_sdk::vec![ - &env, - DeductItem { - amount: 100, - request_id: Some(Symbol::new(&env, "r1")), - }, - DeductItem { - amount: 200, - request_id: None, - }, - ]; - let new_balance = vault.batch_deduct(&caller, &items); - - assert_eq!(new_balance, 700); - assert_eq!(usdc_client.balance(&rev_pool), 300); - assert_eq!(usdc_client.balance(&vault_addr), 700); -} - -#[test] -#[should_panic(expected = "deduct amount exceeds max_deduct")] -fn batch_deduct_exceeds_max_deduct_panics() { - let env = Env::default(); - env.mock_all_auths(); - let owner = Address::generate(&env); - let (vault_addr, vault) = create_vault(&env); - let (usdc, _, usdc_admin) = create_usdc(&env, &owner); - - fund_vault(&usdc_admin, &vault_addr, 10_000); - vault.init(&owner, &usdc, &Some(10_000), &None, &None, &Some(100)); - - let caller = Address::generate(&env); - let items = soroban_sdk::vec![ - &env, - DeductItem { - amount: 101, - request_id: None, - }, - ]; - vault.batch_deduct(&caller, &items); -} - -#[test] -#[should_panic(expected = "amount must be positive")] -fn batch_deduct_zero_amount_panics() { - let env = Env::default(); - env.mock_all_auths(); - let owner = Address::generate(&env); - let (vault_addr, vault) = create_vault(&env); - let (usdc, _, usdc_admin) = create_usdc(&env, &owner); - - fund_vault(&usdc_admin, &vault_addr, 100); - vault.init(&owner, &usdc, &Some(100), &None, &None, &None); - - let caller = Address::generate(&env); - let items = soroban_sdk::vec![ - &env, - DeductItem { - amount: 0, - request_id: None, - }, - ]; - vault.batch_deduct(&caller, &items); -} - -#[test] -#[should_panic(expected = "batch_deduct requires at least one item")] -fn batch_deduct_empty_panics() { - let env = Env::default(); - env.mock_all_auths(); - let owner = Address::generate(&env); - let (vault_addr, vault) = create_vault(&env); - let (usdc, _, usdc_admin) = create_usdc(&env, &owner); - - fund_vault(&usdc_admin, &vault_addr, 100); - vault.init(&owner, &usdc, &Some(100), &None, &None, &None); - - let caller = Address::generate(&env); - let items: soroban_sdk::Vec = soroban_sdk::vec![&env]; - vault.batch_deduct(&caller, &items); -} - -#[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, _, usdc_admin) = create_usdc(&env, &owner); - - // min_deposit = 100 - vault.init(&owner, &usdc, &None, &Some(100), &None, &None); - fund_user(&usdc_admin, &owner, 50); - vault.deposit(&owner, &50); -} - -#[test] -#[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_addr, vault) = create_vault(&env); - let (usdc, _, usdc_admin) = create_usdc(&env, &owner); - - fund_vault(&usdc_admin, &vault_addr, 100); - vault.init(&owner, &usdc, &Some(100), &None, &None, &None); - vault.deduct(&owner, &0, &None); -} - -#[test] -#[should_panic(expected = "amount must be positive")] -fn withdraw_zero_panics() { - let env = Env::default(); - env.mock_all_auths(); - let owner = Address::generate(&env); - let (vault_addr, vault) = create_vault(&env); - let (usdc, _, usdc_admin) = create_usdc(&env, &owner); - - fund_vault(&usdc_admin, &vault_addr, 100); - vault.init(&owner, &usdc, &Some(100), &None, &None, &None); - vault.withdraw(&0); -} - -#[test] -#[should_panic(expected = "insufficient balance")] -fn withdraw_to_exceeds_balance_panics() { - let env = Env::default(); - env.mock_all_auths(); - let owner = Address::generate(&env); - let to = Address::generate(&env); - let (vault_addr, vault) = create_vault(&env); - let (usdc, _, usdc_admin) = create_usdc(&env, &owner); - - fund_vault(&usdc_admin, &vault_addr, 50); - vault.init(&owner, &usdc, &Some(50), &None, &None, &None); - vault.withdraw_to(&to, &100); -} - -#[test] -#[should_panic(expected = "amount must be positive")] -fn withdraw_to_zero_panics() { - let env = Env::default(); - env.mock_all_auths(); - let owner = Address::generate(&env); - let to = Address::generate(&env); - let (vault_addr, vault) = create_vault(&env); - let (usdc, _, usdc_admin) = create_usdc(&env, &owner); - - fund_vault(&usdc_admin, &vault_addr, 100); - vault.init(&owner, &usdc, &Some(100), &None, &None, &None); - vault.withdraw_to(&to, &0); -} - -/// Full lifecycle test exercising init (with revenue_pool + max_deduct), deposit, deduct -/// (with & without request_id), batch_deduct, withdraw, withdraw_to, distribute, and all getters. -#[test] -fn full_lifecycle_with_revenue_pool() { - let env = Env::default(); - env.mock_all_auths(); - let owner = Address::generate(&env); - let rev_pool = Address::generate(&env); - let (vault_addr, vault) = create_vault(&env); - let (usdc, usdc_client, usdc_admin) = create_usdc(&env, &owner); - - // Fund vault and owner - fund_vault(&usdc_admin, &vault_addr, 5000); - fund_user(&usdc_admin, &owner, 2000); - - // Init with revenue_pool and max_deduct - let meta = vault.init( - &owner, - &usdc, - &Some(5000), - &Some(10), - &Some(rev_pool.clone()), - &Some(1000), - ); - assert_eq!(meta.balance, 5000); - assert_eq!(meta.owner, owner); - assert_eq!(meta.min_deposit, 10); - - // Verify getters - assert_eq!(vault.get_admin(), owner); - assert_eq!(vault.get_max_deduct(), 1000); - assert_eq!(vault.get_revenue_pool(), Some(rev_pool.clone())); - - // Deposit - approve_spend(&env, &usdc_client, &owner, &vault_addr, 2000); - let bal = vault.deposit(&owner, &500); - assert_eq!(bal, 5500); - - // Deduct with request_id → transfers to revenue_pool - let caller = Address::generate(&env); - vault.deduct(&caller, &200, &Some(Symbol::new(&env, "full_req"))); - assert_eq!(vault.balance(), 5300); - assert_eq!(usdc_client.balance(&rev_pool), 200); - - // Deduct without request_id - vault.deduct(&caller, &100, &None); - assert_eq!(vault.balance(), 5200); - assert_eq!(usdc_client.balance(&rev_pool), 300); - - // Batch deduct with revenue_pool - let items = soroban_sdk::vec![ - &env, - DeductItem { - amount: 50, - request_id: Some(Symbol::new(&env, "batch1")), - }, - DeductItem { - amount: 75, - request_id: None, - }, - ]; - let bal = vault.batch_deduct(&caller, &items); - assert_eq!(bal, 5075); - assert_eq!(usdc_client.balance(&rev_pool), 425); - - // Withdraw - let bal = vault.withdraw(&100); - assert_eq!(bal, 4975); - - // Withdraw to - let to = Address::generate(&env); - let bal = vault.withdraw_to(&to, &200); - assert_eq!(bal, 4775); - assert_eq!(usdc_client.balance(&to), 200); - - // Distribute - let dev = Address::generate(&env); - vault.distribute(&owner, &dev, &500); - assert_eq!(usdc_client.balance(&dev), 500); - - // set_admin - let new_admin = Address::generate(&env); - vault.set_admin(&owner, &new_admin); - assert_eq!(vault.get_admin(), new_admin); - - // get_meta - let meta = vault.get_meta(); - assert_eq!(meta.balance, 4775); - assert_eq!(vault.balance(), 4775); -} - -#[test] -fn test_transfer_ownership() { - let env = Env::default(); - env.mock_all_auths(); - - let owner = Address::generate(&env); - let new_owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); - let client = CalloraVaultClient::new(&env, &contract_id); - - client.init(&owner, &Some(100)); - - // transfer ownership via client - client.transfer_ownership(&new_owner); - - 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"); - - let topics = &transfer_event.1; - let topic_old_owner: Address = topics.get(1).unwrap().into_val(&env); - assert!(topic_old_owner == owner); - - let topic_new_owner: Address = topics.get(2).unwrap().into_val(&env); - assert!(topic_new_owner == new_owner); -} - -#[test] -#[should_panic(expected = "new_owner must be different from current owner")] -fn test_transfer_ownership_same_address_fails() { - let env = Env::default(); - env.mock_all_auths(); - - let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); - let client = CalloraVaultClient::new(&env, &contract_id); - - client.init(&owner, &Some(100)); - - // This should panic because new_owner is the same as current owner - client.transfer_ownership(&owner); -} - -#[test] -#[should_panic] -fn test_transfer_ownership_not_owner() { - let env = Env::default(); - - let owner = Address::generate(&env); - let new_owner = Address::generate(&env); - let _not_owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); - let client = CalloraVaultClient::new(&env, &contract_id); - - // 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, &Some(100i128)).into_val(&env), - sub_invokes: &[], - }, - }]); - - client.init(&owner, &Some(100)); - - env.mock_auths(&[]); // Clear mock auths so subsequent calls require explicit valid signatures - - // This should panic because neither `owner` nor `not_owner` has provided a valid mock signature. - client.transfer_ownership(&new_owner); -} From e1dd8e859d0911c16b8a7200d38c200374f97a7c Mon Sep 17 00:00:00 2001 From: clintjeff2 Date: Thu, 26 Feb 2026 00:41:14 +0100 Subject: [PATCH 3/7] fixed test --- contracts/vault/src/test.rs | 137 ++---------------------------------- 1 file changed, 5 insertions(+), 132 deletions(-) diff --git a/contracts/vault/src/test.rs b/contracts/vault/src/test.rs index 09107bc..42ceb22 100644 --- a/contracts/vault/src/test.rs +++ b/contracts/vault/src/test.rs @@ -140,7 +140,7 @@ fn init_with_balance_emits_event() { fn init_defaults_balance_to_zero() { 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_token, _, _) = create_usdc(&env, &owner); @@ -153,7 +153,7 @@ fn init_defaults_balance_to_zero() { fn get_meta_returns_owner_and_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_token, _, usdc_admin) = create_usdc(&env, &owner); @@ -233,7 +233,7 @@ fn deduct_reduces_balance() { fn deduct_with_request_id() { 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); let caller = Address::generate(&env); @@ -714,7 +714,7 @@ fn deposit_below_minimum_fails() { let env = Env::default(); let owner = Address::generate(&env); let depositor = Address::generate(&env); - let contract_id = env.register(CalloraVault, ()); + let contract_id = env.register(CalloraVault {}, ()); let client = CalloraVaultClient::new(&env, &contract_id); let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); @@ -839,7 +839,7 @@ fn get_revenue_pool_returns_none_when_not_set() { fn get_max_deduct_returns_configured_value() { 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_token, _, _) = create_usdc(&env, &owner); @@ -1079,130 +1079,3 @@ fn owner_unchanged_after_deposit_and_deduct() { assert_eq!(client.get_meta().owner, owner); } - -#[test] -#[should_panic(expected = "vault already initialized")] -fn init_already_initialized_panics() { - let env = Env::default(); - let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault, ()); - let client = CalloraVaultClient::new(&env, &contract_id); - - env.mock_all_auths(); - 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::Rng; - - let env = Env::default(); - env.mock_all_auths(); - - let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault, ()); - let client = CalloraVaultClient::new(&env, &contract_id); - - let initial_balance: i128 = 1_000; - client.init(&owner, &Some(initial_balance)); - - let mut expected = initial_balance; - let mut rng = rand::thread_rng(); - - 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 = rng.gen_range(1..=expected.min(500)); - client.deduct(&owner, &amount); - expected -= amount; - } - - let balance = client.balance(); - assert!(balance >= 0, "balance went negative: {}", balance); - assert_eq!( - balance, expected, - "balance mismatch: got {}, expected {}", - balance, expected - ); - } - - assert_eq!(client.balance(), expected); -} - -#[test] -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 client = CalloraVaultClient::new(&env, &contract_id); - - client.init(&owner, &Some(100)); - let new_balance = client.deduct(&owner, &30); - assert_eq!(new_balance, 70); - assert_eq!(client.balance(), 70); -} - -#[test] -fn test_concurrent_deposits() { - let env = Env::default(); - env.mock_all_auths(); - - let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault, ()); - let client = CalloraVaultClient::new(&env, &contract_id); - - 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 init_twice_panics_on_reinit() { - let env = Env::default(); - let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault, ()); - let client = CalloraVaultClient::new(&env, &contract_id); - - env.mock_all_auths(); - client.init(&owner, &Some(25)); - assert_eq!(client.balance(), 25); - - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - client.init(&owner, &Some(50)); - })); - - assert!(result.is_err()); - assert_eq!(client.balance(), 25); -} - -#[test] -fn owner_unchanged_after_deposit_and_deduct() { - let env = Env::default(); - let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault, ()); - let client = CalloraVaultClient::new(&env, &contract_id); - - env.mock_all_auths(); - client.init(&owner, &Some(100)); - client.deposit(&owner, &50); - client.deduct(&owner, &30); - assert_eq!(client.get_meta().owner, owner); -} From 6403e7e4b9bc03777abd12f1c7216476761b86c7 Mon Sep 17 00:00:00 2001 From: clintjeff2 Date: Thu, 26 Feb 2026 00:43:48 +0100 Subject: [PATCH 4/7] fixed test --- contracts/revenue_pool/src/test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/revenue_pool/src/test.rs b/contracts/revenue_pool/src/test.rs index 76e4b81..260fed8 100644 --- a/contracts/revenue_pool/src/test.rs +++ b/contracts/revenue_pool/src/test.rs @@ -2,7 +2,7 @@ extern crate std; use super::*; use soroban_sdk::testutils::{Address as _, Events as _}; -use soroban_sdk::{token, vec}; +use soroban_sdk::token; fn create_usdc<'a>( env: &'a Env, From 28add4299eae2717d688b98051c0e15f84d7249e Mon Sep 17 00:00:00 2001 From: clintjeff2 Date: Thu, 26 Feb 2026 12:55:46 +0100 Subject: [PATCH 5/7] 100% passed test coverage --- contracts/revenue_pool/src/lib.rs | 36 +++++---------- contracts/revenue_pool/src/test.rs | 73 ++++++++++++++++++++++++++++++ contracts/vault/src/lib.rs | 18 +++----- coverage/cobertura.xml | 2 +- coverage/tarpaulin-report.html | 4 +- 5 files changed, 93 insertions(+), 40 deletions(-) diff --git a/contracts/revenue_pool/src/lib.rs b/contracts/revenue_pool/src/lib.rs index 2e840cd..04329ff 100644 --- a/contracts/revenue_pool/src/lib.rs +++ b/contracts/revenue_pool/src/lib.rs @@ -23,12 +23,8 @@ impl RevenuePool { if env.storage().instance().has(&Symbol::new(&env, ADMIN_KEY)) { panic!("revenue pool already initialized"); } - env.storage() - .instance() - .set(&Symbol::new(&env, ADMIN_KEY), &admin); - env.storage() - .instance() - .set(&Symbol::new(&env, USDC_KEY), &usdc_token); + env.storage().instance().set(&Symbol::new(&env, ADMIN_KEY), &admin); + env.storage().instance().set(&Symbol::new(&env, USDC_KEY), &usdc_token); env.events() .publish((Symbol::new(&env, "init"), admin), usdc_token); @@ -36,10 +32,8 @@ impl RevenuePool { /// 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!("revenue pool not initialized")) + env.storage().instance().get(&Symbol::new(&env, ADMIN_KEY)) + .expect("revenue pool not initialized") } /// Replace the current admin. Only the existing admin may call this. @@ -49,9 +43,7 @@ impl RevenuePool { if caller != current { panic!("unauthorized: caller is not admin"); } - env.storage() - .instance() - .set(&Symbol::new(&env, ADMIN_KEY), &new_admin); + env.storage().instance().set(&Symbol::new(&env, ADMIN_KEY), &new_admin); } /// Placeholder: record that payment was received (e.g. from vault). @@ -92,11 +84,9 @@ impl RevenuePool { panic!("amount must be positive"); } - let usdc_address: Address = env - .storage() - .instance() + let usdc_address: Address = env.storage().instance() .get(&Symbol::new(&env, USDC_KEY)) - .unwrap_or_else(|| panic!("revenue pool not initialized")); + .expect("revenue pool not initialized"); let usdc = token::Client::new(&env, &usdc_address); let contract_address = env.current_contract_address(); @@ -134,11 +124,9 @@ impl RevenuePool { total_amount += amount; } - let usdc_address: Address = env - .storage() - .instance() + let usdc_address: Address = env.storage().instance() .get(&Symbol::new(&env, USDC_KEY)) - .unwrap_or_else(|| panic!("revenue pool not initialized")); + .expect("revenue pool not initialized"); let usdc = token::Client::new(&env, &usdc_address); let contract_address = env.current_contract_address(); @@ -156,11 +144,9 @@ impl RevenuePool { /// Return this contract's USDC balance (for testing and dashboards). pub fn balance(env: Env) -> i128 { - let usdc_address: Address = env - .storage() - .instance() + let usdc_address: Address = env.storage().instance() .get(&Symbol::new(&env, USDC_KEY)) - .unwrap_or_else(|| panic!("revenue pool not initialized")); + .expect("revenue pool not initialized"); let usdc = token::Client::new(&env, &usdc_address); usdc.balance(&env.current_contract_address()) } diff --git a/contracts/revenue_pool/src/test.rs b/contracts/revenue_pool/src/test.rs index f1f6110..13c9c19 100644 --- a/contracts/revenue_pool/src/test.rs +++ b/contracts/revenue_pool/src/test.rs @@ -267,3 +267,76 @@ fn set_admin_unauthorized_fails() { let result = client.try_set_admin(&intruder, &new_admin); assert!(result.is_err(), "expected error for unauthorized set_admin"); } + +#[test] +fn batch_distribute_success() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let dev1 = Address::generate(&env); + let dev2 = Address::generate(&env); + let (pool_addr, client) = create_pool(&env); + let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &admin); + + client.init(&admin, &usdc_address); + fund_pool(&usdc_admin, &pool_addr, 1_000); + + let payments = soroban_sdk::vec![&env, (dev1.clone(), 300_i128), (dev2.clone(), 200_i128)]; + client.batch_distribute(&admin, &payments); + + assert_eq!(usdc_client.balance(&dev1), 300); + assert_eq!(usdc_client.balance(&dev2), 200); + assert_eq!(usdc_client.balance(&pool_addr), 500); +} + +#[test] +#[should_panic(expected = "unauthorized: caller is not admin")] +fn batch_distribute_unauthorized_panics() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let attacker = Address::generate(&env); + let dev = Address::generate(&env); + let (pool_addr, client) = create_pool(&env); + let (usdc_address, _, usdc_admin) = create_usdc(&env, &admin); + + client.init(&admin, &usdc_address); + fund_pool(&usdc_admin, &pool_addr, 500); + + let payments = soroban_sdk::vec![&env, (dev.clone(), 100_i128)]; + client.batch_distribute(&attacker, &payments); +} + +#[test] +#[should_panic(expected = "amount must be positive")] +fn batch_distribute_zero_amount_panics() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let dev = Address::generate(&env); + let (pool_addr, client) = create_pool(&env); + let (usdc_address, _, usdc_admin) = create_usdc(&env, &admin); + + client.init(&admin, &usdc_address); + fund_pool(&usdc_admin, &pool_addr, 500); + + let payments = soroban_sdk::vec![&env, (dev.clone(), 0_i128)]; + client.batch_distribute(&admin, &payments); +} + +#[test] +#[should_panic(expected = "insufficient USDC balance")] +fn batch_distribute_insufficient_balance_panics() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let dev = Address::generate(&env); + let (pool_addr, client) = create_pool(&env); + let (usdc_address, _, usdc_admin) = create_usdc(&env, &admin); + + client.init(&admin, &usdc_address); + fund_pool(&usdc_admin, &pool_addr, 100); + + let payments = soroban_sdk::vec![&env, (dev.clone(), 200_i128)]; + client.batch_distribute(&admin, &payments); +} diff --git a/contracts/vault/src/lib.rs b/contracts/vault/src/lib.rs index ce0d4b7..ccffdc0 100644 --- a/contracts/vault/src/lib.rs +++ b/contracts/vault/src/lib.rs @@ -204,11 +204,9 @@ impl CalloraVault { meta.min_deposit ); - let usdc_address: Address = env - .storage() - .instance() + let usdc_address: Address = env.storage().instance() .get(&Symbol::new(&env, USDC_KEY)) - .unwrap_or_else(|| panic!("vault not initialized")); + .expect("vault not initialized"); let usdc = token::Client::new(&env, &usdc_address); usdc.transfer_from( &env.current_contract_address(), @@ -305,11 +303,9 @@ impl CalloraVault { assert!(amount > 0, "amount must be positive"); assert!(meta.balance >= amount, "insufficient balance"); - let usdc_address: Address = env - .storage() - .instance() + let usdc_address: Address = env.storage().instance() .get(&Symbol::new(&env, USDC_KEY)) - .unwrap_or_else(|| panic!("vault not initialized")); + .expect("vault not initialized"); let usdc = token::Client::new(&env, &usdc_address); usdc.transfer(&env.current_contract_address(), &meta.owner, &amount); @@ -331,11 +327,9 @@ impl CalloraVault { assert!(amount > 0, "amount must be positive"); assert!(meta.balance >= amount, "insufficient balance"); - let usdc_address: Address = env - .storage() - .instance() + let usdc_address: Address = env.storage().instance() .get(&Symbol::new(&env, USDC_KEY)) - .unwrap_or_else(|| panic!("vault not initialized")); + .expect("vault not initialized"); let usdc = token::Client::new(&env, &usdc_address); usdc.transfer(&env.current_contract_address(), &to, &amount); diff --git a/coverage/cobertura.xml b/coverage/cobertura.xml index 19af539..789c2b5 100644 --- a/coverage/cobertura.xml +++ b/coverage/cobertura.xml @@ -1 +1 @@ -/home/jeffersonyouashi/Documents/DRIPS/Callora-Contracts \ No newline at end of file +/home/jeffersonyouashi/Documents/DRIPS/Callora-Contracts \ No newline at end of file diff --git a/coverage/tarpaulin-report.html b/coverage/tarpaulin-report.html index 0cb9653..4c1762d 100644 --- a/coverage/tarpaulin-report.html +++ b/coverage/tarpaulin-report.html @@ -193,8 +193,8 @@