Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Release artifacts land in `target/wasm32-unknown-unknown/release/<crate>.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.
Expand Down
6 changes: 3 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 7 additions & 1 deletion contracts/vault/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -412,7 +419,6 @@ impl CalloraVault {
pub fn batch_deduct(env: Env, caller: Address, items: Vec<DeductItem>) -> 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");
Expand Down
18 changes: 13 additions & 5 deletions contracts/vault/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -540,18 +546,20 @@ 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!(
topic0,
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);
Expand Down Expand Up @@ -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]
Expand Down
Loading