From b542947d76af389b4a0a0df9b5baab3e3fa1eeba Mon Sep 17 00:00:00 2001 From: Sam_Rytech <107815081+Sam-Rytech@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:29:57 +0000 Subject: [PATCH 1/6] test: require_auth coverage sweep across contracts - Audited all privileged entrypoints in vault, revenue_pool, and settlement - Added negative require_auth tests to each crate's test.rs - Fixed pre-existing setup_contract missing third_party in settlement tests - Documented intentional exceptions: settlement::init and vault::require_owner - Updated SECURITY.md with audit findings and cross-references - All 180 tests pass Closes #160 --- SECURITY.md | 19 ++++ contracts/revenue_pool/src/test.rs | 62 ++++++++++++ contracts/settlement/src/test.rs | 63 ++++++++++++ contracts/vault/src/test.rs | 151 +++++++++++++++++++++++++++++ 4 files changed, 295 insertions(+) diff --git a/SECURITY.md b/SECURITY.md index 95aac34..0ba7430 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -165,3 +165,22 @@ Before any mainnet deployment: --- **Note**: This checklist should be reviewed and updated regularly as new security patterns emerge and the codebase evolves. + +## require_auth() Audit (Issue #160) + +All privileged entrypoints across `vault`, `revenue_pool`, and `settlement` contracts +have been audited for `require_auth()` coverage as part of Issue #160. + +### Findings +- All privileged functions call `require_auth()` on the caller before executing. ✅ +- Negative tests added to each crate's `test.rs` confirming unauthenticated calls are rejected. + +### Intentional Exceptions +| Contract | Function | Reason | +|------------|------------------|--------| +| settlement | `init()` | One-time initializer guarded by already-initialized panic; no auth required by design. | +| vault | `require_owner()`| Internal helper using `assert!` for address equality. All public callers invoke `caller.require_auth()` before calling this helper, so host-level auth is enforced transitively. Documented gap: `require_owner` itself does not call `require_auth()`. | + +### Cross-reference +- Audit branch: `test/require-auth-sweep` +- Tests: `contracts/vault/src/test.rs`, `contracts/revenue_pool/src/test.rs`, `contracts/settlement/src/test.rs` diff --git a/contracts/revenue_pool/src/test.rs b/contracts/revenue_pool/src/test.rs index 7d86bb1..be49527 100644 --- a/contracts/revenue_pool/src/test.rs +++ b/contracts/revenue_pool/src/test.rs @@ -266,3 +266,65 @@ fn batch_distribute_success_events() { } } } + +// --- require_auth audit tests (Issue #160) --- +#[test] +fn require_auth_set_admin_fails_without_auth() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let attacker = Address::generate(&env); + let new_admin = Address::generate(&env); + let (_, client) = create_pool(&env); + let (usdc, _, _) = create_usdc(&env, &admin); + client.init(&admin, &usdc); + env.set_auths(&[]); + let result = client.try_set_admin(&attacker, &new_admin); + assert!(result.is_err(), "set_admin must require auth"); +} +#[test] +fn require_auth_distribute_fails_without_auth() { + let env = Env::default(); + let admin = Address::generate(&env); + let attacker = Address::generate(&env); + let recipient = Address::generate(&env); + let (pool_addr, client) = create_pool(&env); + let (usdc, _, usdc_admin) = create_usdc(&env, &admin); + env.mock_all_auths(); + client.init(&admin, &usdc); + fund_pool(&usdc_admin, &pool_addr, 1000); + env.set_auths(&[]); + let result = client.try_distribute(&attacker, &recipient, &100); + assert!(result.is_err(), "distribute must require auth"); +} +#[test] +fn require_auth_batch_distribute_fails_without_auth() { + let env = Env::default(); + let admin = Address::generate(&env); + let attacker = Address::generate(&env); + let dev = Address::generate(&env); + let (pool_addr, client) = create_pool(&env); + let (usdc, _, usdc_admin) = create_usdc(&env, &admin); + env.mock_all_auths(); + client.init(&admin, &usdc); + fund_pool(&usdc_admin, &pool_addr, 1000); + let mut payments: Vec<(Address, i128)> = Vec::new(&env); + payments.push_back((dev.clone(), 100_i128)); + env.set_auths(&[]); + let result = client.try_batch_distribute(&attacker, &payments); + assert!(result.is_err(), "batch_distribute must require auth"); +} +#[test] +fn require_auth_receive_payment_fails_without_auth() { + let env = Env::default(); + let admin = Address::generate(&env); + let attacker = Address::generate(&env); + let (pool_addr, client) = create_pool(&env); + let (usdc, _, usdc_admin) = create_usdc(&env, &admin); + env.mock_all_auths(); + client.init(&admin, &usdc); + fund_pool(&usdc_admin, &pool_addr, 1000); + env.set_auths(&[]); + let result = client.try_receive_payment(&attacker, &100, &false); + assert!(result.is_err(), "receive_payment must require auth"); +} diff --git a/contracts/settlement/src/test.rs b/contracts/settlement/src/test.rs index 08f19c5..38352f4 100644 --- a/contracts/settlement/src/test.rs +++ b/contracts/settlement/src/test.rs @@ -16,6 +16,7 @@ mod settlement_tests { let addr = env.register(CalloraSettlement, ()); let client = CalloraSettlementClient::new(&env, &addr); client.init(&admin, &vault); + let third_party = Address::generate(&env); (env, addr, admin, vault, third_party) } @@ -913,4 +914,66 @@ mod settlement_tests { assert_eq!(pool_after.last_updated, 1_700_000_100); assert_eq!(pool_after.total_balance, 1500i128); } + + + // --- require_auth audit tests (Issue #160) --- + #[test] + fn require_auth_set_admin_fails_without_auth() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let attacker = Address::generate(&env); + let new_admin = Address::generate(&env); + let vault = Address::generate(&env); + let addr = env.register(CalloraSettlement, ()); + let client = CalloraSettlementClient::new(&env, &addr); + client.init(&admin, &vault); + env.set_auths(&[]); + let result = client.try_set_admin(&attacker, &new_admin); + assert!(result.is_err(), "set_admin must require auth"); + } + #[test] + fn require_auth_set_vault_fails_without_auth() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let attacker = Address::generate(&env); + let vault = Address::generate(&env); + let new_vault = Address::generate(&env); + let addr = env.register(CalloraSettlement, ()); + let client = CalloraSettlementClient::new(&env, &addr); + client.init(&admin, &vault); + env.set_auths(&[]); + let result = client.try_set_vault(&attacker, &new_vault); + assert!(result.is_err(), "set_vault must require auth"); + } + #[test] + fn require_auth_receive_payment_fails_without_auth() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let vault = Address::generate(&env); + let attacker = Address::generate(&env); + let addr = env.register(CalloraSettlement, ()); + let client = CalloraSettlementClient::new(&env, &addr); + client.init(&admin, &vault); + env.set_auths(&[]); + let result = client.try_receive_payment(&attacker, &100i128, &true, &None); + assert!(result.is_err(), "receive_payment must require auth"); + } + // SECURITY NOTE: settlement::init has no require_auth() by design. + // It is a one-time initializer guarded by an already-initialized panic. + // This is an intentional exception documented per Issue #160. + #[test] + fn init_no_auth_required_intentional_exception() { + let env = Env::default(); + let admin = Address::generate(&env); + let vault = Address::generate(&env); + let addr = env.register(CalloraSettlement, ()); + let client = CalloraSettlementClient::new(&env, &addr); + // No mock_all_auths — init should succeed without host auth (intentional) + client.init(&admin, &vault); + assert_eq!(client.get_admin(), admin); + } + } diff --git a/contracts/vault/src/test.rs b/contracts/vault/src/test.rs index e05f35a..77afb65 100644 --- a/contracts/vault/src/test.rs +++ b/contracts/vault/src/test.rs @@ -2141,3 +2141,154 @@ fn non_owner_cannot_clear_allowed_depositors() { let result = client.try_clear_allowed_depositors(&attacker); assert!(result.is_err(), "non-owner must not clear allowlist"); } + +// --- require_auth audit tests (Issue #160) --- +#[test] +fn require_auth_set_admin_fails_without_auth() { + let env = Env::default(); + let owner = Address::generate(&env); + let attacker = Address::generate(&env); + let new_admin = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, _, usdc_admin) = create_usdc(&env, &owner); + env.mock_all_auths(); + fund_vault(&usdc_admin, &vault_address, 100); + client.init(&owner, &usdc, &Some(100), &None, &None, &None, &None); + env.set_auths(&[]); + let result = client.try_set_admin(&attacker, &new_admin); + assert!(result.is_err(), "set_admin must require auth"); +} +#[test] +fn require_auth_distribute_fails_without_auth() { + let env = Env::default(); + let owner = Address::generate(&env); + let attacker = Address::generate(&env); + let recipient = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, _, usdc_admin) = create_usdc(&env, &owner); + env.mock_all_auths(); + fund_vault(&usdc_admin, &vault_address, 1000); + client.init(&owner, &usdc, &Some(1000), &None, &None, &None, &None); + env.set_auths(&[]); + let result = client.try_distribute(&attacker, &recipient, &100); + assert!(result.is_err(), "distribute must require auth"); +} +#[test] +fn require_auth_pause_fails_without_auth() { + let env = Env::default(); + let owner = Address::generate(&env); + let attacker = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, _, usdc_admin) = create_usdc(&env, &owner); + env.mock_all_auths(); + fund_vault(&usdc_admin, &vault_address, 100); + client.init(&owner, &usdc, &Some(100), &None, &None, &None, &None); + env.set_auths(&[]); + let result = client.try_pause(&attacker); + assert!(result.is_err(), "pause must require auth"); +} +#[test] +fn require_auth_unpause_fails_without_auth() { + let env = Env::default(); + let owner = Address::generate(&env); + let attacker = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, _, usdc_admin) = create_usdc(&env, &owner); + env.mock_all_auths(); + fund_vault(&usdc_admin, &vault_address, 100); + client.init(&owner, &usdc, &Some(100), &None, &None, &None, &None); + client.pause(&owner); + env.set_auths(&[]); + let result = client.try_unpause(&attacker); + assert!(result.is_err(), "unpause must require auth"); +} +#[test] +fn require_auth_deposit_fails_without_auth() { + let env = Env::default(); + let owner = Address::generate(&env); + let depositor = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, _, usdc_admin) = create_usdc(&env, &owner); + env.mock_all_auths(); + fund_vault(&usdc_admin, &vault_address, 100); + usdc_admin.mint(&depositor, &500); + client.init(&owner, &usdc, &Some(100), &None, &None, &None, &None); + client.set_allowed_depositor(&owner, &Some(depositor.clone())); + env.set_auths(&[]); + let result = client.try_deposit(&depositor, &100); + assert!(result.is_err(), "deposit must require auth"); +} +#[test] +fn require_auth_deduct_fails_without_auth() { + let env = Env::default(); + let owner = Address::generate(&env); + let attacker = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, _, usdc_admin) = create_usdc(&env, &owner); + env.mock_all_auths(); + fund_vault(&usdc_admin, &vault_address, 1000); + client.init(&owner, &usdc, &Some(1000), &None, &None, &None, &None); + env.set_auths(&[]); + let result = client.try_deduct(&attacker, &100, &None); + assert!(result.is_err(), "deduct must require auth"); +} +#[test] +fn require_auth_set_revenue_pool_fails_without_auth() { + let env = Env::default(); + let owner = Address::generate(&env); + let attacker = Address::generate(&env); + let pool = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, _, usdc_admin) = create_usdc(&env, &owner); + env.mock_all_auths(); + fund_vault(&usdc_admin, &vault_address, 100); + client.init(&owner, &usdc, &Some(100), &None, &None, &None, &None); + env.set_auths(&[]); + let result = client.try_set_revenue_pool(&attacker, &Some(pool)); + assert!(result.is_err(), "set_revenue_pool must require auth"); +} +#[test] +fn require_auth_set_settlement_fails_without_auth() { + let env = Env::default(); + let owner = Address::generate(&env); + let attacker = Address::generate(&env); + let settlement = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, _, usdc_admin) = create_usdc(&env, &owner); + env.mock_all_auths(); + fund_vault(&usdc_admin, &vault_address, 100); + client.init(&owner, &usdc, &Some(100), &None, &None, &None, &None); + env.set_auths(&[]); + let result = client.try_set_settlement(&attacker, &settlement); + assert!(result.is_err(), "set_settlement must require auth"); +} +#[test] +fn require_auth_transfer_ownership_fails_without_auth() { + let env = Env::default(); + let owner = Address::generate(&env); + let new_owner = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, _, usdc_admin) = create_usdc(&env, &owner); + env.mock_all_auths(); + fund_vault(&usdc_admin, &vault_address, 100); + client.init(&owner, &usdc, &Some(100), &None, &None, &None, &None); + env.set_auths(&[]); + let result = client.try_transfer_ownership(&new_owner); + assert!(result.is_err(), "transfer_ownership must require auth"); +} +// SECURITY NOTE: require_owner() uses assert! instead of require_auth(). +// All callers already invoke caller.require_auth() before calling require_owner, +// so host-level auth is enforced transitively. Documented per Issue #160. +#[test] +fn require_owner_rejects_non_owner_via_assert() { + let env = Env::default(); + let owner = Address::generate(&env); + let attacker = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, _, usdc_admin) = create_usdc(&env, &owner); + env.mock_all_auths(); + fund_vault(&usdc_admin, &vault_address, 100); + client.init(&owner, &usdc, &Some(100), &None, &None, &None, &None); + let result = client.try_require_owner(&attacker); + assert!(result.is_err(), "require_owner rejects non-owner via assert"); +} From 538442cf7eec0eb2f150a8c08b7532aca5b4a56b Mon Sep 17 00:00:00 2001 From: Sam_Rytech <107815081+Sam-Rytech@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:33:49 +0000 Subject: [PATCH 2/6] test: require_auth coverage sweep across contracts - Audited all privileged entrypoints in vault, revenue_pool, and settlement - Added negative require_auth tests to each crate's test.rs - Fixed pre-existing setup_contract missing third_party in settlement tests - Documented intentional exceptions: settlement::init and vault::require_owner - Updated SECURITY.md with audit findings and cross-references - All 180 tests pass Closes #160 --- contracts/settlement/src/test.rs | 6 ++---- contracts/vault/src/test.rs | 5 ++++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/contracts/settlement/src/test.rs b/contracts/settlement/src/test.rs index 38352f4..0352c1a 100644 --- a/contracts/settlement/src/test.rs +++ b/contracts/settlement/src/test.rs @@ -13,7 +13,7 @@ mod settlement_tests { env.mock_all_auths(); let admin = Address::generate(&env); let vault = Address::generate(&env); - let addr = env.register(CalloraSettlement, ()); + let addr = env.register(CalloraSettlement, ()); let client = CalloraSettlementClient::new(&env, &addr); client.init(&admin, &vault); let third_party = Address::generate(&env); @@ -153,7 +153,7 @@ mod settlement_tests { env.mock_all_auths(); let admin = Address::generate(&env); let vault = Address::generate(&env); - let addr = env.register(CalloraSettlement, ()); + let addr = env.register(CalloraSettlement, ()); let client = CalloraSettlementClient::new(&env, &addr); client.init(&admin, &vault); client.receive_payment(&admin, &100i128, &true, &None); @@ -915,7 +915,6 @@ mod settlement_tests { assert_eq!(pool_after.total_balance, 1500i128); } - // --- require_auth audit tests (Issue #160) --- #[test] fn require_auth_set_admin_fails_without_auth() { @@ -975,5 +974,4 @@ mod settlement_tests { client.init(&admin, &vault); assert_eq!(client.get_admin(), admin); } - } diff --git a/contracts/vault/src/test.rs b/contracts/vault/src/test.rs index 77afb65..713be28 100644 --- a/contracts/vault/src/test.rs +++ b/contracts/vault/src/test.rs @@ -2290,5 +2290,8 @@ fn require_owner_rejects_non_owner_via_assert() { fund_vault(&usdc_admin, &vault_address, 100); client.init(&owner, &usdc, &Some(100), &None, &None, &None, &None); let result = client.try_require_owner(&attacker); - assert!(result.is_err(), "require_owner rejects non-owner via assert"); + assert!( + result.is_err(), + "require_owner rejects non-owner via assert" + ); } From 26075cb3ec10a7c5927d0007060a3f8c9fbf22ce Mon Sep 17 00:00:00 2001 From: Sam_Rytech <107815081+Sam-Rytech@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:34:32 +0000 Subject: [PATCH 3/6] test: require_auth coverage sweep across contracts - Audited all privileged entrypoints in vault, revenue_pool, and settlement - Added negative require_auth tests to each crate's test.rs - Fixed pre-existing setup_contract missing third_party in settlement tests - Documented intentional exceptions: settlement::init and vault::require_owner - Updated SECURITY.md with audit findings and cross-references - All 180 tests pass Closes #160 --- contracts/revenue_pool/src/test.rs | 80 ++++++++++++++++++++++++++++++ contracts/vault/src/lib.rs | 10 +++- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/contracts/revenue_pool/src/test.rs b/contracts/revenue_pool/src/test.rs index be49527..3f1a907 100644 --- a/contracts/revenue_pool/src/test.rs +++ b/contracts/revenue_pool/src/test.rs @@ -328,3 +328,83 @@ fn require_auth_receive_payment_fails_without_auth() { let result = client.try_receive_payment(&attacker, &100, &false); assert!(result.is_err(), "receive_payment must require auth"); } + +#[test] +fn set_admin_non_admin_caller_panics() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let attacker = Address::generate(&env); + let new_admin = Address::generate(&env); + let (_, client) = create_pool(&env); + let (usdc, _, _) = create_usdc(&env, &admin); + client.init(&admin, &usdc); + let result = client.try_set_admin(&attacker, &new_admin); + assert!(result.is_err(), "non-admin cannot call set_admin"); +} +#[test] +fn claim_admin_wrong_pending_panics() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let new_admin = Address::generate(&env); + let attacker = Address::generate(&env); + let (_, client) = create_pool(&env); + let (usdc, _, _) = create_usdc(&env, &admin); + client.init(&admin, &usdc); + client.set_admin(&admin, &new_admin); + let result = client.try_claim_admin(&attacker); + assert!(result.is_err(), "wrong pending admin cannot claim"); +} +#[test] +fn receive_payment_emits_event() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let (pool_addr, client) = create_pool(&env); + let (usdc, _, usdc_admin) = create_usdc(&env, &admin); + client.init(&admin, &usdc); + fund_pool(&usdc_admin, &pool_addr, 500); + client.receive_payment(&admin, &100, &true); + let events = env.events().all(); + assert!(!events.is_empty()); +} +#[test] +fn distribute_self_recipient_panics() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let (pool_addr, client) = create_pool(&env); + let (usdc, _, usdc_admin) = create_usdc(&env, &admin); + client.init(&admin, &usdc); + fund_pool(&usdc_admin, &pool_addr, 500); + let result = client.try_distribute(&admin, &pool_addr, &100); + assert!(result.is_err(), "cannot distribute to contract itself"); +} +#[test] +fn distribute_non_admin_panics() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let attacker = Address::generate(&env); + let recipient = Address::generate(&env); + let (pool_addr, client) = create_pool(&env); + let (usdc, _, usdc_admin) = create_usdc(&env, &admin); + client.init(&admin, &usdc); + fund_pool(&usdc_admin, &pool_addr, 500); + let result = client.try_distribute(&attacker, &recipient, &100); + assert!(result.is_err(), "non-admin cannot distribute"); +} +#[test] +fn distribute_zero_amount_panics() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let recipient = Address::generate(&env); + let (pool_addr, client) = create_pool(&env); + let (usdc, _, usdc_admin) = create_usdc(&env, &admin); + client.init(&admin, &usdc); + fund_pool(&usdc_admin, &pool_addr, 500); + let result = client.try_distribute(&admin, &recipient, &0); + assert!(result.is_err(), "zero amount distribute must fail"); +} diff --git a/contracts/vault/src/lib.rs b/contracts/vault/src/lib.rs index f27769b..1a04516 100644 --- a/contracts/vault/src/lib.rs +++ b/contracts/vault/src/lib.rs @@ -325,7 +325,10 @@ impl CalloraVault { if let Some(s) = inst.get::(&StorageKey::Settlement) { let ut: Address = inst.get(&StorageKey::UsdcToken).unwrap(); Self::transfer_funds(&env, &ut, &s, amount); - } else if inst.get::(&StorageKey::RevenuePool).is_some() { + } else if inst + .get::(&StorageKey::RevenuePool) + .is_some() + { Self::transfer_to_revenue_pool(env.clone(), amount); } let rid = request_id.unwrap_or(Symbol::new(&env, "")); @@ -372,7 +375,10 @@ impl CalloraVault { if let Some(s) = inst.get::(&StorageKey::Settlement) { let ut: Address = inst.get(&StorageKey::UsdcToken).unwrap(); Self::transfer_funds(&env, &ut, &s, total); - } else if inst.get::(&StorageKey::RevenuePool).is_some() { + } else if inst + .get::(&StorageKey::RevenuePool) + .is_some() + { Self::transfer_to_revenue_pool(env.clone(), total); } meta.balance From 45e3b75ae069223a920d12c46ea1bcd97b748ee7 Mon Sep 17 00:00:00 2001 From: Sam_Rytech <107815081+Sam-Rytech@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:03:11 +0000 Subject: [PATCH 4/6] test(vault): min_deposit boundary coverage --- contracts/vault/src/test.rs | 133 ++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/contracts/vault/src/test.rs b/contracts/vault/src/test.rs index 713be28..36e0e81 100644 --- a/contracts/vault/src/test.rs +++ b/contracts/vault/src/test.rs @@ -2295,3 +2295,136 @@ fn require_owner_rejects_non_owner_via_assert() { "require_owner rejects non-owner via assert" ); } + +// --------------------------------------------------------------------------- +// Issue #151 — min_deposit boundary tests +// --------------------------------------------------------------------------- + +#[test] +fn deposit_exact_min_deposit_succeeds() { + let env = Env::default(); + let owner = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, usdc_client, usdc_admin) = create_usdc(&env, &owner); + + env.mock_all_auths(); + fund_vault(&usdc_admin, &vault_address, 0); + client.init(&owner, &usdc, &None, &None, &Some(50), &None, &None); + + usdc_admin.mint(&owner, &50); + usdc_client.approve(&owner, &vault_address, &50, &1000); + let balance = client.deposit(&owner, &50); + assert_eq!(balance, 50); +} + +#[test] +#[should_panic(expected = "deposit below minimum")] +fn deposit_below_min_deposit_panics() { + let env = Env::default(); + let owner = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, usdc_client, usdc_admin) = create_usdc(&env, &owner); + + env.mock_all_auths(); + fund_vault(&usdc_admin, &vault_address, 0); + client.init(&owner, &usdc, &None, &None, &Some(50), &None, &None); + + usdc_admin.mint(&owner, &49); + usdc_client.approve(&owner, &vault_address, &49, &1000); + client.deposit(&owner, &49); +} + +#[test] +fn deposit_above_min_deposit_succeeds() { + let env = Env::default(); + let owner = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, usdc_client, usdc_admin) = create_usdc(&env, &owner); + + env.mock_all_auths(); + fund_vault(&usdc_admin, &vault_address, 0); + client.init(&owner, &usdc, &None, &None, &Some(50), &None, &None); + + usdc_admin.mint(&owner, &51); + usdc_client.approve(&owner, &vault_address, &51, &1000); + let balance = client.deposit(&owner, &51); + assert_eq!(balance, 51); +} + +#[test] +fn deposit_with_zero_min_deposit_allows_any_positive_amount() { + // When min_deposit is 0 (default), any positive amount should succeed. + let env = Env::default(); + let owner = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, usdc_client, usdc_admin) = create_usdc(&env, &owner); + + env.mock_all_auths(); + fund_vault(&usdc_admin, &vault_address, 0); + client.init(&owner, &usdc, &None, &None, &Some(0), &None, &None); + + usdc_admin.mint(&owner, &1); + usdc_client.approve(&owner, &vault_address, &1, &1000); + let balance = client.deposit(&owner, &1); + assert_eq!(balance, 1); +} + +#[test] +fn init_min_deposit_stored_in_meta() { + let env = Env::default(); + let owner = Address::generate(&env); + let (_, client) = create_vault(&env); + let (usdc, _, _) = create_usdc(&env, &owner); + + env.mock_all_auths(); + client.init(&owner, &usdc, &None, &None, &Some(100), &None, &None); + let meta = client.get_meta(); + assert_eq!(meta.min_deposit, 100); +} + +#[test] +fn init_default_min_deposit_is_zero() { + let env = Env::default(); + let owner = Address::generate(&env); + let (_, client) = create_vault(&env); + let (usdc, _, _) = create_usdc(&env, &owner); + + env.mock_all_auths(); + client.init(&owner, &usdc, &None, &None, &None, &None, &None); + let meta = client.get_meta(); + assert_eq!(meta.min_deposit, 0); +} + +#[test] +fn deposit_one_below_large_min_deposit_panics() { + let env = Env::default(); + let owner = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, usdc_client, usdc_admin) = create_usdc(&env, &owner); + + env.mock_all_auths(); + fund_vault(&usdc_admin, &vault_address, 0); + client.init(&owner, &usdc, &None, &None, &Some(1_000_000), &None, &None); + + usdc_admin.mint(&owner, &999_999); + usdc_client.approve(&owner, &vault_address, &999_999, &1000); + let result = client.try_deposit(&owner, &999_999); + assert!(result.is_err(), "deposit one below large min_deposit must fail"); +} + +#[test] +fn deposit_exact_large_min_deposit_succeeds() { + let env = Env::default(); + let owner = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, usdc_client, usdc_admin) = create_usdc(&env, &owner); + + env.mock_all_auths(); + fund_vault(&usdc_admin, &vault_address, 0); + client.init(&owner, &usdc, &None, &None, &Some(1_000_000), &None, &None); + + usdc_admin.mint(&owner, &1_000_000); + usdc_client.approve(&owner, &vault_address, &1_000_000, &1000); + let balance = client.deposit(&owner, &1_000_000); + assert_eq!(balance, 1_000_000); +} From 3cdb5fdb9afdf5a051be09715d87d2e40debce5c Mon Sep 17 00:00:00 2001 From: Sam_Rytech <107815081+Sam-Rytech@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:07:39 +0000 Subject: [PATCH 5/6] test(vault): min_deposit boundary coverage --- contracts/vault/src/test.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contracts/vault/src/test.rs b/contracts/vault/src/test.rs index 36e0e81..4efa8e0 100644 --- a/contracts/vault/src/test.rs +++ b/contracts/vault/src/test.rs @@ -2409,7 +2409,10 @@ fn deposit_one_below_large_min_deposit_panics() { usdc_admin.mint(&owner, &999_999); usdc_client.approve(&owner, &vault_address, &999_999, &1000); let result = client.try_deposit(&owner, &999_999); - assert!(result.is_err(), "deposit one below large min_deposit must fail"); + assert!( + result.is_err(), + "deposit one below large min_deposit must fail" + ); } #[test] From bd3a05c83f4312bdafa718668b186cf2ad6e5993 Mon Sep 17 00:00:00 2001 From: Sam_Rytech <107815081+Sam-Rytech@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:15:45 +0000 Subject: [PATCH 6/6] test(vault): max_deduct boundary coverage --- contracts/vault/src/test.rs | 118 ++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/contracts/vault/src/test.rs b/contracts/vault/src/test.rs index 4efa8e0..688ed25 100644 --- a/contracts/vault/src/test.rs +++ b/contracts/vault/src/test.rs @@ -2431,3 +2431,121 @@ fn deposit_exact_large_min_deposit_succeeds() { let balance = client.deposit(&owner, &1_000_000); assert_eq!(balance, 1_000_000); } + +// --------------------------------------------------------------------------- +// max_deduct boundary tests +// --------------------------------------------------------------------------- + +#[test] +fn deduct_equal_to_max_deduct_succeeds() { + let env = Env::default(); + let owner = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, usdc_client, usdc_admin) = create_usdc(&env, &owner); + env.mock_all_auths(); + fund_vault(&usdc_admin, &vault_address, 500); + // max_deduct = 100, deposit 200 so balance is sufficient + client.init(&owner, &usdc, &Some(500), &None, &None, &None, &Some(100)); + usdc_admin.mint(&owner, &200); + usdc_client.approve(&owner, &vault_address, &200, &1000); + client.deposit(&owner, &200); + // deduct exactly equal to max_deduct — must succeed + let balance = client.deduct(&owner, &100, &None); + assert_eq!(balance, 600); +} + +#[test] +#[should_panic(expected = "deduct amount exceeds max_deduct")] +fn deduct_above_max_deduct_panics() { + let env = Env::default(); + let owner = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, usdc_client, usdc_admin) = create_usdc(&env, &owner); + env.mock_all_auths(); + fund_vault(&usdc_admin, &vault_address, 500); + client.init(&owner, &usdc, &Some(500), &None, &None, &None, &Some(100)); + usdc_admin.mint(&owner, &200); + usdc_client.approve(&owner, &vault_address, &200, &1000); + client.deposit(&owner, &200); + // deduct 101 > max_deduct 100 — must panic + client.deduct(&owner, &101, &None); +} + +#[test] +fn deduct_default_cap_is_i128_max() { + let env = Env::default(); + let owner = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, usdc_client, usdc_admin) = create_usdc(&env, &owner); + env.mock_all_auths(); + fund_vault(&usdc_admin, &vault_address, 0); + // no max_deduct supplied — default cap (i128::MAX) applies + client.init(&owner, &usdc, &None, &None, &None, &None, &None); + usdc_admin.mint(&owner, &1_000_000); + usdc_client.approve(&owner, &vault_address, &1_000_000, &1000); + client.deposit(&owner, &1_000_000); + // large deduct well below i128::MAX should succeed + let balance = client.deduct(&owner, &999_999, &None); + assert_eq!(balance, 1); +} + +#[test] +fn batch_deduct_each_item_constrained_by_max_deduct() { + let env = Env::default(); + let owner = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, usdc_client, usdc_admin) = create_usdc(&env, &owner); + env.mock_all_auths(); + fund_vault(&usdc_admin, &vault_address, 0); + // max_deduct = 50 + client.init(&owner, &usdc, &None, &None, &None, &None, &Some(50)); + usdc_admin.mint(&owner, &300); + usdc_client.approve(&owner, &vault_address, &300, &1000); + client.deposit(&owner, &300); + // three items each exactly at the cap — all must pass + let items = soroban_sdk::vec![ + &env, + DeductItem { + amount: 50, + request_id: None + }, + DeductItem { + amount: 50, + request_id: None + }, + DeductItem { + amount: 50, + request_id: None + }, + ]; + let balance = client.batch_deduct(&owner, &items); + assert_eq!(balance, 150); +} + +#[test] +#[should_panic(expected = "deduct amount exceeds max_deduct")] +fn batch_deduct_one_item_above_max_deduct_panics() { + let env = Env::default(); + let owner = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, usdc_client, usdc_admin) = create_usdc(&env, &owner); + env.mock_all_auths(); + fund_vault(&usdc_admin, &vault_address, 0); + client.init(&owner, &usdc, &None, &None, &None, &None, &Some(50)); + usdc_admin.mint(&owner, &300); + usdc_client.approve(&owner, &vault_address, &300, &1000); + client.deposit(&owner, &300); + // second item exceeds cap — must panic + let items = soroban_sdk::vec![ + &env, + DeductItem { + amount: 50, + request_id: None + }, + DeductItem { + amount: 51, + request_id: None + }, + ]; + client.batch_deduct(&owner, &items); +}