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
19 changes: 19 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,22 @@ Before any mainnet deployment:
---

**Note**: This checklist should be reviewed and updated regularly as new security patterns emerge and the codebase evolves.

## require_auth() Audit (Issue #160)

All privileged entrypoints across `vault`, `revenue_pool`, and `settlement` contracts
have been audited for `require_auth()` coverage as part of Issue #160.

### Findings
- All privileged functions call `require_auth()` on the caller before executing. ✅
- Negative tests added to each crate's `test.rs` confirming unauthenticated calls are rejected.

### Intentional Exceptions
| Contract | Function | Reason |
|------------|------------------|--------|
| settlement | `init()` | One-time initializer guarded by already-initialized panic; no auth required by design. |
| vault | `require_owner()`| Internal helper using `assert!` for address equality. All public callers invoke `caller.require_auth()` before calling this helper, so host-level auth is enforced transitively. Documented gap: `require_owner` itself does not call `require_auth()`. |

### 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`
142 changes: 142 additions & 0 deletions contracts/revenue_pool/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,3 +266,145 @@ fn batch_distribute_success_events() {
}
}
}

// --- require_auth audit tests (Issue #160) ---
#[test]
fn require_auth_set_admin_fails_without_auth() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let attacker = Address::generate(&env);
let new_admin = Address::generate(&env);
let (_, client) = create_pool(&env);
let (usdc, _, _) = create_usdc(&env, &admin);
client.init(&admin, &usdc);
env.set_auths(&[]);
let result = client.try_set_admin(&attacker, &new_admin);
assert!(result.is_err(), "set_admin must require auth");
}
#[test]
fn require_auth_distribute_fails_without_auth() {
let env = Env::default();
let admin = Address::generate(&env);
let attacker = Address::generate(&env);
let recipient = Address::generate(&env);
let (pool_addr, client) = create_pool(&env);
let (usdc, _, usdc_admin) = create_usdc(&env, &admin);
env.mock_all_auths();
client.init(&admin, &usdc);
fund_pool(&usdc_admin, &pool_addr, 1000);
env.set_auths(&[]);
let result = client.try_distribute(&attacker, &recipient, &100);
assert!(result.is_err(), "distribute must require auth");
}
#[test]
fn require_auth_batch_distribute_fails_without_auth() {
let env = Env::default();
let admin = Address::generate(&env);
let attacker = Address::generate(&env);
let dev = Address::generate(&env);
let (pool_addr, client) = create_pool(&env);
let (usdc, _, usdc_admin) = create_usdc(&env, &admin);
env.mock_all_auths();
client.init(&admin, &usdc);
fund_pool(&usdc_admin, &pool_addr, 1000);
let mut payments: Vec<(Address, i128)> = Vec::new(&env);
payments.push_back((dev.clone(), 100_i128));
env.set_auths(&[]);
let result = client.try_batch_distribute(&attacker, &payments);
assert!(result.is_err(), "batch_distribute must require auth");
}
#[test]
fn require_auth_receive_payment_fails_without_auth() {
let env = Env::default();
let admin = Address::generate(&env);
let attacker = Address::generate(&env);
let (pool_addr, client) = create_pool(&env);
let (usdc, _, usdc_admin) = create_usdc(&env, &admin);
env.mock_all_auths();
client.init(&admin, &usdc);
fund_pool(&usdc_admin, &pool_addr, 1000);
env.set_auths(&[]);
let result = client.try_receive_payment(&attacker, &100, &false);
assert!(result.is_err(), "receive_payment must require auth");
}

#[test]
fn set_admin_non_admin_caller_panics() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let attacker = Address::generate(&env);
let new_admin = Address::generate(&env);
let (_, client) = create_pool(&env);
let (usdc, _, _) = create_usdc(&env, &admin);
client.init(&admin, &usdc);
let result = client.try_set_admin(&attacker, &new_admin);
assert!(result.is_err(), "non-admin cannot call set_admin");
}
#[test]
fn claim_admin_wrong_pending_panics() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let new_admin = Address::generate(&env);
let attacker = Address::generate(&env);
let (_, client) = create_pool(&env);
let (usdc, _, _) = create_usdc(&env, &admin);
client.init(&admin, &usdc);
client.set_admin(&admin, &new_admin);
let result = client.try_claim_admin(&attacker);
assert!(result.is_err(), "wrong pending admin cannot claim");
}
#[test]
fn receive_payment_emits_event() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let (pool_addr, client) = create_pool(&env);
let (usdc, _, usdc_admin) = create_usdc(&env, &admin);
client.init(&admin, &usdc);
fund_pool(&usdc_admin, &pool_addr, 500);
client.receive_payment(&admin, &100, &true);
let events = env.events().all();
assert!(!events.is_empty());
}
#[test]
fn distribute_self_recipient_panics() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let (pool_addr, client) = create_pool(&env);
let (usdc, _, usdc_admin) = create_usdc(&env, &admin);
client.init(&admin, &usdc);
fund_pool(&usdc_admin, &pool_addr, 500);
let result = client.try_distribute(&admin, &pool_addr, &100);
assert!(result.is_err(), "cannot distribute to contract itself");
}
#[test]
fn distribute_non_admin_panics() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let attacker = Address::generate(&env);
let recipient = Address::generate(&env);
let (pool_addr, client) = create_pool(&env);
let (usdc, _, usdc_admin) = create_usdc(&env, &admin);
client.init(&admin, &usdc);
fund_pool(&usdc_admin, &pool_addr, 500);
let result = client.try_distribute(&attacker, &recipient, &100);
assert!(result.is_err(), "non-admin cannot distribute");
}
#[test]
fn distribute_zero_amount_panics() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let recipient = Address::generate(&env);
let (pool_addr, client) = create_pool(&env);
let (usdc, _, usdc_admin) = create_usdc(&env, &admin);
client.init(&admin, &usdc);
fund_pool(&usdc_admin, &pool_addr, 500);
let result = client.try_distribute(&admin, &recipient, &0);
assert!(result.is_err(), "zero amount distribute must fail");
}
65 changes: 63 additions & 2 deletions contracts/settlement/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ mod settlement_tests {
env.mock_all_auths();
let admin = Address::generate(&env);
let vault = Address::generate(&env);
let addr = env.register(CalloraSettlement, ());
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
let third_party = Address::generate(&env);
(env, addr, admin, vault, third_party)
}

Expand Down Expand Up @@ -152,7 +153,7 @@ mod settlement_tests {
env.mock_all_auths();
let admin = Address::generate(&env);
let vault = Address::generate(&env);
let addr = env.register(CalloraSettlement, ());
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
client.receive_payment(&admin, &100i128, &true, &None);
Expand Down Expand Up @@ -913,4 +914,64 @@ mod settlement_tests {
assert_eq!(pool_after.last_updated, 1_700_000_100);
assert_eq!(pool_after.total_balance, 1500i128);
}

// --- require_auth audit tests (Issue #160) ---
#[test]
fn require_auth_set_admin_fails_without_auth() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let attacker = Address::generate(&env);
let new_admin = Address::generate(&env);
let vault = Address::generate(&env);
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
env.set_auths(&[]);
let result = client.try_set_admin(&attacker, &new_admin);
assert!(result.is_err(), "set_admin must require auth");
}
#[test]
fn require_auth_set_vault_fails_without_auth() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let attacker = Address::generate(&env);
let vault = Address::generate(&env);
let new_vault = Address::generate(&env);
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
env.set_auths(&[]);
let result = client.try_set_vault(&attacker, &new_vault);
assert!(result.is_err(), "set_vault must require auth");
}
#[test]
fn require_auth_receive_payment_fails_without_auth() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let vault = Address::generate(&env);
let attacker = Address::generate(&env);
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
client.init(&admin, &vault);
env.set_auths(&[]);
let result = client.try_receive_payment(&attacker, &100i128, &true, &None);
assert!(result.is_err(), "receive_payment must require auth");
}
// SECURITY NOTE: settlement::init has no require_auth() by design.
// It is a one-time initializer guarded by an already-initialized panic.
// This is an intentional exception documented per Issue #160.
#[test]
fn init_no_auth_required_intentional_exception() {
let env = Env::default();
let admin = Address::generate(&env);
let vault = Address::generate(&env);
let addr = env.register(CalloraSettlement, ());
let client = CalloraSettlementClient::new(&env, &addr);
// No mock_all_auths — init should succeed without host auth (intentional)
client.init(&admin, &vault);
assert_eq!(client.get_admin(), admin);
}
}
10 changes: 8 additions & 2 deletions contracts/vault/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,10 @@ impl CalloraVault {
if let Some(s) = inst.get::<StorageKey, Address>(&StorageKey::Settlement) {
let ut: Address = inst.get(&StorageKey::UsdcToken).unwrap();
Self::transfer_funds(&env, &ut, &s, amount);
} else if inst.get::<StorageKey, Address>(&StorageKey::RevenuePool).is_some() {
} else if inst
.get::<StorageKey, Address>(&StorageKey::RevenuePool)
.is_some()
{
Self::transfer_to_revenue_pool(env.clone(), amount);
}
let rid = request_id.unwrap_or(Symbol::new(&env, ""));
Expand Down Expand Up @@ -372,7 +375,10 @@ impl CalloraVault {
if let Some(s) = inst.get::<StorageKey, Address>(&StorageKey::Settlement) {
let ut: Address = inst.get(&StorageKey::UsdcToken).unwrap();
Self::transfer_funds(&env, &ut, &s, total);
} else if inst.get::<StorageKey, Address>(&StorageKey::RevenuePool).is_some() {
} else if inst
.get::<StorageKey, Address>(&StorageKey::RevenuePool)
.is_some()
{
Self::transfer_to_revenue_pool(env.clone(), total);
}
meta.balance
Expand Down
Loading
Loading