diff --git a/README.md b/README.md index af2e9506..3f38c30c 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Release artifacts land in `target/wasm32-unknown-unknown/release/.wasm`. The primary storage and metering contract. Holds USDC on behalf of API consumers and deducts balances on every metered call. -- `init(owner, usdc_token, ..., authorized_caller, min_deposit, revenue_pool, max_deduct)` — Initialize with owner and optional configuration; `min_deposit` defaults to `1` and must be `> 0`. +- `init(owner, usdc_token, initial_balance, authorized_caller, min_deposit, revenue_pool, max_deduct)` — Initialize with owner and optional configuration. `initial_balance` defaults to `0`; when `> 0` the vault verifies the on-ledger USDC balance covers it. `min_deposit` defaults to `1` and must be `> 0`. - `deposit(caller, amount)` — Owner or allowed depositor increases ledger balance. - `deduct(caller, amount, request_id)` — Decrease balance for an API call; routes funds to settlement. - `batch_deduct(caller, items)` — Atomically process multiple deductions. diff --git a/SECURITY.md b/SECURITY.md index c93ac4c2..704b8133 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -20,14 +20,14 @@ This document outlines security best practices and checklist items for Callora v > All balance mutations in `callora-vault` (`deposit`, `deduct`, `batch_deduct`, `withdraw`, `withdraw_to`) and `callora-revenue-pool` (`batch_distribute`) use `checked_add` / `checked_sub` and panic with a descriptive message on overflow. `callora-settlement` (`receive_payment`) does the same. The workspace `Cargo.toml` sets `overflow-checks = true` for both `dev` and `release` profiles, so even plain arithmetic would trap in debug builds — the explicit checked calls make the intent clear and guarantee the same behaviour in all build configurations. Additional hardening note: -- Removed a duplicated `get_max_deduct` entrypoint declaration in `callora-vault` to avoid ambiguous review surfaces and keep ABI-facing code paths singular. +- Removed a duplicated `get_max_deduct` entrypoint declaration in `callora-vault` to avoid ambiguous review surfaces and keep ABI-facing code paths singular. The function is retained as a private internal helper called by `deduct` and `batch_deduct`. ### Initialization / Re-initialization -- [ ] `initialize` function protected against multiple calls (e.g., checking if admin key exists in `instance()` storage) +- [x] `initialize` function protected against multiple calls (e.g., checking if admin key exists in `instance()` storage) - [ ] Contract upgrades (`env.deployer().update_current_contract_wasm()`) protected by `require_auth()` - [ ] No unprotected re-init functions -- [ ] `initialize` validates all input parameters +- [x] `initialize` validates all input parameters ### Pause / Circuit Breaker diff --git a/contracts/vault/src/lib.rs b/contracts/vault/src/lib.rs index eba93c7b..08096f30 100644 --- a/contracts/vault/src/lib.rs +++ b/contracts/vault/src/lib.rs @@ -288,6 +288,13 @@ impl CalloraVault { .publish((Symbol::new(&env, "vault_unpaused"), caller), ()); } + fn get_max_deduct(env: Env) -> i128 { + env.storage() + .instance() + .get(&StorageKey::MaxDeduct) + .unwrap_or(DEFAULT_MAX_DEDUCT) + } + /// Returns the current pause state of the vault. /// /// # Purpose @@ -412,7 +419,6 @@ impl CalloraVault { pub fn batch_deduct(env: Env, caller: Address, items: Vec) -> i128 { Self::require_not_paused(env.clone()); caller.require_auth(); - Self::require_not_paused(env.clone()); let n = items.len(); assert!(n > 0, "batch_deduct requires at least one item"); assert!(n <= MAX_BATCH_SIZE, "batch too large"); diff --git a/contracts/vault/src/test.rs b/contracts/vault/src/test.rs index 666b526a..a3dd24bf 100644 --- a/contracts/vault/src/test.rs +++ b/contracts/vault/src/test.rs @@ -448,9 +448,15 @@ fn owner_deposit_increases_balance_and_emits_event() { }) .expect("expected deposit event"); - assert_eq!(deposit_event.1.len(), 1, "topics must have exactly 1 entry"); + assert_eq!( + deposit_event.1.len(), + 2, + "topics must have exactly 2 entries" + ); let topic0: Symbol = deposit_event.1.get(0).unwrap().into_val(&env); assert_eq!(topic0, Symbol::new(&env, "deposit")); + let topic1: Address = deposit_event.1.get(1).unwrap().into_val(&env); + assert_eq!(topic1, owner); let (amount, new_balance): (i128, i128) = deposit_event.2.into_val(&env); assert_eq!(amount, 300); @@ -540,11 +546,11 @@ fn deposit_event_schema_alignment() { }) .expect("expected deposit event"); - // Schema alignment: exactly 1 topic + // Schema alignment: exactly 2 topics (Symbol + caller Address) assert_eq!( deposit_event.1.len(), - 1, - "deposit event must have exactly 1 topic" + 2, + "deposit event must have exactly 2 topics" ); let topic0: Symbol = deposit_event.1.get(0).unwrap().into_val(&env); assert_eq!( @@ -552,6 +558,8 @@ fn deposit_event_schema_alignment() { Symbol::new(&env, "deposit"), "topic[0] must be Symbol(\"deposit\")" ); + let topic1: Address = deposit_event.1.get(1).unwrap().into_val(&env); + assert_eq!(topic1, owner, "topic[1] must be the depositor address"); // Data must decode as (amount: i128, new_balance: i128) let (amount, new_balance): (i128, i128) = deposit_event.2.into_val(&env); @@ -3009,7 +3017,7 @@ fn batch_deduct_while_paused_fails() { request_id: None, }, ]; - client.batch_deduct(&owner, &items); + client.batch_deduct(&owner, &items); // must panic with "vault is paused" } #[test]