diff --git a/README.md b/README.md index 5c42b92..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,197 +0,0 @@ -# Callora Contracts - -Soroban smart contracts for the Callora API marketplace: prepaid vault (USDC) and balance deduction for pay-per-call. - -## Tech stack - -- **Rust** with **Soroban SDK** (Stellar) -- Contract compiles to WebAssembly and deploys to Stellar/Soroban - -## What’s included - -- **`callora-vault`** contract: - - `init(owner, initial_balance)` — initialize vault for an owner with optional initial balance - - `get_meta()` — view config (owner and current balance) - - `set_allowed_depositor(caller, depositor)` — owner-only; set or clear a backend/allowed depositor that can deposit and manage pricing - - `deposit(caller, amount)` — owner or allowed depositor increases ledger balance - - `deduct(amount)` — decrease balance for an API call (backend uses this after metering usage) - - `balance()` — current ledger balance - - `set_metadata(caller, offering_id, metadata)` — owner-only; attach off-chain metadata reference (IPFS CID or URI) to an offering - - `update_metadata(caller, offering_id, metadata)` — owner-only; update existing offering metadata - - `get_metadata(offering_id)` — retrieve metadata reference for an offering -- **`callora-revenue-pool`** contract (settlement): - - `init(admin, usdc_token)` — set admin and USDC token - - `distribute(caller, to, amount)` — admin sends USDC from this contract to a developer - - Flow: vault deduct → vault transfers USDC to revenue pool → admin calls `distribute(to, amount)` - - `set_price(caller, api_id, price)` — owner or allowed depositor sets the **price per API call** for `api_id` in smallest USDC units (e.g. 1 = 1 cent) - - `get_price(api_id)` — returns `Option` with the configured price per call for `api_id` - -### API pricing resolution - -The backend resolves `(vault_id, api_id) -> price` as follows: - -1. Use the vault contract address as `vault_id`. -2. Call `get_price(api_id)` on that vault. -3. If a price is returned, use it as the per-call price (in smallest USDC units) before calling `deduct(amount)`. - -Events are emitted for init, deposit, deduct, withdraw, and withdraw_to. See [EVENT_SCHEMA.md](EVENT_SCHEMA.md) for indexer/frontend use. Approximate gas/cost notes: [BENCHMARKS.md](BENCHMARKS.md). Upgrade and migration: [UPGRADE.md](UPGRADE.md). - -We've enhanced the `CalloraVault` contract with robust input validation to prevent invalid transactions: - -- **Amount Validation**: Both `deposit()` and `deduct()` now enforce `amount > 0`, rejecting zero and negative values before any state changes -- **Improved Error Messages**: Enhanced panic messages provide clear context (e.g., "insufficient balance: X requested but only Y available") -- **Early Validation**: Checks occur before storage writes, minimizing gas waste on invalid transactions -- **Comprehensive Test Coverage**: Added 5 new test cases covering edge cases: - - `deposit_zero_panics()` — validates zero deposit rejection - - `deposit_negative_panics()` — validates negative deposit rejection - - `deduct_zero_panics()` — validates zero deduction rejection - - `deduct_negative_panics()` — validates negative deduction rejection - - `deduct_exceeds_balance_panics()` — validates insufficient balance checks with detailed error messages - -All tests use `#[should_panic]` assertions for guaranteed validation. This resolves issue #9. - -## Local setup - -1. **Prerequisites:** - - [Rust](https://rustup.rs/) (stable) - - [Stellar Soroban CLI](https://developers.stellar.org/docs/smart-contracts/getting-started/setup) (`cargo install soroban-cli`) - -2. **Build and test:** - - ```bash - cd callora-contracts - cargo build - cargo test - ``` - -3. **Build WASM (for deployment):** - - ```bash - # Build vault contract - cargo build --target wasm32-unknown-unknown --release -p callora-vault - - # Or use the convenience script from project root - ./scripts/check-wasm-size.sh - ``` - - The vault contract WASM binary is optimized to ~17.5KB (17,926 bytes), well under Soroban's 64KB limit. The release profile in `Cargo.toml` uses aggressive size optimizations: - - `opt-level = "z"` - optimize for size - - `lto = true` - link-time optimization - - `strip = "symbols"` - remove debug symbols - - `codegen-units = 1` - better optimization at cost of compile time - - To verify the WASM size stays under 64KB, run: - - ```bash - ./scripts/check-wasm-size.sh - ``` - -## Development - -Use one branch per issue or feature (e.g. `test/minimum-deposit-rejected`, `docs/vault-gas-notes`) to keep PRs small and reduce merge conflicts. Run `cargo fmt`, `cargo clippy --all-targets --all-features -- -D warnings`, and `cargo test` before pushing. -<<<<<<< HEAD - -## Test coverage - -The project enforces a **minimum of 95 % line coverage** on every push and pull-request via GitHub Actions. - -### Run coverage locally - -```bash -# First time only — the script auto-installs cargo-tarpaulin if absent -./scripts/coverage.sh -``` - -The script will: - -1. Check for `cargo-tarpaulin`; install it automatically if it is missing. -2. Run all tests with instrumentation according to `tarpaulin.toml`. -3. Exit with a non-zero code if coverage drops below 95 %. -4. Write reports to the `coverage/` directory (git-ignored). - -| Report file | Description | -| -------------------------------- | ----------------------------------------------- | -| `coverage/tarpaulin-report.html` | Interactive per-file view — open in any browser | -| `coverage/cobertura.xml` | Cobertura XML consumed by CI | - -> **Tip:** You can also run `cargo tarpaulin` directly from the workspace root; -> the settings in `tarpaulin.toml` are picked up automatically. - -### CI enforcement - -`.github/workflows/coverage.yml` runs on every push and pull-request. -It installs tarpaulin, runs coverage, uploads the HTML report as a downloadable -artefact, and posts a coverage summary table as a PR comment. -A result below 95 % causes the workflow — and the required status check — to fail. - - - -## Project layout - -``` -callora-contracts/ -├── .github/workflows/ -│ └── ci.yml # CI: fmt, clippy, test, WASM build -<<<<<<< HEAD -├── Cargo.toml # Workspace and release profile -├── BENCHMARKS.md # Vault operation gas/cost notes -├── EVENT_SCHEMA.md # Event names, topics, and payload types -├── UPGRADE.md # Vault upgrade and migration path -├── tarpaulin.toml # cargo-tarpaulin config (≥ 95 % enforced) -├── scripts/ -│ └── coverage.sh # One-command local coverage runner -├── .github/ -│ └── workflows/ -│ └── coverage.yml # CI: enforces 95 % on every push / PR -└── contracts/ - └── vault/ - ├── Cargo.toml - └── src/ - ├── lib.rs # Contract logic - └── test.rs # Unit tests (covers all code paths) -======= -├── Cargo.toml # Workspace and release profile -├── BENCHMARKS.md # Vault operation gas/cost notes -├── EVENT_SCHEMA.md # Event names, topics, and payload types -├── UPGRADE.md # Vault upgrade and migration path -├── contracts/ -│ ├── vault/ -│ │ ├── Cargo.toml -│ │ └── src/ -│ │ ├── lib.rs # Contract logic -│ │ └── test.rs # Unit tests -│ └── revenue_pool/ -│ ├── Cargo.toml -│ └── src/ -│ ├── lib.rs # Settlement contract -│ └── test.rs # Unit tests -└── README.md ->>>>>>> b0229e42e4d4517da9f548ea3e374a5886304bf2 -``` - -## Security Notes - -- **Checked arithmetic**: All balance mutations use `checked_add` / `checked_sub` — overflow and underflow cause an immediate panic rather than silent wrapping. -- **Input validation**: `deposit` and `deduct` reject zero and negative amounts (`amount > 0`). `init` rejects negative initial balances. -- **`overflow-checks`**: Enabled for **both** `[profile.dev]` and `[profile.release]` in the workspace `Cargo.toml`, ensuring overflow bugs are caught in tests as well as production. -- **Max balance**: `i128::MAX` (≈ 1.7 × 10³⁸ stroops). Deposits that would exceed this limit will panic. - -## Documentation - -- [Core Contracts](contracts/README.md) -- [Vault Access Control Model](contracts/vault/ACCESS_CONTROL.md) -- [Security Audit Notes](docs/SECURITY.md) - -## 🔐 Security - -See [SECURITY.md](SECURITY.md) for the Vault Security Checklist and audit recommendations. - -## Deployment - -Use Soroban CLI or Stellar Laboratory to deploy the built WASM to testnet/mainnet and configure the vault (owner, optional initial balance and optional pricing). The backend will: - -1. Call `get_price(api_id)` on the vault (`vault_id`) to fetch the price per call. -2. Multiply by the number of billable calls to get the total amount. -3. Call `deduct(amount)` on the same vault to charge the user. - -This repo is part of [Callora](https://github.com/your-org/callora). Frontend: `callora-frontend`. Backend: `callora-backend`. diff --git a/contracts/revenue_pool/src/lib.rs b/contracts/revenue_pool/src/lib.rs index 0911fc8..02ad70e 100644 --- a/contracts/revenue_pool/src/lib.rs +++ b/contracts/revenue_pool/src/lib.rs @@ -162,6 +162,7 @@ impl RevenuePool { /// /// # Panics /// * If the caller is not the current admin (`"unauthorized: caller is not admin"`). + /// * If `payments` is empty (`"batch_distribute requires at least one payment"`). /// * If any individual amount is zero or negative (`"amount must be positive"`). /// * If the revenue pool has not been initialized. /// * If the total amount exceeds the contract's available balance (`"insufficient USDC balance"`). @@ -174,6 +175,9 @@ impl RevenuePool { if caller != admin { panic!("unauthorized: caller is not admin"); } + if payments.is_empty() { + panic!("batch_distribute requires at least one payment"); + } let mut total_amount: i128 = 0; for payment in payments.iter() { diff --git a/contracts/revenue_pool/src/test.rs b/contracts/revenue_pool/src/test.rs index ff4fb39..fc862a0 100644 --- a/contracts/revenue_pool/src/test.rs +++ b/contracts/revenue_pool/src/test.rs @@ -347,6 +347,22 @@ fn batch_distribute_zero_amount_panics() { client.batch_distribute(&admin, &payments); } +#[test] +#[should_panic(expected = "batch_distribute requires at least one payment")] +fn batch_distribute_empty_panics() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let (pool_addr, client) = create_pool(&env); + let (usdc_address, _, usdc_admin) = create_usdc(&env, &admin); + + client.init(&admin, &usdc_address); + fund_pool(&usdc_admin, &pool_addr, 500); + + let payments: Vec<(Address, i128)> = Vec::new(&env); + client.batch_distribute(&admin, &payments); +} + #[test] #[should_panic(expected = "insufficient USDC balance")] fn batch_distribute_insufficient_balance_panics() { diff --git a/docs/revenue-pool-batch-distribute.md b/docs/revenue-pool-batch-distribute.md new file mode 100644 index 0000000..2d61e39 --- /dev/null +++ b/docs/revenue-pool-batch-distribute.md @@ -0,0 +1,9 @@ +# Revenue Pool Batch Distribute + +`callora-revenue-pool::batch_distribute` rejects an empty `payments` vector with +`"batch_distribute requires at least one payment"`. + +Rationale: +- This matches the vault contract's `batch_deduct` policy for empty batches. +- Admin tooling gets an explicit failure for malformed payout jobs instead of a silent no-op. +- Indexers and operators do not need to infer whether an empty successful transaction was intentional.