From 62936861044ae108123e9d29d5fcb4bd73f0bb4f Mon Sep 17 00:00:00 2001 From: ayo-ola0710 Date: Wed, 27 May 2026 11:52:45 +0100 Subject: [PATCH] test: cover concentration enforcement boundaries and testnet bypass --- src/lib.rs | 24 +++++++++++++++++++++++ src/milestone_signals.rs | 41 +++++++++++++++++++++++++++++++++++++++- src/proptest_helpers.rs | 24 +++++++++++++++-------- 3 files changed, 80 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3c7d88ac..c0a9780c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1511,6 +1511,12 @@ impl RevoraRevenueShare { } } + /// Enable or disable testnet mode for the contract. + /// + /// ### Security Note + /// This mode MUST only be enabled on test networks. It relaxes critical + /// validation rules (like concentration limits) to facilitate automated + /// testing and integration flows. pub fn set_testnet_mode(env: Env, enabled: bool) -> Result<(), RevoraError> { Self::require_not_frozen(&env)?; let admin: Address = @@ -2149,12 +2155,17 @@ impl RevoraRevenueShare { return Err(RevoraError::PayoutAssetMismatch); } + // Testnet mode bypass: if enabled, skip concentration limit enforcement + // to allow flexible testing of revenue flows without holder constraints. let testnet_mode = Self::is_testnet_mode(env.clone()); if !testnet_mode { let limit_key = DataKey::ConcentrationLimit(offering_id.clone()); if let Some(config) = env.storage().persistent().get::(&limit_key) { + // Concentration Enforcement: if enforce=true and max_bps > 0, + // reject report if current concentration exceeds the limit. + // Allowed: current <= max_bps. Rejected: current > max_bps. if config.enforce && config.max_bps > 0 { let curr_key = DataKey::CurrentConcentration(offering_id.clone()); let current: u32 = env.storage().persistent().get(&curr_key).unwrap_or(0); @@ -3112,6 +3123,14 @@ impl RevoraRevenueShare { /// - `Ok(())` on success. /// - `Err(RevoraError::LimitReached)` if the offering is not found. /// - `Err(RevoraError::ContractFrozen)` if the contract is frozen. + /// Configure the concentration limit for an offering. + /// + /// ### Parameters + /// - `max_bps`: The maximum allowed share for a single holder in basis points. + /// - `enforce`: If true, `report_revenue` will fail if current concentration > `max_bps`. + /// + /// ### Constraints + /// - `max_bps` must be <= 10,000. pub fn set_concentration_limit( env: Env, issuer: Address, @@ -3163,6 +3182,11 @@ impl RevoraRevenueShare { /// Stores the provided concentration value. If it exceeds the configured limit, /// a `conc_warn` event is emitted. The stored value is used for enforcement in `report_revenue`. /// + /// ### Enforcement Boundary + /// - If `enforce` is true in `ConcentrationLimitConfig`: + /// - `concentration_bps <= max_bps`: `report_revenue` is allowed. + /// - `concentration_bps > max_bps`: `report_revenue` is rejected. + /// /// ### Parameters /// - `issuer`: The offering issuer. Must provide authentication. /// - `token`: The token representing the offering. diff --git a/src/milestone_signals.rs b/src/milestone_signals.rs index 3b85dec2..d2fbc103 100644 --- a/src/milestone_signals.rs +++ b/src/milestone_signals.rs @@ -35,7 +35,7 @@ use soroban_sdk::{symbol_short, testutils::Address as _, Address, Env, IntoVal, Symbol}; -use crate::{RevoraRevenueShare, RevoraRevenueShareClient}; +use crate::{RevoraError, RevoraRevenueShare, RevoraRevenueShareClient}; // ── helpers ────────────────────────────────────────────────────────────────── @@ -312,6 +312,45 @@ fn milestone_concentration_warning_event_emitted() { ); } +/// When concentration is exactly one bps over the limit, `report_revenue` is rejected. +#[test] +fn milestone_concentration_one_bps_over_limit_rejected() { + let env = Env::default(); + let client = make_client(&env); + let (issuer, token, payout) = setup_offering(&env, &client); + let ns = symbol_short!("def"); + + client.set_concentration_limit(&issuer, &ns, &token, &5_000u32, &true); + client.report_concentration(&issuer, &ns, &token, &5_001u32); + + let result = client.try_report_revenue(&issuer, &ns, &token, &payout, &1_000i128, &1u64, &false); + assert_eq!(result, Err(Ok(RevoraError::ConcentrationLimitExceeded))); +} + +/// When in testnet mode, concentration enforcement is bypassed even if over limit. +#[test] +fn milestone_concentration_testnet_mode_bypasses_enforcement() { + let env = Env::default(); + let client = make_client(&env); + let (issuer, token, payout) = setup_offering(&env, &client); + let ns = symbol_short!("def"); + + // Enable testnet mode (requires admin auth) + client.set_testnet_mode(&true); + + client.set_concentration_limit(&issuer, &ns, &token, &5_000u32, &true); + client.report_concentration(&issuer, &ns, &token, &6_000u32); + + // Should succeed despite being over limit + client.report_revenue(&issuer, &ns, &token, &payout, &1_000i128, &1u64, &false); + + assert_eq!( + client.get_audit_summary(&issuer, &ns, &token).unwrap().report_count, + 1u64, + "testnet mode must bypass concentration enforcement" + ); +} + // ── 5. Blacklist snapshot in rev_rep ───────────────────────────────────────── /// The blacklist state at report time is observable via `get_blacklist`. diff --git a/src/proptest_helpers.rs b/src/proptest_helpers.rs index 16ed26eb..a6ffe9f7 100644 --- a/src/proptest_helpers.rs +++ b/src/proptest_helpers.rs @@ -192,13 +192,6 @@ pub fn arb_set_concentration_limit() -> impl Strategy { /// Strategy for any single valid operation (uniform distribution across all variants). pub fn any_test_operation() -> impl Strategy { -/// Strategy for a single `ReportConcentration` operation. -pub fn arb_report_concentration() -> impl Strategy { - arb_valid_bps().prop_map(|concentration_bps| TestOperation::ReportConcentration { concentration_bps }) -} - -/// Strategy for any single valid operation (uniform distribution). -pub fn arb_any_operation() -> impl Strategy { prop_oneof![ arb_register_offering(), arb_report_revenue(), @@ -209,10 +202,25 @@ pub fn arb_any_operation() -> impl Strategy { arb_set_concentration_limit(), arb_report_concentration(), Just(TestOperation::Freeze), - arb_claim_delay_secs().prop_map(|d| TestOperation::SetClaimDelay { delay_secs: d }), + arb_claim_delay_secs().prop_map(|d| TestOperation::SetClaimDelay { + issuer: Address::generate(&Env::default()), // placeholder - proptest-helper addresses are updated in sequences + namespace: Symbol::new(&Env::default(), "def"), + token: Address::generate(&Env::default()), + delay_secs: d + }), ] } +/// Strategy for a single `ReportConcentration` operation. +pub fn arb_report_concentration() -> impl Strategy { + arb_valid_bps().prop_map(|concentration_bps| TestOperation::ReportConcentration { concentration_bps }) +} + +/// Strategy for any single valid operation (uniform distribution). +pub fn arb_any_operation() -> impl Strategy { + any_test_operation() +} + /// Strategy for a sequence of `len` valid operations. /// /// Period IDs in `ReportRevenue` and `DepositRevenue` operations are normalised