From f52da17658265337b21dbdc0ad87d34cf3f196e8 Mon Sep 17 00:00:00 2001 From: Ashmuk001 Date: Mon, 19 Oct 2026 03:43:07 +0000 Subject: [PATCH] Add regression coverage for contract boundaries --- contracts/disputes/src/lib.rs | 11 ++++++ contracts/disputes/tests/dispute_storage.rs | 29 +++++++++++++-- contracts/fees/src/lib.rs | 5 +++ contracts/fees/tests/auth_boundary.rs | 39 +++++++++++++++++++++ contracts/monitor/tests/limits.rs | 20 ++++++++--- contracts/validators/tests/auth_boundary.rs | 29 ++++++++++++++- 6 files changed, 126 insertions(+), 7 deletions(-) diff --git a/contracts/disputes/src/lib.rs b/contracts/disputes/src/lib.rs index f660f708..a76a56b3 100644 --- a/contracts/disputes/src/lib.rs +++ b/contracts/disputes/src/lib.rs @@ -72,6 +72,7 @@ pub struct EscalationRecord { pub stake: u32, pub turnout: u32, pub community_outcome: DisputeOutcome, + pub council_voters: soroban_sdk::Vec, pub council_votes: u32, pub uphold_votes: u32, pub overturn_votes: u32, @@ -228,6 +229,7 @@ impl DisputesContract { stake, turnout, community_outcome, + council_voters: soroban_sdk::Vec::new(&_env), council_votes: 0, uphold_votes: 0, overturn_votes: 0, @@ -239,11 +241,19 @@ impl DisputesContract { _env: Env, policy: EscalationPolicy, mut record: EscalationRecord, + voter: soroban_sdk::Address, vote: u32, ) -> EscalationRecord { if record.resolved { panic!("escalation already resolved"); } + if record + .council_voters + .iter() + .any(|member| member == voter.clone()) + { + panic!("council member has already voted"); + } match vote { COUNCIL_VOTE_ABSTAIN => {} COUNCIL_VOTE_UPHOLD => { @@ -254,6 +264,7 @@ impl DisputesContract { } _ => panic!("invalid council vote"), } + record.council_voters.push_back(voter); record.council_votes += 1; if record.uphold_votes >= policy.council_threshold { record.resolved = true; diff --git a/contracts/disputes/tests/dispute_storage.rs b/contracts/disputes/tests/dispute_storage.rs index 2980aa24..fdf05936 100644 --- a/contracts/disputes/tests/dispute_storage.rs +++ b/contracts/disputes/tests/dispute_storage.rs @@ -1,7 +1,8 @@ use disputes::{ - DisputeOutcome, DisputeStatus, DisputesContract, DisputesContractClient, + DisputeOutcome, DisputeStatus, DisputesContract, DisputesContractClient, EscalationPolicy, + COUNCIL_VOTE_UPHOLD, }; -use soroban_sdk::{Env, Symbol}; +use soroban_sdk::{testutils::Address as _, Address, Env, Symbol}; #[test] fn dispute_lifecycle_is_persisted_and_uses_unique_ids() { @@ -33,4 +34,28 @@ fn dispute_lifecycle_is_persisted_and_uses_unique_ids() { let withdrawn = client.withdraw_dispute(&second, &creator); assert_eq!(withdrawn.status, DisputeStatus::Withdrawn); assert_eq!(client.get_dispute(&second.id), Some(withdrawn)); +} + +#[test] +#[should_panic(expected = "council member has already voted")] +fn council_vote_rejects_duplicate_voter() { + let env = Env::default(); + let contract_id = env.register(DisputesContract, ()); + let client = DisputesContractClient::new(&env, &contract_id); + let voter = Address::generate(&env); + let policy = EscalationPolicy { + min_stake: 1, + quorum: 10, + council_threshold: 2, + }; + let record = client.escalate_dispute( + &policy, + &Symbol::new(&env, "MARKET"), + &1, + &0, + &DisputeOutcome::Escalated, + ); + + let record = client.council_vote(&policy, &record, &voter, &COUNCIL_VOTE_UPHOLD); + client.council_vote(&policy, &record, &voter, &COUNCIL_VOTE_UPHOLD); } \ No newline at end of file diff --git a/contracts/fees/src/lib.rs b/contracts/fees/src/lib.rs index cb7fbb57..9a6531d6 100644 --- a/contracts/fees/src/lib.rs +++ b/contracts/fees/src/lib.rs @@ -293,6 +293,7 @@ impl FeesContract { /// - `ContractError::Unauthorized` if caller is not the admin. /// - `ContractError::FeePercentageTooHigh` if platform_fee_percentage > 10_000. /// - `ContractError::InvalidInput` if any fee value is negative. + /// - `ContractError::NoPendingFeeCommit` if no commitment exists. pub fn update_fee_config( env: Env, admin: Address, @@ -308,6 +309,10 @@ impl FeesContract { // Ensure fees are not paused for config updates (consistency) Self::assert_fees_not_paused(&env)?; + if Self::get_pending_fee_commit_internal(&env).is_none() { + return Err(ContractError::NoPendingFeeCommit); + } + Self::queue_fee_config(&env, &admin, new_config, DEFAULT_TIMELOCK_SECONDS) } diff --git a/contracts/fees/tests/auth_boundary.rs b/contracts/fees/tests/auth_boundary.rs index ac3f2846..b4486458 100644 --- a/contracts/fees/tests/auth_boundary.rs +++ b/contracts/fees/tests/auth_boundary.rs @@ -165,6 +165,9 @@ fn test_update_fee_config_succeeds_with_admin_auth() { let client = register_and_initialize(&env, &admin); let config = default_fee_config(); + let hash = soroban_sdk::BytesN::from_array(&env, &[0x11u8; 32]); + client.commit_fee_config(&admin, &hash); + let _ = env.auths(); client.update_fee_config(&admin, &config); let auths = env.auths(); @@ -218,7 +221,39 @@ fn test_update_fee_config_rejects_zero_minimum_when_fees_enabled() { assert!(matches!(result, Err(Ok(ContractError::InvalidInput)))); config.fees_enabled = false; + let hash = soroban_sdk::BytesN::from_array(&env, &[0x22u8; 32]); + client.commit_fee_config(&admin, &hash); + client.update_fee_config(&admin, &config); +} + +#[test] +fn test_update_fee_config_requires_commit_and_apply_timelock() { + let env = Env::default(); + let admin = Address::generate(&env); + let client = register_and_initialize(&env, &admin); + let config = default_fee_config(); + + let no_commit = client.try_update_fee_config(&admin, &config); + assert!(matches!( + no_commit, + Err(Ok(ContractError::NoPendingFeeCommit)) + )); + + let hash = soroban_sdk::BytesN::from_array(&env, &[0x33u8; 32]); + client.commit_fee_config(&admin, &hash); client.update_fee_config(&admin, &config); + + let early_apply = client.try_apply_fee_update(&admin); + assert!(matches!( + early_apply, + Err(Ok(ContractError::FeeRevealTooEarly)) + )); + + env.ledger().with_mut(|ledger| { + ledger.timestamp += 86_401; + }); + client.apply_fee_update(&admin); + assert_eq!(client.get_fee_config(), config); } // ============================================================ @@ -756,6 +791,8 @@ fn test_all_state_changing_entrypoints_succeed_with_proper_auth() { let config = default_fee_config(); + let hash = soroban_sdk::BytesN::from_array(&env, &[0x44u8; 32]); + client.commit_fee_config(&admin, &hash); assert!( client.try_update_fee_config(&admin, &config).is_ok(), "Admin should invoke update_fee_config" @@ -846,6 +883,8 @@ fn test_pending_fee_config_inspection_and_apply() { let new_cfg = default_fee_config(); let original_cfg = client.get_fee_config(); assert_ne!(new_cfg, original_cfg); + let hash = soroban_sdk::BytesN::from_array(&env, &[0x55u8; 32]); + client.commit_fee_config(&admin, &hash); client.update_fee_config(&admin, &new_cfg); assert_eq!(client.get_fee_config(), original_cfg); diff --git a/contracts/monitor/tests/limits.rs b/contracts/monitor/tests/limits.rs index 1b0bd653..2f54c960 100644 --- a/contracts/monitor/tests/limits.rs +++ b/contracts/monitor/tests/limits.rs @@ -247,13 +247,25 @@ fn test_record_bet_after_remove_succeeds() { // --------------------------------------------------------------------------- #[test] -fn test_record_bet_exceeds_cap_returns_error() { +fn test_record_bets_exactly_at_cap_then_rejects_next() { let (_env, setup) = setup(); setup .client - .set_caps(&setup.admin, &CapType::Bets, &2u32); - setup.client.record_bet(&setup.user1); - setup.client.record_bet(&setup.user1); + .set_caps(&setup.admin, &CapType::Bets, &3u32); + let caps = setup.client.get_caps(); + + for expected_count in 1..=caps.max_bets { + assert_eq!( + setup.client.record_bet(&setup.user1), + expected_count, + "recording bet {expected_count} should succeed up to max_bets" + ); + } + assert_eq!( + setup.client.get_account_state(&setup.user1).bets, + caps.max_bets + ); + let result = setup.client.try_record_bet(&setup.user1); assert_eq!(result, Ok(Err(MonitorError::BetCapExceeded))); } diff --git a/contracts/validators/tests/auth_boundary.rs b/contracts/validators/tests/auth_boundary.rs index 127ff6a1..d32d9ed4 100644 --- a/contracts/validators/tests/auth_boundary.rs +++ b/contracts/validators/tests/auth_boundary.rs @@ -15,7 +15,7 @@ use soroban_sdk::{ testutils::{Address as _, Ledger, LedgerInfo}, Address, Env, }; -use validators::{ValidatorsContract, ValidatorsContractClient}; +use validators::{ValidatorError, ValidatorsContract, ValidatorsContractClient}; // --------------------------------------------------------------------------- // Test harness @@ -238,6 +238,33 @@ fn test_update_stake_requires_auth_success() { } } +#[test] +fn test_update_stake_enforces_inclusive_min_and_max_bounds() { + let env = Env::default(); + let ts = setup_with_validator(&env); + + assert_eq!( + ts.client.try_update_stake(&ts.validator, &MIN_STAKE), + Ok(Ok(())), + "stake exactly at min_stake should succeed" + ); + assert_eq!( + ts.client.try_update_stake(&ts.validator, &(MIN_STAKE - 1)), + Ok(Err(ValidatorError::StakeTooLow)), + "stake below min_stake should fail" + ); + assert_eq!( + ts.client.try_update_stake(&ts.validator, &MAX_STAKE), + Ok(Ok(())), + "stake exactly at max_stake should succeed" + ); + assert_eq!( + ts.client.try_update_stake(&ts.validator, &(MAX_STAKE + 1)), + Ok(Err(ValidatorError::StakeTooHigh)), + "stake above max_stake should fail" + ); +} + // =========================================================================== // set_validator_active // ===========================================================================