diff --git a/docs/issues/cross-contract-conservation-invariants/CHECKLIST.md b/docs/issues/cross-contract-conservation-invariants/CHECKLIST.md new file mode 100644 index 00000000..8a412c28 --- /dev/null +++ b/docs/issues/cross-contract-conservation-invariants/CHECKLIST.md @@ -0,0 +1,48 @@ +# Implementation Checklist + +Work through each item in order. Check it off before moving to the next. + +## Settlement contract + +- [ ] Add `TotalReceived` variant to `StorageKey` enum +- [ ] In `receive_payment`: after crediting developer or pool, do + `total = get_total_received() + amount` (checked_add, panic on overflow), + write back to `StorageKey::TotalReceived` +- [ ] Same increment in `batch_receive_payment` (once per item, same pattern) +- [ ] Add `pub fn get_total_received(env: Env) -> i128` (returns 0 if key absent) +- [ ] Add unit test: single receive → `get_total_received() == amount` +- [ ] Add unit test: multiple receives → running total matches sum + +## Vault contract + +- [ ] Add `TotalDeducted` variant to `StorageKey` enum +- [ ] In `deduct`: after `meta.balance` update, increment `TotalDeducted` (checked_add) +- [ ] In the batch loop in `batch_deduct`: accumulate per-item amounts, then + increment `TotalDeducted` once after the loop commits +- [ ] Add `pub fn get_total_deducted(env: Env) -> i128` (returns 0 if key absent) +- [ ] Add unit test: deduct → `get_total_deducted() == amount` +- [ ] Add unit test: batch_deduct → `get_total_deducted() == sum of items` + +## Cross-contract test + +- [ ] Create `contracts/vault/src/test_cross_invariant.rs` +- [ ] Register vault + settlement (+ optional revenue pool stub) in one env +- [ ] Run 64-seed × 48-step random sequence +- [ ] After every step assert: + `vault.get_total_deducted() == settlement.get_total_received()` +- [ ] After every step assert settlement internal conservation: + `settlement.get_total_received() + == Σ developer_balance + global_pool.total_balance + cumulative_withdrawn` +- [ ] Add `#[cfg(test)] mod test_cross_invariant;` to `contracts/vault/src/lib.rs` + +## Docs + +- [ ] `docs/interfaces/vault.json`: add `get_total_deducted` function entry +- [ ] `docs/interfaces/settlement.json`: add `get_total_received` function entry + +## Final checks + +- [ ] `cargo fmt --all -- --check` passes +- [ ] `cargo clippy --all-targets --all-features -- -D warnings` passes +- [ ] `cargo test --workspace` green (all existing + new tests) +- [ ] Coverage ≥ 95% (`./scripts/coverage.sh`) diff --git a/docs/issues/cross-contract-conservation-invariants/README.md b/docs/issues/cross-contract-conservation-invariants/README.md new file mode 100644 index 00000000..a0fae1bd --- /dev/null +++ b/docs/issues/cross-contract-conservation-invariants/README.md @@ -0,0 +1,142 @@ +# Cross-Contract Cumulative Conservation Invariants + +**Campaign:** GrantFox +**Scope:** `callora-vault`, `callora-settlement`, `callora-revenue-pool` +**Type:** Smart-contract invariant hardening + +--- + +## Problem + +Each contract has its own per-contract invariant tests, but no test verifies that +funds are conserved **across** the full vault → settlement → revenue-pool pipeline. +A bug in the routing logic (e.g. double-credit, missing transfer, misrouted amount) +could pass all three individual test suites while violating conservation end-to-end. + +--- + +## Invariant to Enforce + +``` +vault.deducted_total + == settlement.developer_balances_sum + + settlement.global_pool.total_balance + + revenue_pool.usdc_on_ledger +``` + +More precisely, for every sequence of operations: + +1. **Vault conservation** — `vault.balance + Σ(deducted)` equals the initial deposit. +2. **Settlement conservation** — `Σ(receive_payment amounts)` equals + `Σ(developer_balance[i]) + global_pool.total_balance`. +3. **Revenue-pool conservation** — `Σ(yield_deposit amounts)` equals + `revenue_pool USDC on-ledger + Σ(distributed amounts)`. +4. **End-to-end** — `vault.deducted_total` equals the sum of all credits that + eventually arrived in settlement or revenue pool. + +--- + +## Implementation Plan + +### 1. `callora-settlement` — add `get_total_received` view + +Settlement currently tracks per-developer balances and a global pool balance but +has no running total of all funds received. Add a persistent `TotalReceived` +storage key incremented on every `receive_payment` / `batch_receive_payment` call. + +``` +StorageKey::TotalReceived → i128 (default 0) +``` + +- Increment in `receive_payment` using `checked_add` (panic on overflow). +- Expose as `pub fn get_total_received(env: Env) -> i128`. +- Invariant: `get_total_received() == Σ developer_balance[i] + global_pool.total_balance + Σ withdrawn`. + +### 2. `callora-vault` — add `get_total_deducted` view + +Vault tracks `meta.balance` (decremented on deduct) but not cumulative outflow. +Add a persistent `TotalDeducted` storage key incremented on every successful +`deduct` / `batch_deduct`. + +``` +StorageKey::TotalDeducted → i128 (default 0) +``` + +- Increment after `meta.balance` update using `checked_add`. +- Expose as `pub fn get_total_deducted(env: Env) -> i128`. +- Invariant: `initial_balance - meta.balance == get_total_deducted()` (ignoring withdrawals). + +### 3. Cross-contract test — `test_cross_invariant.rs` + +A new integration-style test module wired up in the vault crate (which already +imports `callora-settlement` as a dev-dependency) that: + +- Deploys vault + settlement + revenue pool together. +- Runs a random sequence (64 seeds × 48 steps) of: deposit, deduct (→ settlement), + batch_deduct, developer withdraw, pool distribute. +- After every step checks: + ``` + vault.get_total_deducted() + == settlement.get_total_received() + ``` + and: + ``` + settlement.get_total_received() + == Σ settlement.get_developer_balance(dev_i) + + settlement.get_global_pool().total_balance + + total_withdrawn_from_settlement + ``` + +--- + +## Files to Change + +| File | Change | +|------|--------| +| `contracts/settlement/src/lib.rs` | Add `StorageKey::TotalReceived`, increment in `receive_payment`, expose `get_total_received` | +| `contracts/vault/src/lib.rs` | Add `StorageKey::TotalDeducted`, increment in `deduct`/`batch_deduct`, expose `get_total_deducted` | +| `contracts/vault/src/test_cross_invariant.rs` | New: cross-contract conservation test | +| `contracts/vault/src/lib.rs` | Add `#[cfg(test)] mod test_cross_invariant` | +| `docs/interfaces/vault.json` | Document `get_total_deducted` | +| `docs/interfaces/settlement.json` | Document `get_total_received` | + +--- + +## Acceptance Criteria + +- [ ] `get_total_deducted` view exists on vault; returns cumulative deducted amount. +- [ ] `get_total_received` view exists on settlement; returns cumulative received amount. +- [ ] `test_cross_invariant.rs` passes with ≥ 64 seeds, ≥ 48 steps each. +- [ ] All three per-contract invariant tests continue to pass. +- [ ] `cargo clippy -- -D warnings` clean. +- [ ] `cargo test --workspace` green. +- [ ] Interface JSON docs updated. + +--- + +## Branch + +```bash +git checkout -b feature/cross-contract-conservation-invariants +``` + +Commit message: + +``` +feat: cross-contract cumulative conservation invariants + +- vault: add TotalDeducted storage key + get_total_deducted() view +- settlement: add TotalReceived storage key + get_total_received() view +- test_cross_invariant: 64-seed × 48-step end-to-end conservation check +- docs: update vault.json and settlement.json interface summaries + +Closes # +``` + +--- + +## Security Notes + +- Both new counters use `checked_add` — overflow panics loudly rather than wrapping silently. +- Both are view-only getter additions; no existing auth surface is changed. +- `TotalDeducted` / `TotalReceived` are informational — they do not gate any transfer logic.