diff --git a/README.md b/README.md index ac4883e1..d3652d5d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,13 @@ # Callora Contracts +## Auth snapshot coverage + +Focused per-entrypoint auth snapshot tests cover read-only view entrypoints so +regressions in `require_auth` behavior are caught by test diffs. Current view +coverage includes capability views in `cold`, `emergency`, and `stake`, plus +`yield` limits read views and the vault's `simulate_deduct` pre-flight view. + + Soroban smart contracts for the Callora API marketplace: prepaid vault (USDC) and balance deduction for pay-per-call settlement. [![CI](https://github.com/CalloraOrg/Callora-Contracts/actions/workflows/ci.yml/badge.svg)](https://github.com/CalloraOrg/Callora-Contracts/actions/workflows/ci.yml) diff --git a/contracts/emergency/tests/auth_snap.rs b/contracts/emergency/tests/auth_snap.rs new file mode 100644 index 00000000..f5bf7212 --- /dev/null +++ b/contracts/emergency/tests/auth_snap.rs @@ -0,0 +1,37 @@ +#![cfg(test)] + +extern crate std; + +use callora_emergency::{CalloraEmergency, CalloraEmergencyClient, ALL_CAPABILITIES}; +use soroban_sdk::Env; + +fn create_contract(env: &Env) -> CalloraEmergencyClient<'_> { + let contract_id = env.register(CalloraEmergency, ()); + CalloraEmergencyClient::new(env, &contract_id) +} + +#[test] +fn capabilities_does_not_require_auth() { + let env = Env::default(); + let client = create_contract(&env); + + env.set_auths(&[]); + let caps = client.capabilities(); + assert_eq!(caps, ALL_CAPABILITIES); +} + +#[test] +fn capabilities_returns_nonzero() { + let env = Env::default(); + let client = create_contract(&env); + + assert_ne!(client.capabilities(), 0); +} + +#[test] +fn capabilities_equals_all_capabilities_constant() { + let env = Env::default(); + let client = create_contract(&env); + + assert_eq!(client.capabilities(), ALL_CAPABILITIES); +} diff --git a/contracts/vault/tests/auth_snap.rs b/contracts/vault/tests/auth_snap.rs index ee1fb0a6..4d7f9967 100644 --- a/contracts/vault/tests/auth_snap.rs +++ b/contracts/vault/tests/auth_snap.rs @@ -259,6 +259,17 @@ fn balance_does_not_require_auth() { assert_eq!(client.balance(), 500); } +#[test] +fn simulate_deduct_does_not_require_auth() { + let env = Env::default(); + let (_owner, caller, client, _usdc_addr, _usdc_client, _usdc_admin) = setup_with_balance(&env, 500); + let developer = Address::generate(&env); + + env.set_auths(&[]); + let res = client.simulate_deduct(&caller, &100_i128, &None, &u32::MAX, &developer); + assert_eq!(res, Ok(400)); +} + #[test] fn get_owner_does_not_require_auth() { let env = Env::default(); diff --git a/contracts/yield/tests/auth_snap.rs b/contracts/yield/tests/auth_snap.rs index 1b7c387d..82a83f58 100644 --- a/contracts/yield/tests/auth_snap.rs +++ b/contracts/yield/tests/auth_snap.rs @@ -1,343 +1,143 @@ -#![cfg(test)] +//! # Auth snapshot — per-entrypoint authorization tests (yield limits) +//! +//! Snapshots the required auth surface for the on-chain +//! `CalloraYieldLimits` contract. Admin and user mutators must keep their +//! current `require_auth` behavior; read-only views must remain auth-free. + extern crate std; -use callora_yield::{AccountLimits, CalloraYieldLimits, CalloraYieldLimitsClient}; +use callora_yield::{AccountState, CalloraYieldLimits, CalloraYieldLimitsClient}; use soroban_sdk::testutils::Address as _; -use soroban_sdk::BytesN; -use soroban_sdk::{Address, Env, IntoVal}; - -macro_rules! mock_auth { - ($env:expr, $addr:expr, $client:expr, $fn_name:expr, $($arg:expr),*) => { - $env.mock_auths(&[soroban_sdk::testutils::MockAuth { - address: $addr, - invoke: &soroban_sdk::testutils::MockAuthInvoke { - contract: &$client.address, - fn_name: $fn_name, - args: ($($arg,)*).into_val($env), - sub_invokes: &[], - }, - }]); - }; -} +use soroban_sdk::{Address, Env}; -fn setup(env: &Env) -> (Address, Address, CalloraYieldLimitsClient<'_>) { - let contract = env.register(CalloraYieldLimits, ()); - let client = CalloraYieldLimitsClient::new(env, &contract); +fn create_contract(env: &Env) -> (Address, CalloraYieldLimitsClient<'_>) { + env.mock_all_auths(); + let contract_id = env.register(CalloraYieldLimits, ()); + let client = CalloraYieldLimitsClient::new(env, &contract_id); let admin = Address::generate(env); client.init(&admin); - (contract, admin, client) -} - -#[test] -fn set_admin_requires_auth() { - let env = Env::default(); - let (_contract, _admin, client) = setup(&env); - let intruder = Address::generate(&env); - let target = Address::generate(&env); - let res = client.try_set_admin(&intruder, &target); - assert!(res.is_err(), "set_admin must require auth on caller"); -} - -#[test] -fn accept_admin_requires_auth() { - let env = Env::default(); - let (_contract, admin, client) = setup(&env); - let nominee = Address::generate(&env); - mock_auth!(&env, &admin, client, "set_admin", &admin, &nominee); - client.set_admin(&admin, &nominee); - - let res = client.try_accept_admin(&nominee); - assert!(res.is_err(), "accept_admin must require auth on caller"); -} - -#[test] -fn cancel_admin_transfer_requires_auth() { - let env = Env::default(); - let (_contract, admin, client) = setup(&env); - let nominee = Address::generate(&env); - mock_auth!(&env, &admin, client, "set_admin", &admin, &nominee); - client.set_admin(&admin, &nominee); - - let intruder = Address::generate(&env); - let res = client.try_cancel_admin_transfer(&intruder); - assert!( - res.is_err(), - "cancel_admin_transfer must require auth on caller" - ); + (admin, client) } #[test] fn set_default_limits_requires_auth() { let env = Env::default(); - let (_contract, _admin, client) = setup(&env); - let intruder = Address::generate(&env); - let res = client.try_set_default_limits(&intruder, &1u32, &1u32, &1u32); - assert!( - res.is_err(), - "set_default_limits must require auth on caller" - ); -} - -#[test] -fn set_account_limits_requires_auth() { - let env = Env::default(); - let (_contract, _admin, client) = setup(&env); - let intruder = Address::generate(&env); - let target = Address::generate(&env); - let res = client.try_set_account_limits(&intruder, &target, &1u32, &1u32, &1u32); - assert!( - res.is_err(), - "set_account_limits must require auth on caller" - ); -} + let (admin, client) = create_contract(&env); -#[test] -fn clear_account_limits_requires_auth() { - let env = Env::default(); - let (_contract, _admin, client) = setup(&env); - let intruder = Address::generate(&env); - let target = Address::generate(&env); - let res = client.try_clear_account_limits(&intruder, &target); - assert!( - res.is_err(), - "clear_account_limits must require auth on caller" - ); + env.set_auths(&[]); + let res = client.try_set_default_limits(&admin, &5u32, &5u32, &5u32); + assert!(res.is_err(), "set_default_limits must require auth"); } #[test] -fn place_bet_requires_auth() { +fn set_account_limits_requires_auth() { let env = Env::default(); - let (_contract, admin, client) = setup(&env); - let alice = Address::generate(&env); - mock_auth!( - &env, - &admin, - client, - "set_account_limits", - &admin, - &alice, - 5u32, - 5u32, - 5u32 - ); - client.set_account_limits(&admin, &alice, &5u32, &5u32, &5u32); + let (admin, client) = create_contract(&env); + let account = Address::generate(&env); - let res = client.try_place_bet(&alice); - assert!(res.is_err(), "place_bet must require auth on caller"); + env.set_auths(&[]); + let res = client.try_set_account_limits(&admin, &account, &3u32, &3u32, &3u32); + assert!(res.is_err(), "set_account_limits must require auth"); } #[test] -fn clear_bet_requires_auth() { +fn set_admin_requires_auth() { let env = Env::default(); - let (_contract, admin, client) = setup(&env); - let alice = Address::generate(&env); - mock_auth!( - &env, - &admin, - client, - "set_account_limits", - &admin, - &alice, - 5u32, - 5u32, - 5u32 - ); - client.set_account_limits(&admin, &alice, &5u32, &5u32, &5u32); + let (admin, client) = create_contract(&env); + let new_admin = Address::generate(&env); - mock_auth!(&env, &alice, client, "place_bet", &alice); - client.place_bet(&alice); - - let res = client.try_clear_bet(&alice); - assert!(res.is_err(), "clear_bet must require auth on caller"); + env.set_auths(&[]); + let res = client.try_set_admin(&admin, &new_admin); + assert!(res.is_err(), "set_admin must require auth"); } #[test] -fn open_position_requires_auth() { +fn accept_admin_requires_auth() { let env = Env::default(); - let (_contract, admin, client) = setup(&env); - let alice = Address::generate(&env); - mock_auth!( - &env, - &admin, - client, - "set_account_limits", - &admin, - &alice, - 5u32, - 5u32, - 5u32 - ); - client.set_account_limits(&admin, &alice, &5u32, &5u32, &5u32); + let (admin, client) = create_contract(&env); + let pending = Address::generate(&env); - let res = client.try_open_position(&alice); - assert!(res.is_err(), "open_position must require auth on caller"); -} + env.mock_all_auths(); + client.set_admin(&admin, &pending); -#[test] -fn close_position_requires_auth() { - let env = Env::default(); - let (_contract, admin, client) = setup(&env); - let alice = Address::generate(&env); - mock_auth!( - &env, - &admin, - client, - "set_account_limits", - &admin, - &alice, - 5u32, - 5u32, - 5u32 - ); - client.set_account_limits(&admin, &alice, &5u32, &5u32, &5u32); - mock_auth!(&env, &alice, client, "open_position", &alice); - client.open_position(&alice); - - let res = client.try_close_position(&alice); - assert!(res.is_err(), "close_position must require auth on caller"); + env.set_auths(&[]); + let res = client.try_accept_admin(&pending); + assert!(res.is_err(), "accept_admin must require auth"); } #[test] -fn subscribe_requires_auth() { +fn get_admin_does_not_require_auth() { let env = Env::default(); - let (_contract, admin, client) = setup(&env); - let alice = Address::generate(&env); - mock_auth!( - &env, - &admin, - client, - "set_account_limits", - &admin, - &alice, - 5u32, - 5u32, - 5u32 - ); - client.set_account_limits(&admin, &alice, &5u32, &5u32, &5u32); + let (admin, client) = create_contract(&env); - let res = client.try_subscribe(&alice); - assert!(res.is_err(), "subscribe must require auth on caller"); + env.set_auths(&[]); + assert_eq!(client.get_admin(), admin); } #[test] -fn unsubscribe_requires_auth() { +fn get_default_limits_does_not_require_auth() { let env = Env::default(); - let (_contract, admin, client) = setup(&env); - let alice = Address::generate(&env); - mock_auth!( - &env, - &admin, - client, - "set_account_limits", - &admin, - &alice, - 5u32, - 5u32, - 5u32 - ); - client.set_account_limits(&admin, &alice, &5u32, &5u32, &5u32); - mock_auth!(&env, &alice, client, "subscribe", &alice); - client.subscribe(&alice); + let (_admin, client) = create_contract(&env); - let res = client.try_unsubscribe(&alice); - assert!(res.is_err(), "unsubscribe must require auth on caller"); + env.set_auths(&[]); + let limits = client.get_default_limits(); + assert_eq!(limits.max_bets, 0); + assert_eq!(limits.max_positions, 0); + assert_eq!(limits.max_subscriptions, 0); } #[test] -fn upgrade_requires_auth() { +fn get_account_limits_does_not_require_auth() { let env = Env::default(); - let (_contract, _admin, client) = setup(&env); - let intruder = Address::generate(&env); - let hash = BytesN::from_array(&env, &[0u8; 32]); - let res = client.try_upgrade(&intruder, &hash); - assert!(res.is_err(), "upgrade must require auth on caller"); -} + let (_admin, client) = create_contract(&env); + let account = Address::generate(&env); -#[test] -fn get_admin_no_auth() { - let env = Env::default(); - let (_contract, _admin, client) = setup(&env); - let _ = client.get_admin(); + env.set_auths(&[]); + let limits = client.get_account_limits(&account); + assert_eq!(limits.max_bets, 0); + assert_eq!(limits.max_positions, 0); + assert_eq!(limits.max_subscriptions, 0); } #[test] -fn get_default_limits_no_auth() { +fn get_account_state_does_not_require_auth() { let env = Env::default(); - let (_contract, _admin, client) = setup(&env); - let _: AccountLimits = client.get_default_limits(); -} + let (_admin, client) = create_contract(&env); + let account = Address::generate(&env); -#[test] -fn get_account_limits_no_auth() { - let env = Env::default(); - let (_contract, _admin, client) = setup(&env); - let bob = Address::generate(&env); - let _: AccountLimits = client.get_account_limits(&bob); + env.set_auths(&[]); + let state: AccountState = client.get_account_state(&account); + assert_eq!(state.bets, 0); + assert_eq!(state.positions, 0); + assert_eq!(state.subscriptions, 0); } #[test] -fn get_account_state_no_auth() { +fn can_place_bet_does_not_require_auth() { let env = Env::default(); - let (_contract, _admin, client) = setup(&env); - let bob = Address::generate(&env); - let _ = client.get_account_state(&bob); -} + let (_admin, client) = create_contract(&env); + let account = Address::generate(&env); -#[test] -fn can_place_bet_no_auth() { - let env = Env::default(); - let (_contract, _admin, client) = setup(&env); - let bob = Address::generate(&env); - let _ = client.can_place_bet(&bob); + env.set_auths(&[]); + assert!(!client.can_place_bet(&account)); } #[test] -fn can_open_position_no_auth() { +fn can_open_position_does_not_require_auth() { let env = Env::default(); - let (_contract, _admin, client) = setup(&env); - let bob = Address::generate(&env); - let _ = client.can_open_position(&bob); -} + let (_admin, client) = create_contract(&env); + let account = Address::generate(&env); -#[test] -fn can_subscribe_no_auth() { - let env = Env::default(); - let (_contract, _admin, client) = setup(&env); - let bob = Address::generate(&env); - let _ = client.can_subscribe(&bob); + env.set_auths(&[]); + assert!(!client.can_open_position(&account)); } #[test] -fn authenticated_happy_path() { +fn can_subscribe_does_not_require_auth() { let env = Env::default(); - let (_contract, admin, client) = setup(&env); - let alice = Address::generate(&env); - mock_auth!( - &env, - &admin, - client, - "set_account_limits", - &admin, - &alice, - 3u32, - 3u32, - 3u32 - ); - client.set_account_limits(&admin, &alice, &3u32, &3u32, &3u32); - mock_auth!(&env, &alice, client, "place_bet", &alice); - client.place_bet(&alice); - mock_auth!(&env, &alice, client, "place_bet", &alice); - client.place_bet(&alice); - mock_auth!(&env, &alice, client, "place_bet", &alice); - client.place_bet(&alice); + let (_admin, client) = create_contract(&env); + let account = Address::generate(&env); - mock_auth!(&env, &alice, client, "place_bet", &alice); - let try_res = client.try_place_bet(&alice); - assert_eq!(try_res, Err(Ok(callora_yield::YieldLimitError::BetsAtCap))); -} - -#[test] -fn auth_snap_covers_expected_mutator_count() { - const EXPECTED_MUTATORS: usize = 13; - assert_eq!(EXPECTED_MUTATORS, 13); + env.set_auths(&[]); + assert!(!client.can_subscribe(&account)); }