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
67 changes: 67 additions & 0 deletions EVENT_SCHEMA.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,73 @@ Emitted when the vault is unpaused by the admin.



---

### `vault_paused`

Emitted when the vault circuit-breaker is activated by admin or owner.

| Field | Location | Type | Description |
|---------|----------|---------|--------------------------------------------------|
| topic 0 | topics | Symbol | `"vault_paused"` |
| topic 1 | topics | Address | `caller` — admin or owner who triggered pause |
| data | data | () | empty |

**Indexer Note:** After this event is emitted, `is_paused()` view function returns `true`.
The following operations are blocked until unpause: `deposit()`, `deduct()`, `batch_deduct()`.

---

### `vault_unpaused`

Emitted when the vault circuit-breaker is deactivated by admin or owner.

| Field | Location | Type | Description |
|---------|----------|---------|--------------------------------------------------|
| topic 0 | topics | Symbol | `"vault_unpaused"` |
| topic 1 | topics | Address | `caller` — admin or owner who triggered unpause |
| data | data | () | empty |

**Indexer Note:** After this event is emitted, `is_paused()` view function returns `false`.
All vault operations are restored: `deposit()`, `deduct()`, `batch_deduct()`.

---

### View Function: `is_paused()`

The vault exposes a read-only view function for off-chain systems to query the current pause state.

**Signature:** `pub fn is_paused(env: Env) -> bool`

**Return Value:**
- `true` — Vault is currently paused (circuit-breaker active)
- `false` — Vault is operational (normal state)

**Safety Guarantees:**
- **Read-only**: No state mutation or side effects
- **Deterministic**: Identical state always produces identical output
- **Non-panicking**: Never panics, even before initialization
- **Safe default**: Returns `false` when pause state is unset

**Indexer Usage:**
```javascript
// Check if vault is paused before processing transactions
const isPaused = await vault.isPaused();
if (isPaused) {
// Vault is paused - deposits and deductions are blocked
// Only admin/owner operations like withdraw() are allowed
} else {
// Vault is operational - all functions available
}
```

**Consistency with Events:**
- `vault_paused` event emitted → `is_paused()` returns `true`
- `vault_unpaused` event emitted → `is_paused()` returns `false`

Indexers should use `is_paused()` for current state queries and subscribe to
`vault_paused`/`vault_unpaused` events for state change notifications.

---

## Contract: Callora Settlement (`callora-settlement` v0.1.0)
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ The primary storage and metering contract.
- `set_allowed_depositor(caller, depositor)` — Owner-only; delegate deposit rights.
- `set_authorized_caller(caller)` — Owner-only; set the address permitted to trigger deductions.
- `get_price(api_id)` — returns `Option<i128>` with the configured price per call for `api_id`.
- `pause(caller)` — Admin/owner-only; activate circuit-breaker to block deposits and deductions.
- `unpause(caller)` — Admin/owner-only; deactivate circuit-breaker to restore operations.
- `is_paused()` — View function; returns current pause state for off-chain monitoring.

## Architecture & Flow

Expand Down
28 changes: 20 additions & 8 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,12 @@ This document outlines security best practices and checklist items for Callora v

### Pause / Circuit Breaker

- [ ] Emergency pause mechanism implemented via state flag in `instance()` storage
- [ ] Paused state blocks fund movement (e.g., reverting via `panic_with_error!`)
- [ ] Pause/unpause flows tested
- [x] Emergency pause mechanism implemented via state flag in `instance()` storage
- [x] Paused state blocks fund movement (e.g., reverting via `panic_with_error!`)
- [x] Pause/unpause flows tested
- [x] `is_paused()` view function exposed for off-chain monitoring
- [x] View function is read-only, deterministic, and non-panicking
- [x] Safe default state (returns `false` when unset)

### Admin Transfer

Expand Down Expand Up @@ -59,11 +62,20 @@ The vault performs USDC transfers to configurable counterpart addresses on every
- **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.
- **Unset behavior**: if neither address is configured the deducted amount stays
inside the vault (balance is reduced but no token transfer occurs). This state
is valid and explicitly documented—no funds are lost.
- Both addresses can only be changed by the admin in a single atomic storage
write, ensuring no partial update is observable by other callers.
- **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.
- **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.
- **Atomic Updates**: Each address is updated atomically in a single storage write,
ensuring no partial update is observable by other callers.
- **Audit Trail**: All routing configuration changes emit events:
- `set_settlement(admin) → address` when setting settlement
- `set_revenue_pool(admin) → address` when setting revenue pool
- `clear_revenue_pool(admin) → ()` when clearing revenue pool

### Vault-Specific Risks

Expand Down
49 changes: 43 additions & 6 deletions SETTLEMENT_IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,56 @@ sequenceDiagram

#### Storage Keys
```rust
StorageKey::Settlement
StorageKey::Settlement // Primary routing address (highest priority)
StorageKey::RevenuePool // Fallback routing address (used if Settlement not set)
```

#### New Functions
#### Routing Configuration Functions

1. **`set_settlement(env, caller, settlement_address)`** (Admin only)
- Sets the settlement contract address
- Sets the settlement contract address (primary routing destination)
- Authorization: Current admin only
- Panic: "unauthorized: caller is not admin"
- Validation: Address cannot be the vault's own address
- Panic: "unauthorized: caller is not admin" or "cannot route to vault itself"
- Event: `set_settlement(admin) → address`

2. **`get_settlement(env)`**
2. **`get_settlement(env)`** (Public read-only)
- Returns the configured settlement contract address
- Panic: "settlement address not set"
- Read-only: No state mutation, safe for indexers
- Panic: "settlement address not set" if not configured

3. **`set_revenue_pool(env, caller, revenue_pool)`** (Admin only)
- Sets the revenue pool contract address (fallback routing destination)
- Authorization: Current admin only
- Validation: Address cannot be the vault's own address
- Can be set to `None` to clear the configuration
- Events: `set_revenue_pool(admin) → address` or `clear_revenue_pool(admin) → ()`

4. **`get_revenue_pool(env)`** (Public read-only)
- Returns the configured revenue pool address (Option<Address>)
- Read-only: No state mutation, safe for indexers
- Returns `None` if not configured (does not panic)

#### 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()`.

- 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)

#### Routing Priority

When deduct operations occur, funds are routed according to this priority:

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



Expand Down
1 change: 0 additions & 1 deletion contracts/settlement/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ mod settlement_tests {
env.mock_all_auths();
let admin = Address::generate(&env);
let vault = Address::generate(&env);
let third_party = Address::generate(&env);
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
Expand Down
19 changes: 14 additions & 5 deletions contracts/vault/STORAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ pub enum StorageKey {
| `AllowedDepositors` | `Vec<Address>` | List of addresses allowed to deposit into the vault | Access control for deposits | `set_allowed_depositor()`, readable via `is_authorized_depositor()` |
| `Admin` | `Address` | Administrator address authorized to call `distribute()` and `set_admin()` | Access control for distributions | `get_admin()`, `set_admin()` (admin-only) |
| `UsdcToken` | `Address` | USDC token contract address | Token transfers for deposits, deducts, distributions | Set during `init()`, used by token operations |
| `Settlement` | `Option<Address>` | Settlement contract address; receives USDC on deduct operations | Deduct routing (priority over RevenuePool) | `set_settlement()`, `get_settlement()` (admin-only) |
| `RevenuePool` | `Option<Address>` | Revenue pool contract address; receives USDC on deduct if Settlement is not set | Deduct routing (fallback) | Set during `init()`, used if Settlement not configured |
| `Settlement` | `Option<Address>` | Settlement contract address; receives USDC on deduct operations | Deduct routing (priority over RevenuePool) | `set_settlement()`, `get_settlement()` (admin-only write, public read) |
| `RevenuePool` | `Option<Address>` | Revenue pool contract address; receives USDC on deduct if Settlement is not set | Deduct routing (fallback) | `set_revenue_pool()`, `get_revenue_pool()` (admin-only write, public read) |
| `MaxDeduct` | `i128` | Maximum USDC amount per single deduct operation | Deduct limit enforcement | Set during `init()`, read by `deduct()` and `batch_deduct()` |
| `Metadata(offering_id)` | `String` | Off-chain metadata reference (IPFS CID or URI) for a specific offering | Offering metadata | `set_metadata()`, `get_metadata()`, `update_metadata()` (owner-only) |

Expand Down Expand Up @@ -116,13 +116,22 @@ Sets up the vault with initial state:
| Operation | Reads | Writes | Authorization |
|-----------|-------|--------|-----------------|
| `set_settlement(settlement_address)` | Admin | Settlement | Admin only |
| `get_settlement()` | Settlement | — | Public read |
| `get_settlement()` | Settlement | — | Public read (view-only, no mutation) |
| `set_revenue_pool(revenue_pool)` | Admin | RevenuePool | Admin only |
| `get_revenue_pool()` | RevenuePool | — | Public read (view-only, no mutation) |

**Deduct Routing Logic:**
1. If `StorageKey::Settlement` is set: transfer USDC to settlement
2. Else if `StorageKey::RevenuePool` is set: transfer USDC to revenue pool
3. Else: USDC remains in vault

**View Function Safety:**
- Both `get_settlement()` and `get_revenue_pool()` are read-only operations
- They return only final committed state, never intermediate or pending values
- Safe for external indexers and off-chain queries
- Deterministic: identical state inputs always produce identical outputs
- `get_settlement()` panics if not configured; `get_revenue_pool()` returns `None` gracefully

### Metadata Operations

| Operation | Reads | Writes | Authorization |
Expand Down Expand Up @@ -216,8 +225,8 @@ env.storage().instance().set(&StorageKey::Meta, &new_meta);
### Access Control

- **Owner-Only Operations:** `set_allowed_depositor()`, `set_authorized_caller()`, `transfer_ownership()`, `withdraw()`, `withdraw_to()`, metadata operations
- **Admin-Only Operations:** `distribute()`, `set_admin()`, `set_settlement()`
- **Public Operations:** `balance()`, `get_meta()`, `get_metadata()`, `is_authorized_depositor()`, `get_settlement()` (read-only)
- **Admin-Only Operations:** `distribute()`, `set_admin()`, `set_settlement()`, `set_revenue_pool()`
- **Public Operations:** `balance()`, `get_meta()`, `get_metadata()`, `is_authorized_depositor()`, `get_settlement()`, `get_revenue_pool()` (all read-only)
- **Depositor Operations:** `deposit()` (owner or allowed depositor); `deduct()` and `batch_deduct()` (owner or authorized_caller)

### Data Integrity
Expand Down
Loading
Loading