diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1fae54f1..cb9ae71f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: ${{ runner.os }}-cargo- - name: Check format (workspace crates) - run: cargo fmt --package callora-vault --package callora-settlement --package callora-revenue-pool --package callora-validators --package callora-cross-contract-tests -- --check + run: cargo fmt --package callora-settlement --package callora-revenue-pool --package callora-validators --package callora-cross-contract-tests -- --check - name: Clippy run: cargo clippy --all-targets --all-features -- -D warnings diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 5506724b..b69461e0 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -30,7 +30,7 @@ jobs: ${{ runner.os }}-cargo- - name: Check format - run: cargo fmt --package callora-vault --package callora-settlement --package callora-revenue-pool --package callora-cross-contract-tests -- --check + run: cargo fmt --package callora-settlement --package callora-revenue-pool --package callora-cross-contract-tests -- --check - name: Clippy run: cargo clippy --all-targets --all-features -- -D warnings diff --git a/Cargo.lock b/Cargo.lock index 9cad96e6..67443b0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -258,7 +258,6 @@ version = "0.0.0" dependencies = [ "callora-revenue-pool", "callora-settlement", - "callora-vault", "soroban-sdk", ] @@ -268,7 +267,6 @@ version = "0.0.1" dependencies = [ "callora-revenue-pool", "callora-settlement", - "callora-vault", "rand 0.8.7", "soroban-sdk", ] @@ -282,26 +280,28 @@ dependencies = [ ] [[package]] -name = "callora-freeze-fuzz" -version = "0.0.0" +name = "callora-helpers" +version = "0.1.0" + +[[package]] +name = "callora-hot" +version = "0.1.0" dependencies = [ - "arbitrary", - "callora-freeze", - "callora-revenue-pool", - "libfuzzer-sys", "soroban-sdk", ] [[package]] -name = "callora-helpers" +name = "callora-migrate" version = "0.1.0" +dependencies = [ + "callora-settlement", + "soroban-sdk", +] [[package]] -name = "callora-limits" -version = "0.0.1" +name = "callora-registry" +version = "0.1.0" dependencies = [ - "callora-revenue-pool", - "callora-settlement", "soroban-sdk", ] @@ -330,31 +330,42 @@ dependencies = [ "soroban-sdk", ] +[[package]] +name = "callora-settlement-fuzz" +version = "0.0.0" +dependencies = [ + "callora-settlement", + "libfuzzer-sys", + "soroban-sdk", +] + +[[package]] +name = "callora-upgrade" +version = "0.0.0" +dependencies = [ + "soroban-sdk", +] + [[package]] name = "callora-validators" version = "0.1.0" dependencies = [ + "proptest", "soroban-sdk", ] [[package]] -name = "callora-vault" -version = "0.0.1" +name = "callora-whitelist" +version = "0.1.0" dependencies = [ - "callora-revenue-pool", - "callora-settlement", - "callora-validators", - "rand 0.8.7", "soroban-sdk", ] [[package]] -name = "callora-vault-fuzz" -version = "0.0.0" +name = "callora-yield" +version = "0.0.1" dependencies = [ - "arbitrary", - "callora-vault", - "libfuzzer-sys", + "callora-revenue-pool", "soroban-sdk", ] diff --git a/Cargo.toml b/Cargo.toml index 0c6f5091..e3041b49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,6 @@ publish = false [workspace] resolver = "2" members = [ - "contracts/vault", "contracts/revenue_pool", "contracts/settlement", "contracts/settlement/fuzz", @@ -16,18 +15,23 @@ members = [ "contracts/helpers", "contracts/validators", "contracts/revenue_pool/fuzz", - "contracts/migrate/fuzz", + "contracts/migrate", "contracts/tests", "contracts/whitelist", "contracts/cold", + "contracts/hot", + "contracts/registry", + "contracts/upgrade", + "contracts/yield", ] default-members = [ - "contracts/vault", "contracts/revenue_pool", "contracts/settlement", "contracts/checkpoint", "contracts/freeze", "contracts/helpers", + "contracts/cold", + "contracts/hot", "contracts/validators", ] @@ -36,7 +40,6 @@ default-members = [ soroban-sdk = "22" [dev-dependencies] -callora-vault = { path = "contracts/vault" } callora-settlement = { path = "contracts/settlement" } callora-revenue-pool = { path = "contracts/revenue_pool" } soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/contracts/checkpoint/tests/proptest.rs b/contracts/checkpoint/tests/proptest.rs index 7d08eba9..8b77ca92 100644 --- a/contracts/checkpoint/tests/proptest.rs +++ b/contracts/checkpoint/tests/proptest.rs @@ -28,7 +28,7 @@ enum CheckpointAction { fn checkpoint_action_strategy() -> impl Strategy { prop_oneof![ - 5 => (0_i128..=1_000_000_i128).prop_map(CheckpointAction::CreateSingle), + 5 => (0_i128..=1_000_000_i128).prop_map(|balance| CheckpointAction::CreateSingle { balance }), 3 => (1_u32..=MAX_BATCH_SIZE, 0_i128..=1_000_000_i128) .prop_map(|(count, balance)| CheckpointAction::CreateBatch { count, balance }), 2 => (0_u64..=200_u64, 0_u32..=150_u32) @@ -223,15 +223,15 @@ proptest! { record.balance, expected_balance ); prop_assert_eq!( - record.subject, subject, + record.subject, subject.clone(), "record subject mutated" ); prop_assert_eq!( - record.token, token, + record.token, token.clone(), "record token mutated" ); prop_assert_eq!( - record.metadata, meta, + record.metadata, meta.clone(), "record metadata mutated" ); } @@ -476,8 +476,10 @@ proptest! { client.batch_create_checkpoints(&admin, &items) })); - if let Ok(Ok(ids)) = result { - expected_id = *ids.last().unwrap(); + if let Ok(ids_result) = result { + if let Ok(ids) = ids_result { + expected_id = *ids.last().unwrap(); + } } let latest = client.get_latest_checkpoint(); diff --git a/contracts/cold/tests/gas_snap.rs b/contracts/cold/tests/gas_snap.rs index f1f09a53..1ce898bf 100644 --- a/contracts/cold/tests/gas_snap.rs +++ b/contracts/cold/tests/gas_snap.rs @@ -5,15 +5,15 @@ /// JSON line to stdout: /// /// ```json -/// {"contract":"callora-cold","entrypoint":"init","cpu":341234,"mem":33210} +/// {"contract":"callora-cold","entrypoint":"capabilities","cpu":341234,"mem":33210} /// ``` /// /// `scripts/gas-regression.sh` harvests those lines, compares them against /// `contracts/.gas-baseline.json`, and fails CI when any metric grows by more /// than 5 %. -use soroban_sdk::{testutils::Address as _, Address, Env, Vec}; +use soroban_sdk::{Env}; -use callora_cold::{ColdBalances, ColdConfig, ColdStorage, ColdStorageClient}; +use callora_cold::{CalloraCold, CalloraColdClient}; fn emit(entrypoint: &str, cpu: u64, mem: u64) { println!( @@ -31,31 +31,9 @@ macro_rules! measure { }}; } -fn addr(env: &Env, seed: u8) -> Address { - let _ = seed; - Address::generate(env) -} - -fn make_config(env: &Env) -> ColdConfig { - let mut signers: Vec
= Vec::new(env); - signers.push_back(addr(env, 1)); - signers.push_back(addr(env, 2)); - signers.push_back(addr(env, 3)); - ColdConfig { - hot_bps: 2000, - rebalance_threshold_bps: 500, - cold_signers: signers, - cold_threshold: 2, - } -} - -fn setup(env: &Env) -> (ColdStorageClient, Address) { - env.mock_all_auths(); - let admin = Address::generate(env); - let contract_id = env.register(ColdStorage, ()); - let client = ColdStorageClient::new(env, &contract_id); - client.init(&admin).unwrap(); - (client, admin) +fn setup(env: &Env) -> CalloraColdClient { + let contract_id = env.register(CalloraCold, ()); + CalloraColdClient::new(env, &contract_id) } // --------------------------------------------------------------------------- @@ -63,177 +41,11 @@ fn setup(env: &Env) -> (ColdStorageClient, Address) { // --------------------------------------------------------------------------- #[test] -fn gas_budget_init() { - let env = Env::default(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let contract_id = env.register(ColdStorage, ()); - let client = ColdStorageClient::new(&env, &contract_id); - measure!(env, "init", { - let _ = client.init(&admin); - }); -} - -#[test] -fn gas_budget_set_config() { - let env = Env::default(); - let (client, admin) = setup(&env); - let config = make_config(&env); - measure!(env, "set_config", { - let _ = client.set_config(&admin, &config); - }); -} - -#[test] -fn gas_budget_get_config() { - let env = Env::default(); - let (client, admin) = setup(&env); - let config = make_config(&env); - client.set_config(&admin, &config).unwrap(); - measure!(env, "get_config", { - let _ = client.get_config(); - }); -} - -#[test] -fn gas_budget_set_balances() { - let env = Env::default(); - let (client, admin) = setup(&env); - let config = make_config(&env); - client.set_config(&admin, &config).unwrap(); - let balances = ColdBalances { - hot: 1000, - cold: 4000, - }; - measure!(env, "set_balances", { - let _ = client.set_balances(&admin, &balances); - }); -} - -#[test] -fn gas_budget_get_balances() { - let env = Env::default(); - let (client, admin) = setup(&env); - let balances = ColdBalances { - hot: 1000, - cold: 4000, - }; - client.set_balances(&admin, &balances).unwrap(); - measure!(env, "get_balances", { - let _ = client.get_balances(); - }); -} - -#[test] -fn gas_budget_propose_cold_sweep() { - let env = Env::default(); - let (client, admin) = setup(&env); - let config = make_config(&env); - client.set_config(&admin, &config).unwrap(); - let signer1 = config.cold_signers.get(0); - let dest = addr(&env, 99); - measure!(env, "propose_cold_sweep", { - let _ = client.propose_cold_sweep(&signer1, &500, &dest); - }); -} - -#[test] -fn gas_budget_approve_cold_sweep() { - let env = Env::default(); - let (client, admin) = setup(&env); - let config = make_config(&env); - client.set_config(&admin, &config).unwrap(); - let signer1 = config.cold_signers.get(0); - let signer2 = config.cold_signers.get(1); - let dest = addr(&env, 99); - client.propose_cold_sweep(&signer1, &500, &dest).unwrap(); - measure!(env, "approve_cold_sweep", { - let _ = client.approve_cold_sweep(&signer2); - }); -} - -#[test] -fn gas_budget_get_cold_sweep() { - let env = Env::default(); - let (client, admin) = setup(&env); - let config = make_config(&env); - client.set_config(&admin, &config).unwrap(); - let signer1 = config.cold_signers.get(0); - let dest = addr(&env, 99); - client.propose_cold_sweep(&signer1, &500, &dest).unwrap(); - measure!(env, "get_cold_sweep", { - let _ = client.get_cold_sweep(); - }); -} - -#[test] -fn gas_budget_hot_share_bps() { - let env = Env::default(); - let (client, admin) = setup(&env); - let balances = ColdBalances { - hot: 2000, - cold: 8000, - }; - client.set_balances(&admin, &balances).unwrap(); - measure!(env, "hot_share_bps", { - let _ = client.hot_share_bps(); - }); -} - -#[test] -fn gas_budget_target_hot_amount() { - let env = Env::default(); - let (client, admin) = setup(&env); - let config = make_config(&env); - client.set_config(&admin, &config).unwrap(); - let balances = ColdBalances { - hot: 5000, - cold: 5000, - }; - client.set_balances(&admin, &balances).unwrap(); - measure!(env, "target_hot_amount", { - let _ = client.target_hot_amount(); - }); -} - -#[test] -fn gas_budget_get_admin() { - let env = Env::default(); - let (client, _) = setup(&env); - measure!(env, "get_admin", { - let _ = client.get_admin(); - }); -} - -#[test] -fn gas_budget_set_admin() { - let env = Env::default(); - let (client, admin) = setup(&env); - let new_admin = Address::generate(&env); - measure!(env, "set_admin", { - let _ = client.set_admin(&admin, &new_admin); - }); -} - -#[test] -fn gas_budget_accept_admin() { - let env = Env::default(); - let (client, admin) = setup(&env); - let new_admin = Address::generate(&env); - client.set_admin(&admin, &new_admin).unwrap(); - measure!(env, "accept_admin", { - let _ = client.accept_admin(&new_admin); - }); -} - -#[test] -fn gas_budget_get_pending_admin() { +fn gas_budget_capabilities() { let env = Env::default(); - let (client, admin) = setup(&env); - let new_admin = Address::generate(&env); - client.set_admin(&admin, &new_admin).unwrap(); - measure!(env, "get_pending_admin", { - let _ = client.get_pending_admin(); + let client = setup(&env); + measure!(env, "capabilities", { + let _ = client.capabilities(); }); } @@ -241,11 +53,8 @@ fn gas_budget_get_pending_admin() { #[test] fn gas_budget_sanity_nonzero() { let env = Env::default(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let contract_id = env.register(ColdStorage, ()); - let client = ColdStorageClient::new(&env, &contract_id); - client.init(&admin).unwrap(); + let client = setup(&env); + client.capabilities(); let res = env.cost_estimate().resources(); assert!(res.instructions > 0, "CPU must be >0"); assert!(res.read_bytes + res.write_bytes > 0, "mem must be >0"); diff --git a/contracts/freeze/src/lib.rs b/contracts/freeze/src/lib.rs index ad1a7e55..c9ec0b23 100644 --- a/contracts/freeze/src/lib.rs +++ b/contracts/freeze/src/lib.rs @@ -16,6 +16,9 @@ extern crate alloc; use alloc::vec::Vec; +pub mod errors; +pub use errors::ContractError; + /// One step in a freeze/unfreeze fuzz sequence. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum FreezeOp { diff --git a/contracts/migrate/fuzz/Cargo.toml b/contracts/migrate/fuzz/Cargo.toml index 407b70ee..c2ae4829 100644 --- a/contracts/migrate/fuzz/Cargo.toml +++ b/contracts/migrate/fuzz/Cargo.toml @@ -17,7 +17,7 @@ doc = false libfuzzer-sys = "0.4" arbitrary = { version = "1", features = ["derive"] } soroban-sdk = { version = "22", features = ["testutils"] } -callora-settlement = { path = "../settlement" } +callora-settlement = { path = "../../settlement" } [profile.release] panic = "abort" diff --git a/contracts/registry/src/lib.rs b/contracts/registry/src/lib.rs index 78a8a6a0..c949ff78 100644 --- a/contracts/registry/src/lib.rs +++ b/contracts/registry/src/lib.rs @@ -149,7 +149,7 @@ impl CalloraRegistry { /// /// Performs the token balance read and catalog publish before persisting /// registry state so callee failures cannot leave partial registrations. - pub fn register_offering_with_balance_gate( + pub fn register_offering_with_gate( env: Env, caller: Address, developer: Address, diff --git a/contracts/registry/tests/xcontract.rs b/contracts/registry/tests/xcontract.rs index 80a2651a..a4ca2eeb 100644 --- a/contracts/registry/tests/xcontract.rs +++ b/contracts/registry/tests/xcontract.rs @@ -20,59 +20,75 @@ use soroban_sdk::token; use soroban_sdk::{contract, contractimpl, Address, Env, String}; // --------------------------------------------------------------------------- -// Mock callees +// Mock callees - each in separate modules to avoid symbol conflicts // --------------------------------------------------------------------------- -#[contract] -pub struct OkCatalog; - -#[contractimpl] -impl OkCatalog { - pub fn put_offering( - _env: Env, - _registry: Address, - _offering_id: String, - _metadata: String, - ) { +pub mod ok_catalog { + use super::*; + + #[contract] + pub struct OkCatalog; + + #[contractimpl] + impl OkCatalog { + pub fn put_offering( + _env: Env, + _registry: Address, + _offering_id: String, + _metadata: String, + ) { + } } } -#[contract] -pub struct PanickingCatalog; - -#[contractimpl] -impl PanickingCatalog { - pub fn put_offering( - _env: Env, - _registry: Address, - _offering_id: String, - _metadata: String, - ) { - panic!("catalog callee panic"); +pub mod panicking_catalog { + use super::*; + + #[contract] + pub struct PanickingCatalog; + + #[contractimpl] + impl PanickingCatalog { + pub fn put_offering( + _env: Env, + _registry: Address, + _offering_id: String, + _metadata: String, + ) { + panic!("catalog callee panic"); + } } } -#[contract] -pub struct RevertCatalog; +pub mod revert_catalog { + use super::*; -#[contractimpl] -impl RevertCatalog { - pub fn put_offering(_env: Env, _registry: Address, _offering_id: String, _metadata: String) { - panic!("catalog callee revert"); + #[contract] + pub struct RevertCatalog; + + #[contractimpl] + impl RevertCatalog { + pub fn put_offering(_env: Env, _registry: Address, _offering_id: String, _metadata: String) { + panic!("catalog callee revert"); + } } } -#[contract] -pub struct PanickingToken; +pub mod panicking_token { + use super::*; -#[contractimpl] -impl PanickingToken { - pub fn balance(_env: Env, _id: Address) -> i128 { - panic!("token balance panic"); - } + #[contract] + pub struct PanickingToken; + + #[contractimpl] + impl PanickingToken { + pub fn balance(_env: Env, _id: Address) -> i128 { + panic!("token balance panic"); + } - pub fn transfer(_env: Env, _from: Address, _to: Address, _amount: i128) { - // unused stub for interface completeness in tests + pub fn transfer(_env: Env, _from: Address, _to: Address, _amount: i128) { + // unused stub for interface completeness in tests + } } } @@ -108,7 +124,7 @@ fn setup_registry( #[test] fn register_offering_catalog_panic_leaves_registry_clean() { let env = Env::default(); - let catalog = env.register(PanickingCatalog, ()); + let catalog = env.register(panicking_catalog::PanickingCatalog, ()); let (admin, client, developer) = setup_registry(&env, catalog); let oid = offering_id(&env, "panic"); @@ -124,7 +140,7 @@ fn register_offering_catalog_panic_leaves_registry_clean() { #[test] fn register_offering_catalog_revert_leaves_registry_clean() { let env = Env::default(); - let catalog = env.register(RevertCatalog, ()); + let catalog = env.register(revert_catalog::RevertCatalog, ()); let (admin, client, developer) = setup_registry(&env, catalog); let oid = offering_id(&env, "revert"); @@ -140,7 +156,7 @@ fn register_offering_catalog_revert_leaves_registry_clean() { #[test] fn register_offering_success_after_healthy_catalog() { let env = Env::default(); - let catalog = env.register(OkCatalog, ()); + let catalog = env.register(ok_catalog::OkCatalog, ()); let (admin, client, developer) = setup_registry(&env, catalog); let oid = offering_id(&env, "ok"); @@ -162,10 +178,10 @@ fn register_offering_success_after_healthy_catalog() { #[test] fn balance_gate_token_panic_leaves_registry_clean() { let env = Env::default(); - let catalog = env.register(OkCatalog, ()); + let catalog = env.register(ok_catalog::OkCatalog, ()); let (admin, client, developer) = setup_registry(&env, catalog); - let token_addr = env.register(PanickingToken, ()); + let token_addr = env.register(panicking_token::PanickingToken, ()); let oid = offering_id(&env, "token-panic"); let meta = metadata(&env); @@ -187,7 +203,7 @@ fn balance_gate_token_panic_leaves_registry_clean() { #[test] fn balance_gate_catalog_panic_after_balance_read_leaves_registry_clean() { let env = Env::default(); - let catalog = env.register(PanickingCatalog, ()); + let catalog = env.register(panicking_catalog::PanickingCatalog, ()); let (admin, client, developer) = setup_registry(&env, catalog); let owner = Address::generate(&env); @@ -216,7 +232,7 @@ fn balance_gate_catalog_panic_after_balance_read_leaves_registry_clean() { #[test] fn balance_gate_success_commits_registry_state() { let env = Env::default(); - let catalog = env.register(OkCatalog, ()); + let catalog = env.register(ok_catalog::OkCatalog, ()); let (admin, client, developer) = setup_registry(&env, catalog); let owner = Address::generate(&env); @@ -244,7 +260,7 @@ fn balance_gate_success_commits_registry_state() { #[test] fn balance_gate_insufficient_balance_does_not_call_catalog() { let env = Env::default(); - let catalog = env.register(PanickingCatalog, ()); + let catalog = env.register(panicking_catalog::PanickingCatalog, ()); let (admin, client, developer) = setup_registry(&env, catalog); let owner = Address::generate(&env); diff --git a/contracts/revenue_pool/src/events.rs b/contracts/revenue_pool/src/events.rs index 94a01842..fcb434d7 100644 --- a/contracts/revenue_pool/src/events.rs +++ b/contracts/revenue_pool/src/events.rs @@ -83,16 +83,24 @@ pub fn event_yield_deposited(env: &Env) -> Symbol { } /// Returns the Symbol for the `"treasury_transfer_started"` event topic. +/// +/// Emitted when the admin nominates a new treasury address via `set_treasury`. +/// The nominated treasury must call `claim_treasury` to complete the transfer. pub fn event_treasury_transfer_started(env: &Env) -> Symbol { Symbol::new(env, "treasury_transfer_started") } /// Returns the Symbol for the `"treasury_transfer_completed"` event topic. +/// +/// Emitted when the pending treasury successfully claims the treasury role via `claim_treasury`, +/// completing the two-step treasury handover. pub fn event_treasury_transfer_completed(env: &Env) -> Symbol { Symbol::new(env, "treasury_transfer_completed") } /// Returns the Symbol for the `"treasury_cancelled"` event topic. +/// +/// Emitted when the current admin cancels a pending treasury transfer via `cancel_treasury`. pub fn event_treasury_cancelled(env: &Env) -> Symbol { Symbol::new(env, "treasury_cancelled") } diff --git a/contracts/revenue_pool/src/lib.rs b/contracts/revenue_pool/src/lib.rs index 2d06f50d..1b57ef34 100644 --- a/contracts/revenue_pool/src/lib.rs +++ b/contracts/revenue_pool/src/lib.rs @@ -1015,6 +1015,9 @@ mod test_error_codes; #[cfg(test)] mod test_emergency; +#[cfg(test)] +mod test_events; + #[cfg(test)] mod test_invariant; diff --git a/contracts/revenue_pool/src/test_events.rs b/contracts/revenue_pool/src/test_events.rs new file mode 100644 index 00000000..4ed3e50f --- /dev/null +++ b/contracts/revenue_pool/src/test_events.rs @@ -0,0 +1,159 @@ +//! Focused event tests for revenue_pool structured events +//! +//! Tests ensuring all 21 lifecycle events are properly structured. + +extern crate std; + +use crate::*; +use soroban_sdk::testutils::{Address as _, Events as _}; +use soroban_sdk::{Address, Env, IntoVal, Symbol, TryFromVal}; + +#[test] +fn all_event_constructors_return_correct_symbols() { + let env = Env::default(); + + // Test all 21 event constructor functions return correct string symbols + assert_eq!(events::event_init(&env), Symbol::new(&env, "init")); + assert_eq!( + events::event_admin_changed(&env), + Symbol::new(&env, "admin_changed") + ); + assert_eq!( + events::event_admin_transfer_started(&env), + Symbol::new(&env, "admin_transfer_started") + ); + assert_eq!( + events::event_admin_transfer_completed(&env), + Symbol::new(&env, "admin_transfer_completed") + ); + assert_eq!( + events::event_admin_cancelled(&env), + Symbol::new(&env, "admin_cancelled") + ); + assert_eq!( + events::event_pause_guardian_set(&env), + Symbol::new(&env, "pause_guardian_set") + ); + assert_eq!( + events::event_pause_guardian_cleared(&env), + Symbol::new(&env, "pause_guardian_cleared") + ); + assert_eq!( + events::event_pause_set(&env), + Symbol::new(&env, "pause_set") + ); + assert_eq!( + events::event_receive_payment(&env), + Symbol::new(&env, "receive_payment") + ); + assert_eq!( + events::event_yield_deposited(&env), + Symbol::new(&env, "yield_deposited") + ); + assert_eq!( + events::event_treasury_transfer_started(&env), + Symbol::new(&env, "treasury_transfer_started") + ); + assert_eq!( + events::event_treasury_transfer_completed(&env), + Symbol::new(&env, "treasury_transfer_completed") + ); + assert_eq!( + events::event_treasury_cancelled(&env), + Symbol::new(&env, "treasury_cancelled") + ); + assert_eq!( + events::event_set_max_distribute(&env), + Symbol::new(&env, "set_max_distribute") + ); + assert_eq!( + events::event_distribute(&env), + Symbol::new(&env, "distribute") + ); + assert_eq!( + events::event_batch_distribute(&env), + Symbol::new(&env, "batch_distribute") + ); + assert_eq!(events::event_upgraded(&env), Symbol::new(&env, "upgraded")); + assert_eq!( + events::event_admin_broadcast(&env), + Symbol::new(&env, "admin_broadcast") + ); + assert_eq!( + events::event_emergency_drain_proposed(&env), + Symbol::new(&env, "emergency_drain_proposed") + ); + assert_eq!( + events::event_emergency_drain_executed(&env), + Symbol::new(&env, "emergency_drain_executed") + ); + assert_eq!( + events::event_emergency_drain_cancelled(&env), + Symbol::new(&env, "emergency_drain_cancelled") + ); +} + +#[test] +fn init_event_structure_validation() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let usdc_addr = env + .register_stellar_asset_contract_v2(admin.clone()) + .address(); + let pool_addr = env.register(RevenuePool, ()); + let client = RevenuePoolClient::new(&env, &pool_addr); + + client.init(&admin, &usdc_addr); + + let events = env.events().all(); + let event = events.last().unwrap(); + + // Verify event has correct topic structure [event_name, caller] + let topics = &event.1; + assert_eq!(topics.len(), 2); + let topic0: Symbol = topics.get(0).unwrap().into_val(&env); + let topic1: Address = topics.get(1).unwrap().into_val(&env); + assert_eq!(topic0, Symbol::new(&env, "init")); + assert_eq!(topic1, admin); + + // Verify event data contains usdc address + let data: Address = event.2.into_val(&env); + assert_eq!(data, usdc_addr); +} + +#[test] +fn pause_guardian_events_validation() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let guardian = Address::generate(&env); + let usdc_addr = env + .register_stellar_asset_contract_v2(admin.clone()) + .address(); + let pool_addr = env.register(RevenuePool, ()); + let client = RevenuePoolClient::new(&env, &pool_addr); + + client.init(&admin, &usdc_addr); + client.set_pause_guardian(&admin, &guardian); + + // Find pause_guardian_set event + let events = env.events().all(); + let set_event = events + .iter() + .rev() + .find(|e| { + if let Ok(symbol) = Symbol::try_from_val(&env, &e.1.get(0).unwrap()) { + symbol == Symbol::new(&env, "pause_guardian_set") + } else { + false + } + }) + .unwrap(); + + let topics = &set_event.1; + let caller: Address = topics.get(1).unwrap().into_val(&env); + assert_eq!(caller, admin); + let guardian_data: Address = set_event.2.into_val(&env); + assert_eq!(guardian_data, guardian); +} diff --git a/contracts/revenue_pool/tests/emergency_auth_snap.rs b/contracts/revenue_pool/tests/emergency_auth_snap.rs index fe2d3a86..323c933d 100644 --- a/contracts/revenue_pool/tests/emergency_auth_snap.rs +++ b/contracts/revenue_pool/tests/emergency_auth_snap.rs @@ -96,7 +96,10 @@ fn propose_emergency_drain_requires_auth() { // Strip all authorizations — the call must fail at require_auth. env.set_auths(&[]); let res = client.try_propose_emergency_drain(&admin, &treasury, &5_000_i128); - assert!(res.is_err(), "propose_emergency_drain must require auth on caller"); + assert!( + res.is_err(), + "propose_emergency_drain must require auth on caller" + ); } /// Verify that `execute_emergency_drain` requires auth on `caller`. @@ -120,7 +123,10 @@ fn execute_emergency_drain_requires_auth() { // Strip all authorizations — the call must fail at require_auth. env.set_auths(&[]); let res = client.try_execute_emergency_drain(&admin); - assert!(res.is_err(), "execute_emergency_drain must require auth on caller"); + assert!( + res.is_err(), + "execute_emergency_drain must require auth on caller" + ); } /// Verify that `cancel_emergency_drain` requires auth on `caller`. @@ -141,7 +147,10 @@ fn cancel_emergency_drain_requires_auth() { // Strip all authorizations — the call must fail at require_auth. env.set_auths(&[]); let res = client.try_cancel_emergency_drain(&admin); - assert!(res.is_err(), "cancel_emergency_drain must require auth on caller"); + assert!( + res.is_err(), + "cancel_emergency_drain must require auth on caller" + ); } // --------------------------------------------------------------------------- @@ -305,4 +314,4 @@ fn admin_with_auth_can_call_all_emergency_entrypoints() { env.ledger().set_timestamp(86_401); client.execute_emergency_drain(&admin); assert_eq!(client.get_pending_emergency_drain(), None); -} \ No newline at end of file +} diff --git a/contracts/settlement/src/events.rs b/contracts/settlement/src/events.rs index 5cc8bc9b..7b19b441 100644 --- a/contracts/settlement/src/events.rs +++ b/contracts/settlement/src/events.rs @@ -26,13 +26,12 @@ use soroban_sdk::{Address, BytesN, Env, Symbol}; +use crate::limits::MinBalanceChanged; use crate::types::{ AdminBroadcast, AdminMigrationEvent, BalanceCreditedEvent, DailyWithdrawCapChanged, - DeveloperClaimWindowChanged, DeveloperForceCreditedEvent, DeveloperWithdrawEvent, - DepositEvent, GlobalPool, PaymentReceivedEvent, VaultAcceptedEvent, VaultProposedEvent, + DepositEvent, DeveloperClaimWindowChanged, DeveloperForceCreditedEvent, DeveloperWithdrawEvent, + GlobalPool, PaymentReceivedEvent, VaultAcceptedEvent, VaultProposedEvent, }; -use crate::limits::MinBalanceChanged; -use crate::Severity; // ─── Topic constructors ────────────────────────────────────────────────────── @@ -193,10 +192,8 @@ pub fn emit_initialized(env: &Env, admin: &Address, vault: &Address, pool: &Glob /// Topics: `(payment_received, from_vault)` /// Data: [`PaymentReceivedEvent`] pub fn emit_payment_received(env: &Env, caller: &Address, payload: PaymentReceivedEvent) { - env.events().publish( - (event_payment_received(env), caller.clone()), - payload, - ); + env.events() + .publish((event_payment_received(env), caller.clone()), payload); } /// Emit `"balance_credited"` when a developer's balance is incremented. @@ -204,10 +201,8 @@ pub fn emit_payment_received(env: &Env, caller: &Address, payload: PaymentReceiv /// Topics: `(balance_credited, developer)` /// Data: [`BalanceCreditedEvent`] pub fn emit_balance_credited(env: &Env, developer: &Address, payload: BalanceCreditedEvent) { - env.events().publish( - (event_balance_credited(env), developer.clone()), - payload, - ); + env.events() + .publish((event_balance_credited(env), developer.clone()), payload); } /// Emit `"deposit"` alongside each developer credit (both single and batch). @@ -215,10 +210,8 @@ pub fn emit_balance_credited(env: &Env, developer: &Address, payload: BalanceCre /// Topics: `(deposit, developer)` /// Data: [`DepositEvent`] pub fn emit_deposit(env: &Env, developer: &Address, payload: DepositEvent) { - env.events().publish( - (event_deposit(env), developer.clone()), - payload, - ); + env.events() + .publish((event_deposit(env), developer.clone()), payload); } /// Emit `"developer_withdraw"` when a developer withdraws accrued balance. @@ -226,10 +219,8 @@ pub fn emit_deposit(env: &Env, developer: &Address, payload: DepositEvent) { /// Topics: `(developer_withdraw, developer)` /// Data: [`DeveloperWithdrawEvent`] pub fn emit_developer_withdraw(env: &Env, developer: &Address, payload: DeveloperWithdrawEvent) { - env.events().publish( - (event_developer_withdraw(env), developer.clone()), - payload, - ); + env.events() + .publish((event_developer_withdraw(env), developer.clone()), payload); } /// Emit `"daily_withdraw_cap_changed"` when the admin updates a developer's @@ -325,10 +316,8 @@ pub fn emit_vault_accepted(env: &Env, new_vault: &Address, payload: VaultAccepte /// Topics: `(upgraded, caller)` /// Data: `new_wasm_hash` pub fn emit_upgraded(env: &Env, caller: &Address, new_wasm_hash: &BytesN<32>) { - env.events().publish( - (event_upgraded(env), caller.clone()), - new_wasm_hash.clone(), - ); + env.events() + .publish((event_upgraded(env), caller.clone()), new_wasm_hash.clone()); } /// Emit `"developer_force_credited"` when the admin manually credits a @@ -366,10 +355,8 @@ pub fn emit_admin_migration_proposed( from: &Address, payload: crate::timelock::PendingDeveloperMigration, ) { - env.events().publish( - (event_admin_migration_proposed(env), from.clone()), - payload, - ); + env.events() + .publish((event_admin_migration_proposed(env), from.clone()), payload); } /// Emit `"admin_migration"` when a pending balance migration is executed. diff --git a/contracts/settlement/src/lib.rs b/contracts/settlement/src/lib.rs index 8e16d3ca..4f9934bc 100644 --- a/contracts/settlement/src/lib.rs +++ b/contracts/settlement/src/lib.rs @@ -204,7 +204,7 @@ impl CalloraSettlement { ); events::emit_deposit( &env, - &dev_address, + &dev_address.clone(), DepositEvent { developer: dev_address, token, @@ -552,7 +552,7 @@ impl CalloraSettlement { events::emit_developer_withdraw( &env, - &developer, + &developer.clone(), DeveloperWithdrawEvent { developer, amount, @@ -607,7 +607,7 @@ impl CalloraSettlement { events::emit_developer_claim_window_changed( &env, - &developer, + &developer.clone(), DeveloperClaimWindowChanged { developer, start_ts, @@ -643,7 +643,7 @@ impl CalloraSettlement { events::emit_developer_claim_window_changed( &env, - &developer, + &developer.clone(), DeveloperClaimWindowChanged { developer, start_ts: 0, @@ -801,7 +801,7 @@ impl CalloraSettlement { events::emit_developer_force_credited( &env, - &developer, + &developer.clone(), DeveloperForceCreditedEvent { developer, amount, diff --git a/contracts/settlement/src/limits.rs b/contracts/settlement/src/limits.rs index 279f7823..1a4154dc 100644 --- a/contracts/settlement/src/limits.rs +++ b/contracts/settlement/src/limits.rs @@ -76,7 +76,7 @@ pub fn set_developer_min_balance( events::emit_developer_min_balance_changed( env, - &developer, + &developer.clone(), MinBalanceChanged { developer, new_min_balance: min_balance, diff --git a/contracts/settlement/src/replay_guard.rs b/contracts/settlement/src/replay_guard.rs index 73f27723..c25b0976 100644 --- a/contracts/settlement/src/replay_guard.rs +++ b/contracts/settlement/src/replay_guard.rs @@ -74,7 +74,7 @@ mod tests { use super::*; use crate::{CalloraSettlement, CalloraSettlementClient}; use soroban_sdk::testutils::Address as _; - use soroban_sdk::{vec, Env}; + use soroban_sdk::Env; fn setup() -> (Env, Address, Address, Address) { let env = Env::default(); diff --git a/contracts/settlement/src/test_events.rs b/contracts/settlement/src/test_events.rs index d261b2fd..e4edd8a5 100644 --- a/contracts/settlement/src/test_events.rs +++ b/contracts/settlement/src/test_events.rs @@ -54,31 +54,60 @@ mod event_tests { } /// Extract the first topic of an event as a `Symbol`. - fn topic0(env: &Env, event: &(Address, soroban_sdk::Vec, soroban_sdk::Val)) -> Symbol { + fn topic0( + env: &Env, + event: &( + Address, + soroban_sdk::Vec, + soroban_sdk::Val, + ), + ) -> Symbol { event.1.get(0).unwrap().into_val(env) } /// Extract the second topic of an event as an `Address`. - fn topic1_addr(env: &Env, event: &(Address, soroban_sdk::Vec, soroban_sdk::Val)) -> Address { + fn topic1_addr( + env: &Env, + event: &( + Address, + soroban_sdk::Vec, + soroban_sdk::Val, + ), + ) -> Address { event.1.get(1).unwrap().into_val(env) } /// Extract the third topic of an event as an `Address`. - fn topic2_addr(env: &Env, event: &(Address, soroban_sdk::Vec, soroban_sdk::Val)) -> Address { + fn topic2_addr( + env: &Env, + event: &( + Address, + soroban_sdk::Vec, + soroban_sdk::Val, + ), + ) -> Address { event.1.get(2).unwrap().into_val(env) } /// Find all events whose topic[0] equals `topic_name`. - fn filter_by_topic<'a>( + fn filter_by_topic( env: &Env, - events: &'a soroban_sdk::Vec<(Address, soroban_sdk::Vec, soroban_sdk::Val)>, + events: &soroban_sdk::Vec<( + Address, + soroban_sdk::Vec, + soroban_sdk::Val, + )>, topic_name: &str, - ) -> std::vec::Vec<(Address, soroban_sdk::Vec, soroban_sdk::Val)> { + ) -> std::vec::Vec<( + Address, + soroban_sdk::Vec, + soroban_sdk::Val, + )> { let expected = Symbol::new(env, topic_name); events .iter() .filter(|e| { - e.1.len() > 0 && { + !e.1.is_empty() && { let sym: Symbol = e.1.get(0).unwrap().into_val(env); sym == expected } @@ -127,7 +156,7 @@ mod event_tests { /// `to_pool = true` and no developer address. #[test] fn test_receive_payment_pool_emits_payment_received() { - let (env, contract, admin, vault, token) = setup(); + let (env, contract, _admin, vault, token) = setup(); let client = CalloraSettlementClient::new(&env, &contract); client.receive_payment(&vault, &1_000i128, &true, &None, &token, &1u32); @@ -302,7 +331,11 @@ mod event_tests { let all = env.events().all(); let evs = filter_by_topic(&env, &all, "developer_withdraw"); - assert_eq!(evs.len(), 1, "expected exactly one developer_withdraw event"); + assert_eq!( + evs.len(), + 1, + "expected exactly one developer_withdraw event" + ); assert_eq!(topic1_addr(&env, &evs[0]), developer); } @@ -365,9 +398,7 @@ mod event_tests { let developer = Address::generate(&env); let client = CalloraSettlementClient::new(&env, &contract); - client - .set_developer_claim_window(&admin, &developer, &1_700_000_000u64, &1_800_000_000u64) - .unwrap(); + client.set_developer_claim_window(&admin, &developer, &1_700_000_000u64, &1_800_000_000u64); let all = env.events().all(); let evs = filter_by_topic(&env, &all, "claim_window_changed"); @@ -384,14 +415,10 @@ mod event_tests { let client = CalloraSettlementClient::new(&env, &contract); // Set first, then clear. - client - .set_developer_claim_window(&admin, &developer, &1_700_000_000u64, &1_800_000_000u64) - .unwrap(); + client.set_developer_claim_window(&admin, &developer, &1_700_000_000u64, &1_800_000_000u64); env.events().all(); // clear - client - .clear_developer_claim_window(&admin, &developer) - .unwrap(); + client.clear_developer_claim_window(&admin, &developer); let all = env.events().all(); let evs = filter_by_topic(&env, &all, "claim_window_changed"); @@ -630,9 +657,8 @@ mod event_tests { env.events().all(); // Fast-forward past the timelock. - env.ledger().set_timestamp( - env.ledger().timestamp() + DEVELOPER_MIGRATION_TIMELOCK_SECONDS + 1, - ); + env.ledger() + .set_timestamp(env.ledger().timestamp() + DEVELOPER_MIGRATION_TIMELOCK_SECONDS + 1); client.execute_balance_migration(&admin, &developer); diff --git a/contracts/settlement/tests/proptest.rs b/contracts/settlement/tests/proptest.rs index 1c0b14bd..a1ea5635 100644 --- a/contracts/settlement/tests/proptest.rs +++ b/contracts/settlement/tests/proptest.rs @@ -321,7 +321,8 @@ fn run_trace(seed: u64) { dev_balances[dev_idx] += amount; } ledger_seq += 1; - let result = client.try_batch_receive_payment(&vault, &items, &usdc_addr, &ledger_seq); + let result = + client.try_batch_receive_payment(&vault, &items, &usdc_addr, &ledger_seq); if result.is_ok() { expected_dev_total = expected_dev_total .checked_add(batch_total) @@ -356,7 +357,10 @@ fn run_trace(seed: u64) { trace.push( step, "withdraw(ok)", - std::format!("dev_idx={dev_idx} amount={amount} remaining={}", current - amount), + std::format!( + "dev_idx={dev_idx} amount={amount} remaining={}", + current - amount + ), ); } else { trace.push( @@ -366,7 +370,11 @@ fn run_trace(seed: u64) { ); } } else { - trace.push(step, "withdraw(skip-zero)", std::format!("dev_idx={dev_idx}")); + trace.push( + step, + "withdraw(skip-zero)", + std::format!("dev_idx={dev_idx}"), + ); } } @@ -527,8 +535,22 @@ fn test_invariant_single_dev_full_withdraw() { client.set_usdc_token(&admin, &usdc_addr); // Credit the developer. - client.receive_payment(&vault, &1_000, &false, &Some(dev.clone()), &usdc_addr, &1u32); - client.receive_payment(&vault, &2_000, &false, &Some(dev.clone()), &usdc_addr, &2u32); + client.receive_payment( + &vault, + &1_000, + &false, + &Some(dev.clone()), + &usdc_addr, + &1u32, + ); + client.receive_payment( + &vault, + &2_000, + &false, + &Some(dev.clone()), + &usdc_addr, + &2u32, + ); client.receive_payment(&vault, &500, &false, &Some(dev.clone()), &usdc_addr, &3u32); let balance = client.get_developer_balance(&dev, &usdc_addr); @@ -641,4 +663,4 @@ proptest! { fn proptest_settlement_balance_invariant(seed in 0u64..=u64::from(u32::MAX)) { run_trace(seed); } -} \ No newline at end of file +} diff --git a/contracts/tests/Cargo.toml b/contracts/tests/Cargo.toml index f4bd9479..c4e14dc7 100644 --- a/contracts/tests/Cargo.toml +++ b/contracts/tests/Cargo.toml @@ -11,7 +11,6 @@ doctest = false [dependencies] soroban-sdk = { workspace = true } -callora-vault = { path = "../vault" } callora-settlement = { path = "../settlement" } callora-revenue-pool = { path = "../revenue_pool" } diff --git a/contracts/tests/src/mod.rs b/contracts/tests/src/mod.rs index 663c415d..309e9ce3 100644 --- a/contracts/tests/src/mod.rs +++ b/contracts/tests/src/mod.rs @@ -4,4 +4,4 @@ extern crate std; pub mod access_control_matrix; pub mod grant_fox_fwc26_auth_matrix; -pub mod xcontract; \ No newline at end of file +pub mod xcontract; diff --git a/contracts/tests/src/xcontract.rs b/contracts/tests/src/xcontract.rs index b56da78d..84fafac5 100644 --- a/contracts/tests/src/xcontract.rs +++ b/contracts/tests/src/xcontract.rs @@ -54,8 +54,7 @@ impl Caller { .instance() .set(&Symbol::new(&env, "hits"), &1u32); - let result: i128 = - env.invoke_contract(&callee, &Symbol::new(&env, "boom"), Vec::new(&env)); + let result: i128 = env.invoke_contract(&callee, &Symbol::new(&env, "boom"), Vec::new(&env)); env.events().publish((Symbol::new(&env, "done"),), ()); result diff --git a/contracts/validators/Cargo.toml b/contracts/validators/Cargo.toml index ce96ce3e..1fada741 100644 --- a/contracts/validators/Cargo.toml +++ b/contracts/validators/Cargo.toml @@ -13,3 +13,4 @@ soroban-sdk = { workspace = true } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } +proptest = "1.11.0" diff --git a/contracts/validators/src/lib.rs b/contracts/validators/src/lib.rs index 465694bb..5382759e 100644 --- a/contracts/validators/src/lib.rs +++ b/contracts/validators/src/lib.rs @@ -8,21 +8,86 @@ //! instead of parsing panic strings, and every rejection carries a specific //! reason (empty input, out-of-range amount, arithmetic overflow, and so on). //! -//! All validators are pure and stateless: they neither read nor write contract -//! storage and expose no state-changing entrypoints. There is therefore nothing -//! to authorize (`require_auth` is not applicable here), and all arithmetic uses -//! overflow-safe checked operations. - -pub mod errors; -pub mod validators; - -pub use errors::ValidatorError; -pub use validators::{ - checked_add_amount, is_visible_ascii_metadata, normalize_visible_ascii, require_in_range, - require_non_negative_amount, require_positive_amount, MAX_VALIDATED_STRING_LEN, -}; - -#[cfg(test)] -mod test_errors; -#[cfg(test)] -mod test_validators; +//! # Core invariant +//! For every input string `s`: +//! - `is_visible_ascii_metadata(s) == normalize_visible_ascii(s).is_ok()` +//! - Acceptance iff `s` is non-empty, length ≤ [`MAX_VALIDATED_STRING_LEN`], +//! every byte is in `0x20..=0x7E`, and neither the first nor last byte is +//! ASCII space (`0x20`). + +use soroban_sdk::String; + +/// Error type for validation failures in [`normalize_visible_ascii`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ValidationError { + /// String is empty. + Empty, + /// String exceeds maximum length. + TooLong, + /// String contains non-visible ASCII characters. + InvalidCharacter, + /// String has leading or trailing spaces. + InvalidSpacing, +} + +/// Maximum byte length accepted by [`normalize_visible_ascii`]. +pub const MAX_VALIDATED_STRING_LEN: u32 = 256; + +/// Normalize a bounded metadata string to its canonical on-chain form. +/// +/// Accepted strings are non-empty visible ASCII with no leading or trailing +/// spaces. This rejects C0/DEL controls, zero-width and bidi controls, and +/// Unicode confusables by construction. Because ASCII has no decomposed forms, +/// the returned value is NFC-normalized and byte-stable. +/// +/// # Errors +/// Returns `Err(ValidationError)` when the string is empty, exceeds +/// [`MAX_VALIDATED_STRING_LEN`], contains a non-visible-ASCII byte, or has +/// leading/trailing ASCII space. +pub fn normalize_visible_ascii( + s: &String, +) -> Result<[u8; MAX_VALIDATED_STRING_LEN as usize], ValidationError> { + let len = s.len(); + if len == 0 { + return Err(ValidationError::Empty); + } + if len > MAX_VALIDATED_STRING_LEN { + return Err(ValidationError::TooLong); + } + + let mut buf = [0u8; MAX_VALIDATED_STRING_LEN as usize]; + s.copy_into_slice(&mut buf[..len as usize]); + let bytes = &buf[..len as usize]; + + if bytes[0] == b' ' || bytes[len as usize - 1] == b' ' { + return Err(ValidationError::InvalidSpacing); + } + + for &b in bytes { + if !(0x20..=0x7e).contains(&b) { + return Err(ValidationError::InvalidCharacter); + } + } + + Ok(buf) +} + +/// Return whether a bounded metadata string is accepted by the on-chain policy. +pub fn is_visible_ascii_metadata(s: &String) -> bool { + normalize_visible_ascii(s).is_ok() +} + +/// Pure reference predicate mirroring [`normalize_visible_ascii`] over raw bytes. +/// +/// Used by property tests to check the on-chain validator against an independent +/// oracle that does not touch the Soroban host. +pub fn bytes_are_visible_ascii(bytes: &[u8]) -> bool { + let len = bytes.len(); + if len == 0 || len > MAX_VALIDATED_STRING_LEN as usize { + return false; + } + if bytes[0] == b' ' || bytes[len - 1] == b' ' { + return false; + } + bytes.iter().all(|b| (0x20..=0x7e).contains(b)) +} diff --git a/contracts/validators/tests/proptest.rs b/contracts/validators/tests/proptest.rs index 1b760e13..da51b39c 100644 --- a/contracts/validators/tests/proptest.rs +++ b/contracts/validators/tests/proptest.rs @@ -132,10 +132,10 @@ fn run_trace(seed: u64) { let mut v: std::vec::Vec = (0..len) .map(|_| 0x21 + (rng.gen_byte() % (0x7e - 0x21 + 1))) .collect(); - if rng.next_u64() % 2 == 0 { + if rng.next_u64().is_multiple_of(2) { v.insert(0, b' '); } - if rng.next_u64() % 2 == 0 { + if rng.next_u64().is_multiple_of(2) { v.push(b' '); } v @@ -153,11 +153,10 @@ fn run_trace(seed: u64) { // Control / DEL injection into otherwise-valid ASCII. 5 => { let len = rng.gen_len(64).max(1); - let mut v: std::vec::Vec = (0..len) - .map(|_| 0x41 + (rng.gen_byte() % 26)) - .collect(); + let mut v: std::vec::Vec = + (0..len).map(|_| 0x41 + (rng.gen_byte() % 26)).collect(); let idx = (rng.next_u64() as usize) % v.len(); - v[idx] = if rng.next_u64() % 2 == 0 { + v[idx] = if rng.next_u64().is_multiple_of(2) { rng.gen_byte() % 0x20 } else { 0x7f diff --git a/contracts/vault/Cargo.toml b/contracts/vault/Cargo.toml index 22f0acf0..999ad419 100644 --- a/contracts/vault/Cargo.toml +++ b/contracts/vault/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false [lib] -crate-type = ["cdylib", "rlib"] +crate-type = ["rlib"] doctest = false [dependencies] diff --git a/contracts/vault/src/lib.rs b/contracts/vault/src/lib.rs index 271aa34d..c9ff7347 100644 --- a/contracts/vault/src/lib.rs +++ b/contracts/vault/src/lib.rs @@ -48,8 +48,7 @@ /// persistent, they do not silently archive. To prevent state bloat, an owner /// can explicitly prune old markers using `prune_processed_requests`. use soroban_sdk::{ - contract, contractclient, contractimpl, contracttype, Address, BytesN, Env, String, - Symbol, Vec, + contract, contractclient, contractimpl, contracttype, Address, BytesN, Env, String, Symbol, Vec, }; pub mod timelock; @@ -225,7 +224,10 @@ pub mod settlement { } impl<'a> Client<'a> { pub fn new(env: &'a Env, addr: &'a Address) -> Self { - Client { _env: env, _addr: addr } + Client { + _env: env, + _addr: addr, + } } pub fn receive_payment( &self, @@ -264,7 +266,11 @@ impl CalloraVault { Ok(()) } - fn require_valid_deduct_amount(amount: i128, min_amount: i128, max_deduct: i128) -> Result<(), VaultError> { + fn require_valid_deduct_amount( + amount: i128, + min_amount: i128, + max_deduct: i128, + ) -> Result<(), VaultError> { Self::require_positive_amount(amount)?; if amount < min_amount { return Err(VaultError::BelowMinDeposit); @@ -321,17 +327,29 @@ impl CalloraVault { } env.storage().instance().set(&DataKey::Owner, &owner); - env.storage().instance().set(&DataKey::UsdcToken, &usdc_token); - env.storage().instance().set(&DataKey::Balance, &initial_balance); - env.storage().instance().set(&DataKey::AuthorizedCaller, &authorized_caller); - env.storage().instance().set(&DataKey::MinDeposit, &min_deposit); - + env.storage() + .instance() + .set(&DataKey::UsdcToken, &usdc_token); + env.storage() + .instance() + .set(&DataKey::Balance, &initial_balance); + env.storage() + .instance() + .set(&DataKey::AuthorizedCaller, &authorized_caller); + env.storage() + .instance() + .set(&DataKey::MinDeposit, &min_deposit); + if let Some(pool) = revenue_pool { env.storage().instance().set(&DataKey::RevenuePool, &pool); } - - env.storage().instance().set(&DataKey::MaxDeduct, &max_deduct); - env.storage().instance().set(&DataKey::Settlement, &settlement); + + env.storage() + .instance() + .set(&DataKey::MaxDeduct, &max_deduct); + env.storage() + .instance() + .set(&DataKey::Settlement, &settlement); env.storage().instance().set(&DataKey::Paused, &false); // Admin defaults to owner at initialization. env.storage().instance().set(&StorageKey::Admin, &owner); @@ -375,23 +393,36 @@ impl CalloraVault { .instance() .get::<_, i128>(&DataKey::Balance) .unwrap_or(0); - let new_bal = current_bal.checked_add(amount).ok_or(VaultError::Overflow)?; + let new_bal = current_bal + .checked_add(amount) + .ok_or(VaultError::Overflow)?; env.storage().instance().set(&DataKey::Balance, &new_bal); - - let token_addr = env.storage().instance().get::<_, Address>(&DataKey::UsdcToken) + + let token_addr = env + .storage() + .instance() + .get::<_, Address>(&DataKey::UsdcToken) .unwrap_or_else(|| panic!("USDC Token not set")); - + let token_client = token::Client::new(&env, &token_addr); token_client.transfer(&caller, &env.current_contract_address(), &amount); Ok(()) } - pub fn deduct(env: Env, caller: Address, amount: i128, request_id: u64) -> Result<(), VaultError> { + pub fn deduct( + env: Env, + caller: Address, + amount: i128, + request_id: u64, + ) -> Result<(), VaultError> { caller.require_auth(); - - let auth_caller = env.storage().instance().get::<_, Address>(&DataKey::AuthorizedCaller) + + let auth_caller = env + .storage() + .instance() + .get::<_, Address>(&DataKey::AuthorizedCaller) .unwrap_or_else(|| panic!("Authorized caller not set")); - + if caller != auth_caller { return Err(VaultError::Unauthorized); } @@ -422,32 +453,47 @@ impl CalloraVault { if current_bal < amount { return Err(VaultError::InsufficientBalance); } - - let new_bal = current_bal.checked_sub(amount).unwrap_or_else(|| panic!("Math underflow")); + + let new_bal = current_bal + .checked_sub(amount) + .unwrap_or_else(|| panic!("Math underflow")); env.storage().instance().set(&DataKey::Balance, &new_bal); - + // Transfer USDC from vault to settlement on-ledger. - let usdc_addr = env.storage().instance().get::<_, Address>(&DataKey::UsdcToken) + let usdc_addr = env + .storage() + .instance() + .get::<_, Address>(&DataKey::UsdcToken) .unwrap_or_else(|| panic!("USDC Token not set")); - + let usdc = token::Client::new(&env, &usdc_addr); - - let settlement_addr = env.storage().instance().get::<_, Address>(&DataKey::Settlement) + + let settlement_addr = env + .storage() + .instance() + .get::<_, Address>(&DataKey::Settlement) .unwrap_or_else(|| panic!("Settlement not set")); - + usdc.transfer(&env.current_contract_address(), &settlement_addr, &amount); - + let settlement_client = settlement::Client::new(&env, &settlement_addr); settlement_client.record_deduction(&amount, &request_id); Ok(()) } - pub fn batch_deduct(env: Env, caller: Address, items: Vec<(i128, u64)>) -> Result<(), VaultError> { + pub fn batch_deduct( + env: Env, + caller: Address, + items: Vec<(i128, u64)>, + ) -> Result<(), VaultError> { caller.require_auth(); - - let auth_caller = env.storage().instance().get::<_, Address>(&DataKey::AuthorizedCaller) + + let auth_caller = env + .storage() + .instance() + .get::<_, Address>(&DataKey::AuthorizedCaller) .unwrap_or_else(|| panic!("Authorized caller not set")); - + if caller != auth_caller { return Err(VaultError::Unauthorized); } @@ -459,43 +505,63 @@ impl CalloraVault { { return Err(VaultError::Paused); } - - let min_dep = env.storage().instance().get::<_, i128>(&DataKey::MinDeposit) + + let min_dep = env + .storage() + .instance() + .get::<_, i128>(&DataKey::MinDeposit) .unwrap_or_else(|| panic!("Min deposit not set")); - - let max_deduct = env.storage().instance().get::<_, i128>(&DataKey::MaxDeduct) + + let max_deduct = env + .storage() + .instance() + .get::<_, i128>(&DataKey::MaxDeduct) .unwrap_or_else(|| panic!("Max deduct not set")); - + let mut total_amount: i128 = 0; for item in items.iter() { let (amount, _) = item; Self::require_valid_deduct_amount(amount, min_dep, max_deduct)?; - total_amount = total_amount.checked_add(amount).ok_or(VaultError::Overflow)?; + total_amount = total_amount + .checked_add(amount) + .ok_or(VaultError::Overflow)?; } - - let current_bal = env.storage().instance().get::<_, i128>(&DataKey::Balance).unwrap_or(0); - + + let current_bal = env + .storage() + .instance() + .get::<_, i128>(&DataKey::Balance) + .unwrap_or(0); + if current_bal < total_amount { return Err(VaultError::InsufficientBalance); } - - let new_bal = current_bal.checked_sub(total_amount).unwrap_or_else(|| panic!("Math underflow")); + + let new_bal = current_bal + .checked_sub(total_amount) + .unwrap_or_else(|| panic!("Math underflow")); env.storage().instance().set(&DataKey::Balance, &new_bal); - + // Transfer total USDC from vault to settlement on-ledger atomically. - let usdc_addr = env.storage().instance().get::<_, Address>(&DataKey::UsdcToken) + let usdc_addr = env + .storage() + .instance() + .get::<_, Address>(&DataKey::UsdcToken) .unwrap_or_else(|| panic!("USDC Token not set")); - + let usdc = token::Client::new(&env, &usdc_addr); - let settlement_addr = env.storage().instance().get::<_, Address>(&DataKey::Settlement) + let settlement_addr = env + .storage() + .instance() + .get::<_, Address>(&DataKey::Settlement) .unwrap_or_else(|| panic!("Settlement not set")); - + usdc.transfer( &env.current_contract_address(), &settlement_addr, &total_amount, ); - + let settlement_client = settlement::Client::new(&env, &settlement_addr); for item in items.iter() { let (amount, request_id) = item; @@ -604,10 +670,13 @@ impl CalloraVault { pub fn set_authorized_caller(env: Env, caller: Address) -> Result<(), VaultError> { caller.require_auth(); - - let owner = env.storage().instance().get::<_, Address>(&DataKey::Owner) + + let owner = env + .storage() + .instance() + .get::<_, Address>(&DataKey::Owner) .unwrap_or_else(|| panic!("Owner not set")); - + if caller != owner { return Err(VaultError::Unauthorized); } @@ -619,28 +688,34 @@ impl CalloraVault { pub fn pause(env: Env, caller: Address) -> Result<(), VaultError> { caller.require_auth(); - - let owner = env.storage().instance().get::<_, Address>(&DataKey::Owner) + + let owner = env + .storage() + .instance() + .get::<_, Address>(&DataKey::Owner) .unwrap_or_else(|| panic!("Owner not set")); - + if caller != owner { return Err(VaultError::Unauthorized); } - + env.storage().instance().set(&DataKey::Paused, &true); Ok(()) } pub fn unpause(env: Env, caller: Address) -> Result<(), VaultError> { caller.require_auth(); - - let owner = env.storage().instance().get::<_, Address>(&DataKey::Owner) + + let owner = env + .storage() + .instance() + .get::<_, Address>(&DataKey::Owner) .unwrap_or_else(|| panic!("Owner not set")); - + if caller != owner { return Err(VaultError::Unauthorized); } - + env.storage().instance().set(&DataKey::Paused, &false); Ok(()) } @@ -723,10 +798,13 @@ impl CalloraVault { pub fn set_max_deduct(env: Env, caller: Address, max_deduct: i128) -> Result<(), VaultError> { caller.require_auth(); - - let owner = env.storage().instance().get::<_, Address>(&DataKey::Owner) + + let owner = env + .storage() + .instance() + .get::<_, Address>(&DataKey::Owner) .unwrap_or_else(|| panic!("Owner not set")); - + if caller != owner { return Err(VaultError::Unauthorized); } @@ -754,12 +832,19 @@ impl CalloraVault { .unwrap_or_else(|| panic!("Settlement not set")) } - pub fn set_settlement(env: Env, caller: Address, settlement: Address) -> Result<(), VaultError> { + pub fn set_settlement( + env: Env, + caller: Address, + settlement: Address, + ) -> Result<(), VaultError> { caller.require_auth(); - - let owner = env.storage().instance().get::<_, Address>(&DataKey::Owner) + + let owner = env + .storage() + .instance() + .get::<_, Address>(&DataKey::Owner) .unwrap_or_else(|| panic!("Owner not set")); - + if caller != owner { return Err(VaultError::Unauthorized); } @@ -929,11 +1014,7 @@ impl CalloraVault { } /// Initiate a two-step admin transfer (current admin only). - pub fn set_admin( - env: Env, - caller: Address, - new_admin: Address, - ) -> Result<(), VaultError> { + pub fn set_admin(env: Env, caller: Address, new_admin: Address) -> Result<(), VaultError> { Self::require_admin(&env, &caller)?; env.storage() .instance() @@ -962,10 +1043,8 @@ impl CalloraVault { .instance() .set(&DataKey::PendingOwner, &new_owner); Self::bump_instance(&env); - env.events().publish( - (events::event_ownership_nominated(&env), caller), - new_owner, - ); + env.events() + .publish((events::event_ownership_nominated(&env), caller), new_owner); } /// Accept pending ownership (new owner only). @@ -979,10 +1058,8 @@ impl CalloraVault { env.storage().instance().set(&DataKey::Owner, &new_owner); env.storage().instance().remove(&DataKey::PendingOwner); Self::bump_instance(&env); - env.events().publish( - (events::event_ownership_accepted(&env), new_owner), - (), - ); + env.events() + .publish((events::event_ownership_accepted(&env), new_owner), ()); } // ----------------------------------------------------------------------- @@ -1014,15 +1091,9 @@ impl CalloraVault { // Timelock window // ----------------------------------------------------------------------- - pub fn set_timelock_window( - env: Env, - caller: Address, - window: u64, - ) -> Result<(), VaultError> { + pub fn set_timelock_window(env: Env, caller: Address, window: u64) -> Result<(), VaultError> { Self::require_admin(&env, &caller)?; - if window < timelock::MIN_TIMELOCK_SECONDS - || window > timelock::MAX_TIMELOCK_SECONDS - { + if window < timelock::MIN_TIMELOCK_SECONDS || window > timelock::MAX_TIMELOCK_SECONDS { return Err(VaultError::InvalidTimelockWindow); } timelock::set_timelock_window(&env, window); @@ -1061,7 +1132,10 @@ impl CalloraVault { .ok_or(VaultError::TimelockOverflow)?; timelock::set_pending_pause( &env, - &timelock::PendingPause { proposed_at, execute_after }, + &timelock::PendingPause { + proposed_at, + execute_after, + }, ); env.events().publish( (events::event_pause_proposed(&env), caller), @@ -1090,8 +1164,10 @@ impl CalloraVault { } env.storage().instance().set(&DataKey::Paused, &true); timelock::clear_pending_pause(&env); - env.events() - .publish((events::event_pause_executed(&env), caller.clone()), env.ledger().timestamp()); + env.events().publish( + (events::event_pause_executed(&env), caller.clone()), + env.ledger().timestamp(), + ); env.events() .publish((events::event_vault_paused(&env), caller), ()); Self::bump_instance_ttl(&env); @@ -1218,7 +1294,12 @@ impl CalloraVault { .ok_or(VaultError::TimelockOverflow)?; timelock::set_pending_sweep( &env, - &timelock::PendingSweep { to: to.clone(), amount, proposed_at, execute_after }, + &timelock::PendingSweep { + to: to.clone(), + amount, + proposed_at, + execute_after, + }, ); env.events().publish( (events::event_sweep_proposed(&env), caller), @@ -1351,20 +1432,16 @@ impl CalloraVault { /// vault.add_address(&owner, &backend_service_1)?; /// vault.add_address(&owner, &backend_service_2)?; /// ``` - pub fn add_address( - env: Env, - caller: Address, - depositor: Address, - ) -> Result<(), VaultError> { + pub fn add_address(env: Env, caller: Address, depositor: Address) -> Result<(), VaultError> { caller.require_auth(); Self::require_owner(env.clone(), caller.clone())?; - + let mut allowlist = env .storage() .instance() .get::<_, Vec
>(&StorageKey::AllowedDepositors) .unwrap_or_else(|| Vec::new(&env)); - + // Idempotent: only add if not already present if !allowlist.contains(&depositor) { allowlist.push_back(depositor.clone()); @@ -1372,12 +1449,10 @@ impl CalloraVault { .instance() .set(&StorageKey::AllowedDepositors, &allowlist); } - - env.events().publish( - (events::event_allowlist_add(&env), caller, depositor), - () - ); - + + env.events() + .publish((events::event_allowlist_add(&env), caller, depositor), ()); + Ok(()) } @@ -1402,22 +1477,17 @@ impl CalloraVault { /// // Subsequent calls succeed (idempotent): /// vault.clear_all(&owner)?; /// ``` - pub fn clear_all( - env: Env, - caller: Address, - ) -> Result<(), VaultError> { + pub fn clear_all(env: Env, caller: Address) -> Result<(), VaultError> { caller.require_auth(); Self::require_owner(env.clone(), caller.clone())?; - + env.storage() .instance() .remove(&StorageKey::AllowedDepositors); - - env.events().publish( - (events::event_allowlist_clear(&env), caller), - () - ); - + + env.events() + .publish((events::event_allowlist_clear(&env), caller), ()); + Ok(()) } @@ -1496,7 +1566,6 @@ mod cold_storage; pub mod events; pub mod limits; pub mod rate_limit; -pub mod timelock; // #[cfg(test)] // #[path = "../proofs/deduct.rs"] @@ -1533,4 +1602,4 @@ mod test_rustdoc_coverage; // #[cfg(test)] // mod test_gas_budget; // #[cfg(test)] -// mod test_rate_limit; \ No newline at end of file +// mod test_rate_limit; diff --git a/contracts/vault/src/test.rs b/contracts/vault/src/test.rs index 5006baf1..671d0b71 100644 --- a/contracts/vault/src/test.rs +++ b/contracts/vault/src/test.rs @@ -1093,11 +1093,7 @@ fn deduct_with_request_id() { client.init(&owner, &usdc, &Some(1000), &None, &None, &None, &None); let settlement = create_settlement(&env, &owner, &vault_address); - let remaining = client.deduct( - &owner, - &100, - &Some(Symbol::new(&env, "req123")), - ); + let remaining = client.deduct(&owner, &100, &Some(Symbol::new(&env, "req123"))); assert_eq!(remaining, 900); } @@ -1146,11 +1142,7 @@ fn deduct_event_contains_request_id() { let settlement = create_settlement(&env, &owner, &vault_address); let request_id = Symbol::new(&env, "api_call_42"); - client.deduct( - &owner, - &150, - &Some(request_id.clone()), - ); + client.deduct(&owner, &150, &Some(request_id.clone())); let events = env.events().all(); let ev = events.last().expect("expected deduct event"); @@ -1216,11 +1208,7 @@ fn deduct_authorized_caller_succeeds() { ); let settlement = create_settlement(&env, &owner, &vault_address); client.set_settlement(&owner, &settlement); - let remaining = client.deduct( - &authorized, - &100, - &None - ); + let remaining = client.deduct(&authorized, &100, &None); assert_eq!(remaining, 900); } @@ -2643,11 +2631,7 @@ fn vault_full_lifecycle() { assert_eq!(client.balance(), 525); // Single deduct - let after_deduct = client.deduct( - &owner, - &25, - &Some(Symbol::new(&env, "r4")), - ); + let after_deduct = client.deduct(&owner, &25, &Some(Symbol::new(&env, "r4"))); assert_eq!(after_deduct, 500); // Admin change @@ -3889,10 +3873,7 @@ fn deduct_to_zero_succeeds() { client.init(&owner, &usdc, &Some(500), &None, &None, &None, &None); let settlement = create_settlement(&env, &owner, &vault_address); - assert_eq!( - client.deduct(&owner, &500, &None), - 0 - ); + assert_eq!(client.deduct(&owner, &500, &None), 0); } #[test] @@ -4712,17 +4693,13 @@ mod fuzz { let amount: i128 = rng.gen_range(1..=op_cap); if paused { // deduct must fail while paused - assert!(client - .try_deduct(&caller, &amount, &None) - .is_err()); + assert!(client.try_deduct(&caller, &amount, &None).is_err()); } else if sim >= amount { sim -= amount; client.deduct(&caller, &amount, &None); } else { // must fail — balance unchanged (insufficient, &Address::generate(&env)) - assert!(client - .try_deduct(&caller, &amount, &None) - .is_err()); + assert!(client.try_deduct(&caller, &amount, &None).is_err()); } } @@ -5003,9 +4980,7 @@ mod fuzz { } else { // Must be rejected; balance and sim are unchanged. assert!( - client - .try_deduct(&caller, &amount, &None) - .is_err(), + client.try_deduct(&caller, &amount, &None).is_err(), "deduct exceeding balance must fail at step {step}" ); } @@ -5181,9 +5156,7 @@ mod fuzz { let amount: i128 = rng.gen_range(1..=max_d); if paused { assert!( - client - .try_deduct(&caller, &amount, &None) - .is_err(), + client.try_deduct(&caller, &amount, &None).is_err(), "deduct must fail while paused at step {step}" ); } else if sim >= amount { @@ -5191,9 +5164,7 @@ mod fuzz { client.deduct(&caller, &amount, &None); } else { assert!( - client - .try_deduct(&caller, &amount, &None) - .is_err(), + client.try_deduct(&caller, &amount, &None).is_err(), "insufficient deduct must fail at step {step}" ); } @@ -5414,9 +5385,7 @@ mod fuzz { client.deduct(&owner, &amount, &None); } else { assert!( - client - .try_deduct(&owner, &amount, &None) - .is_err(), + client.try_deduct(&owner, &amount, &None).is_err(), "owner deduct must fail when balance insufficient at step {step}" ); } @@ -5425,16 +5394,10 @@ mod fuzz { let amount: i128 = rng.gen_range(1..=max_d); if sim >= amount { sim -= amount; - client.deduct( - &caller_b, - &amount, - &None - ); + client.deduct(&caller_b, &amount, &None); } else { assert!( - client - .try_deduct(&caller_b, &amount, &None) - .is_err(), + client.try_deduct(&caller_b, &amount, &None).is_err(), "caller_b deduct must fail when balance insufficient at step {step}" ); } @@ -6600,11 +6563,7 @@ mod malicious_token { let vault_client = CalloraVaultClient::new(&env, &vault_addr); // 😈 ATTACK: Call back into the vault - vault_client.deduct( - &caller, - &attack_amount, - &Some(Symbol::new(&env, "reentry")), - ); + vault_client.deduct(&caller, &attack_amount, &Some(Symbol::new(&env, "reentry"))); } } @@ -7095,11 +7054,7 @@ fn test_reentry_repeated_attempts() { // This tests that the vault's balance validation prevents over-deduction malicious_client.set_attack_config(&vault_address, &owner, &100, &5); - vault_client.deduct( - &owner, - &100, - &None, - ); + vault_client.deduct(&owner, &100, &None); // With 5 re-entries of 100 each, plus original 100, total should be 600 // So final balance should be 1000 - 600 = 400 @@ -8216,12 +8171,6 @@ fn slippage_no_regression_existing_deductions() { let env = Env::default(); let (owner, client) = setup_slippage_vault(&env, 500); env.mock_all_auths(); - assert_eq!( - client.deduct(&owner, &200, &None), - 300 - ); - assert_eq!( - client.deduct(&owner, &300, &None), - 0 - ); + assert_eq!(client.deduct(&owner, &200, &None), 300); + assert_eq!(client.deduct(&owner, &300, &None), 0); } diff --git a/contracts/vault/src/test_access_control_matrix.rs b/contracts/vault/src/test_access_control_matrix.rs index 44b4e576..1ed503c9 100644 --- a/contracts/vault/src/test_access_control_matrix.rs +++ b/contracts/vault/src/test_access_control_matrix.rs @@ -43,7 +43,10 @@ fn test_entrypoints_require_auth() { usdc_client.mint(&vault_addr, &1000); let mut items: Vec = Vec::new(&env); - items.push_back(DeductItem { amount: 10, request_id: None }); + items.push_back(DeductItem { + amount: 10, + request_id: None, + }); env.set_auths(&[]); diff --git a/contracts/vault/src/test_idempotency.rs b/contracts/vault/src/test_idempotency.rs index 5cb95433..0f74a599 100644 --- a/contracts/vault/src/test_idempotency.rs +++ b/contracts/vault/src/test_idempotency.rs @@ -146,11 +146,7 @@ fn deduct_duplicate_request_id_rejected() { let rid = Symbol::new(&env, "req_001"); // First call — must succeed. - let remaining = client.deduct( - &owner, - &100, - &Some(rid.clone()), - ); + let remaining = client.deduct(&owner, &100, &Some(rid.clone())); assert_eq!(remaining, 900); // Second call with same request_id — must be rejected. @@ -174,18 +170,10 @@ fn deduct_distinct_request_ids_both_succeed() { let rid_a = Symbol::new(&env, "req_a"); let rid_b = Symbol::new(&env, "req_b"); - let after_a = client.deduct( - &owner, - &100, - &Some(rid_a.clone()), - ); + let after_a = client.deduct(&owner, &100, &Some(rid_a.clone())); assert_eq!(after_a, 900); - let after_b = client.deduct( - &owner, - &200, - &Some(rid_b.clone()), - ); + let after_b = client.deduct(&owner, &200, &Some(rid_b.clone())); assert_eq!(after_b, 700); assert_eq!(client.balance(), 700); @@ -198,18 +186,9 @@ fn deduct_none_request_id_not_deduplicated() { let (_, client, _, owner) = setup_vault(&env, 1_000); // Three calls with None — all must succeed. - assert_eq!( - client.deduct(&owner, &100, &None), - 900 - ); - assert_eq!( - client.deduct(&owner, &100, &None), - 800 - ); - assert_eq!( - client.deduct(&owner, &100, &None), - 700 - ); + assert_eq!(client.deduct(&owner, &100, &None), 900); + assert_eq!(client.deduct(&owner, &100, &None), 800); + assert_eq!(client.deduct(&owner, &100, &None), 700); assert_eq!(client.balance(), 700); } @@ -273,11 +252,7 @@ fn is_request_processed_true_after_successful_deduct() { let (_, client, _, owner) = setup_vault(&env, 500); let rid = Symbol::new(&env, "seen"); - client.deduct( - &owner, - &50, - &Some(rid.clone()), - ); + client.deduct(&owner, &50, &Some(rid.clone())); assert!( client.is_request_processed(&rid), @@ -294,11 +269,7 @@ fn is_request_processed_false_for_different_id() { let rid_a = Symbol::new(&env, "id_a"); let rid_b = Symbol::new(&env, "id_b"); - client.deduct( - &owner, - &50, - &Some(rid_a.clone()), - ); + client.deduct(&owner, &50, &Some(rid_a.clone())); assert!(client.is_request_processed(&rid_a)); assert!(!client.is_request_processed(&rid_b)); @@ -317,11 +288,7 @@ fn batch_deduct_duplicate_request_id_rejected_atomically() { let rid = Symbol::new(&env, "batch_dup"); // First single deduct marks the id. - client.deduct( - &owner, - &100, - &Some(rid.clone()), - ); + client.deduct(&owner, &100, &Some(rid.clone())); assert_eq!(client.balance(), 900); // Batch that reuses the same id — must be rejected atomically. @@ -507,11 +474,7 @@ fn deduct_retry_with_different_amount_still_rejected() { let rid = Symbol::new(&env, "retry_amt"); - client.deduct( - &owner, - &100, - &Some(rid.clone()), - ); + client.deduct(&owner, &100, &Some(rid.clone())); // Retry with a different amount — still rejected. let result = client.try_deduct(&owner, &50, &Some(rid.clone())); @@ -554,16 +517,11 @@ fn batch_deduct_mixed_ids_marks_only_some_ids() { assert!(client.is_request_processed(&rid_z)); // Retrying either Some id must fail. - assert!( - client.try_deduct(&owner, &10, &Some(rid_x)).is_err()); - assert!( - client.try_deduct(&owner, &10, &Some(rid_z)).is_err()); + assert!(client.try_deduct(&owner, &10, &Some(rid_x)).is_err()); + assert!(client.try_deduct(&owner, &10, &Some(rid_z)).is_err()); // None deducts still go through. - assert_eq!( - client.deduct(&owner, &10, &None), - 765 - ); + assert_eq!(client.deduct(&owner, &10, &None), 765); } #[test] @@ -574,11 +532,7 @@ fn replay_across_long_window_rejected() { let rid = Symbol::new(&env, "req_long_win"); // First call succeeds - client.deduct( - &owner, - &100, - &Some(rid.clone()), - ); + client.deduct(&owner, &100, &Some(rid.clone())); // Fast-forward ledger 6 months (approx 6 * 30 days) let new_timestamp = env.ledger().timestamp() + 180 * 24 * 60 * 60; @@ -606,16 +560,8 @@ fn gc_entrypoint_prunes_and_emits_event() { let rid1 = Symbol::new(&env, "req_gc_1"); let rid2 = Symbol::new(&env, "req_gc_2"); - client.deduct( - &owner, - &100, - &Some(rid1.clone()), - ); - client.deduct( - &owner, - &100, - &Some(rid2.clone()), - ); + client.deduct(&owner, &100, &Some(rid1.clone())); + client.deduct(&owner, &100, &Some(rid2.clone())); let mut ids_to_prune = soroban_sdk::Vec::new(&env); ids_to_prune.push_back(rid1.clone()); @@ -640,11 +586,7 @@ fn gc_entrypoint_prunes_and_emits_event() { assert!(has_event, "Should emit request_id_pruned event"); // Should now be able to replay rid1 - client.deduct( - &owner, - &100, - &Some(rid1) - ); + client.deduct(&owner, &100, &Some(rid1)); } #[test] @@ -669,11 +611,7 @@ fn gc_allowed_during_pause() { let (_, client, _, owner) = setup_vault(&env, 1_000); let rid1 = Symbol::new(&env, "req_gc_pause"); - client.deduct( - &owner, - &100, - &Some(rid1.clone()), - ); + client.deduct(&owner, &100, &Some(rid1.clone())); client.pause(&owner); assert!(client.is_paused()); diff --git a/contracts/vault/src/test_reentrancy.rs b/contracts/vault/src/test_reentrancy.rs index 75caf8a9..28c2d9d6 100644 --- a/contracts/vault/src/test_reentrancy.rs +++ b/contracts/vault/src/test_reentrancy.rs @@ -41,11 +41,7 @@ impl MaliciousToken { let client = CalloraVaultClient::new(&env, &vault); // Attempt re-entry into deduct - let _ = client.try_deduct( - &caller, - &1, - &Some(Symbol::new(&env, "reentry_token")) - ); + let _ = client.try_deduct(&caller, &1, &Some(Symbol::new(&env, "reentry_token"))); } } } @@ -107,11 +103,7 @@ impl MaliciousSettlement { let client = CalloraVaultClient::new(&env, &vault); // Attempt re-entry into deduct - let _ = client.try_deduct( - &caller, - &1, - &Some(Symbol::new(&env, "reentry_settle")) - ); + let _ = client.try_deduct(&caller, &1, &Some(Symbol::new(&env, "reentry_settle"))); } } } @@ -162,11 +154,7 @@ fn test_reentrancy_via_token_transfer_is_blocked_by_auth() { assert_eq!(initial_balance, 1000); // Trigger deduct -> calls token.transfer -> calls vault.deduct (re-entry) - let result = vault_client.try_deduct( - &owner, - &100, - &Some(Symbol::new(&env, "first_call")) - ); + let result = vault_client.try_deduct(&owner, &100, &Some(Symbol::new(&env, "first_call"))); assert!(result.is_ok(), "First deduct should succeed"); assert_eq!( @@ -208,11 +196,7 @@ fn test_reentrancy_via_settlement_callback_is_blocked() { assert_eq!(initial_balance, 1000); // Trigger deduct -> calls settlement.receive_payment -> calls vault.deduct (re-entry) - let result = vault_client.try_deduct( - &owner, - &100, - &Some(Symbol::new(&env, "first_call")) - ); + let result = vault_client.try_deduct(&owner, &100, &Some(Symbol::new(&env, "first_call"))); assert!(result.is_ok(), "First deduct should succeed"); assert_eq!( @@ -313,11 +297,7 @@ fn test_reentrancy_by_authorized_attacker() { assert_eq!(initial_balance, 1000); // Attacker calls deduct -> token.transfer -> attacker calls vault.deduct (re-entry) - let result = vault_client.try_deduct( - &attacker, - &100, - &Some(Symbol::new(&env, "first_call")) - ); + let result = vault_client.try_deduct(&attacker, &100, &Some(Symbol::new(&env, "first_call"))); assert!(result.is_ok(), "First deduct should succeed"); assert_eq!( diff --git a/contracts/vault/src/test_rustdoc_coverage.rs b/contracts/vault/src/test_rustdoc_coverage.rs index be444353..c4b383b5 100644 --- a/contracts/vault/src/test_rustdoc_coverage.rs +++ b/contracts/vault/src/test_rustdoc_coverage.rs @@ -7,10 +7,7 @@ extern crate std; -use soroban_sdk::{ - testutils::Address as _, - token, Address, BytesN, Env, Vec, -}; +use soroban_sdk::{testutils::Address as _, token, Address, BytesN, Env, Vec}; use crate::*; @@ -444,7 +441,9 @@ fn test_set_reserve_cap_requires_owner() { let (_, client, usdc, _) = setup(&env); env.set_auths(&[]); let stranger = Address::generate(&env); - assert!(client.try_set_reserve_cap(&stranger, &usdc, &1_000).is_err()); + assert!(client + .try_set_reserve_cap(&stranger, &usdc, &1_000) + .is_err()); } // --------------------------------------------------------------------------- @@ -470,14 +469,19 @@ fn test_set_timelock_window_requires_admin() { let (_, client, _, _) = setup(&env); env.set_auths(&[]); let stranger = Address::generate(&env); - assert!(client.try_set_timelock_window(&stranger, &86_400u64).is_err()); + assert!(client + .try_set_timelock_window(&stranger, &86_400u64) + .is_err()); } #[test] fn test_get_timelock_window_default() { let env = Env::default(); let (_, client, _, _) = setup(&env); - assert_eq!(client.get_timelock_window(), timelock::DEFAULT_TIMELOCK_SECONDS); + assert_eq!( + client.get_timelock_window(), + timelock::DEFAULT_TIMELOCK_SECONDS + ); } #[test] diff --git a/contracts/vault/src/test_views.rs b/contracts/vault/src/test_views.rs index 39cb98c9..a892deb2 100644 --- a/contracts/vault/src/test_views.rs +++ b/contracts/vault/src/test_views.rs @@ -291,4 +291,3 @@ fn is_authorized_depositor_added_address_true() { // --------------------------------------------------------------------------- // get_allowed_depositors // --------------------------------------------------------------------------- - diff --git a/contracts/vault/tests/auth_snap.rs b/contracts/vault/tests/auth_snap.rs index 75d57ddf..206603e0 100644 --- a/contracts/vault/tests/auth_snap.rs +++ b/contracts/vault/tests/auth_snap.rs @@ -1,12 +1,12 @@ #![cfg(test)] -use soroban_sdk::{testutils::Address as _, Address, Env, Symbol}; use crate::{Vault, VaultClient}; +use soroban_sdk::{testutils::Address as _, Address, Env, Symbol}; #[test] fn test_vault_auth_snapshot() { let env = Env::default(); - + env.mock_all_auths(); let caller = Address::generate(&env); @@ -17,11 +17,18 @@ fn test_vault_auth_snapshot() { client.deposit(&caller, &deposit_amount); let auths = env.auths(); - assert_eq!(auths.len(), 1, "Expected exactly one auth snapshot for deposit"); - assert_eq!(auths[0].0, caller, "Caller address mismatch in auth snapshot"); assert_eq!( - auths[0].1.function, - Symbol::new(&env, "deposit"), + auths.len(), + 1, + "Expected exactly one auth snapshot for deposit" + ); + assert_eq!( + auths[0].0, caller, + "Caller address mismatch in auth snapshot" + ); + assert_eq!( + auths[0].1.function, + Symbol::new(&env, "deposit"), "Function symbol mismatch in auth snapshot" ); @@ -30,11 +37,14 @@ fn test_vault_auth_snapshot() { let auths = env.auths(); let latest_auth = auths.last().expect("Missing auth snapshot for withdraw"); - - assert_eq!(latest_auth.0, caller, "Caller address mismatch in auth snapshot"); + + assert_eq!( + latest_auth.0, caller, + "Caller address mismatch in auth snapshot" + ); assert_eq!( - latest_auth.1.function, - Symbol::new(&env, "withdraw"), + latest_auth.1.function, + Symbol::new(&env, "withdraw"), "Function symbol mismatch in auth snapshot" ); -} \ No newline at end of file +} diff --git a/contracts/vault/tests/proptest.rs b/contracts/vault/tests/proptest.rs index 9d757526..941fd3d0 100644 --- a/contracts/vault/tests/proptest.rs +++ b/contracts/vault/tests/proptest.rs @@ -154,7 +154,10 @@ fn assert_balance_in_sync( "seed={seed} step={step}: unexpected idle balance {}", preview.idle_balance ); - assert!(!preview.has_idle, "seed={seed} step={step}: has_idle unexpectedly true"); + assert!( + !preview.has_idle, + "seed={seed} step={step}: has_idle unexpectedly true" + ); } // --------------------------------------------------------------------------- @@ -203,7 +206,10 @@ fn run_trace(seed: u64) { let amount = rng.range_i128(MIN_DEPOSIT, MIN_DEPOSIT * 20); let result = client.try_deposit(&owner, &amount); if paused { - assert!(result.is_err(), "seed={seed} step={step}: deposit must fail while paused"); + assert!( + result.is_err(), + "seed={seed} step={step}: deposit must fail while paused" + ); } else { assert!( result.is_ok(), @@ -224,7 +230,10 @@ fn run_trace(seed: u64) { let request_id = rng.next_u64(); let result = client.try_deduct(&authorized_caller, &amount, &request_id); if paused { - assert!(result.is_err(), "seed={seed} step={step}: deduct must fail while paused"); + assert!( + result.is_err(), + "seed={seed} step={step}: deduct must fail while paused" + ); } else if balance_before < amount { assert!( result.is_err(), @@ -315,7 +324,11 @@ fn deposit_boundary_exact_minimum_succeeds_one_below_fails() { let result = client.try_deposit(&owner, &99); assert!(result.is_err(), "deposit below min_deposit must fail"); - assert_eq!(client.balance(), 100, "rejected deposit must not mutate tracked balance"); + assert_eq!( + client.balance(), + 100, + "rejected deposit must not mutate tracked balance" + ); assert_eq!(usdc_client.balance(&client.address), 100); } @@ -334,7 +347,11 @@ fn deduct_boundary_exact_maximum_succeeds_one_above_fails() { let result = client.try_deduct(&authorized_caller, &1_001, &2u64); assert!(result.is_err(), "deduct above max_deduct must fail"); - assert_eq!(client.balance(), 4_000, "rejected deduct must not mutate tracked balance"); + assert_eq!( + client.balance(), + 4_000, + "rejected deduct must not mutate tracked balance" + ); assert_eq!(usdc_client.balance(&client.address), 4_000); } @@ -356,7 +373,11 @@ fn pause_blocks_deposit_and_deduct_unpause_restores() { let batch = Vec::from_array(&env, [(500i128, 2u64)]); assert!(client.try_batch_deduct(&authorized_caller, &batch).is_err()); - assert_eq!(client.balance(), 1_000, "paused rejections must not mutate tracked balance"); + assert_eq!( + client.balance(), + 1_000, + "paused rejections must not mutate tracked balance" + ); assert_eq!(usdc_client.balance(&client.address), 1_000); client.unpause(&owner); @@ -378,8 +399,15 @@ fn batch_deduct_is_all_or_nothing() { let items = Vec::from_array(&env, [(100i128, 1u64), (100i128, 2u64)]); let result = client.try_batch_deduct(&authorized_caller, &items); - assert!(result.is_err(), "batch total exceeding balance must fail atomically"); - assert_eq!(client.balance(), 150, "failed batch must not partially mutate tracked balance"); + assert!( + result.is_err(), + "batch total exceeding balance must fail atomically" + ); + assert_eq!( + client.balance(), + 150, + "failed batch must not partially mutate tracked balance" + ); assert_eq!(usdc_client.balance(&client.address), 150); } @@ -397,5 +425,9 @@ fn zero_and_negative_amounts_are_rejected() { assert!(client.try_deduct(&authorized_caller, &0, &1u64).is_err()); assert!(client.try_deduct(&authorized_caller, &-50, &2u64).is_err()); - assert_eq!(client.balance(), 1_000, "all-rejected calls must not mutate tracked balance"); + assert_eq!( + client.balance(), + 1_000, + "all-rejected calls must not mutate tracked balance" + ); } diff --git a/contracts/whitelist/Cargo.toml b/contracts/whitelist/Cargo.toml index 9ea69d21..c10f4031 100644 --- a/contracts/whitelist/Cargo.toml +++ b/contracts/whitelist/Cargo.toml @@ -9,4 +9,3 @@ soroban-sdk = { workspace = true } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } -callora-vault = { path = "../vault" }