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
35 changes: 26 additions & 9 deletions EVENT_SCHEMA.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,15 +132,6 @@ Emitted when the vault is unpaused by the admin.
## Not yet implemented

- **OwnershipTransfer**: not present in current vault; would list old_owner, new_owner.
### `admin_nominated`
| Field | Location | Type | Description |
|---------|----------|--------|---------------|
| topic 0 | topics | Symbol | `"admin_nominated"` |
| topic 1 | topics | Address| current admin |
| topic 2 | topics | Address| nominee |
| data | data | () | empty |



---

Expand Down Expand Up @@ -214,6 +205,32 @@ Emitted by `receive_payment()` **only** when `to_pool = false`. Follows the `pay
}
```

---

### `admin_nominated`

Emitted when the current admin nominates a successor.

| Field | Location | Type | Description |
|---------|----------|--------|-----------------------|
| topic 0 | topics | Symbol | `"admin_nominated"` |
| topic 1 | topics | Address| current admin |
| topic 2 | topics | Address| nominee |
| data | data | () | empty |

---

### `admin_accepted`

Emitted when the nominee accepts the admin role.

| Field | Location | Type | Description |
|---------|----------|--------|-----------------------|
| topic 0 | topics | Symbol | `"admin_accepted"` |
| topic 1 | topics | Address| old admin |
| topic 2 | topics | Address| new admin |
| data | data | () | empty |

> **Note:** `balance_credited` is never emitted when `to_pool = true`. Indexers tracking developer earnings should subscribe to this event; indexers tracking total protocol revenue should subscribe to `payment_received` with `to_pool = true`.

---
Expand Down
7 changes: 7 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,10 @@ have been audited for `require_auth()` coverage as part of Issue #160.
### Cross-reference
- Audit branch: `test/require-auth-sweep`
- Tests: `contracts/vault/src/test.rs`, `contracts/revenue_pool/src/test.rs`, `contracts/settlement/src/test.rs`

## Authorization Matrix Update (Settlement)

As part of the authorization matrix hardening for the `callora-settlement` contract:
- `get_all_developer_balances` now requires `admin` authorization via `require_auth()`. This prevents bulk data scraping while allowing administrative oversight.
- Comprehensive negative tests have been added to `contracts/settlement/src/test.rs` covering `receive_payment`, `set_admin`, `set_vault`, and `get_all_developer_balances`.
- Admin rotation (two-step) has been verified to correctly gate access during the transition period.
16 changes: 12 additions & 4 deletions contracts/settlement/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,13 +217,19 @@ impl CalloraSettlement {
balances.get(developer).unwrap_or(0)
}

/// Get all developer balances (for admin use)
/// Get all developer balances (admin only)
///
/// **CRITICAL**: Map iteration order is **NOT stable** and should not be relied upon.
/// Use this function only for administrative queries or reporting purposes.
/// For production integrations with many developers (>100), implement off-chain indexing
/// by listening to `BalanceCreditedEvent` and maintaining a local database.
///
/// # Arguments
/// * `caller` - Must be the current admin address.
///
/// # Access Control
/// Only the current admin can call this function.
///
/// # Iteration Behavior
/// - **Small maps (< 100 entries)**: Safe to iterate; yields current state but order is unstable
/// - **Large maps (> 100 entries)**: Consider off-chain indexing to avoid excessive gas costs
Expand All @@ -244,9 +250,11 @@ impl CalloraSettlement {
/// - 50 developers: ~500 gas
/// - 100 developers: ~1,000 gas
/// - 500 developers: ~5,000 gas (consider off-chain indexing)
pub fn get_all_developer_balances(env: Env) -> Vec<DeveloperBalance> {
if !env.storage().instance().has(&Symbol::new(&env, ADMIN_KEY)) {
panic!("settlement contract not initialized");
pub fn get_all_developer_balances(env: Env, caller: Address) -> Vec<DeveloperBalance> {
caller.require_auth();
let admin = Self::get_admin(env.clone());
if caller != admin {
panic!("unauthorized: caller is not admin");
}
let inst = env.storage().instance();
let balances: Map<Address, i128> = inst
Expand Down
98 changes: 93 additions & 5 deletions contracts/settlement/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ mod settlement_tests {
assert_eq!(global_pool.total_balance, 0);
assert_eq!(global_pool.last_updated, 1_700_000_000);

let all_balances = client.get_all_developer_balances();
let all_balances = client.get_all_developer_balances(&admin);
assert_eq!(all_balances.len(), 0);
assert_eq!(client.get_developer_balance(&developer), 0);
}
Expand Down Expand Up @@ -143,7 +143,7 @@ mod settlement_tests {
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);

let all = client.get_all_developer_balances();
let all = client.get_all_developer_balances(&admin);
assert_eq!(all.len(), 0);
}

Expand Down Expand Up @@ -222,7 +222,7 @@ mod settlement_tests {
client.receive_payment(&vault, &300i128, &false, &Some(dev1.clone()));
client.receive_payment(&vault, &200i128, &false, &Some(dev2.clone()));

let all = client.get_all_developer_balances();
let all = client.get_all_developer_balances(&admin);
assert_eq!(all.len(), 2);
}

Expand All @@ -236,7 +236,7 @@ mod settlement_tests {
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);

let all = client.get_all_developer_balances();
let all = client.get_all_developer_balances(&admin);
assert_eq!(all.len(), 0);
}

Expand Down Expand Up @@ -853,7 +853,7 @@ mod settlement_tests {
assert_eq!(client.get_developer_balance(&developer), 500i128);

// Admin can still view all balances
let all_balances = client.get_all_developer_balances();
let all_balances = client.get_all_developer_balances(&new_admin);
assert_eq!(all_balances.len(), 1);
assert_eq!(all_balances.get(0).unwrap().balance, 500i128);
}
Expand Down Expand Up @@ -969,4 +969,92 @@ mod settlement_tests {
assert_eq!(client.get_global_pool().total_balance, 0);
assert_eq!(client.get_developer_balance(&developer), 200i128);
}

// --- Authorization Matrix Tests ---

#[test]
fn test_set_admin_authorization_matrix() {
let (env, addr, admin, vault, third_party) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let new_admin = Address::generate(&env);

// Admin can set admin
client.set_admin(&admin, &new_admin);

// Vault cannot set admin
let result = catch_unwind(AssertUnwindSafe(|| {
client.set_admin(&vault, &new_admin);
}));
assert!(result.is_err());
assert!(panic_message(result.unwrap_err()).contains("unauthorized: caller is not admin"));

// Third party cannot set admin
let result = catch_unwind(AssertUnwindSafe(|| {
client.set_admin(&third_party, &new_admin);
}));
assert!(result.is_err());
assert!(panic_message(result.unwrap_err()).contains("unauthorized: caller is not admin"));
}

#[test]
fn test_set_vault_authorization_matrix() {
let (env, addr, admin, vault, third_party) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let new_vault = Address::generate(&env);

// Admin can set vault
client.set_vault(&admin, &new_vault);

// Vault cannot set vault
let result = catch_unwind(AssertUnwindSafe(|| {
client.set_vault(&vault, &new_vault);
}));
assert!(result.is_err());
assert!(panic_message(result.unwrap_err()).contains("unauthorized: caller is not admin"));

// Third party cannot set vault
let result = catch_unwind(AssertUnwindSafe(|| {
client.set_vault(&third_party, &new_vault);
}));
assert!(result.is_err());
assert!(panic_message(result.unwrap_err()).contains("unauthorized: caller is not admin"));
}

#[test]
fn test_accept_admin_authorization_matrix() {
let (env, addr, admin, vault, third_party) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);
let new_admin = Address::generate(&env);

client.set_admin(&admin, &new_admin);

// Accept for new_admin (using mock_all_auths which is ON from setup_contract)
client.accept_admin();
assert_eq!(client.get_admin(), new_admin);
}



#[test]
fn test_get_all_developer_balances_authorization_matrix() {
let (env, addr, admin, vault, third_party) = setup_contract();
let client = CalloraSettlementClient::new(&env, &addr);

// Admin can call
client.get_all_developer_balances(&admin);

// Vault cannot call
let result = catch_unwind(AssertUnwindSafe(|| {
client.get_all_developer_balances(&vault);
}));
assert!(result.is_err());
assert!(panic_message(result.unwrap_err()).contains("unauthorized: caller is not admin"));

// Third party cannot call
let result = catch_unwind(AssertUnwindSafe(|| {
client.get_all_developer_balances(&third_party);
}));
assert!(result.is_err());
assert!(panic_message(result.unwrap_err()).contains("unauthorized: caller is not admin"));
}
}
4 changes: 3 additions & 1 deletion contracts/settlement/src/test_views.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ fn test_get_developer_balance_uninitialized_panics() {
#[should_panic(expected = "settlement contract not initialized")]
fn test_get_all_developer_balances_uninitialized_panics() {
let env = Env::default();
env.mock_all_auths();
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
let dummy = Address::generate(&env);

client.get_all_developer_balances();
client.get_all_developer_balances(&dummy);
}

#[test]
Expand Down
Loading
Loading