diff --git a/EVENT_SCHEMA.md b/EVENT_SCHEMA.md index 1b482478..615309d4 100644 --- a/EVENT_SCHEMA.md +++ b/EVENT_SCHEMA.md @@ -73,6 +73,12 @@ Emitted on each deduction — once per `deduct()` call and once per item in `bat - **Indexer rule**: treat `Symbol("")` as “no request_id provided”. - **Ambiguity note**: `Some(Symbol(""))` is indistinguishable from `None` on-chain. Clients **SHOULD NOT** intentionally pass an empty symbol as a real request id. +**Precondition (Issue #263):** `deduct` / `batch_deduct` require a settlement +address to be configured via `set_settlement`. If the settlement address is +not set, the call panics with `"settlement address not set"` **before** any +`deduct` event is emitted — indexers will therefore never observe a `deduct` +event for a call that lacked a configured settlement destination. + --- ### `withdraw` diff --git a/INVARIANTS.md b/INVARIANTS.md index 280d5db2..68f650ff 100644 --- a/INVARIANTS.md +++ b/INVARIANTS.md @@ -79,17 +79,24 @@ Helper and view functions such as `get_meta`, `get_max_deduct`, `get_revenue_poo **Pre-conditions** - Caller is authorized: - `caller.require_auth()` -- Vault is initialized. +- Vault is initialized and not paused. - Amount constraints: - `amount > 0` - `amount <= get_max_deduct(env)` - Sufficient balance: - `meta.balance >= amount` +- **Settlement configured (Issue #263)**: + - `StorageKey::Settlement` is present — i.e. `set_settlement` has been called. + - If absent, the call panics with `"settlement address not set"` before any + balance mutation, guaranteeing no partial state update. **Post-conditions** - `VaultMeta.balance' = balance - amount` - Because of the `meta.balance >= amount` assertion and `amount > 0`, we have: - `VaultMeta.balance' >= 0` +- The on-ledger USDC decrease at the vault equals the internal balance decrease + (both equal `amount`), because the deducted USDC is always transferred to the + settlement address. --- @@ -100,7 +107,7 @@ Helper and view functions such as `get_meta`, `get_max_deduct`, `get_revenue_poo **Pre-conditions** - Caller is authorized: `caller.require_auth()` -- Vault is initialized. +- Vault is initialized and not paused. - `1 <= items.len() <= MAX_BATCH_SIZE` (50) - The explicit batch cap is a practical Soroban resource bound: it limits looped validation work, transfer/event overhead, and invocation @@ -109,6 +116,9 @@ Helper and view functions such as `get_meta`, `get_max_deduct`, `get_revenue_poo - For every item: `item.amount > 0` and `item.amount <= get_max_deduct(env)` - Cumulative deductions do not exceed balance: - Validated in a single pass before any state is written. +- **Settlement configured (Issue #263)**: `StorageKey::Settlement` is present; + missing settlement causes `"settlement address not set"` panic before any + state write, so the batch is atomically reverted. **Post-conditions** - `VaultMeta.balance' = balance - sum_i(amount_i) >= 0` diff --git a/SECURITY.md b/SECURITY.md index 704b8133..d92a7a90 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -95,21 +95,19 @@ The vault performs USDC transfers to configurable counterpart addresses on every `deduct` and `batch_deduct` call. These external transfers are justified as follows: - **settlement address**: set and updated exclusively by the on-chain admin via - `set_settlement`. This function emits a `set_settlement` event to provide a - clear audit trail for address rotation. Transfers to this address implement - the documented `Vault → Settlement` revenue flow described in + `set_settlement`. This function emits a `set_settlement` event to provide a + clear audit trail for address rotation. Transfers to this address implement + the documented `Vault → Settlement` revenue flow described in `SETTLEMENT_IMPLEMENTATION.md`. -- **revenue_pool address**: set and updated exclusively by the on-chain admin via - `set_revenue_pool`. Transfers to this address route product revenue to the - designated pool contract. -- **Priority rule**: when both are configured, `settlement` takes priority and - `revenue_pool` is not used in the same deduct. This prevents "half updated" - routing states where funds could be split unexpectedly across two recipients. -- **CRITICAL - Routing Required**: At least one routing address (settlement OR - revenue_pool) MUST be configured before any deduct operations can succeed. - If neither is configured, `deduct()` and `batch_deduct()` will panic with - `"routing not configured: set settlement or revenue_pool address"`. This - prevents silent fund retention and ensures explicit routing configuration. +- **revenue_pool address**: retained as an informational configuration slot via + `set_revenue_pool` / `get_revenue_pool`. It is **no longer consulted during + deducts** — `deduct` and `batch_deduct` always route to the settlement address. +- **CRITICAL — Settlement Required (Issue #263)**: `deduct` and `batch_deduct` + panic with `"settlement address not set"` when `set_settlement` has not been + called. The panic occurs before any balance mutation or event emission, so + the transaction reverts atomically with no observable state change. This + closes the silent-loss-of-accounting window where the internal `balance` + could previously decrement without a corresponding on-ledger USDC transfer. - **Address Validation**: Both `set_settlement()` and `set_revenue_pool()` validate that the provided address is NOT the vault's own address, preventing self-referential routing loops. diff --git a/SETTLEMENT_IMPLEMENTATION.md b/SETTLEMENT_IMPLEMENTATION.md index b8da0fc7..1044189e 100644 --- a/SETTLEMENT_IMPLEMENTATION.md +++ b/SETTLEMENT_IMPLEMENTATION.md @@ -77,24 +77,18 @@ StorageKey::RevenuePool // Fallback routing address (used if Settlement not s #### Routing Validation -**CRITICAL**: The vault enforces that at least one routing address MUST be configured before any deduct operations can succeed. This is validated via `require_routing_configured()` which is called at the beginning of both `deduct()` and `batch_deduct()`. +**CRITICAL**: The vault enforces that the settlement address MUST be configured before any deduct operation can succeed. This is validated via `require_settlement()` which is consulted by both `deduct()` and `batch_deduct()`. -- If neither `settlement` nor `revenue_pool` is configured: **PANIC** with `"routing not configured: set settlement or revenue_pool address"` -- This prevents silent fund retention and ensures explicit routing configuration -- Both addresses are validated to prevent self-referential routing (vault → vault) +- If `settlement` is not configured: **PANIC** with `"settlement address not set"` and the transaction reverts with no state change. +- This prevents silent loss-of-accounting where the vault's internal `balance` could drift from the on-ledger USDC balance. +- The settlement address is validated at configuration time to prevent self-referential routing (vault → vault). -#### Routing Priority +#### Routing -When deduct operations occur, funds are routed according to this priority: +Every `deduct` / `batch_deduct` call routes the deducted USDC to the configured settlement address. `revenue_pool` is **not** consulted during deducts; it is retained as an informational configuration slot only. -1. **If `settlement` is configured** → Route to settlement contract (highest priority) -2. **Else if `revenue_pool` is configured** → Route to revenue pool contract -3. **Else** → Deduct operation FAILS (routing not configured) - -This priority system ensures: -- No "half-configured" states where funds could be split unexpectedly -- Deterministic routing behavior -- Clear audit trail for all fund movements +- **`settlement` set** → funds transferred to settlement contract. +- **`settlement` unset** → deduct panics with `"settlement address not set"`, no balance change, no event emitted. diff --git a/contracts/vault/src/lib.rs b/contracts/vault/src/lib.rs index 3dfe6139..96e1de63 100644 --- a/contracts/vault/src/lib.rs +++ b/contracts/vault/src/lib.rs @@ -402,6 +402,23 @@ impl CalloraVault { meta.balance } + /// Deduct USDC from the vault and transfer it to the configured settlement address. + /// + /// # Preconditions + /// - The settlement address must have been registered via `set_settlement` + /// before this function can succeed. Attempting to deduct without a + /// configured settlement address panics with `"settlement address not set"` + /// and reverts the transaction, leaving vault state unchanged. + /// - `amount` must be positive and less than or equal to `max_deduct`. + /// - `caller` must be either the owner or the `authorized_caller` (if set). + /// - The vault's internal balance must cover `amount`. + /// + /// # Panics + /// - `"settlement address not set"` — `set_settlement` has not been called. + /// - `"amount must be positive"` — `amount <= 0`. + /// - `"deduct amount exceeds max_deduct"` — `amount > max_deduct`. + /// - `"unauthorized caller"` — caller is not owner or authorized caller. + /// - `"insufficient balance"` — vault balance below `amount`. pub fn deduct(env: Env, caller: Address, amount: i128, request_id: Option) -> i128 { Self::require_not_paused(env.clone()); caller.require_auth(); @@ -421,16 +438,12 @@ impl CalloraVault { .checked_sub(amount) .unwrap_or_else(|| panic!("balance underflow")); env.storage().instance().set(&StorageKey::Meta, &meta); - let inst = env.storage().instance(); - 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() - { - Self::transfer_to_revenue_pool(env.clone(), amount); - } + let ut: Address = env + .storage() + .instance() + .get(&StorageKey::UsdcToken) + .unwrap(); + Self::transfer_funds(&env, &ut, &settlement, amount); let rid = request_id.unwrap_or(Symbol::new(&env, "")); env.events().publish( (Symbol::new(&env, "deduct"), caller, rid), @@ -477,16 +490,12 @@ impl CalloraVault { meta.balance = running; - let inst = env.storage().instance(); - 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() - { - Self::transfer_to_revenue_pool(env.clone(), total); - } + let ut: Address = env + .storage() + .instance() + .get(&StorageKey::UsdcToken) + .unwrap(); + Self::transfer_funds(&env, &ut, &settlement, total); meta.balance = running; env.storage().instance().set(&StorageKey::Meta, &meta); @@ -618,8 +627,9 @@ impl CalloraVault { /// Store the settlement contract address (admin only). /// /// Once set, every `deduct` / `batch_deduct` call transfers the deducted USDC to - /// this address. Settlement takes priority over `revenue_pool` when both are - /// configured. + /// this address. `set_settlement` is a hard precondition: `deduct` and + /// `batch_deduct` panic with `"settlement address not set"` until this function + /// has been called. `revenue_pool` is no longer consulted during deductions. /// /// # Panics /// Panics if `caller` is not the current admin. @@ -664,10 +674,10 @@ impl CalloraVault { /// A tuple `(usdc_token, settlement, revenue_pool)`: /// - `usdc_token` — always `Some` after `init`; the USDC token contract address. /// - `settlement` — `Some` after `set_settlement` is called, otherwise `None`. + /// Must be `Some` before any `deduct` / `batch_deduct` call can succeed. /// - `revenue_pool` — `Some` after `set_revenue_pool` is called, otherwise `None`. - /// - /// When both `settlement` and `revenue_pool` are `Some`, **`settlement` takes - /// priority** and the revenue pool is not used in the same deduct call. + /// Informational only; `deduct` / `batch_deduct` always route to `settlement` + /// and never fall back to the revenue pool. /// /// # Example — Stellar CLI /// ```text @@ -679,8 +689,9 @@ impl CalloraVault { /// # Operator checklist /// 1. `usdc_token` must be the canonical Stellar USDC issuer /// (`GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN` on mainnet). - /// 2. `settlement` should be `Some` before routing production traffic. - /// 3. `revenue_pool` is optional; only active when `settlement` is `None`. + /// 2. `settlement` must be `Some` before any deduct call; it is the sole + /// destination for deducted USDC. + /// 3. `revenue_pool` is informational only and is not consulted during deducts. pub fn get_contract_addresses(env: Env) -> (Option
, Option
, Option
) { let inst = env.storage().instance(); let usdc: Option
= inst.get(&StorageKey::UsdcToken); @@ -756,15 +767,18 @@ impl CalloraVault { token::Client::new(env, usdc_token).transfer(&env.current_contract_address(), to, &amount); } - fn transfer_to_revenue_pool(env: Env, amount: i128) { - let inst = env.storage().instance(); - let rp: Address = inst - .get(&StorageKey::RevenuePool) - .expect("revenue pool address not set"); - let ua: Address = inst - .get(&StorageKey::UsdcToken) - .expect("vault not initialized"); - token::Client::new(&env, &ua).transfer(&env.current_contract_address(), &rp, &amount); + fn require_settlement(env: &Env) -> Address { + env.storage() + .instance() + .get(&StorageKey::Settlement) + .unwrap_or_else(|| panic!("settlement address not set")) + } + + fn get_max_deduct(env: Env) -> i128 { + env.storage() + .instance() + .get(&StorageKey::MaxDeduct) + .unwrap_or(DEFAULT_MAX_DEDUCT) } fn require_not_paused(env: Env) { diff --git a/contracts/vault/src/test.rs b/contracts/vault/src/test.rs index 861497a0..7395b072 100644 --- a/contracts/vault/src/test.rs +++ b/contracts/vault/src/test.rs @@ -1085,6 +1085,8 @@ fn set_authorized_caller_sets_and_emits_event() { env.mock_all_auths(); fund_vault(&usdc_admin, &vault_address, 200); client.init(&owner, &usdc, &Some(200), &None, &None, &None, &None); + let settlement = Address::generate(&env); + client.set_settlement(&owner, &settlement); client.set_authorized_caller(&Some(new_caller.clone())); @@ -1357,11 +1359,23 @@ fn batch_deduct_events_contain_request_ids() { ]; client.batch_deduct(&caller, &items); - let all_events = env.events().all(); - // Last two events are the two deduct events - let len = all_events.len(); - let ev_a = all_events.get(len - 2).unwrap(); - let ev_b = all_events.get(len - 1).unwrap(); + // Filter to the two deduct events emitted by the vault (topic 0 == "deduct"). + // The settlement transfer emits an additional event after the deducts. + let deduct_sym = Symbol::new(&env, "deduct"); + let deduct_events: std::vec::Vec<_> = env + .events() + .all() + .iter() + .filter(|e| { + e.0 == vault_address && !e.1.is_empty() && { + let t: Symbol = e.1.get(0).unwrap().into_val(&env); + t == deduct_sym + } + }) + .collect(); + assert_eq!(deduct_events.len(), 2, "expected exactly two deduct events"); + let ev_a = &deduct_events[0]; + let ev_b = &deduct_events[1]; 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); @@ -2124,6 +2138,10 @@ fn vault_full_lifecycle() { assert_eq!(client.balance(), 500); assert_eq!(client.get_admin(), owner); + // Configure settlement address (precondition for deduct/batch_deduct) + let settlement = Address::generate(&env); + client.set_settlement(&owner, &settlement); + // Allow depositor and deposit 200 client.set_allowed_depositor(&owner, &Some(depositor.clone())); usdc_admin.mint(&depositor, &200); @@ -2205,13 +2223,15 @@ fn init_with_revenue_pool_stores_address() { } #[test] -fn deduct_with_revenue_pool_transfers_usdc() { +#[should_panic(expected = "settlement address not set")] +fn deduct_with_only_revenue_pool_panics() { + // Revenue pool is no longer a deduct destination; settlement is mandatory. let env = Env::default(); let owner = Address::generate(&env); let caller = Address::generate(&env); let revenue_pool = Address::generate(&env); let (vault_address, client) = create_vault(&env); - let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &owner); + let (usdc_address, _usdc_client, usdc_admin) = create_usdc(&env, &owner); env.mock_all_auths(); fund_vault(&usdc_admin, &vault_address, 1000); @@ -2221,14 +2241,11 @@ fn deduct_with_revenue_pool_transfers_usdc() { &Some(1000), &Some(caller.clone()), &None, - &Some(revenue_pool.clone()), + &Some(revenue_pool), &None, ); client.deduct(&caller, &300, &None); - - assert_eq!(client.balance(), 700); - assert_eq!(usdc_client.balance(&revenue_pool), 300); } #[test] @@ -2260,13 +2277,15 @@ fn deduct_with_settlement_transfers_usdc() { } #[test] -fn batch_deduct_with_revenue_pool_transfers_total_usdc() { +#[should_panic(expected = "settlement address not set")] +fn batch_deduct_with_only_revenue_pool_panics() { + // Revenue pool is no longer a deduct destination; settlement is mandatory. let env = Env::default(); let owner = Address::generate(&env); let caller = Address::generate(&env); let revenue_pool = Address::generate(&env); let (vault_address, client) = create_vault(&env); - let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &owner); + let (usdc_address, _usdc_client, usdc_admin) = create_usdc(&env, &owner); env.mock_all_auths(); fund_vault(&usdc_admin, &vault_address, 1000); @@ -2276,7 +2295,7 @@ fn batch_deduct_with_revenue_pool_transfers_total_usdc() { &Some(1000), &Some(caller.clone()), &None, - &Some(revenue_pool.clone()), + &Some(revenue_pool), &None, ); @@ -2292,9 +2311,6 @@ fn batch_deduct_with_revenue_pool_transfers_total_usdc() { }, ]; client.batch_deduct(&caller, &items); - - assert_eq!(client.balance(), 650); - assert_eq!(usdc_client.balance(&revenue_pool), 350); } #[test] @@ -2491,12 +2507,14 @@ fn get_revenue_pool_consistent_after_deduct_operations() { &Some(revenue_pool.clone()), &None, ); + let settlement = Address::generate(&env); + client.set_settlement(&owner, &settlement); // Query revenue pool before deduct let before = client.get_revenue_pool(); assert_eq!(before, Some(revenue_pool.clone())); - // Perform deduct operation + // Perform deduct operation (routes to settlement, not revenue_pool) client.deduct(&caller, &200, &None); // Query revenue pool after deduct - should be unchanged @@ -2504,9 +2522,10 @@ fn get_revenue_pool_consistent_after_deduct_operations() { assert_eq!(after, Some(revenue_pool.clone())); assert_eq!(before, after); - // Verify no state mutation occurred + // Funds flow to settlement; revenue_pool receives nothing. assert_eq!(client.balance(), 800); - assert_eq!(usdc_client.balance(&revenue_pool), 200); + assert_eq!(usdc_client.balance(&settlement), 200); + assert_eq!(usdc_client.balance(&revenue_pool), 0); } #[test] @@ -2972,6 +2991,8 @@ fn deduct_to_zero_succeeds() { env.mock_all_auths(); fund_vault(&usdc_admin, &vault_address, 500); client.init(&owner, &usdc, &Some(500), &None, &None, &None, &None); + let settlement = Address::generate(&env); + client.set_settlement(&owner, &settlement); assert_eq!(client.deduct(&owner, &500, &None), 0); } @@ -3016,6 +3037,8 @@ fn batch_deduct_to_zero_succeeds() { env.mock_all_auths(); fund_vault(&usdc_admin, &vault_address, 0); client.init(&owner, &usdc, &None, &None, &None, &None, &None); + let settlement = Address::generate(&env); + client.set_settlement(&owner, &settlement); usdc_admin.mint(&owner, &600); usdc_client.approve(&owner, &vault_address, &600, &1000); client.deposit(&owner, &600); @@ -3405,6 +3428,8 @@ fn deduct_while_paused_fails() { env.mock_all_auths(); fund_vault(&usdc_admin, &vault_address, 500); client.init(&owner, &usdc, &Some(500), &None, &None, &None, &None); + let settlement = Address::generate(&env); + client.set_settlement(&owner, &settlement); client.pause(&owner); client.deduct(&owner, &100, &None); } @@ -3419,6 +3444,8 @@ fn batch_deduct_while_paused_fails() { env.mock_all_auths(); fund_vault(&usdc_admin, &vault_address, 500); client.init(&owner, &usdc, &Some(500), &None, &None, &None, &None); + let settlement = Address::generate(&env); + client.set_settlement(&owner, &settlement); client.pause(&owner); let items = soroban_sdk::vec![ &env, @@ -3567,23 +3594,22 @@ fn withdraw_to_negative_fails() { } #[test] -fn deduct_no_routing_stays_in_vault() { - // When neither settlement nor revenue_pool is configured, USDC stays in vault. +#[should_panic(expected = "settlement address not set")] +fn deduct_without_settlement_panics() { + // Settlement is a hard precondition for deduct; missing address must panic. 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); + 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, &None); client.deduct(&owner, &200, &None); - assert_eq!(client.balance(), 300); - // USDC stays in vault contract - assert_eq!(usdc_client.balance(&vault_address), 500); } #[test] -fn batch_deduct_no_routing_stays_in_vault() { +fn deduct_without_settlement_does_not_mutate_state() { + // When deduct panics due to missing settlement, vault state must be unchanged. let env = Env::default(); let owner = Address::generate(&env); let (vault_address, client) = create_vault(&env); @@ -3591,6 +3617,23 @@ fn batch_deduct_no_routing_stays_in_vault() { env.mock_all_auths(); fund_vault(&usdc_admin, &vault_address, 500); client.init(&owner, &usdc, &Some(500), &None, &None, &None, &None); + + let result = client.try_deduct(&owner, &200, &None); + assert!(result.is_err(), "expected panic for missing settlement"); + assert_eq!(client.balance(), 500); + assert_eq!(usdc_client.balance(&vault_address), 500); +} + +#[test] +#[should_panic(expected = "settlement address not set")] +fn batch_deduct_without_settlement_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, &None); let items = soroban_sdk::vec![ &env, DeductItem { @@ -3603,7 +3646,31 @@ fn batch_deduct_no_routing_stays_in_vault() { }, ]; client.batch_deduct(&owner, &items); - assert_eq!(client.balance(), 350); +} + +#[test] +fn batch_deduct_without_settlement_does_not_mutate_state() { + 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, &None); + let items = soroban_sdk::vec![ + &env, + DeductItem { + amount: 100, + request_id: None, + }, + DeductItem { + amount: 50, + request_id: None, + }, + ]; + let result = client.try_batch_deduct(&owner, &items); + assert!(result.is_err(), "expected panic for missing settlement"); + assert_eq!(client.balance(), 500); assert_eq!(usdc_client.balance(&vault_address), 500); } @@ -3773,6 +3840,8 @@ mod fuzz { &None, &Some(max_deduct_val), ); + // Settlement is a precondition for deduct / batch_deduct. + client.set_settlement(&owner, &settlement); // Give the depositor (owner) plenty of USDC. // Use a very large amount to handle large max_deduct scenarios let deposit_reserve: i128 = 10_000_000_000_000; // 10 trillion to handle large deposits @@ -3959,6 +4028,8 @@ mod fuzz { &None, &Some(200), ); + let settlement = Address::generate(&env); + client.set_settlement(&owner, &settlement); let mut rng = StdRng::seed_from_u64(0x5eed_0001); // Build batches that sometimes overdraw; assert atomicity each time. @@ -4006,6 +4077,8 @@ mod fuzz { &None, &Some(max_d), ); + let settlement = Address::generate(&env); + client.set_settlement(&owner, &settlement); let mut rng = StdRng::seed_from_u64(0x5eed_0002); for _ in 0..40 { @@ -4186,6 +4259,8 @@ fn deduct_equal_to_max_deduct_succeeds() { 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)); + let settlement = Address::generate(&env); + client.set_settlement(&owner, &settlement); usdc_admin.mint(&owner, &200); usdc_client.approve(&owner, &vault_address, &200, &1000); client.deposit(&owner, &200); @@ -4221,6 +4296,8 @@ fn deduct_default_cap_is_i128_max() { 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); + let settlement = Address::generate(&env); + client.set_settlement(&owner, &settlement); usdc_admin.mint(&owner, &1_000_000); usdc_client.approve(&owner, &vault_address, &1_000_000, &1000); client.deposit(&owner, &1_000_000); @@ -4239,6 +4316,8 @@ fn batch_deduct_each_item_constrained_by_max_deduct() { fund_vault(&usdc_admin, &vault_address, 0); // max_deduct = 50 client.init(&owner, &usdc, &None, &None, &None, &None, &Some(50)); + let settlement = Address::generate(&env); + client.set_settlement(&owner, &settlement); usdc_admin.mint(&owner, &300); usdc_client.approve(&owner, &vault_address, &300, &1000); client.deposit(&owner, &300);