diff --git a/docs/payment-token-decimal-compatibility.md b/docs/payment-token-decimal-compatibility.md new file mode 100644 index 00000000..6ef1be04 --- /dev/null +++ b/docs/payment-token-decimal-compatibility.md @@ -0,0 +1,61 @@ +# Payment Token Decimal Compatibility + +## Overview + +Different payment tokens on the Stellar network use different decimal precisions. For example: + +| Token | Decimals | Example raw amount | Canonical (7-dec) | +|-------|----------|-------------------|-------------------| +| XLM | 7 | 1_000_000_0 | 1_000_000_0 | +| USDC | 6 | 1_000_000 | 10_000_000 | +| WBTC | 8 | 1_000_000_00 | 1_000_000_0 | + +Without normalization, reporting USDC revenue in raw amounts and then computing holder shares produces silent arithmetic errors — holders receive 10× too little or too much. + +## How It Works + +This contract stores a per-offering decimal configuration for the payout asset. Before any holder share computation (in `claim`, `get_claimable`, and `get_claimable_chunk`), the raw revenue amount is normalized to Stellar's canonical 7-decimal precision. + +### Normalization Rules + +- **`from_decimals == 7`**: no-op, amount returned unchanged. +- **`from_decimals < 7`** (e.g., USDC at 6): scale **up** by `10^(7 - from_decimals)`. +- **`from_decimals > 7`** (e.g., WBTC at 8): scale **down** by `10^(from_decimals - 7)` using integer truncation. +- **Overflow protection**: if multiplication overflows `i128`, the function returns `0` to prevent fund inflation. This is logged as a zero-payout distribution. + +## API + +### `set_payment_token_decimals(issuer, namespace, token, decimals: u32)` + +Sets the decimal precision of the payout asset for an offering. Requires issuer authorization. + +- **Range**: `0..=18`. Values outside this range return `RevoraError::LimitReached`. +- **Default**: If not set, `7` (canonical Stellar stroops) is assumed. +- **Event**: Emits `dec_set` event with the configured value. + +### `get_payment_token_decimals(issuer, namespace, token) -> u32` + +Returns the configured decimal precision or `7` if not set. + +## Security Assumptions + +1. **Issuer responsibility**: The `issuer` is trusted to supply the correct on-chain token decimal value. An incorrect value directly affects all future claim payouts. Issuers should verify the decimal on-chain before calling this function. +2. **Immutable after set**: There is no restriction on updating decimals after the fact, but changing decimals mid-offering will affect future claims inconsistently with past revenue reports. Issuers should set decimals before the first revenue report. +3. **Overflow is safe**: All multiplications are guarded with `checked_mul`. Overflow returns `0`, preventing fund inflation but potentially causing zero payouts for extremely large amounts with low-decimal tokens. +4. **Scope**: Decimals are per-offering, not per-asset globally. Two offerings with the same payout asset may have different decimal configurations. + +## Example + +```rust +// Register offering with USDC (6 decimals) as payout asset +client.register_offering(&issuer, &ns, &token, &shares_bps, &usdc_address, &0); + +// Configure decimals +client.set_payment_token_decimals(&issuer, &ns, &token, &6); + +// Report 1,000,000 raw USDC units = 0.1 USDC +client.deposit_revenue(&issuer, &ns, &token, &usdc_address, &1_000_000, &1); + +// After normalization: 1_000_000 (6-dec) → 10_000_000 (7-dec) +// Holder with 50% share receives: 10_000_000 * 5_000 / 10_000 = 5_000_000 canonical units +``` diff --git a/src/lib.rs b/src/lib.rs index 22dc7451..ddcc2156 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -206,6 +206,10 @@ const EVENT_ROUNDING_MODE_SET: Symbol = symbol_short!("rnd_mode"); const EVENT_ADMIN_SET: Symbol = symbol_short!("admin_set"); const EVENT_PLATFORM_FEE_SET: Symbol = symbol_short!("fee_set"); const BPS_DENOMINATOR: i128 = 10_000; +/// Stellar network canonical decimal precision (7 decimal places, i.e., stroops). +const STELLAR_CANONICAL_DECIMALS: u32 = 7; +/// Maximum accepted decimal precision (safety cap for normalization math). +const MAX_TOKEN_DECIMALS: u32 = 18; /// Represents a revenue-share offering registered on-chain. /// Offerings are immutable once registered. @@ -2617,6 +2621,83 @@ impl RevoraRevenueShare { core::cmp::min(core::cmp::max(share, lo), hi) } + /// Normalize `amount` from the token's native decimal precision to Stellar's canonical 7-decimal + /// (stroop) precision used internally by this contract. + /// + /// - If `from_decimals == 7`: returns `amount` unchanged. + /// - If `from_decimals < 7`: scales **up** by `10^(7 - from_decimals)` (e.g., 6-decimal USDC → 7). + /// - If `from_decimals > 7`: scales **down** by `10^(from_decimals - 7)` using integer truncation. + /// + /// Returns `0` if intermediate arithmetic overflows to prevent fund inflation bugs. + fn normalize_amount(amount: i128, from_decimals: u32) -> i128 { + if from_decimals == STELLAR_CANONICAL_DECIMALS { + return amount; + } + if from_decimals < STELLAR_CANONICAL_DECIMALS { + let exp = STELLAR_CANONICAL_DECIMALS - from_decimals; + let factor: i128 = match 10_i128.checked_pow(exp) { + Some(f) => f, + None => return 0, + }; + amount.checked_mul(factor).unwrap_or(0) + } else { + let exp = from_decimals - STELLAR_CANONICAL_DECIMALS; + let factor: i128 = match 10_i128.checked_pow(exp) { + Some(f) => f, + None => return 0, + }; + amount.checked_div(factor).unwrap_or(0) + } + } + + /// Set the decimal precision of the payout asset for an offering. + /// + /// Must be called by the offering `issuer`. Accepted range is `0..=18`. + /// If not set, the contract defaults to `7` (Stellar canonical stroops). + /// + /// ### Security + /// - Only the offering issuer may configure decimals. + /// - Misconfigured decimals directly affect payout arithmetic; issuers must supply + /// the on-chain token's actual decimal value. + /// + /// ### Errors + /// - `RevoraError::NotAuthorized` if caller is not the issuer. + /// - `RevoraError::LimitReached` if `decimals > 18`. + pub fn set_payment_token_decimals( + env: Env, + issuer: Address, + namespace: Symbol, + token: Address, + decimals: u32, + ) -> Result<(), RevoraError> { + issuer.require_auth(); + if decimals > MAX_TOKEN_DECIMALS { + return Err(RevoraError::LimitReached); + } + let offering_id = OfferingId { issuer: issuer.clone(), namespace: namespace.clone(), token: token.clone() }; + env.storage() + .persistent() + .set(&DataKey::PaymentTokenDecimals(offering_id), &decimals); + env.events() + .publish((EVENT_DECIMAL_SET, issuer, namespace, token), decimals); + Ok(()) + } + + /// Get the configured decimal precision of the payout asset for an offering. + /// Defaults to `7` (Stellar canonical stroops) if not explicitly set. + pub fn get_payment_token_decimals( + env: Env, + issuer: Address, + namespace: Symbol, + token: Address, + ) -> u32 { + let offering_id = OfferingId { issuer, namespace, token }; + env.storage() + .persistent() + .get(&DataKey::PaymentTokenDecimals(offering_id)) + .unwrap_or(STELLAR_CANONICAL_DECIMALS) + } + // ── Multi-period aggregated claims ─────────────────────────── /// Deposit revenue for a specific period of an offering. diff --git a/src/test.rs b/src/test.rs index c89c45f4..e69de29b 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,9538 +0,0 @@ -#![cfg(test)] -#![allow(warnings)] -#![allow(unused_variables, dead_code, unused_imports)] - -use crate::proptest_helpers::{any_test_operation, TestOperation}; -use crate::{ - AmountValidationCategory, AmountValidationMatrix, ClippyFormatGateAttestationInput, - ProposalAction, RevoraError, RevoraRevenueShare, RevoraRevenueShareClient, RoundingMode, -}; -use proptest::{prelude::*, prop}; -use soroban_sdk::{ - symbol_short, - testutils::{Address as _, Events as _, Ledger as _}, - token, vec, Address, BytesN, Env, IntoVal, String as SdkString, Symbol, Vec, -}; - -// ── helper ──────────────────────────────────────────────────── - -fn make_client(env: &Env) -> RevoraRevenueShareClient { - let id = env.register_contract(None, RevoraRevenueShare); - RevoraRevenueShareClient::new(env, &id) -} - -/// Helper to extract legacy events skipping ev_idx2 indexed events -#[allow(clippy::all)] -fn legacy_events( - env: &soroban_sdk::Env, -) -> soroban_sdk::Vec<(soroban_sdk::Address, soroban_sdk::Val, soroban_sdk::Val)> { - let all = env.events().all(); - let mut filtered = soroban_sdk::Vec::new(env); - let idx2_sym: soroban_sdk::Val = soroban_sdk::symbol_short!("ev_idx2").into_val(env); - for i in 0..all.len() { - let ev = all.get(i).unwrap(); - let topics: soroban_sdk::Vec = ev.1.clone().into_val(env); - let is_indexed = - if !topics.is_empty() { topics.first().unwrap() == idx2_sym } else { false }; - if !is_indexed { - filtered.push_back(ev); - } - } - filtered -} - -const BOUNDARY_AMOUNTS: [i128; 7] = [i128::MIN, i128::MIN + 1, -1, 0, 1, i128::MAX - 1, i128::MAX]; -const BOUNDARY_PERIODS: [u64; 6] = [0, 1, 2, 10_000, u64::MAX - 1, u64::MAX]; -const FUZZ_ITERATIONS: usize = 128; -const STORAGE_STRESS_OFFERING_COUNT: u32 = 100; - -fn next_u64(seed: &mut u64) -> u64 { - // Deterministic LCG for repeatable pseudo-random test values. - *seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407); - - *seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407); - - *seed -} - -fn next_amount(seed: &mut u64) -> i128 { - let hi = next_u64(seed) as u128; - let lo = next_u64(seed) as u128; - ((hi << 64) | lo) as i128 -} - -fn next_period(seed: &mut u64) -> u64 { - next_u64(seed) -} - -// ─── Event-to-flow mapping ─────────────────────────────────────────────────── -// -// Flow: Offering Registration (register_offering) -// topic[0] = Symbol("offer_reg") -// topic[1] = Address (issuer) -// data = (Address (token), u32 (revenue_share_bps)) -// -// Flow: Revenue Report (report_revenue) -// topic[0] = Symbol("rev_rep") -// topic[1] = Address (issuer) -// topic[2] = Address (token) -// data = (i128 (amount), u64 (period_id), Vec
(blacklist)) -// -// ───────────────────────────────────────────────────────────────────────────── - -// ── Single-event structure tests ───────────────────────────────────────────── - -#[test] -fn register_offering_emits_exact_event() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - let bps: u32 = 1_500; - - client.register_offering(&issuer, &symbol_short!("def"), &token, &bps, &token, &0); - - assert_eq!( - legacy_events(&env), - soroban_sdk::vec![ - &env, - ( - contract_id, - (symbol_short!("offer_reg"), issuer).into_val(&env), - (token.clone(), bps, token).into_val(&env), - ), - ] - ); -} - -#[test] -fn report_revenue_emits_exact_event() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let amount: i128 = 5_000_000; - let period_id: u64 = 42; - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &token, - &amount, - &period_id, - &false, - ); - - let empty_bl = Vec::
::new(&env); - let events = env.events().all(); - // register(2) + report(6) = 8 - assert_eq!(events.len(), 8); - let (_, t0, _) = events.get(0).unwrap(); - { - let sym: Symbol = t0.get(0).unwrap().into_val(&env); - assert_eq!(sym, symbol_short!("offer_reg")); - } - let (_, t5, _) = events.get(5).unwrap(); - { - let sym: Symbol = t5.get(0).unwrap().into_val(&env); - assert_eq!(sym, symbol_short!("rev_rep")); - } -} - -// ── Ordering tests ─────────────────────────────────────────────────────────── - -#[test] -fn combined_flow_preserves_event_order() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let bps: u32 = 1_000; - let amount: i128 = 1_000_000; - let period_id: u64 = 1; - - client.register_offering(&issuer, &symbol_short!("def"), &token, &bps, &token, &0); - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &token, - &amount, - &period_id, - &false, - ); - - let events = env.events().all(); - assert_eq!(events.len(), 8); - - let empty_bl = Vec::
::new(&env); - assert_eq!( - events, - vec![ - &env, - ( - contract_id.clone(), - (symbol_short!("offer_reg"), issuer.clone()).into_val(&env), - (token.clone(), bps, token.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_init"), issuer.clone(), token.clone()).into_val(&env), - (amount, period_id, empty_bl.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_inia"), issuer.clone(), token.clone(), token.clone()) - .into_val(&env), - (amount, period_id, empty_bl.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_rep"), issuer.clone(), token.clone()).into_val(&env), - (amount, period_id, empty_bl.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_repa"), issuer.clone(), token.clone(), token.clone()) - .into_val(&env), - (amount, period_id).into_val(&env), - ), - ] - ); -} - -#[test] -fn complex_mixed_flow_events_in_order() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - - let issuer_a = Address::generate(&env); - let issuer = issuer_a.clone(); - - let issuer_b = Address::generate(&env); - let issuer = issuer_b.clone(); - - let token_x = Address::generate(&env); - let token_y = Address::generate(&env); - client.register_offering(&issuer_a, &symbol_short!("def"), &token_x, &500, &token_x, &0); - client.register_offering(&issuer_b, &symbol_short!("def"), &token_y, &750, &token_y, &0); - - client.report_revenue( - &issuer_a, - &symbol_short!("def"), - &token_x, - &token_x, - &100_000, - &1, - &false, - ); - client.report_revenue( - &issuer_b, - &symbol_short!("def"), - &token_y, - &token_y, - &200_000, - &1, - &false, - ); - - let events = legacy_events(&env); - assert_eq!(events.len(), 10); - - let empty_bl = Vec::
::new(&env); - assert_eq!( - events, - vec![ - &env, - ( - contract_id.clone(), - (symbol_short!("offer_reg"), issuer_a.clone()).into_val(&env), - (token_x.clone(), 500u32, token_x.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("offer_reg"), issuer_b.clone()).into_val(&env), - (token_y.clone(), 750u32, token_y.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_init"), issuer_a.clone(), token_x.clone()).into_val(&env), - (100_000i128, 1u64, empty_bl.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_inia"), issuer_a.clone(), token_x.clone(), token_x.clone(),) - .into_val(&env), - (100_000i128, 1u64, empty_bl.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_rep"), issuer_a.clone(), token_x.clone()).into_val(&env), - (100_000i128, 1u64, empty_bl.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_repa"), issuer_a.clone(), token_x.clone(), token_x.clone(),) - .into_val(&env), - (100_000i128, 1u64).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_init"), issuer_b.clone(), token_y.clone()).into_val(&env), - (200_000i128, 1u64, empty_bl.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_inia"), issuer_b.clone(), token_y.clone(), token_y.clone(),) - .into_val(&env), - (200_000i128, 1u64, empty_bl.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_rep"), issuer_b.clone(), token_y.clone()).into_val(&env), - (200_000i128, 1u64, empty_bl.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_repa"), issuer_b.clone(), token_y.clone(), token_y.clone(),) - .into_val(&env), - (200_000i128, 1u64).into_val(&env), - ), - ] - ); -} - -// ── Multi-entity tests ─────────────────────────────────────────────────────── - -#[test] -fn multiple_offerings_emit_distinct_events() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - - let issuer = Address::generate(&env); - let token_a = Address::generate(&env); - let token_b = Address::generate(&env); - let token_c = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token_a, &100, &token_a, &0); - client.register_offering(&issuer, &symbol_short!("def"), &token_b, &200, &token_b, &0); - client.register_offering(&issuer, &symbol_short!("def"), &token_c, &300, &token_c, &0); - - let events = legacy_events(&env); - assert_eq!(events.len(), 3); - - assert_eq!( - events, - vec![ - &env, - ( - contract_id.clone(), - (symbol_short!("offer_reg"), issuer.clone()).into_val(&env), - (token_a.clone(), 100u32, token_a.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("offer_reg"), issuer.clone()).into_val(&env), - (token_b.clone(), 200u32, token_b.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("offer_reg"), issuer.clone()).into_val(&env), - (token_c.clone(), 300u32, token_c.clone()).into_val(&env), - ), - ] - ); -} - -#[test] -fn multiple_revenue_reports_same_offering() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - client.report_revenue(&issuer, &symbol_short!("def"), &token, &token, &10_000, &1, &false); - client.report_revenue(&issuer, &symbol_short!("def"), &token, &token, &20_000, &2, &false); - client.report_revenue(&issuer, &symbol_short!("def"), &token, &token, &30_000, &3, &false); - - let events = legacy_events(&env); - assert_eq!(events.len(), 13); - - let empty_bl = Vec::
::new(&env); - assert_eq!( - events, - vec![ - &env, - ( - contract_id.clone(), - (symbol_short!("offer_reg"), issuer.clone()).into_val(&env), - (token.clone(), 1000_u32, token.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_init"), issuer.clone(), token.clone()).into_val(&env), - (10_000i128, 1u64, empty_bl.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_inia"), issuer.clone(), token.clone(), token.clone()) - .into_val(&env), - (10_000i128, 1u64, empty_bl.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_rep"), issuer.clone(), token.clone()).into_val(&env), - (10_000i128, 1u64, empty_bl.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_repa"), issuer.clone(), token.clone(), token.clone()) - .into_val(&env), - (10_000i128, 1u64).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_init"), issuer.clone(), token.clone()).into_val(&env), - (20_000i128, 2u64, empty_bl.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_inia"), issuer.clone(), token.clone(), token.clone()) - .into_val(&env), - (20_000i128, 2u64, empty_bl.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_rep"), issuer.clone(), token.clone()).into_val(&env), - (20_000i128, 2u64, empty_bl.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_repa"), issuer.clone(), token.clone(), token.clone()) - .into_val(&env), - (20_000i128, 2u64).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_init"), issuer.clone(), token.clone()).into_val(&env), - (30_000i128, 3u64, empty_bl.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_inia"), issuer.clone(), token.clone(), token.clone()) - .into_val(&env), - (30_000i128, 3u64, empty_bl.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_rep"), issuer.clone(), token.clone()).into_val(&env), - (30_000i128, 3u64, empty_bl.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_repa"), issuer.clone(), token.clone(), token.clone()) - .into_val(&env), - (30_000i128, 3u64).into_val(&env), - ), - ] - ); -} - -#[test] -fn same_issuer_different_tokens() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - - let issuer = Address::generate(&env); - let token_x = Address::generate(&env); - let token_y = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token_x, &1_000, &token_x, &0); - client.register_offering(&issuer, &symbol_short!("def"), &token_y, &2_000, &token_y, &0); - client.report_revenue(&issuer, &symbol_short!("def"), &token_x, &token_x, &500_000, &1, &false); - client.report_revenue(&issuer, &symbol_short!("def"), &token_y, &token_y, &750_000, &1, &false); - - let events = legacy_events(&env); - assert_eq!(events.len(), 10); - - let empty_bl = Vec::
::new(&env); - assert_eq!( - events, - vec![ - &env, - ( - contract_id.clone(), - (symbol_short!("offer_reg"), issuer.clone()).into_val(&env), - (token_x.clone(), 1_000u32, token_x.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("offer_reg"), issuer.clone()).into_val(&env), - (token_y.clone(), 2_000u32, token_y.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_init"), issuer.clone(), token_x.clone()).into_val(&env), - (500_000i128, 1u64, empty_bl.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_inia"), issuer.clone(), token_x.clone(), token_x.clone()) - .into_val(&env), - (500_000i128, 1u64, empty_bl.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_rep"), issuer.clone(), token_x.clone()).into_val(&env), - (500_000i128, 1u64, empty_bl.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_repa"), issuer.clone(), token_x.clone(), token_x.clone()) - .into_val(&env), - (500_000i128, 1u64).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_init"), issuer.clone(), token_y.clone()).into_val(&env), - (750_000i128, 1u64, empty_bl.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_inia"), issuer.clone(), token_y.clone(), token_y.clone()) - .into_val(&env), - (750_000i128, 1u64, empty_bl.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_rep"), issuer.clone(), token_y.clone()).into_val(&env), - (750_000i128, 1u64, empty_bl.clone()).into_val(&env), - ), - ( - contract_id.clone(), - (symbol_short!("rev_repa"), issuer.clone(), token_y.clone(), token_y.clone()) - .into_val(&env), - (750_000i128, 1u64).into_val(&env), - ), - ] - ); -} - -// ── Topic / symbol inspection tests ────────────────────────────────────────── - -#[test] -fn topic_symbols_are_distinct() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &token, &0); - client.report_revenue(&issuer, &symbol_short!("def"), &token, &token, &1_000_000, &1, &false); - - let events = env.events().all(); - assert_eq!(events.len(), 8); - let (_, t0, _) = events.get(0).unwrap(); - { - let sym: Symbol = t0.get(0).unwrap().into_val(&env); - assert_eq!(sym, symbol_short!("offer_reg")); - } - let (_, t5, _) = events.get(5).unwrap(); - { - let sym: Symbol = t5.get(0).unwrap().into_val(&env); - assert_eq!(sym, symbol_short!("rev_rep")); - } -} - -#[test] -fn rev_rep_topics_include_token_address() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - client.report_revenue(&issuer, &symbol_short!("def"), &token, &token, &999, &7, &false); - - let events = env.events().all(); - assert_eq!(events.len(), 8); - let (_, t0, _) = events.get(0).unwrap(); - { - let sym: Symbol = t0.get(0).unwrap().into_val(&env); - assert_eq!(sym, symbol_short!("offer_reg")); - } - let (_, t5, _) = events.get(5).unwrap(); - { - let sym: Symbol = t5.get(0).unwrap().into_val(&env); - assert_eq!(sym, symbol_short!("rev_rep")); - } -} - -// ── Boundary / edge-case tests ─────────────────────────────────────────────── - -#[test] -fn zero_bps_offering() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &0, &token, &0); - - let events = env.events().all(); - assert_eq!(events.len(), 2); - let (_, t0, _) = events.get(0).unwrap(); - { - let sym: Symbol = t0.get(0).unwrap().into_val(&env); - assert_eq!(sym, symbol_short!("offer_reg")); - } -} - -#[test] -fn max_bps_offering() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - // 10_000 bps == 100% - client.register_offering(&issuer, &symbol_short!("def"), &token, &10_000, &token, &0); - - let events = env.events().all(); - assert_eq!(events.len(), 2); - let (_, t0, _) = events.get(0).unwrap(); - { - let sym: Symbol = t0.get(0).unwrap().into_val(&env); - assert_eq!(sym, symbol_short!("offer_reg")); - } -} - -#[test] -fn zero_amount_revenue_report_rejected() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - let result = - client.try_report_revenue(&issuer, &symbol_short!("def"), &token, &token, &0, &1, &false); - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), RevoraError::InvalidAmount); -} - -#[test] -fn negative_amount_revenue_report_rejected() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - let result = - client.try_report_revenue(&issuer, &symbol_short!("def"), &token, &token, &-1, &1, &false); - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), RevoraError::InvalidAmount); -} - -#[test] -fn large_revenue_amount() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - let large_amount: i128 = i128::MAX; - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &token, - &large_amount, - &u64::MAX, - &false, - ); - - let events = env.events().all(); - assert_eq!(events.len(), 8); - let (_, t0, _) = events.get(0).unwrap(); - { - let sym: Symbol = t0.get(0).unwrap().into_val(&env); - assert_eq!(sym, symbol_short!("offer_reg")); - } - let (_, t5, _) = events.get(5).unwrap(); - { - let sym: Symbol = t5.get(0).unwrap().into_val(&env); - assert_eq!(sym, symbol_short!("rev_rep")); - } -} - -#[test] -fn negative_revenue_amount() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - // Negative revenue is rejected by input validation (#35). - let negative: i128 = -500_000; - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - let r = client.try_report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &token, - &negative, - &99, - &false, - ); - assert!(r.is_err()); -} - -// ── original smoke test ─────────────────────────────────────── - -#[test] -fn it_emits_events_on_register_and_report() { - let env = Env::default(); - let (_client, _issuer, _token, _payout_asset, _amount, _period_id) = - setup_with_revenue_report(&env, 1_000_000, 1); - assert!(legacy_events(&env).len() >= 2); -} - -#[test] -fn it_emits_versioned_events() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout = Address::generate(&env); - let bps: u32 = 1_000; - let amount: i128 = 1_000_000; - let period_id: u64 = 1; - - // enable versioned events for this test - env.as_contract(&contract_id, || { - env.storage().persistent().set(&crate::DataKey::ContractFlags, &(true, false)); - }); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &bps, &payout, &0); - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout, - &amount, - &period_id, - &false, - ); - - let events = legacy_events(&env); - - let expected = ( - contract_id.clone(), - (symbol_short!("ofr_reg1"), issuer.clone()).into_val(&env), - (crate::EVENT_SCHEMA_VERSION, token.clone(), bps, payout.clone()).into_val(&env), - ); - - assert!(events.contains(&expected)); -} - -// ── period/amount fuzz coverage ─────────────────────────────── - -#[test] -fn fuzz_period_and_amount_boundaries_do_not_panic() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - - // Valid boundary inputs: non-negative amounts and non-zero period IDs. - // Invalid inputs (period_id == 0, negative amounts) are expected to be rejected. - let valid_amounts: [i128; 5] = [0, 1, i128::MAX - 1, i128::MAX, 100_000]; - let valid_periods: [u64; 5] = [1, 2, 10_000, u64::MAX - 1, u64::MAX]; - let invalid_amounts: [i128; 3] = [i128::MIN, i128::MIN + 1, -1]; - let invalid_periods: [u64; 1] = [0]; - - let mut accepted = 0usize; - let mut rejected = 0usize; - - // Valid combinations must all succeed (first call per period is initial, rest are rejected - // without override=true, so use unique periods per amount to avoid collision). - for (i, &amount) in valid_amounts.iter().enumerate() { - let period = valid_periods[i % valid_periods.len()]; - let r = client.try_report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &amount, - &period, - &false, - ); - if r.is_ok() { - accepted += 1; - } else { - rejected += 1; - } - } - - // Invalid amounts must all be rejected. - for &amount in &invalid_amounts { - let r = client.try_report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &amount, - &1, - &false, - ); - assert!(r.is_err(), "negative amount {amount} should be rejected"); - rejected += 1; - } - - // Invalid period IDs must all be rejected. - for &period in &invalid_periods { - let r = client.try_report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &100, - &period, - &false, - ); - assert!(r.is_err(), "period_id {period} should be rejected"); - rejected += 1; - } - - assert!(accepted > 0, "at least one valid input must be accepted"); - assert!(rejected > 0, "at least one invalid input must be rejected"); -} - -#[test] -fn fuzz_period_and_amount_repeatable_sweep_do_not_panic() { - let env = Env::default(); - let (client, issuer, token, payout_asset) = setup_with_offering(&env); - - // Same seed must produce the exact same sequence (determinism check). - let mut seed_a = 0x00A1_1CE5_ED19_u64; - let mut seed_b = 0x00A1_1CE5_ED19_u64; - for _ in 0..64 { - assert_eq!(next_amount(&mut seed_a), next_amount(&mut seed_b)); - assert_eq!(next_period(&mut seed_a), next_period(&mut seed_b)); - } - - // Reset and run deterministic fuzz-style inputs through contract entrypoint. - // Input validation (#35) rejects negative amounts and period_id == 0. - // Use try_ variant and count successes/rejections without asserting exact event count, - // since the number of accepted calls depends on validation outcomes. - let mut seed = 0x00A1_1CE5_ED19_u64; - let mut accepted = 0usize; - let mut rejected_invalid = 0usize; - for i in 0..FUZZ_ITERATIONS { - let mut amount = next_amount(&mut seed); - let mut period = next_period(&mut seed); - - // Inject boundary values periodically. - if i % 64 == 0 { - amount = i128::MAX; - } else if i % 64 == 1 { - amount = 0; - } - if i % 97 == 0 { - period = u64::MAX; - } else if i % 97 == 1 { - // period_id == 0 is invalid; force a rejection. - period = 0; - } - - // Ensure amount is non-negative (negative values are rejected by validation). - if amount < 0 { - amount = amount.saturating_neg().max(0); - } - - let r = client.try_report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &amount, - &period, - &false, - ); - if r.is_ok() { - accepted += 1; - } else { - rejected_invalid += 1; - } - } - - // Each report_revenue call emits 2 events (specific + backward-compatible rev_rep). - assert_eq!(env.events().all().len(), 1 + (FUZZ_ITERATIONS as u32) * 4); - - assert!(accepted > 0); -} - -// --------------------------------------------------------------------------- -// Pagination tests -// --------------------------------------------------------------------------- - -/// Helper: set up env + client, return (env, client, issuer). -fn setup<'a>(env: &'a Env) -> (RevoraRevenueShareClient<'a>, Address) { - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(env, &contract_id); - let issuer = Address::generate(env); - (client, issuer) -} - -/// Register `n` offerings for `issuer`, each with a unique token. -fn register_n(env: &Env, client: &RevoraRevenueShareClient, issuer: &Address, n: u32) { - for i in 0..n { - let token = Address::generate(env); - let payout_asset = Address::generate(env); - client.register_offering( - issuer, - &symbol_short!("def"), - &token, - &(100 + i), - &payout_asset, - &0, - ); - } -} - -#[test] -fn get_revenue_range_chunk_matches_full_sum() { - let env = Env::default(); - env.mock_all_auths(); - - let client = make_client(&env); - - let issuer = Address::generate(&env); - let token = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000u32, &token, &0i128); - - // Report revenue for periods 1..=10 - for p in 1u64..=10u64 { - client.report_revenue(&issuer, &symbol_short!("def"), &token, &token, &100i128, &p, &false); - } - - // Full sum - let full = client.get_revenue_range(&issuer, &symbol_short!("def"), &token, &1u64, &10u64); - - // Sum in chunks of 3 - let mut cursor = 1u64; - let mut acc: i128 = 0; - loop { - let (partial, next) = client.get_revenue_range_chunk( - &issuer, - &symbol_short!("def"), - &token, - &cursor, - &10u64, - &3u32, - ); - acc += partial; - if let Some(n) = next { - cursor = n; - } else { - break; - } - } - - assert_eq!(full, acc); -} - -#[test] -fn pending_periods_page_and_claimable_chunk_consistent() { - let env = Env::default(); - env.mock_all_auths(); - - let client = make_client(&env); - - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let holder = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000u32, &token, &0i128); - - // Insert periods directly (no token transfer needed) - for p in 1u64..=8u64 { - client.test_insert_period(&issuer, &symbol_short!("def"), &token, &p, &1000i128); - } - - // Set holder share - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &1000u32); - - // get_pending_periods full - let full = client.get_pending_periods(&issuer, &symbol_short!("def"), &token, &holder); - - // Page through with limit 3 - let mut cursor = 0u32; - let mut all = Vec::new(&env); - loop { - let (page, next) = client.get_pending_periods_page( - &issuer, - &symbol_short!("def"), - &token, - &holder, - &cursor, - &3u32, - ); - for i in 0..page.len() { - all.push_back(page.get(i).unwrap()); - } - if let Some(n) = next { - cursor = n; - } else { - break; - } - } - - // Compare lengths - assert_eq!(full.len(), all.len()); - - // Now check claimable chunk matches full - let full_claim = client.get_claimable(&issuer, &symbol_short!("def"), &token, &holder); - - // Sum claimable in chunks from index 0, count 2 - let mut idx = 0u32; - let mut acc: i128 = 0; - loop { - let (partial, next) = client.get_claimable_chunk( - &issuer, - &symbol_short!("def"), - &token, - &holder, - &idx, - &2u32, - ); - acc += partial; - if let Some(n) = next { - idx = n; - } else { - break; - } - } - assert_eq!(full_claim, acc); -} - -/// Helper (#30): create env, client, and one registered offering. Returns (env, client, issuer, token, payout_asset). -fn setup_with_offering<'a>( - env: &'a Env, -) -> (RevoraRevenueShareClient<'a>, Address, Address, Address) { - let (client, issuer) = setup(env); - let token = Address::generate(env); - let payout_asset = Address::generate(env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - (client, issuer, token, payout_asset) -} - -/// Helper (#30): create env, client, one offering, and one revenue report. Returns (env, client, issuer, token, payout_asset, amount, period_id). -fn setup_with_revenue_report<'a>( - env: &'a Env, - amount: i128, - period_id: u64, -) -> (RevoraRevenueShareClient<'a>, Address, Address, Address, i128, u64) { - let (client, issuer, token, payout_asset) = setup_with_offering(env); - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &amount, - &period_id, - &false, - ); - (client, issuer, token, payout_asset, amount, period_id) -} - -#[test] -fn empty_issuer_returns_empty_page() { - let (_env, client, issuer) = setup(); - - let (page, cursor) = client.get_offerings_page(&issuer, &symbol_short!("def"), &0, &10); - assert_eq!(page.len(), 0); - assert_eq!(cursor, None); -} - -#[test] -fn empty_issuer_count_is_zero() { - let (_env, client, issuer) = setup(); - assert_eq!(client.get_offering_count(&issuer, &symbol_short!("def")), 0); -} - -#[test] -fn register_persists_and_count_increments() { - let env = Env::default(); - let (client, issuer) = setup(&env); - register_n(&env, &client, &issuer, 3); - assert_eq!(client.get_offering_count(&issuer, &symbol_short!("def")), 3); -} - -#[test] -fn single_page_returns_all_no_cursor() { - let env = Env::default(); - let (client, issuer) = setup(&env); - register_n(&env, &client, &issuer, 5); - - let (page, cursor) = client.get_offerings_page(&issuer, &symbol_short!("def"), &0, &10); - assert_eq!(page.len(), 5); - assert_eq!(cursor, None); -} - -#[test] -fn multi_page_cursor_progression() { - let env = Env::default(); - let (client, issuer) = setup(&env); - register_n(&env, &client, &issuer, 7); - - // First page: items 0..3 - let (page1, cursor1) = client.get_offerings_page(&issuer, &symbol_short!("def"), &0, &3); - assert_eq!(page1.len(), 3); - assert_eq!(cursor1, Some(3)); - - // Second page: items 3..6 - let (page2, cursor2) = - client.get_offerings_page(&issuer, &symbol_short!("def"), &cursor1.unwrap_or(0), &3); - assert_eq!(page2.len(), 3); - assert_eq!(cursor2, Some(6)); - - // Third (final) page: items 6..7 - let (page3, cursor3) = - client.get_offerings_page(&issuer, &symbol_short!("def"), &cursor2.unwrap_or(0), &3); - assert_eq!(page3.len(), 1); - assert_eq!(cursor3, None); -} - -#[test] -fn final_page_has_no_cursor() { - let env = Env::default(); - let (client, issuer) = setup(&env); - register_n(&env, &client, &issuer, 4); - - let (page, cursor) = client.get_offerings_page(&issuer, &symbol_short!("def"), &2, &10); - assert_eq!(page.len(), 2); - assert_eq!(cursor, None); -} - -#[test] -fn out_of_bounds_cursor_returns_empty() { - let env = Env::default(); - let (client, issuer) = setup(&env); - register_n(&env, &client, &issuer, 3); - - let (page, cursor) = client.get_offerings_page(&issuer, &symbol_short!("def"), &100, &5); - assert_eq!(page.len(), 0); - assert_eq!(cursor, None); -} - -#[test] -fn limit_zero_uses_max_page_limit() { - let env = Env::default(); - let (client, issuer) = setup(&env); - register_n(&env, &client, &issuer, 5); - - // limit=0 should behave like MAX_PAGE_LIMIT (20), returning all 5. - let (page, cursor) = client.get_offerings_page(&issuer, &symbol_short!("def"), &0, &0); - assert_eq!(page.len(), 5); - assert_eq!(cursor, None); -} - -#[test] -fn limit_one_iterates_one_at_a_time() { - let env = Env::default(); - let (client, issuer) = setup(&env); - register_n(&env, &client, &issuer, 3); - - let (p1, c1) = client.get_offerings_page(&issuer, &symbol_short!("def"), &0, &1); - assert_eq!(p1.len(), 1); - assert_eq!(c1, Some(1)); - - let (p2, c2) = client.get_offerings_page(&issuer, &symbol_short!("def"), &c1.unwrap(), &1); - assert_eq!(p2.len(), 1); - assert_eq!(c2, Some(2)); - - let (p3, c3) = client.get_offerings_page(&issuer, &symbol_short!("def"), &c2.unwrap(), &1); - assert_eq!(p3.len(), 1); - assert_eq!(c3, None); -} - -#[test] -fn limit_exceeding_max_is_capped() { - let env = Env::default(); - let (client, issuer) = setup(&env); - register_n(&env, &client, &issuer, 25); - - // limit=50 should be capped to 20. - let (page, cursor) = client.get_offerings_page(&issuer, &symbol_short!("def"), &0, &50); - assert_eq!(page.len(), 20); - assert_eq!(cursor, Some(20)); -} - -#[test] -fn offerings_preserve_correct_data() { - let env = Env::default(); - let (client, issuer) = setup(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &500, &payout_asset, &0); - - let (page, _) = client.get_offerings_page(&issuer, &symbol_short!("def"), &0, &10); - let offering = page.get(0); - assert_eq!(offering.clone().clone().unwrap().issuer, issuer); - assert_eq!(offering.clone().clone().unwrap().token, token); - assert_eq!(offering.clone().clone().unwrap().revenue_share_bps, 500); - assert_eq!(offering.clone().clone().unwrap().payout_asset, payout_asset); -} - -#[test] -fn separate_issuers_have_independent_pages() { - let (env, client, issuer_a) = setup(); - let issuer_b = Address::generate(&env); - let issuer = issuer_b.clone(); - - register_n(&env, &client, &issuer_a, 3); - register_n(&env, &client, &issuer_b, 5); - - assert_eq!(client.get_offering_count(&issuer_a, &symbol_short!("def")), 3); - assert_eq!(client.get_offering_count(&issuer_b, &symbol_short!("def")), 5); - - let (page_a, _) = client.get_offerings_page(&issuer_a, &symbol_short!("def"), &0, &20); - let (page_b, _) = client.get_offerings_page(&issuer_b, &symbol_short!("def"), &0, &20); - assert_eq!(page_a.len(), 3); - assert_eq!(page_b.len(), 5); -} - -#[test] -fn exact_page_boundary_no_cursor() { - let env = Env::default(); - let (client, issuer) = setup(&env); - register_n(&env, &client, &issuer, 6); - - // Exactly 2 pages of 3 - let (p1, c1) = client.get_offerings_page(&issuer, &symbol_short!("def"), &0, &3); - assert_eq!(p1.len(), 3); - assert_eq!(c1, Some(3)); - - let (p2, c2) = client.get_offerings_page(&issuer, &symbol_short!("def"), &c1.unwrap(), &3); - assert_eq!(p2.len(), 3); - assert_eq!(c2, None); -} - -// ── blacklist CRUD ──────────────────────────────────────────── - -fn blacklist_setup() -> (Env, RevoraRevenueShareClient<'static>, Address, Address, Address) { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let admin = Address::generate(&env); - client.initialize(&admin, &None::
, &None::); - let issuer = admin.clone(); - let token = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - let payout_asset = Address::generate(&env); - let investor = Address::generate(&env); - - client.initialize(&admin, &None::
, &None::); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - - assert!(!client.is_blacklisted(&issuer, &symbol_short!("def"), &token, &investor)); - client.blacklist_add(&issuer, &issuer, &symbol_short!("def"), &token, &investor); - assert!(client.is_blacklisted(&issuer, &symbol_short!("def"), &token, &investor)); -} - -#[test] -fn remove_unmarks_investor() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - let investor = Address::generate(&env); - - client.initialize(&admin, &None::
, &None::); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - - client.blacklist_add(&issuer, &issuer, &symbol_short!("def"), &token, &investor); - client.blacklist_remove(&issuer, &issuer, &symbol_short!("def"), &token, &investor); - assert!(!client.is_blacklisted(&issuer, &symbol_short!("def"), &token, &investor)); -} - -#[test] -fn get_blacklist_returns_all_blocked_investors() { - let (env, client, admin, issuer, token) = blacklist_setup(); - let inv_a = Address::generate(&env); - let inv_b = Address::generate(&env); - let inv_c = Address::generate(&env); - - client.blacklist_add(&issuer, &issuer, &symbol_short!("def"), &token, &inv_a); - client.blacklist_add(&issuer, &issuer, &symbol_short!("def"), &token, &inv_b); - client.blacklist_add(&issuer, &issuer, &symbol_short!("def"), &token, &inv_c); - - let list = client.get_blacklist(&issuer, &symbol_short!("def"), &token); - assert_eq!(list.len(), 3); - assert!(list.contains(&inv_a)); - assert!(list.contains(&inv_b)); - assert!(list.contains(&inv_c)); -} - -#[test] -fn get_blacklist_empty_before_any_add() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let token = Address::generate(&env); - let admin = Address::generate(&env); - let issuer = Address::generate(&env); - client.initialize(&admin, &None::
, &None::); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &token, &0); - - assert_eq!(client.get_blacklist(&issuer, &symbol_short!("def"), &token).len(), 0); -} - -// ── idempotency ─────────────────────────────────────────────── - -#[test] -fn double_add_is_idempotent() { - let (env, client, admin, issuer, token) = blacklist_setup(); - let investor = Address::generate(&env); - - client.initialize(&admin, &None::
, &None::); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - client.blacklist_add(&admin, &issuer, &symbol_short!("def"), &token, &investor); - client.blacklist_add(&admin, &issuer, &symbol_short!("def"), &token, &investor); - - assert_eq!(client.get_blacklist(&issuer, &symbol_short!("def"), &token).len(), 1); -} - -#[test] -fn remove_nonexistent_is_idempotent() { - let (env, client, admin, issuer, token) = blacklist_setup(); - let investor = Address::generate(&env); - - client.initialize(&admin, &None::
, &None::); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - client.blacklist_remove(&admin, &issuer, &symbol_short!("def"), &token, &investor); // must not panic - assert!(!client.is_blacklisted(&issuer, &symbol_short!("def"), &token, &investor)); -} - -// ── per-offering isolation ──────────────────────────────────── - -#[test] -fn blacklist_is_scoped_per_offering() { - let (env, client, admin, issuer, token_a) = blacklist_setup(); - let token_b = Address::generate(&env); - let payout_asset_b = Address::generate(&env); - let investor = Address::generate(&env); - - client.initialize(&admin, &None::
, &None::); - client.register_offering(&issuer, &symbol_short!("def"), &token_a, &1_000, &token_a, &0); - client.register_offering(&issuer, &symbol_short!("def"), &token_b, &1_000, &token_b, &0); - - client.blacklist_add(&admin, &issuer, &symbol_short!("def"), &token_a, &investor); - - assert!(client.is_blacklisted(&issuer, &symbol_short!("def"), &token_a, &investor)); - assert!(!client.is_blacklisted(&issuer, &symbol_short!("def"), &token_b, &investor)); -} - -#[test] -fn removing_from_one_offering_does_not_affect_another() { - let (env, client, admin, issuer, token_a) = blacklist_setup(); - let token_b = Address::generate(&env); - let payout_asset_b = Address::generate(&env); - let investor = Address::generate(&env); - - client.initialize(&admin, &None::
, &None::); - client.register_offering(&issuer, &symbol_short!("def"), &token_a, &1_000, &token_a, &0); - client.register_offering(&issuer, &symbol_short!("def"), &token_b, &1_000, &token_b, &0); - - client.blacklist_add(&admin, &issuer, &symbol_short!("def"), &token_a, &investor); - client.blacklist_add(&admin, &issuer, &symbol_short!("def"), &token_b, &investor); - client.blacklist_remove(&admin, &issuer, &symbol_short!("def"), &token_a, &investor); - - assert!(!client.is_blacklisted(&issuer, &symbol_short!("def"), &token_a, &investor)); - assert!(client.is_blacklisted(&issuer, &symbol_short!("def"), &token_b, &investor)); -} - -// ── event emission ──────────────────────────────────────────── - -#[test] -fn blacklist_add_emits_event() { - let (env, client, admin, issuer, token) = blacklist_setup(); - let investor = Address::generate(&env); - - let before = env.events().all().len(); - client.initialize(&admin, &None::
, &None::); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - client.blacklist_add(&admin, &issuer, &symbol_short!("def"), &token, &investor); - assert!(env.events().all().len() > before); -} - -#[test] -fn blacklist_remove_emits_event() { - let (env, client, admin, issuer, token) = blacklist_setup(); - let investor = Address::generate(&env); - - client.initialize(&admin, &None::
, &None::); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - client.blacklist_add(&admin, &issuer, &symbol_short!("def"), &token, &investor); - let before = env.events().all().len(); - client.blacklist_remove(&issuer, &issuer, &symbol_short!("def"), &token, &investor); - assert!(env.events().all().len() > before); -} - -// ── distribution enforcement ────────────────────────────────── - -#[test] -fn blacklisted_investor_excluded_from_distribution_filter() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - let allowed = Address::generate(&env); - let blocked = Address::generate(&env); - - client.initialize(&admin, &None::
, &None::); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - - client.blacklist_add(&admin, &issuer, &symbol_short!("def"), &token, &blocked); - - let investors = [allowed.clone(), blocked.clone()]; - let eligible = investors - .iter() - .filter(|inv| !client.is_blacklisted(&issuer, &symbol_short!("def"), &token, inv)) - .count(); - - assert_eq!(eligible, 1); -} - -#[test] -fn blacklist_takes_precedence_over_whitelist() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - let investor = Address::generate(&env); - - client.initialize(&admin, &None::
, &None::); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - - client.blacklist_add(&issuer, &issuer, &symbol_short!("def"), &token, &investor); - - // Even if investor were on a whitelist, blacklist must win - assert!(client.is_blacklisted(&issuer, &symbol_short!("def"), &token, &investor)); -} - -// ── auth enforcement ────────────────────────────────────────── - -#[test] -#[ignore] -#[should_panic] -#[ignore] -fn blacklist_add_requires_auth() { - let env = Env::default(); // no mock_all_auths - let client = make_client(&env); - let bad_actor = Address::generate(&env); - let issuer = bad_actor.clone(); - let token = Address::generate(&env); - let victim = Address::generate(&env); - - let r = client.try_blacklist_add(&bad_actor, &issuer, &symbol_short!("def"), &token, &victim); - assert!(r.is_err()); -} - -#[test] -#[ignore] -#[should_panic] -#[ignore] -fn blacklist_remove_requires_auth() { - let env = Env::default(); // no mock_all_auths - let client = make_client(&env); - let bad_actor = Address::generate(&env); - let issuer = bad_actor.clone(); - let token = Address::generate(&env); - let investor = Address::generate(&env); - - let r = - client.try_blacklist_remove(&bad_actor, &issuer, &symbol_short!("def"), &token, &investor); - assert!(r.is_err()); -} - -#[test] -fn blacklist_add_requires_issuer_auth() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = Address::generate(&env); // different from admin - let non_issuer = Address::generate(&env); - - let token = Address::generate(&env); - let investor = Address::generate(&env); - - // Non-issuer cannot add to blacklist - let r = - client.try_blacklist_add(&non_issuer, &issuer, &symbol_short!("def"), &token, &investor); - assert!(r.is_err()); - assert_eq!(r.unwrap_err(), RevoraError::NotAuthorized); - - // Admin cannot add to blacklist if not issuer - let r = client.try_blacklist_add(&admin, &issuer, &symbol_short!("def"), &token, &investor); - assert!(r.is_err()); - assert_eq!(r.unwrap_err(), &RevoraError::NotAuthorized); - - // Issuer can add - let r = client.try_blacklist_add(&issuer, &issuer, &symbol_short!("def"), &token, &investor); - assert!(r.is_ok()); -} - -#[test] -fn blacklist_remove_requires_issuer_auth() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = Address::generate(&env); // different from admin - let non_issuer = Address::generate(&env); - - let token = Address::generate(&env); - let investor = Address::generate(&env); - - // First add with issuer - client.blacklist_add(&issuer, &issuer, &symbol_short!("def"), &token, &investor); - - // Non-issuer cannot remove - let r = - client.try_blacklist_remove(&non_issuer, &issuer, &symbol_short!("def"), &token, &investor); - assert!(r.is_err()); - assert_eq!(r.unwrap_err(), RevoraError::NotAuthorized); - - // Admin cannot remove if not issuer - let r = client.try_blacklist_remove(&admin, &issuer, &symbol_short!("def"), &token, &investor); - assert!(r.is_err()); - assert_eq!(r.unwrap_err(), RevoraError::NotAuthorized); - - // Issuer can remove - let r = client.try_blacklist_remove(&issuer, &issuer, &symbol_short!("def"), &token, &investor); - assert!(r.is_ok()); -} - -// ── whitelist CRUD ──────────────────────────────────────────── - -#[test] -fn whitelist_add_marks_investor_as_whitelisted() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - let investor = Address::generate(&env); - - assert!(!client.is_whitelisted(&issuer, &symbol_short!("def"), &token, &investor)); - client.whitelist_add(&admin, &issuer, &symbol_short!("def"), &token, &investor); - assert!(client.is_whitelisted(&issuer, &symbol_short!("def"), &token, &investor)); -} - -#[test] -fn whitelist_remove_unmarks_investor() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - let investor = Address::generate(&env); - - client.whitelist_add(&admin, &issuer, &symbol_short!("def"), &token, &investor); - client.whitelist_remove(&admin, &issuer, &symbol_short!("def"), &token, &investor); - assert!(!client.is_whitelisted(&issuer, &symbol_short!("def"), &token, &investor)); -} - -#[test] -fn get_whitelist_returns_all_approved_investors() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - let inv_a = Address::generate(&env); - let inv_b = Address::generate(&env); - let inv_c = Address::generate(&env); - - client.whitelist_add(&admin, &issuer, &symbol_short!("def"), &token, &inv_a); - client.whitelist_add(&admin, &issuer, &symbol_short!("def"), &token, &inv_b); - client.whitelist_add(&admin, &issuer, &symbol_short!("def"), &token, &inv_c); - - let list = client.get_whitelist(&issuer, &symbol_short!("def"), &token); - assert_eq!(list.len(), 3); - assert!(list.contains(&inv_a)); - assert!(list.contains(&inv_b)); - assert!(list.contains(&inv_c)); -} - -#[test] -fn get_whitelist_empty_before_any_add() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - - for period_id in 1..=100_u64 { - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &(period_id as i128 * 10_000), - &period_id, - &false, - ); - } - assert!(legacy_events(&env).len() >= 100); - assert_eq!(client.get_whitelist(&issuer, &symbol_short!("def"), &token).len(), 0); -} - -// ── whitelist idempotency ───────────────────────────────────── - -#[test] -fn whitelist_double_add_is_idempotent() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - let investor = Address::generate(&env); - - client.whitelist_add(&admin, &issuer, &symbol_short!("def"), &token, &investor); - client.whitelist_add(&admin, &issuer, &symbol_short!("def"), &token, &investor); - - assert_eq!(client.get_whitelist(&issuer, &symbol_short!("def"), &token).len(), 1); -} - -#[test] -fn whitelist_remove_nonexistent_is_idempotent() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - let investor = Address::generate(&env); - - client.whitelist_remove(&admin, &issuer, &symbol_short!("def"), &token, &investor); // must not panic - assert!(!client.is_whitelisted(&issuer, &symbol_short!("def"), &token, &investor)); -} - -// ── whitelist per-offering isolation ────────────────────────── - -#[test] -fn whitelist_is_scoped_per_offering() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token_a = Address::generate(&env); - let token_b = Address::generate(&env); - let investor = Address::generate(&env); - - client.whitelist_add(&admin, &issuer, &symbol_short!("def"), &token_a, &investor); - - assert!(client.is_whitelisted(&issuer, &symbol_short!("def"), &token_a, &investor)); - assert!(!client.is_whitelisted(&issuer, &symbol_short!("def"), &token_b, &investor)); -} - -#[test] -fn whitelist_removing_from_one_offering_does_not_affect_another() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token_a = Address::generate(&env); - let token_b = Address::generate(&env); - let investor = Address::generate(&env); - - client.whitelist_add(&admin, &issuer, &symbol_short!("def"), &token_a, &investor); - client.whitelist_add(&admin, &issuer, &symbol_short!("def"), &token_b, &investor); - client.whitelist_remove(&admin, &issuer, &symbol_short!("def"), &token_a, &investor); - - assert!(!client.is_whitelisted(&issuer, &symbol_short!("def"), &token_a, &investor)); - assert!(client.is_whitelisted(&issuer, &symbol_short!("def"), &token_b, &investor)); -} - -// ── whitelist event emission ────────────────────────────────── - -#[test] -fn whitelist_add_emits_event() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - let investor = Address::generate(&env); - - let before = legacy_events(&env).len(); - client.whitelist_add(&admin, &issuer, &symbol_short!("def"), &token, &investor); - assert!(legacy_events(&env).len() > before); -} - -#[test] -fn whitelist_remove_emits_event() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - let investor = Address::generate(&env); - - client.whitelist_add(&admin, &issuer, &symbol_short!("def"), &token, &investor); - let before = legacy_events(&env).len(); - client.whitelist_remove(&admin, &issuer, &symbol_short!("def"), &token, &investor); - assert!(legacy_events(&env).len() > before); -} - -// ── whitelist distribution enforcement ──────────────────────── - -#[test] -fn whitelist_enabled_only_includes_whitelisted_investors() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - let whitelisted = Address::generate(&env); - let not_listed = Address::generate(&env); - - client.whitelist_add(&admin, &issuer, &symbol_short!("def"), &token, &whitelisted); - - let investors = [whitelisted.clone(), not_listed.clone()]; - let whitelist_enabled = client.is_whitelist_enabled(&issuer, &symbol_short!("def"), &token); - - let eligible = investors - .iter() - .filter(|inv| { - let blacklisted = client.is_blacklisted(&issuer, &symbol_short!("def"), &token, inv); - let whitelisted = client.is_whitelisted(&issuer, &symbol_short!("def"), &token, inv); - - if blacklisted { - return false; - } - if whitelist_enabled { - return whitelisted; - } - true - }) - .count(); - - assert_eq!(eligible, 1); -} - -#[test] -fn whitelist_disabled_includes_all_non_blacklisted() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let token = Address::generate(&env); - let inv_a = Address::generate(&env); - let inv_b = Address::generate(&env); - let issuer = Address::generate(&env); - - // No whitelist entries - whitelist disabled - assert!(!client.is_whitelist_enabled(&issuer, &symbol_short!("def"), &token)); - - let investors = [inv_a.clone(), inv_b.clone()]; - let whitelist_enabled = client.is_whitelist_enabled(&issuer, &symbol_short!("def"), &token); - - let eligible = investors - .iter() - .filter(|inv| { - let blacklisted = client.is_blacklisted(&issuer, &symbol_short!("def"), &token, inv); - let whitelisted = client.is_whitelisted(&issuer, &symbol_short!("def"), &token, inv); - - if blacklisted { - return false; - } - if whitelist_enabled { - return whitelisted; - } - true - }) - .count(); - - assert_eq!(eligible, 2); -} - -#[test] -fn blacklist_overrides_whitelist() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - let investor = Address::generate(&env); - - client.initialize(&admin, &None::
, &None::); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &token, &0); - - // Add to both whitelist and blacklist - client.whitelist_add(&issuer, &issuer, &symbol_short!("def"), &token, &investor); - client.blacklist_add(&issuer, &issuer, &symbol_short!("def"), &token, &investor); - - // Blacklist must take precedence - let whitelist_enabled = client.is_whitelist_enabled(&issuer, &symbol_short!("def"), &token); - let is_eligible = { - let blacklisted = client.is_blacklisted(&issuer, &symbol_short!("def"), &token, &investor); - let whitelisted = client.is_whitelisted(&issuer, &symbol_short!("def"), &token, &investor); - - if blacklisted { - false - } else if whitelist_enabled { - whitelisted - } else { - true - } - }; - - assert!(!is_eligible); -} - -// ── whitelist auth enforcement ──────────────────────────────── - -#[test] -#[ignore] -#[should_panic] -#[ignore] -fn whitelist_add_requires_auth() { - let env = Env::default(); // no mock_all_auths - let client = make_client(&env); - let bad_actor = Address::generate(&env); - let issuer = bad_actor.clone(); - - let token = Address::generate(&env); - let investor = Address::generate(&env); - - let r = client.try_whitelist_add(&bad_actor, &issuer, &symbol_short!("def"), &token, &investor); - assert!(r.is_err()); -} - -#[test] -#[ignore] -#[should_panic] -#[ignore] -fn whitelist_remove_requires_auth() { - let env = Env::default(); // no mock_all_auths - let client = make_client(&env); - let bad_actor = Address::generate(&env); - let issuer = bad_actor.clone(); - - let token = Address::generate(&env); - let investor = Address::generate(&env); - - let r = - client.try_whitelist_remove(&bad_actor, &issuer, &symbol_short!("def"), &token, &investor); - assert!(r.is_err()); -} - -// ── large whitelist handling ────────────────────────────────── - -#[test] -fn large_whitelist_operations() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - - // Add 50 investors to whitelist - let mut investors = soroban_sdk::Vec::new(&env); - for _ in 0..50 { - let inv = Address::generate(&env); - client.whitelist_add(&admin, &issuer, &symbol_short!("def"), &token, &inv); - investors.push_back(inv); - } - - let whitelist = client.get_whitelist(&issuer, &symbol_short!("def"), &token); - assert_eq!(whitelist.len(), 50); - - // Verify all are whitelisted - for i in 0..investors.len() { - assert!(client.is_whitelisted( - &issuer, - &symbol_short!("def"), - &token, - &investors.get(i).unwrap() - )); - } -} - -// ── repeated operations on same address ─────────────────────── - -#[test] -fn repeated_whitelist_operations_on_same_address() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - let investor = Address::generate(&env); - - // Add, remove, add again - client.whitelist_add(&admin, &issuer, &symbol_short!("def"), &token, &investor); - assert!(client.is_whitelisted(&issuer, &symbol_short!("def"), &token, &investor)); - - client.whitelist_remove(&admin, &issuer, &symbol_short!("def"), &token, &investor); - assert!(!client.is_whitelisted(&issuer, &symbol_short!("def"), &token, &investor)); - - client.whitelist_add(&admin, &issuer, &symbol_short!("def"), &token, &investor); - assert!(client.is_whitelisted(&issuer, &symbol_short!("def"), &token, &investor)); -} - -// ── whitelist enabled state ─────────────────────────────────── - -#[test] -fn whitelist_enabled_when_non_empty() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - let investor = Address::generate(&env); - - assert!(!client.is_whitelist_enabled(&issuer, &symbol_short!("def"), &token)); - - client.whitelist_add(&admin, &issuer, &symbol_short!("def"), &token, &investor); - assert!(client.is_whitelist_enabled(&issuer, &symbol_short!("def"), &token)); - - client.whitelist_remove(&admin, &issuer, &symbol_short!("def"), &token, &investor); - assert!(!client.is_whitelist_enabled(&issuer, &symbol_short!("def"), &token)); -} - -// ── structured error codes (#41) ────────────────────────────── - -#[test] -fn register_offering_rejects_bps_over_10000() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - - let result = client.try_register_offering( - &issuer, - &symbol_short!("def"), - &token, - &10_001, - &payout_asset, - &0, - ); - assert!( - result.is_err(), - "contract must return Err(RevoraError::InvalidRevenueShareBps) for bps > 10000" - ); - assert_eq!(RevoraError::InvalidRevenueShareBps as u32, 1, "error code for integrators"); -} - -#[test] -fn register_offering_accepts_bps_exactly_10000() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - - let result = client.try_register_offering( - &issuer, - &symbol_short!("def"), - &token, - &10_000, - &payout_asset, - &0, - ); - assert!(result.is_ok()); -} - -// ── revenue index ───────────────────────────────────────────── - -#[test] -fn single_report_is_persisted() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &token, &0); - client.report_revenue(&issuer, &symbol_short!("def"), &token, &token, &5_000, &1, &false); - assert_eq!(client.get_revenue_by_period(&issuer, &symbol_short!("def"), &token, &1), 5_000); -} - -#[test] -fn storage_stress_many_offerings_no_panic() { - let env = Env::default(); - let (client, issuer) = setup(&env); - register_n(&env, &client, &issuer, STORAGE_STRESS_OFFERING_COUNT); - let count = client.get_offering_count(&issuer, &symbol_short!("def")); - assert_eq!(count, STORAGE_STRESS_OFFERING_COUNT); - let (page, cursor) = client.get_offerings_page( - &issuer, - &symbol_short!("def"), - &(STORAGE_STRESS_OFFERING_COUNT - 5), - &10, - ); - assert_eq!(page.len(), 5); - assert_eq!(cursor, None); -} - -#[test] -fn multiple_reports_same_period_accumulate() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - - for period_id in 1..=100_u64 { - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &(period_id as i128 * 10_000), - &period_id, - &false, - ); - } - assert!(legacy_events(&env).len() >= 100); -} - -#[test] -fn multiple_reports_same_period_accumulate_is_disabled() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &3_000, - &7, - &false, - ); - // Second report without override is rejected; first value persists - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &2_000, - &7, - &false, - ); - assert_eq!(client.get_revenue_by_period(&issuer, &symbol_short!("def"), &token, &7), 3_000); -} - -#[test] -fn empty_period_returns_zero() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let token = Address::generate(&env); - - let issuer = Address::generate(&env); - assert_eq!(client.get_revenue_by_period(&issuer, &symbol_short!("def"), &token, &99), 0); -} - -#[test] -fn get_revenue_range_sums_periods() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &payout_asset, &0); - client.report_revenue(&issuer, &symbol_short!("def"), &token, &payout_asset, &100, &1, &false); - client.report_revenue(&issuer, &symbol_short!("def"), &token, &payout_asset, &200, &2, &false); - assert_eq!(client.get_revenue_range(&issuer, &symbol_short!("def"), &token, &1, &2), 300); -} - -#[test] -fn gas_characterization_many_offerings_single_issuer() { - let env = Env::default(); - let (client, issuer) = setup(&env); - let n = 50_u32; - register_n(&env, &client, &issuer, n); - - let (page, _) = client.get_offerings_page(&issuer, &symbol_short!("def"), &0, &20); - assert_eq!(page.len(), 20); -} - -#[test] -fn gas_characterization_report_revenue_with_large_blacklist() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &500, &payout_asset, &0); - - for _ in 0..30 { - client.blacklist_add( - &Address::generate(&env), - &issuer, - &symbol_short!("def"), - &token, - &Address::generate(&env), - ); - } - let admin = Address::generate(&env); - let issuer = admin.clone(); - - env.mock_all_auths(); - client.blacklist_add(&issuer, &issuer, &symbol_short!("def"), &token, &Address::generate(&env)); - - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &1_000_000, - &1, - &false, - ); - assert!(!legacy_events(&env).is_empty()); -} - -#[test] -fn revenue_matches_event_amount() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let amount: i128 = 42_000; - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &token, &0); - client.report_revenue(&issuer, &symbol_short!("def"), &token, &token, &amount, &5, &false); - - assert_eq!(client.get_revenue_by_period(&issuer, &symbol_short!("def"), &token, &5), amount); - assert!(!legacy_events(&env).is_empty()); -} - -#[test] -fn large_period_range_sums_correctly() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &token, &0); - client.report_revenue(&issuer, &symbol_short!("def"), &token, &token, &1_000, &1, &false); -} - -// --------------------------------------------------------------------------- -// Holder concentration guardrail (#26) -// --------------------------------------------------------------------------- - -#[test] -fn concentration_limit_not_set_allows_report_revenue() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &1_000, - &1, - &false, - ); -} - -#[test] -fn set_concentration_limit_requires_offering_to_exist() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - // No offering registered - let r = - client.try_set_concentration_limit(&issuer, &symbol_short!("def"), &token, &5000, &false); - assert!(r.is_err()); -} - -#[test] -fn set_concentration_limit_stores_config() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - client.set_concentration_limit(&issuer, &symbol_short!("def"), &token, &5000, &false); - let config = client.get_concentration_limit(&issuer, &symbol_short!("def"), &token); - assert_eq!(config.clone().unwrap().max_bps, 5000); - assert!(!config.clone().unwrap().enforce); - let cfg = config.unwrap(); - assert_eq!(cfg.max_bps, 5000); - assert!(!cfg.enforce); -} - -#[test] -fn set_concentration_limit_bounds_check() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - - let res = - client.try_set_concentration_limit(&issuer, &symbol_short!("def"), &token, &10001, &false); - assert!(res.is_err()); -} - -#[test] -fn report_concentration_bounds_check() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - - let res = client.try_report_concentration(&issuer, &symbol_short!("def"), &token, &10001); - assert!(res.is_err()); -} - -#[test] -fn set_concentration_limit_respects_pause() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let issuer = admin.clone(); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.initialize(&admin, &None, &None::); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - - client.pause_admin(&admin); - let res = - client.try_set_concentration_limit(&issuer, &symbol_short!("def"), &token, &5000, &false); - assert!(res.is_err()); -} - -#[test] -fn report_concentration_respects_pause() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let issuer = admin.clone(); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.initialize(&admin, &None, &None::); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - - client.pause_admin(&admin); - let res = client.try_report_concentration(&issuer, &symbol_short!("def"), &token, &5000); - assert!(res.is_err()); -} - -#[test] -fn report_concentration_emits_audit_event() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - - let before = env.events().all().len(); - client.report_concentration(&issuer, &symbol_short!("def"), &token, &3000); - - let events = env.events().all(); - assert!(events.len() > before); -} - -#[test] -fn report_concentration_emits_warning_when_over_limit() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - client.set_concentration_limit(&issuer, &symbol_short!("def"), &token, &5000, &false); - let before = env.events().all().len(); - client.report_concentration(&issuer, &symbol_short!("def"), &token, &6000); - assert!(env.events().all().len() > before); - assert_eq!( - client.get_current_concentration(&issuer, &symbol_short!("def"), &token), - Some(6000) - ); -} - -#[test] -fn report_concentration_no_warning_when_below_limit() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - client.set_concentration_limit(&issuer, &symbol_short!("def"), &token, &5000, &false); - client.report_concentration(&issuer, &symbol_short!("def"), &token, &4000); - assert_eq!( - client.get_current_concentration(&issuer, &symbol_short!("def"), &token), - Some(4000) - ); -} - -#[test] -fn concentration_enforce_blocks_report_revenue_when_over_limit() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - client.set_concentration_limit(&issuer, &symbol_short!("def"), &token, &5000, &true); - client.report_concentration(&issuer, &symbol_short!("def"), &token, &6000); - let r = client.try_report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &1_000, - &1, - &false, - ); - assert!( - r.is_err(), - "report_revenue must fail when concentration exceeds limit with enforce=true" - ); -} - -#[test] -fn concentration_enforce_allows_report_revenue_when_at_or_below_limit() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - client.set_concentration_limit(&issuer, &symbol_short!("def"), &token, &5000, &true); - client.report_concentration(&issuer, &symbol_short!("def"), &token, &5000); - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &1_000, - &1, - &false, - ); - client.report_concentration(&issuer, &symbol_short!("def"), &token, &4999); - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &1_000, - &2, - &false, - ); -} - -#[test] -fn concentration_near_threshold_boundary() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - client.set_concentration_limit(&issuer, &symbol_short!("def"), &token, &5000, &true); - client.report_concentration(&issuer, &symbol_short!("def"), &token, &5001); - - assert!(client - .try_report_revenue(&issuer, &symbol_short!("def"), &token, &token, &1_000, &1, &false) - .is_err()); - - assert!(client - .try_report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &1_000, - &1, - &false - ) - .is_err()); -} - -// --------------------------------------------------------------------------- -// On-chain audit log summary (#34) -// --------------------------------------------------------------------------- - -#[test] -fn audit_summary_empty_before_any_report() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - let summary = client.get_audit_summary(&issuer, &symbol_short!("def"), &token); - assert!(summary.is_none()); -} - -#[test] -fn audit_summary_aggregates_revenue_and_count() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - client.report_revenue(&issuer, &symbol_short!("def"), &token, &payout_asset, &100, &1, &false); - client.report_revenue(&issuer, &symbol_short!("def"), &token, &payout_asset, &200, &2, &false); - client.report_revenue(&issuer, &symbol_short!("def"), &token, &payout_asset, &300, &3, &false); - let summary = client.get_audit_summary(&issuer, &symbol_short!("def"), &token); - assert_eq!(summary.clone().unwrap().total_revenue, 600); - assert_eq!(summary.clone().unwrap().report_count, 3); - let s = summary.unwrap(); - assert_eq!(s.total_revenue, 600); - assert_eq!(s.report_count, 3); -} - -#[test] -fn audit_summary_per_offering_isolation() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token_a = Address::generate(&env); - let token_b = Address::generate(&env); - let payout_asset_a = Address::generate(&env); - let payout_asset_b = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token_a, &1_000, &payout_asset_a, &0); - client.register_offering(&issuer, &symbol_short!("def"), &token_b, &1_000, &payout_asset_b, &0); - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token_a, - &payout_asset_a, - &1000, - &1, - &false, - ); - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token_b, - &payout_asset_b, - &2000, - &1, - &false, - ); - let sum_a = client.get_audit_summary(&issuer, &symbol_short!("def"), &token_a); - let sum_b = client.get_audit_summary(&issuer, &symbol_short!("def"), &token_b); - assert_eq!(sum_a.clone().unwrap().total_revenue, 1000); - assert_eq!(sum_a.clone().unwrap().report_count, 1); - assert_eq!(sum_b.clone().unwrap().total_revenue, 2000); - assert_eq!(sum_b.clone().unwrap().report_count, 1); - let a = sum_a.unwrap(); - let b = sum_b.unwrap(); - assert_eq!(a.total_revenue, 1000); - assert_eq!(a.report_count, 1); - assert_eq!(b.total_revenue, 2000); - assert_eq!(b.report_count, 1); -} - -// --------------------------------------------------------------------------- -// Configurable rounding modes (#44) -// --------------------------------------------------------------------------- - -#[test] -fn compute_share_truncation() { - let env = Env::default(); - let client = make_client(&env); - // 1000 * 2500 / 10000 = 250 - let share = client.compute_share(&1000, &2500, &RoundingMode::Truncation); - assert_eq!(share, 250); -} - -#[test] -fn compute_share_round_half_up() { - let env = Env::default(); - let client = make_client(&env); - // 1000 * 2500 = 2_500_000; half-up: (2_500_000 + 5000) / 10000 = 250 - let share = client.compute_share(&1000, &2500, &RoundingMode::RoundHalfUp); - assert_eq!(share, 250); -} - -#[test] -fn compute_share_round_half_up_rounds_up_at_half() { - let env = Env::default(); - let client = make_client(&env); - // 1 * 2500 = 2500; 2500/10000 trunc = 0; half-up (2500+5000)/10000 = 0.75 -> 0? No: (2500+5000)/10000 = 7500/10000 = 0. So 1 bps would be 1*100/10000 = 0.01 -> 0 trunc, round half up (100+5000)/10000 = 0.51 -> 1. So 1 * 100 = 100, (100+5000)/10000 = 0. - // 3 * 3333 = 9999; 9999/10000 = 0 trunc. (9999+5000)/10000 = 14999/10000 = 1 round half up. - let share_trunc = client.compute_share(&3, &3333, &RoundingMode::Truncation); - let share_half = client.compute_share(&3, &3333, &RoundingMode::RoundHalfUp); - assert_eq!(share_trunc, 0); - assert_eq!(share_half, 1); -} - -#[test] -fn compute_share_bps_over_10000_returns_zero() { - let env = Env::default(); - let client = make_client(&env); - let share = client.compute_share(&1000, &10_001, &RoundingMode::Truncation); - assert_eq!(share, 0); -} - -#[test] -fn set_and_get_rounding_mode() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &token, &0); - assert_eq!( - client.get_rounding_mode(&issuer, &symbol_short!("def"), &token), - RoundingMode::Truncation - ); - - let payout_asset = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - assert_eq!( - client.get_rounding_mode(&issuer, &symbol_short!("def"), &token), - RoundingMode::Truncation - ); - - client.set_rounding_mode(&issuer, &symbol_short!("def"), &token, &RoundingMode::RoundHalfUp); - assert_eq!( - client.get_rounding_mode(&issuer, &symbol_short!("def"), &token), - RoundingMode::RoundHalfUp - ); -} - -#[test] -fn set_rounding_mode_requires_offering() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let r = client.try_set_rounding_mode( - &issuer, - &symbol_short!("def"), - &token, - &RoundingMode::RoundHalfUp, - ); - assert!(r.is_err()); -} - -#[test] -fn compute_share_tiny_payout_truncation() { - let env = Env::default(); - let client = make_client(&env); - let share = client.compute_share(&1, &1, &RoundingMode::Truncation); - assert_eq!(share, 0); -} - -#[test] -fn compute_share_no_overflow_bounds() { - let env = Env::default(); - let client = make_client(&env); - let amount = 1_000_000_i128; - let share = client.compute_share(&amount, &10_000, &RoundingMode::Truncation); - assert_eq!(share, amount); - let share2 = client.compute_share(&amount, &10_000, &RoundingMode::RoundHalfUp); - assert_eq!(share2, amount); -} - -// =========================================================================== -// Multi-period aggregated claim tests -// =========================================================================== - -/// Helper: create a Stellar Asset Contract for testing token transfers. -/// Returns (token_contract_address, admin_address). -fn create_payment_token(env: &Env) -> (Address, Address) { - let admin = Address::generate(env); - let token_id = env.register_stellar_asset_contract(admin.clone()); - (token_id, admin) -} - -/// Mint `amount` of payment token to `recipient`. -fn mint_tokens( - env: &Env, - payment_token: &Address, - admin: &Address, - recipient: &Address, - amount: &i128, -) { - let _ = admin; - token::StellarAssetClient::new(env, payment_token).mint(recipient, amount); -} - -/// Check balance of `who` for `payment_token`. -fn balance(env: &Env, payment_token: &Address, who: &Address) -> i128 { - token::Client::new(env, payment_token).balance(who) -} - -/// Full setup for claim tests: env, client, issuer, offering token, payment token, contract addr. -fn claim_setup() -> (Env, RevoraRevenueShareClient<'static>, Address, Address, Address, Address) { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let (payment_token, pt_admin) = create_payment_token(&env); - - // Register offering - client.register_offering(&issuer, &symbol_short!("def"), &token, &5_000, &payment_token, &0); // 50% revenue share - - // Mint payment tokens to the issuer so they can deposit - mint_tokens(&env, &payment_token, &pt_admin, &issuer, &10_000_000); - - (env, client, issuer, token, payment_token, contract_id) -} - -// ── deposit_revenue tests ───────────────────────────────────── - -#[test] -#[ignore] -fn deposit_revenue_stores_period_data() { - let (env, client, issuer, token, payment_token, contract_id) = claim_setup(); - - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - - assert_eq!(client.get_period_count(&issuer, &symbol_short!("def"), &token), 1); - // Contract should hold the deposited tokens - assert_eq!(balance(&env, &payment_token, &contract_id), 100_000); -} - -#[test] -#[ignore] -fn deposit_revenue_multiple_periods() { - let (_env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &200_000, &2); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &300_000, &3); - - assert_eq!(client.get_period_count(&issuer, &symbol_short!("def"), &token), 3); -} - -#[test] -fn deposit_revenue_fails_for_nonexistent_offering() { - let (env, client, issuer, _token, payment_token, _contract_id) = claim_setup(); - let unknown_token = Address::generate(&env); - - let result = client.try_deposit_revenue( - &issuer, - &symbol_short!("def"), - &unknown_token, - &payment_token, - &100_000, - &1, - ); - assert!(result.is_err()); -} - -#[test] -#[ignore] -fn deposit_revenue_fails_for_duplicate_period() { - let (_env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - let result = client.try_deposit_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &100_000, - &1, - ); - assert!(result.is_err()); -} - -#[test] -#[ignore] -fn deposit_revenue_fails_for_payment_token_mismatch() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &200_000, &2); - assert_eq!( - client.get_payment_token(&issuer, &symbol_short!("def"), &token), - Some(payment_token) - ); -} - -#[test] -fn report_revenue_rejects_mismatched_payout_asset() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - let wrong_asset = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - let r = client.try_report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &wrong_asset, - &1_000, - &1, - &false, - ); - assert!(r.is_err()); -} - -#[test] -#[ignore] -fn deposit_revenue_rejects_mismatched_payout_asset_on_first_deposit() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let issuer = Address::generate(&env); - let offering_token = Address::generate(&env); - let (configured_asset, configured_admin) = create_payment_token(&env); - - client.register_offering( - &issuer, - &symbol_short!("def"), - &offering_token, - &5_000, - &configured_asset, - &0, - ); - mint_tokens(&env, &configured_asset, &configured_admin, &issuer, &1_000_000); - - client.deposit_revenue( - &issuer, - &symbol_short!("def"), - &offering_token, - &configured_asset, - &100_000, - &1, - ); - assert_eq!(client.get_period_count(&issuer, &symbol_short!("def"), &offering_token), 1); - assert_eq!( - client.get_payment_token(&issuer, &symbol_short!("def"), &offering_token), - Some(configured_asset) - ); -} - -#[test] -fn snapshot_deposit_preserves_registered_payment_token_lock() { - let (_env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - - client.set_snapshot_config(&issuer, &symbol_short!("def"), &token, &true); - - client.deposit_revenue_with_snapshot( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &100_000, - &1, - &42, - ); - assert_eq!( - client.get_payment_token(&issuer, &symbol_short!("def"), &token), - Some(payment_token) - ); -} - -#[test] -#[ignore] -fn deposit_revenue_emits_event() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - - let before = legacy_events(&env).len(); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - assert!(legacy_events(&env).len() > before); -} - -#[test] -#[ignore] -fn deposit_revenue_transfers_tokens() { - let (env, client, issuer, token, payment_token, contract_id) = claim_setup(); - - let issuer_balance_before = balance(&env, &payment_token, &issuer); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - - assert_eq!(balance(&env, &payment_token, &issuer), issuer_balance_before - 100_000); - assert_eq!(balance(&env, &payment_token, &contract_id), 100_000); -} - -#[test] -#[ignore] -fn deposit_revenue_sparse_period_ids() { - let (_env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - - // Deposit with non-sequential period IDs - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &10); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &200_000, &50); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &300_000, &100); - - assert_eq!(client.get_period_count(&issuer, &symbol_short!("def"), &token), 3); -} - -#[test] -#[ignore] -#[should_panic] -#[ignore] -fn deposit_revenue_requires_auth() { - let env = Env::default(); - let cid = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &cid); - let issuer = Address::generate(&env); - let tok = Address::generate(&env); - // No mock_all_auths — should panic on require_auth - let r = client.try_deposit_revenue( - &issuer, - &symbol_short!("def"), - &tok, - &Address::generate(&env), - &100, - &1, - ); - assert!(r.is_err()); -} - -// ── set_holder_share tests ──────────────────────────────────── - -#[test] -fn set_holder_share_stores_share() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &2_500); // 25% - assert_eq!(client.get_holder_share(&issuer, &symbol_short!("def"), &token, &holder), 2_500); -} - -#[test] -fn set_holder_share_updates_existing() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &2_500); - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &5_000); - assert_eq!(client.get_holder_share(&issuer, &symbol_short!("def"), &token, &holder), 5_000); -} - -#[test] -fn set_holder_share_fails_for_nonexistent_offering() { - let (env, client, issuer, _token, _payment_token, _contract_id) = claim_setup(); - let unknown_token = Address::generate(&env); - let holder = Address::generate(&env); - - let result = client.try_set_holder_share( - &issuer, - &symbol_short!("def"), - &unknown_token, - &holder, - &2_500, - ); - assert!(result.is_err()); -} - -#[test] -fn set_holder_share_fails_for_bps_over_10000() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - let result = - client.try_set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &10_001); - assert!(result.is_err()); -} - -#[test] -fn set_holder_share_accepts_bps_exactly_10000() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - let result = - client.try_set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &10_000); - assert!(result.is_ok()); - assert_eq!(client.get_holder_share(&issuer, &symbol_short!("def"), &token, &holder), 10_000); -} - -#[test] -fn set_holder_share_emits_event() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - let before = legacy_events(&env).len(); - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &2_500); - assert!(legacy_events(&env).len() > before); -} - -#[test] -fn get_holder_share_returns_zero_for_unknown() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let unknown = Address::generate(&env); - assert_eq!(client.get_holder_share(&issuer, &symbol_short!("def"), &token, &unknown), 0); -} - -// ── Share-sum invariant tests ───────────────────────────────── -// -// Security assumption: the sum of all holder share_bps for a single offering -// must never exceed 10 000 bps (100 %). These tests verify that the invariant -// is enforced on every write path and that the aggregate counter is always -// consistent with the individual holder values. - -/// Initial state: no shares set → total is 0. -#[test] -fn share_sum_starts_at_zero() { - let (env, client, issuer, token, _pt, _cid) = claim_setup(); - assert_eq!(client.get_total_share_bps(&issuer, &symbol_short!("def"), &token), 0); -} - -/// Setting a single holder's share updates the aggregate correctly. -#[test] -fn share_sum_reflects_single_holder() { - let (env, client, issuer, token, _pt, _cid) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &3_000); - assert_eq!(client.get_total_share_bps(&issuer, &symbol_short!("def"), &token), 3_000); -} - -/// Multiple holders: aggregate equals the sum of individual shares. -#[test] -fn share_sum_accumulates_across_holders() { - let (env, client, issuer, token, _pt, _cid) = claim_setup(); - let h1 = Address::generate(&env); - let h2 = Address::generate(&env); - let h3 = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &h1, &3_000); - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &h2, &4_000); - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &h3, &2_000); - - assert_eq!(client.get_total_share_bps(&issuer, &symbol_short!("def"), &token), 9_000); -} - -/// Aggregate exactly at the ceiling (10 000) is accepted. -#[test] -fn share_sum_at_ceiling_is_accepted() { - let (env, client, issuer, token, _pt, _cid) = claim_setup(); - let h1 = Address::generate(&env); - let h2 = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &h1, &6_000); - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &h2, &4_000); - - assert_eq!(client.get_total_share_bps(&issuer, &symbol_short!("def"), &token), 10_000); -} - -/// Adding a share that would push the sum above 10 000 is rejected with ShareSumExceeded. -#[test] -fn share_sum_rejects_overflow_by_one() { - let (env, client, issuer, token, _pt, _cid) = claim_setup(); - let h1 = Address::generate(&env); - let h2 = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &h1, &10_000); - let result = client.try_set_holder_share(&issuer, &symbol_short!("def"), &token, &h2, &1); - assert_eq!(result, Err(Ok(RevoraError::ShareSumExceeded))); -} - -/// Aggregate is unchanged after a rejected write. -#[test] -fn share_sum_unchanged_after_rejected_write() { - let (env, client, issuer, token, _pt, _cid) = claim_setup(); - let h1 = Address::generate(&env); - let h2 = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &h1, &9_000); - let _ = client.try_set_holder_share(&issuer, &symbol_short!("def"), &token, &h2, &1_001); - - // Sum must still be 9 000 — the failed write must not have mutated state. - assert_eq!(client.get_total_share_bps(&issuer, &symbol_short!("def"), &token), 9_000); - // h2 must still have no share. - assert_eq!(client.get_holder_share(&issuer, &symbol_short!("def"), &token, &h2), 0); -} - -/// Updating an existing holder's share adjusts the aggregate by the delta (not by the new value). -#[test] -fn share_sum_delta_on_update() { - let (env, client, issuer, token, _pt, _cid) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &4_000); - assert_eq!(client.get_total_share_bps(&issuer, &symbol_short!("def"), &token), 4_000); - - // Increase: 4 000 → 7 000; delta = +3 000 - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &7_000); - assert_eq!(client.get_total_share_bps(&issuer, &symbol_short!("def"), &token), 7_000); - - // Decrease: 7 000 → 2 000; delta = -5 000 - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &2_000); - assert_eq!(client.get_total_share_bps(&issuer, &symbol_short!("def"), &token), 2_000); -} - -/// Reducing one holder's share makes room for another holder to be added. -#[test] -fn share_sum_reduce_then_add_succeeds() { - let (env, client, issuer, token, _pt, _cid) = claim_setup(); - let h1 = Address::generate(&env); - let h2 = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &h1, &10_000); - // h2 cannot be added yet - assert!(client.try_set_holder_share(&issuer, &symbol_short!("def"), &token, &h2, &1).is_err()); - - // Reduce h1 to make room - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &h1, &5_000); - // Now h2 can take up to 5 000 - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &h2, &5_000); - assert_eq!(client.get_total_share_bps(&issuer, &symbol_short!("def"), &token), 10_000); -} - -/// Setting a holder's share to 0 removes their contribution from the aggregate. -#[test] -fn share_sum_zero_share_removes_contribution() { - let (env, client, issuer, token, _pt, _cid) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &5_000); - assert_eq!(client.get_total_share_bps(&issuer, &symbol_short!("def"), &token), 5_000); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &0); - assert_eq!(client.get_total_share_bps(&issuer, &symbol_short!("def"), &token), 0); -} - -/// Share-sum invariant is scoped per offering: two offerings with the same issuer -/// do not share a counter. -#[test] -fn share_sum_is_scoped_per_offering() { - let (env, client, issuer, token_a, _pt, _cid) = claim_setup(); - let token_b = Address::generate(&env); - let payout_b = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token_b, &1_000, &payout_b, &0); - - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token_a, &holder, &8_000); - client.set_holder_share(&issuer, &symbol_short!("def"), &token_b, &holder, &9_000); - - assert_eq!(client.get_total_share_bps(&issuer, &symbol_short!("def"), &token_a), 8_000); - assert_eq!(client.get_total_share_bps(&issuer, &symbol_short!("def"), &token_b), 9_000); -} - -/// share_sum event is emitted on every successful write. -#[test] -fn share_sum_event_emitted_on_set() { - let (env, client, issuer, token, _pt, _cid) = claim_setup(); - let holder = Address::generate(&env); - - let before = env.events().all().len(); - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &2_000); - // At minimum the share_set and share_sum events must have been emitted. - assert!(env.events().all().len() >= before + 2); -} - -/// Abuse path: a single holder cannot claim 100 % and then a second holder -/// is silently added — the invariant blocks it. -#[test] -fn share_sum_abuse_second_holder_after_full_allocation() { - let (env, client, issuer, token, _pt, _cid) = claim_setup(); - let attacker = Address::generate(&env); - let victim = Address::generate(&env); - - // Attacker takes 100 % - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &attacker, &10_000); - - // Any attempt to add a second holder must fail - let r1 = client.try_set_holder_share(&issuer, &symbol_short!("def"), &token, &victim, &1); - let r2 = client.try_set_holder_share(&issuer, &symbol_short!("def"), &token, &victim, &5_000); - let r3 = client.try_set_holder_share(&issuer, &symbol_short!("def"), &token, &victim, &10_000); - - assert_eq!(r1, Err(Ok(RevoraError::ShareSumExceeded))); - assert_eq!(r2, Err(Ok(RevoraError::ShareSumExceeded))); - assert_eq!(r3, Err(Ok(RevoraError::ShareSumExceeded))); - - // Aggregate must remain exactly 10 000 - assert_eq!(client.get_total_share_bps(&issuer, &symbol_short!("def"), &token), 10_000); -} - -// ── claim tests (core multi-period aggregation) ─────────────── - -#[test] -#[ignore] -fn claim_single_period() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &5_000); // 50% - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - - let payout = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout, 50_000); // 50% of 100_000 - assert_eq!(balance(&env, &payment_token, &holder), 50_000); -} - -#[test] -#[ignore] -fn claim_multiple_periods_aggregated() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &2_000); // 20% - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &200_000, &2); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &300_000, &3); - - // Claim all 3 periods in one transaction - // 20% of (100k + 200k + 300k) = 20% of 600k = 120k - let payout = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout, 120_000); - assert_eq!(balance(&env, &payment_token, &holder), 120_000); -} - -#[test] -#[ignore] -fn claim_max_periods_zero_claims_all() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &10_000); // 100% - for i in 1..=5_u64 { - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &10_000, &i); - } - - let payout = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout, 50_000); // 100% of 5 * 10k -} - -#[test] -#[ignore] -fn claim_partial_then_rest() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &10_000); // 100% - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &200_000, &2); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &300_000, &3); - - // Claim first 2 periods - let payout1 = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &2); - assert_eq!(payout1, 300_000); // 100k + 200k - - // Claim remaining period - let payout2 = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout2, 300_000); // 300k - - assert_eq!(balance(&env, &payment_token, &holder), 600_000); -} - -#[test] -#[ignore] -fn claim_no_double_counting() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &10_000); // 100% - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - - let payout1 = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout1, 100_000); - - // Second claim should fail - nothing pending - let result = client.try_claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert!(result.is_err()); -} - -#[test] -#[ignore] -fn claim_advances_index_correctly() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &5_000); // 50% - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &200_000, &2); - - // Claim period 1 only - client.claim(&holder, &issuer, &symbol_short!("def"), &token, &1); - - // Deposit another period - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &400_000, &3); - - // Claim remaining - should get periods 2 and 3 only - let payout2 = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout2, 300_000); // 50% of (200k + 400k) -} - -#[test] -#[ignore] -fn claim_emits_event() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &5_000); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - - let before = legacy_events(&env).len(); - client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert!(legacy_events(&env).len() > before); -} - -#[test] -#[ignore] -fn claim_fails_for_blacklisted_holder() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &5_000); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - - // Blacklist the holder - client.blacklist_add(&issuer, &issuer, &symbol_short!("def"), &token, &holder); - - let result = client.try_claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert!(result.is_err()); -} - -#[test] -fn claim_fails_when_no_pending_periods() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &5_000); - // No deposits made - let result = client.try_claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert!(result.is_err()); -} - -#[test] -#[ignore] -fn claim_fails_for_zero_share_holder() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - // Don't set any share - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - - let result = client.try_claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert!(result.is_err()); -} - -#[test] -#[ignore] -fn claim_sparse_period_ids() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &10_000); // 100% - - // Non-sequential period IDs - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &50_000, &10); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &75_000, &50); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &125_000, &100); - - let payout = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout, 250_000); // 50k + 75k + 125k -} - -#[test] -#[ignore] -fn claim_multiple_holders_same_periods() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder_a = Address::generate(&env); - let holder_b = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder_a, &3_000); // 30% - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder_b, &2_000); // 20% - - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &200_000, &2); - - let payout_a = client.claim(&holder_a, &issuer, &symbol_short!("def"), &token, &0); - let payout_b = client.claim(&holder_b, &issuer, &symbol_short!("def"), &token, &0); - - // A: 30% of 300k = 90k; B: 20% of 300k = 60k - assert_eq!(payout_a, 90_000); - assert_eq!(payout_b, 60_000); - assert_eq!(balance(&env, &payment_token, &holder_a), 90_000); - assert_eq!(balance(&env, &payment_token, &holder_b), 60_000); -} - -#[test] -#[ignore] -fn claim_with_max_periods_cap() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &10_000); // 100% - - // Deposit 5 periods - for i in 1..=5_u64 { - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &10_000, &i); - } - - // Claim only 3 at a time - let payout1 = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &3); - assert_eq!(payout1, 30_000); - - let payout2 = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &3); - assert_eq!(payout2, 20_000); // only 2 remaining - - // No more pending - let result = client.try_claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert!(result.is_err()); -} - -#[test] -#[ignore] -fn claim_zero_revenue_periods_still_advance() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &10_000); // 100% - - // Deposit minimal-value periods then a larger one (#35: amount must be > 0). - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &1, &1); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &1, &2); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &3); - - // Claim first 2 (minimal value) - payout is 2 (1+1) but index advances - let payout1 = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &2); - assert_eq!(payout1, 2); - - // Now claim the remaining period - let payout2 = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout2, 100_000); -} - -#[test] -#[ignore] -#[should_panic] -#[ignore] -fn claim_requires_auth() { - let env = Env::default(); - let cid = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &cid); - let holder = Address::generate(&env); - // No mock_all_auths — should panic on require_auth - let r = client.try_claim( - &holder, - &Address::generate(&env), - &symbol_short!("def"), - &Address::generate(&env), - &0, - ); - assert!(r.is_err()); -} - -// ── view function tests ─────────────────────────────────────── - -#[test] -fn get_pending_periods_returns_unclaimed() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &5_000); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &10); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &200_000, &20); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &300_000, &30); - - let pending = client.get_pending_periods(&issuer, &symbol_short!("def"), &token, &holder); - assert_eq!(pending.len(), 3); - assert_eq!(pending.get(0).unwrap(), 10); - assert_eq!(pending.get(1).unwrap(), 20); - assert_eq!(pending.get(2).unwrap(), 30); -} - -#[test] -fn get_pending_periods_after_partial_claim() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &5_000); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &200_000, &2); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &300_000, &3); - - // Claim first 2 - client.claim(&holder, &issuer, &symbol_short!("def"), &token, &2); - - let pending = client.get_pending_periods(&issuer, &symbol_short!("def"), &token, &holder); - assert_eq!(pending.len(), 1); - assert_eq!(pending.get(0).unwrap(), 3); -} - -#[test] -fn get_pending_periods_empty_after_full_claim() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &5_000); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - - client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - - let pending = client.get_pending_periods(&issuer, &symbol_short!("def"), &token, &holder); - assert_eq!(pending.len(), 0); -} - -#[test] -fn get_pending_periods_empty_for_new_holder() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let unknown = Address::generate(&env); - - let pending = client.get_pending_periods(&issuer, &symbol_short!("def"), &token, &unknown); - assert_eq!(pending.len(), 0); -} - -#[test] -fn get_claimable_returns_correct_amount() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &2_500); // 25% - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &200_000, &2); - - let claimable = client.get_claimable(&issuer, &symbol_short!("def"), &token, &holder); - assert_eq!(claimable, 75_000); // 25% of 300k -} - -#[test] -fn get_claimable_after_partial_claim() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &10_000); // 100% - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &200_000, &2); - - client.claim(&holder, &issuer, &symbol_short!("def"), &token, &1); // claim period 1 - - let claimable = client.get_claimable(&issuer, &symbol_short!("def"), &token, &holder); - assert_eq!(claimable, 200_000); // only period 2 remains -} - -#[test] -fn get_claimable_returns_zero_for_unknown_holder() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - - let unknown = Address::generate(&env); - assert_eq!(client.get_claimable(&issuer, &symbol_short!("def"), &token, &unknown), 0); -} - -#[test] -fn get_claimable_returns_zero_after_full_claim() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &10_000); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - - client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(client.get_claimable(&issuer, &symbol_short!("def"), &token, &holder), 0); -} - -#[test] -fn get_claimable_chunk_clamps_stale_cursor_to_unclaimed_frontier() { - let (_env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&_env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &10_000); - client.test_insert_period(&issuer, &symbol_short!("def"), &token, &1, &100_000); - client.test_insert_period(&issuer, &symbol_short!("def"), &token, &2, &200_000); - client.test_insert_period(&issuer, &symbol_short!("def"), &token, &3, &300_000); - client.test_set_last_claimed_idx(&issuer, &symbol_short!("def"), &token, &holder, &1); - - let full_claimable = client.get_claimable(&issuer, &symbol_short!("def"), &token, &holder); - let (chunk_claimable, next) = - client.get_claimable_chunk(&issuer, &symbol_short!("def"), &token, &holder, &0, &10); - - assert_eq!(full_claimable, 500_000); - assert_eq!(chunk_claimable, full_claimable); - assert_eq!(next, None); -} - -#[test] -fn get_claimable_chunk_stops_at_first_delay_barrier() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - env.ledger().with_mut(|li| li.timestamp = 1_000); - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &10_000); - client.set_claim_delay(&issuer, &symbol_short!("def"), &token, &100); - client.test_insert_period(&issuer, &symbol_short!("def"), &token, &1, &100_000); - - env.ledger().with_mut(|li| li.timestamp = 1_050); - client.test_insert_period(&issuer, &symbol_short!("def"), &token, &2, &200_000); - - env.ledger().with_mut(|li| li.timestamp = 1_100); - - let full_claimable = client.get_claimable(&issuer, &symbol_short!("def"), &token, &holder); - let (chunk_claimable, next) = - client.get_claimable_chunk(&issuer, &symbol_short!("def"), &token, &holder, &0, &10); - - assert_eq!(full_claimable, 100_000); - assert_eq!(chunk_claimable, 100_000); - assert_eq!(next, Some(1)); -} - -#[test] -fn get_claimable_chunk_returns_zero_for_blacklisted_holder() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_admin(&issuer); - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &10_000); - client.test_insert_period(&issuer, &symbol_short!("def"), &token, &1, &100_000); - client.blacklist_add(&issuer, &issuer, &symbol_short!("def"), &token, &holder); - - let full_claimable = client.get_claimable(&issuer, &symbol_short!("def"), &token, &holder); - let (chunk_claimable, next) = - client.get_claimable_chunk(&issuer, &symbol_short!("def"), &token, &holder, &0, &10); - - assert_eq!(full_claimable, 0); - assert_eq!(chunk_claimable, 0); - assert_eq!(next, None); -} - -#[test] -fn get_claimable_chunk_returns_zero_when_claim_window_closed() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - env.ledger().with_mut(|li| li.timestamp = 1_000); - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &10_000); - let _ = payment_token; - client.test_insert_period(&issuer, &symbol_short!("def"), &token, &1, &100_000); - client.set_claim_window(&issuer, &symbol_short!("def"), &token, &1_100, &1_200); - - let full_claimable = client.get_claimable(&issuer, &symbol_short!("def"), &token, &holder); - let (chunk_claimable, next) = - client.get_claimable_chunk(&issuer, &symbol_short!("def"), &token, &holder, &0, &10); - - assert_eq!(full_claimable, 0); - assert_eq!(chunk_claimable, 0); - assert_eq!(next, None); - - env.ledger().with_mut(|li| li.timestamp = 1_100); - assert_eq!(client.get_claimable(&issuer, &symbol_short!("def"), &token, &holder), 100_000); -} - -#[test] -fn get_claimable_chunk_normalizes_zero_and_oversized_counts() { - let (_env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&_env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &10_000); - for period_id in 1..=3u64 { - client.test_insert_period(&issuer, &symbol_short!("def"), &token, &period_id, &100); - } - - let (zero_count_total, zero_count_next) = - client.get_claimable_chunk(&issuer, &symbol_short!("def"), &token, &holder, &0, &0); - let (oversized_total, oversized_next) = - client.get_claimable_chunk(&issuer, &symbol_short!("def"), &token, &holder, &0, &999); - - assert_eq!(zero_count_total, 300); - assert_eq!(zero_count_next, None); - assert_eq!(oversized_total, zero_count_total); - assert_eq!(oversized_next, zero_count_next); -} - -#[test] -fn get_period_count_default_zero() { - let (env, client, issuer, _token, _payment_token, _contract_id) = claim_setup(); - let random_token = Address::generate(&env); - assert_eq!(client.get_period_count(&issuer, &symbol_short!("def"), &random_token), 0); -} - -// ── multi-holder correctness ────────────────────────────────── - -#[test] -fn multiple_holders_independent_claim_indices() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder_a = Address::generate(&env); - let holder_b = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder_a, &5_000); // 50% - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder_b, &3_000); // 30% - - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &200_000, &2); - - // A claims period 1 only - client.claim(&holder_a, &issuer, &symbol_short!("def"), &token, &1); - - // B still has both periods pending - let pending_b = client.get_pending_periods(&issuer, &symbol_short!("def"), &token, &holder_b); - assert_eq!(pending_b.len(), 2); - - // B claims all - let payout_b = client.claim(&holder_b, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout_b, 90_000); // 30% of 300k - - // A claims remaining period 2 - let payout_a = client.claim(&holder_a, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout_a, 100_000); // 50% of 200k - - assert_eq!(balance(&env, &payment_token, &holder_a), 150_000); // 50k + 100k - assert_eq!(balance(&env, &payment_token, &holder_b), 90_000); -} - -#[test] -#[ignore] -fn claim_after_holder_share_change() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &5_000); // 50% - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - - // Claim at 50% - let payout1 = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout1, 50_000); - - // Change share to 25% and deposit new period - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &2_500); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &2); - - // Claim at new 25% rate - let payout2 = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout2, 25_000); -} - -// ── stress / gas characterization for claims ────────────────── - -#[test] -#[ignore] -fn claim_many_periods_stress() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &1_000); // 10% - - // Deposit 50 periods (MAX_CLAIM_PERIODS) - for i in 1..=50_u64 { - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &10_000, &i); - } - - // Claim all 50 in one transaction - let payout = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout, 50_000); // 10% of 50 * 10k - - let pending = client.get_pending_periods(&issuer, &symbol_short!("def"), &token, &holder); - assert_eq!(pending.len(), 0); - // Gas note: claim iterates over 50 periods, each requiring 2 storage reads - // (PeriodEntry + PeriodRevenue). Total: ~100 persistent reads + 1 write - // for LastClaimedIdx + 1 token transfer. Well within Soroban compute limits. -} - -#[test] -#[ignore] -fn claim_exceeding_max_is_capped() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &10_000); // 100% - - // Deposit 55 periods (more than MAX_CLAIM_PERIODS of 50) - for i in 1..=55_u64 { - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &1_000, &i); - } - - // Request 100 periods - should be capped at 50 - let payout1 = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout1, 50_000); // 50 * 1k - - // 5 remaining - let pending = client.get_pending_periods(&issuer, &symbol_short!("def"), &token, &holder); - assert_eq!(pending.len(), 5); - - let payout2 = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout2, 5_000); -} - -#[test] -fn get_claimable_stress_many_periods() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &5_000); // 50% - - let period_count = 40_u64; - let amount_per_period: i128 = 10_000; - for i in 1..=period_count { - client.deposit_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &amount_per_period, - &i, - ); - } - - let claimable = client.get_claimable(&issuer, &symbol_short!("def"), &token, &holder); - assert_eq!(claimable, (period_count as i128) * amount_per_period / 2); - // Gas note: get_claimable is a read-only view that iterates all unclaimed periods. - // Cost: O(n) persistent reads. For 40 periods: ~80 reads. Acceptable for views. -} - -// ── edge cases ──────────────────────────────────────────────── - -#[test] -#[ignore] -fn claim_with_rounding() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &3_333); // 33.33% - - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100, &1); - - // 100 * 3333 / 10000 = 33 (integer division, rounds down) - let payout = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout, 33); -} - -#[test] -#[ignore] -fn claim_single_unit_revenue() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &10_000); // 100% - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &1, &1); - - let payout = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout, 1); -} - -#[test] -#[ignore] -fn deposit_then_claim_then_deposit_then_claim() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &10_000); // 100% - - // Round 1 - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - let p1 = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(p1, 100_000); - - // Round 2 - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &200_000, &2); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &300_000, &3); - let p2 = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(p2, 500_000); - - assert_eq!(balance(&env, &payment_token, &holder), 600_000); -} - -#[test] -fn offering_isolation_claims_independent() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - - // Register a second offering - let token_b = Address::generate(&env); - let (pt_b, pt_b_admin) = create_payment_token(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token_b, &3_000, &pt_b, &0); - - // Create a second payment token for offering B - mint_tokens(&env, &pt_b, &pt_b_admin, &issuer, &5_000_000); - - let holder = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &5_000); // 50% of offering A - client.set_holder_share(&issuer, &symbol_short!("def"), &token_b, &holder, &10_000); // 100% of offering B - - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token_b, &pt_b, &50_000, &1); - - let payout_a = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - let payout_b = client.claim(&holder, &issuer, &symbol_short!("def"), &token_b, &0); - - assert_eq!(payout_a, 50_000); // 50% of 100k - assert_eq!(payout_b, 50_000); // 100% of 50k - - // Verify token A claim doesn't affect token B pending - assert_eq!( - client.get_pending_periods(&issuer, &symbol_short!("def"), &token, &holder).len(), - 0 - ); - assert_eq!( - client.get_pending_periods(&issuer, &symbol_short!("def"), &token_b, &holder).len(), - 0 - ); -} - -// =========================================================================== -// Time-delayed revenue claim (#27) -// =========================================================================== - -#[test] -fn set_claim_delay_stores_and_returns_delay() { - let (_env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - - assert_eq!(client.get_claim_delay(&issuer, &symbol_short!("def"), &token), 0); - client.set_claim_delay(&issuer, &symbol_short!("def"), &token, &3600); - assert_eq!(client.get_claim_delay(&issuer, &symbol_short!("def"), &token), 3600); -} - -#[test] -fn set_claim_delay_requires_offering() { - let (env, client, issuer, _token, _payment_token, _contract_id) = claim_setup(); - let unknown_token = Address::generate(&env); - - let r = client.try_set_claim_delay(&issuer, &symbol_short!("def"), &unknown_token, &3600); - assert!(r.is_err()); -} - -#[test] -#[ignore] -fn claim_before_delay_returns_claim_delay_not_elapsed() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - env.ledger().with_mut(|li| li.timestamp = 1000); - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &10_000); - client.set_claim_delay(&issuer, &symbol_short!("def"), &token, &100); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - // Still at 1000, delay 100 -> claimable at 1100 - let r = client.try_claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert!(r.is_err()); -} - -#[test] -#[ignore] -fn claim_after_delay_succeeds() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - env.ledger().with_mut(|li| li.timestamp = 1000); - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &10_000); - client.set_claim_delay(&issuer, &symbol_short!("def"), &token, &100); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - env.ledger().with_mut(|li| li.timestamp = 1100); - let payout = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout, 100_000); - assert_eq!(balance(&env, &payment_token, &holder), 100_000); -} - -#[test] -fn get_claimable_respects_delay() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - env.ledger().with_mut(|li| li.timestamp = 2000); - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &5_000); - client.set_claim_delay(&issuer, &symbol_short!("def"), &token, &500); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - // At 2000, deposit at 2000, claimable at 2500 - assert_eq!(client.get_claimable(&issuer, &symbol_short!("def"), &token, &holder), 0); - env.ledger().with_mut(|li| li.timestamp = 2500); - assert_eq!(client.get_claimable(&issuer, &symbol_short!("def"), &token, &holder), 50_000); -} - -#[test] -#[ignore] -fn claim_delay_partial_periods_only_claimable_after_delay() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - env.ledger().with_mut(|li| li.timestamp = 1000); - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &10_000); - client.set_claim_delay(&issuer, &symbol_short!("def"), &token, &100); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - env.ledger().with_mut(|li| li.timestamp = 1050); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &200_000, &2); - // At 1100: period 1 claimable (1000+100<=1100), period 2 not (1050+100>1100) - env.ledger().with_mut(|li| li.timestamp = 1100); - let payout = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout, 100_000); - // At 1160: period 2 claimable (1050+100<=1160) - env.ledger().with_mut(|li| li.timestamp = 1160); - let payout2 = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout2, 200_000); -} - -#[test] -fn set_claim_delay_emits_event() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - - let before = legacy_events(&env).len(); - client.set_claim_delay(&issuer, &symbol_short!("def"), &token, &3600); - assert!(legacy_events(&env).len() > before); -} - -// =========================================================================== -// On-chain distribution simulation (#29) -// =========================================================================== - -#[test] -fn simulate_distribution_returns_correct_payouts() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let holder_a = Address::generate(&env); - let holder_b = Address::generate(&env); - - let mut shares = Vec::new(&env); - shares.push_back((holder_a.clone(), 3_000u32)); - shares.push_back((holder_b.clone(), 2_000u32)); - - let result = - client.simulate_distribution(&issuer, &symbol_short!("def"), &token, &100_000, &shares); - assert_eq!(result.total_distributed, 50_000); // 30% + 20% of 100k - assert_eq!(result.payouts.len(), 2); - assert_eq!(result.payouts.get(0).unwrap(), (holder_a, 30_000)); - assert_eq!(result.payouts.get(1).unwrap(), (holder_b, 20_000)); -} - -#[test] -fn simulate_distribution_zero_holders() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - - let shares = Vec::new(&env); - let result = - client.simulate_distribution(&issuer, &symbol_short!("def"), &token, &100_000, &shares); - assert_eq!(result.total_distributed, 0); - assert_eq!(result.payouts.len(), 0); -} - -#[test] -fn simulate_distribution_zero_revenue() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - let mut shares = Vec::new(&env); - shares.push_back((holder.clone(), 5_000u32)); - let result = client.simulate_distribution(&issuer, &symbol_short!("def"), &token, &0, &shares); - assert_eq!(result.total_distributed, 0); - assert_eq!(result.payouts.get(0).clone().unwrap().1, 0); -} - -#[test] -fn simulate_distribution_read_only_no_state_change() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - - let mut shares = Vec::new(&env); - shares.push_back((holder.clone(), 10_000u32)); - client.simulate_distribution(&issuer, &symbol_short!("def"), &token, &1_000_000, &shares); - let count_before = client.get_period_count(&issuer, &symbol_short!("def"), &token); - client.simulate_distribution(&issuer, &symbol_short!("def"), &token, &999_999, &shares); - assert_eq!(client.get_period_count(&issuer, &symbol_short!("def"), &token), count_before); -} - -#[test] -fn simulate_distribution_uses_rounding_mode() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - client.set_rounding_mode(&issuer, &symbol_short!("def"), &token, &RoundingMode::RoundHalfUp); - let holder = Address::generate(&env); - - let mut shares = Vec::new(&env); - shares.push_back((holder.clone(), 3_333u32)); - let result = - client.simulate_distribution(&issuer, &symbol_short!("def"), &token, &100, &shares); - assert_eq!(result.total_distributed, 33); - assert_eq!(result.payouts.get(0).clone().unwrap().1, 33); -} - -// =========================================================================== -// Upgradeability guard and freeze (#32) -// =========================================================================== - -#[test] -fn set_admin_once_succeeds() { - let (env, client, issuer, _token, _payment_token, _contract_id) = claim_setup(); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.set_admin(&admin); - assert_eq!(client.get_admin(), Some(admin)); -} - -#[test] -fn set_admin_twice_fails() { - let (env, client, issuer, _token, _payment_token, _contract_id) = claim_setup(); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.set_admin(&admin); - let other = Address::generate(&env); - let r = client.try_set_admin(&other); - assert!(r.is_err()); -} - -#[test] -fn freeze_sets_flag_and_emits_event() { - let (env, client, issuer, _token, _payment_token, _contract_id) = claim_setup(); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.set_admin(&admin); - assert!(!client.is_frozen()); - let before = legacy_events(&env).len(); - client.freeze(); - assert!(client.is_frozen()); - assert!(legacy_events(&env).len() > before); -} - -#[test] -fn frozen_blocks_register_offering() { - let (env, client, issuer, _token, _payment_token, _contract_id) = claim_setup(); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let new_token = Address::generate(&env); - let payout_asset = Address::generate(&env); - - client.set_admin(&admin); - client.freeze(); - let r = client.try_register_offering( - &issuer, - &symbol_short!("def"), - &new_token, - &1_000, - &payout_asset, - &0, - ); - assert!(r.is_err()); -} - -#[test] -fn frozen_blocks_deposit_revenue() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.set_admin(&admin); - client.freeze(); - let r = client.try_deposit_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &100_000, - &99, - ); - assert!(r.is_err()); -} - -#[test] -fn frozen_blocks_set_holder_share() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let holder = Address::generate(&env); - - client.set_admin(&admin); - client.freeze(); - let r = client.try_set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &2_500); - assert!(r.is_err()); -} - -#[test] -fn frozen_allows_claim() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - let admin = Address::generate(&env); - - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &10_000); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - client.set_admin(&admin); - client.freeze(); - - let payout = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout, 100_000); - assert_eq!(balance(&env, &payment_token, &holder), 100_000); -} - -#[test] -fn freeze_succeeds_when_called_by_admin() { - let (env, client, issuer, _token, _payment_token, _contract_id) = claim_setup(); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.set_admin(&admin); - env.mock_all_auths(); - let r = client.try_freeze(); - assert!(r.is_ok()); - assert!(client.is_frozen()); -} - -#[test] -fn freeze_offering_sets_flag_and_emits_event() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let before = env.events().all().len(); - - assert!(!client.is_offering_frozen(&issuer, &symbol_short!("def"), &token)); - client.freeze_offering(&issuer, &issuer, &symbol_short!("def"), &token); - assert!(client.is_offering_frozen(&issuer, &symbol_short!("def"), &token)); - assert!(env.events().all().len() > before); -} - -#[test] -fn freeze_offering_blocks_only_target_offering() { - let (env, client, issuer, token_a, payment_token, _contract_id) = claim_setup(); - let token_b = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token_b, &5_000, &payment_token, &0); - - let holder = Address::generate(&env); - client.freeze_offering(&issuer, &issuer, &symbol_short!("def"), &token_a); - - let blocked = - client.try_set_holder_share(&issuer, &symbol_short!("def"), &token_a, &holder, &2_500); - assert!(blocked.is_err()); - - let allowed = - client.try_set_holder_share(&issuer, &symbol_short!("def"), &token_b, &holder, &2_500); - assert!(allowed.is_ok()); -} - -#[test] -fn freeze_offering_rejects_unauthorized_caller_no_mutation() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let bad_actor = Address::generate(&env); - - let r = client.try_freeze_offering(&bad_actor, &issuer, &symbol_short!("def"), &token); - assert!(r.is_err()); - assert!(!client.is_offering_frozen(&issuer, &symbol_short!("def"), &token)); -} - -#[test] -fn freeze_offering_missing_offering_rejected() { - let (env, client, issuer, _token, _payment_token, _contract_id) = claim_setup(); - let unknown_token = Address::generate(&env); - - let r = client.try_freeze_offering(&issuer, &issuer, &symbol_short!("def"), &unknown_token); - assert!(r.is_err()); -} - -#[test] -fn freeze_offering_unfreeze_by_admin_restores_mutation_path() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let admin = Address::generate(&env); - let holder = Address::generate(&env); - - client.set_admin(&admin); - client.freeze_offering(&admin, &issuer, &symbol_short!("def"), &token); - assert!(client.is_offering_frozen(&issuer, &symbol_short!("def"), &token)); - - let blocked = - client.try_set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &2_500); - assert!(blocked.is_err()); - - client.unfreeze_offering(&admin, &issuer, &symbol_short!("def"), &token); - assert!(!client.is_offering_frozen(&issuer, &symbol_short!("def"), &token)); - - let allowed = - client.try_set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &2_500); - assert!(allowed.is_ok()); -} - -#[test] -fn global_freeze_blocks_offering_freeze_endpoints() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let admin = Address::generate(&env); - - client.set_admin(&admin); - client.freeze(); - - let freeze_r = client.try_freeze_offering(&admin, &issuer, &symbol_short!("def"), &token); - assert!(freeze_r.is_err()); - - let unfreeze_r = client.try_unfreeze_offering(&admin, &issuer, &symbol_short!("def"), &token); - assert!(unfreeze_r.is_err()); -} - -// =========================================================================== -// Snapshot-based distribution (#Snapshot) -// =========================================================================== - -#[test] -fn set_snapshot_config_stores_and_returns_config() { - let (_env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - - assert!(!client.get_snapshot_config(&issuer, &symbol_short!("def"), &token)); - client.set_snapshot_config(&issuer, &symbol_short!("def"), &token, &true); - assert!(client.get_snapshot_config(&issuer, &symbol_short!("def"), &token)); - client.set_snapshot_config(&issuer, &symbol_short!("def"), &token, &false); - assert!(!client.get_snapshot_config(&issuer, &symbol_short!("def"), &token)); -} - -#[test] -fn deposit_revenue_with_snapshot_succeeds_when_enabled() { - let (_env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - - client.set_snapshot_config(&issuer, &symbol_short!("def"), &token, &true); - let snapshot_ref: u64 = 123456; - let period_id: u64 = 1; - let amount: i128 = 100_000; - - let r = client.try_deposit_revenue_with_snapshot( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &amount, - &period_id, - &snapshot_ref, - ); - assert!(r.is_ok()); - assert_eq!(client.get_last_snapshot_ref(&issuer, &symbol_short!("def"), &token), snapshot_ref); - assert_eq!(client.get_period_count(&issuer, &symbol_short!("def"), &token), 1); -} - -#[test] -fn deposit_revenue_with_snapshot_fails_when_disabled() { - let (_env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - - // Disabled by default - let result = client.try_deposit_revenue_with_snapshot( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &100_000, - &1, - &123456, - ); - - // Should fail with SnapshotNotEnabled (12) - assert!(result.is_err()); -} - -#[test] -fn deposit_with_snapshot_enforces_monotonicity() { - let (_env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - - client.set_snapshot_config(&issuer, &symbol_short!("def"), &token, &true); - - // First deposit at ref 100 - client.deposit_revenue_with_snapshot( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &10_000, - &1, - &100, - ); - - // Second deposit at ref 100 should fail (duplicate) - let r2 = client.try_deposit_revenue_with_snapshot( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &10_000, - &2, - &100, - ); - assert!(r2.is_err()); - let err2 = r2.err(); - assert!(matches!(err2, Some(Ok(RevoraError::OutdatedSnapshot)))); - - // Third deposit at ref 99 should fail (outdated) - let r3 = client.try_deposit_revenue_with_snapshot( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &10_000, - &3, - &99, - ); - assert!(r3.is_err()); - let err3 = r3.err(); - assert!(matches!(err3, Some(Ok(RevoraError::OutdatedSnapshot)))); - - // Fourth deposit at ref 101 should succeed - let r4 = client.try_deposit_revenue_with_snapshot( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &10_000, - &4, - &101, - ); - assert!(r4.is_ok()); - assert_eq!(client.get_last_snapshot_ref(&issuer, &symbol_short!("def"), &token), 101); -} - -#[test] -fn deposit_with_snapshot_emits_specialized_event() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - - client.set_snapshot_config(&issuer, &symbol_short!("def"), &token, &true); - let before = legacy_events(&env).len(); - - client.deposit_revenue_with_snapshot( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &10_000, - &1, - &1000, - ); - - let all_events = legacy_events(&env); - assert!(all_events.len() > before); - // The last event should be rev_snap - // (Actual event validation depends on being able to parse the events which is complex inSDK tests without helper) -} - -#[test] -fn set_snapshot_config_requires_offering() { - let (env, client, issuer, _token, _payment_token, _contract_id) = claim_setup(); - let unknown_token = Address::generate(&env); - - let r = client.try_set_snapshot_config(&issuer, &symbol_short!("def"), &unknown_token, &true); - assert!(r.is_err()); -} - -#[test] -#[ignore = "require_auth causes non-unwinding panic in no_std; use mock_all_auths to test auth paths"] -fn set_snapshot_config_requires_auth() { - let env = Env::default(); - let cid = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &cid); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - // No mock_all_auths - let result = client.try_set_snapshot_config(&issuer, &symbol_short!("def"), &token, &true); - assert!(result.is_err()); -} - -// =========================================================================== -// Testnet mode tests (#24) -// =========================================================================== - -#[test] -fn testnet_mode_disabled_by_default() { - let env = Env::default(); - let client = make_client(&env); - assert!(!client.is_testnet_mode()); -} - -#[test] -fn set_testnet_mode_requires_admin() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - // Set admin first - client.set_admin(&admin); - - // Now admin can toggle testnet mode - client.set_testnet_mode(&true); - assert!(client.is_testnet_mode()); -} - -#[test] -fn set_testnet_mode_fails_without_admin() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - - // No admin set - should fail - let result = client.try_set_testnet_mode(&true); - assert!(result.is_err()); -} - -#[test] -fn set_testnet_mode_emits_event() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.set_admin(&admin); - let before = legacy_events(&env).len(); - client.set_testnet_mode(&true); - assert!(legacy_events(&env).len() > before); -} - -#[test] -fn clippy_format_gate_policy_roundtrip() { - let (_env, client, issuer, token, _payout) = setup_with_offering(); - - let result = client.try_set_clippy_format_gate( - &issuer, - &issuer, - &symbol_short!("def"), - &token, - &true, - &3600, - ); - assert!(result.is_ok()); - - let cfg = client.get_clippy_format_gate(&issuer, &symbol_short!("def"), &token); - assert!(cfg.is_some()); - let cfg = cfg.unwrap(); - assert!(cfg.enforce); - assert_eq!(cfg.max_attestation_age_secs, 3600); -} - -#[test] -fn clippy_format_gate_rejects_invalid_policy_window() { - let (_env, client, issuer, token, _payout) = setup_with_offering(); - - let result = client.try_set_clippy_format_gate( - &issuer, - &issuer, - &symbol_short!("def"), - &token, - &true, - &0, - ); - assert!(result.is_err()); - assert!(matches!(result.err(), Some(Ok(RevoraError::GatePolicyInvalid)))); -} - -#[test] -fn clippy_format_gate_rejects_unauthorized_actor() { - let (env, client, issuer, token, _payout) = setup_with_offering(); - let bad_actor = Address::generate(&env); - - let result = client.try_set_clippy_format_gate( - &bad_actor, - &issuer, - &symbol_short!("def"), - &token, - &true, - &3600, - ); - assert!(result.is_err()); - assert!(matches!(result.err(), Some(Ok(RevoraError::NotInitialized)))); -} - -#[test] -fn clippy_format_gate_blocks_revenue_without_attestation() { - let (_env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - - client.set_clippy_format_gate(&issuer, &issuer, &symbol_short!("def"), &token, &true, &3600); - - let result = client.try_deposit_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &10_000, - &1, - ); - assert!(result.is_err()); - assert!(matches!(result.err(), Some(Ok(RevoraError::GateCheckFailed)))); -} - -#[test] -fn clippy_format_gate_blocks_failed_attestation() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let hash = BytesN::from_array(&env, &[7u8; 32]); - let attestation_input = - ClippyFormatGateAttestationInput { format_ok: true, clippy_ok: false, artifact_hash: hash }; - - client.set_clippy_format_gate(&issuer, &issuer, &symbol_short!("def"), &token, &true, &3600); - client.attest_clippy_format_gate( - &issuer, - &issuer, - &symbol_short!("def"), - &token, - &attestation_input, - ); - - let result = client.try_report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &50_000, - &9, - &false, - ); - assert!(result.is_err()); - assert!(matches!(result.err(), Some(Ok(RevoraError::GateCheckFailed)))); -} - -#[test] -fn clippy_format_gate_blocks_stale_attestation() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let hash = BytesN::from_array(&env, &[9u8; 32]); - let attestation_input = - ClippyFormatGateAttestationInput { format_ok: true, clippy_ok: true, artifact_hash: hash }; - - client.set_clippy_format_gate(&issuer, &issuer, &symbol_short!("def"), &token, &true, &60); - client.attest_clippy_format_gate( - &issuer, - &issuer, - &symbol_short!("def"), - &token, - &attestation_input, - ); - - env.ledger().with_mut(|li| li.timestamp += 61); - let result = client.try_deposit_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &10_000, - &1, - ); - assert!(result.is_err()); - assert!(matches!(result.err(), Some(Ok(RevoraError::GateAttestationExpired)))); -} - -#[test] -fn clippy_format_gate_allows_fresh_green_attestation() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let hash = BytesN::from_array(&env, &[3u8; 32]); - let attestation_input = - ClippyFormatGateAttestationInput { format_ok: true, clippy_ok: true, artifact_hash: hash }; - - client.set_clippy_format_gate(&issuer, &issuer, &symbol_short!("def"), &token, &true, &3600); - client.attest_clippy_format_gate( - &issuer, - &issuer, - &symbol_short!("def"), - &token, - &attestation_input, - ); - - let result = client.try_report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &10_000, - &1, - &false, - ); - assert!(result.is_ok()); -} - -#[test] -fn clippy_format_gate_admin_can_manage_policy_and_attestation() { - let (env, client, issuer, token, _payout) = setup_with_offering(); - let admin = Address::generate(&env); - let hash = BytesN::from_array(&env, &[11u8; 32]); - let attestation_input = - ClippyFormatGateAttestationInput { format_ok: true, clippy_ok: true, artifact_hash: hash }; - client.set_admin(&admin); - - let r1 = client.try_set_clippy_format_gate( - &admin, - &issuer, - &symbol_short!("def"), - &token, - &true, - &1800, - ); - assert!(r1.is_ok()); - - let r2 = client.try_attest_clippy_format_gate( - &admin, - &issuer, - &symbol_short!("def"), - &token, - &attestation_input, - ); - assert!(r2.is_ok()); -} - -#[test] -fn issuer_transfer_accept_completes_transfer() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let new_issuer = Address::generate(&env); - - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer); - client.accept_issuer_transfer(&new_issuer, &issuer, &symbol_short!("def"), &token); - - // Verify no pending transfer after acceptance - assert_eq!(client.get_pending_issuer_transfer(&issuer, &symbol_short!("def"), &token), None); - - // Verify offering issuer is updated - offering is now stored under new_issuer - let offering = client.get_offering(&new_issuer, &symbol_short!("def"), &token); - assert!(offering.is_some()); - assert_eq!(offering.clone().unwrap().issuer, new_issuer); -} - -#[test] -fn issuer_transfer_accept_emits_event() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let new_issuer = Address::generate(&env); - - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer); - let before = legacy_events(&env).len(); - client.accept_issuer_transfer(&new_issuer, &issuer, &symbol_short!("def"), &token); - assert!(legacy_events(&env).len() > before); -} - -#[test] -fn issuer_transfer_new_issuer_can_deposit_revenue() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let new_issuer = Address::generate(&env); - - // Mint tokens to new issuer - let (_, pt_admin) = create_payment_token(&env); - mint_tokens(&env, &payment_token, &pt_admin, &new_issuer, &5_000_000); - - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer); - client.accept_issuer_transfer(&new_issuer, &issuer, &symbol_short!("def"), &token); - - // New issuer should be able to deposit revenue - let result = client.try_deposit_revenue( - &new_issuer, - &symbol_short!("def"), - &token, - &payment_token, - &100_000, - &1, - ); - assert!(result.is_ok()); -} - -#[test] -fn testnet_mode_can_be_toggled() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.set_admin(&admin); - - // Enable - client.set_testnet_mode(&true); - assert!(client.is_testnet_mode()); - - // Disable - client.set_testnet_mode(&false); - assert!(!client.is_testnet_mode()); - - // Enable again - client.set_testnet_mode(&true); - assert!(client.is_testnet_mode()); -} - -#[test] -fn testnet_mode_allows_bps_over_10000() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - - // Set admin and enable testnet mode - client.set_admin(&admin); - client.set_testnet_mode(&true); - - // Should allow bps > 10000 in testnet mode - let result = client.try_register_offering( - &issuer, - &symbol_short!("def"), - &token, - &15_000, - &payout_asset, - &0, - ); - assert!(result.is_ok()); - - // Verify offering was registered - let offering = client.get_offering(&issuer, &symbol_short!("def"), &token); - assert_eq!(offering.clone().clone().unwrap().revenue_share_bps, 15_000); -} - -#[test] -fn testnet_mode_disabled_rejects_bps_over_10000() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - - // Testnet mode is disabled by default - let result = client.try_register_offering( - &issuer, - &symbol_short!("def"), - &token, - &15_000, - &payout_asset, - &0, - ); - assert!(result.is_err()); -} - -#[test] -fn testnet_mode_skips_concentration_enforcement() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - - // Set admin and enable testnet mode - client.set_admin(&admin); - client.set_testnet_mode(&true); - - // Register offering and set concentration limit with enforcement - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - client.set_concentration_limit(&issuer, &symbol_short!("def"), &token, &5000, &true); - client.report_concentration(&issuer, &symbol_short!("def"), &token, &8000); // Over limit - - // In testnet mode, report_revenue should succeed despite concentration being over limit - let result = client.try_report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &1_000, - &1, - &false, - ); - assert!(result.is_ok()); -} - -#[test] -fn issuer_transfer_new_issuer_can_set_holder_share() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let new_issuer = Address::generate(&env); - let holder = Address::generate(&env); - - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer); - client.accept_issuer_transfer(&new_issuer, &issuer, &symbol_short!("def"), &token); - - // New issuer should be able to set holder shares - let result = - client.try_set_holder_share(&new_issuer, &symbol_short!("def"), &token, &holder, &5_000); - assert!(result.is_ok()); - assert_eq!(client.get_holder_share(&new_issuer, &symbol_short!("def"), &token, &holder), 5_000); -} - -#[test] -fn issuer_transfer_old_issuer_loses_access() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let new_issuer = Address::generate(&env); - - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer); - client.accept_issuer_transfer(&new_issuer, &issuer, &symbol_short!("def"), &token); - - // Old issuer should not be able to deposit revenue - let result = client.try_deposit_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &100_000, - &1, - ); - assert!(result.is_err()); -} - -#[test] -fn issuer_transfer_old_issuer_cannot_set_holder_share() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let new_issuer = Address::generate(&env); - let holder = Address::generate(&env); - - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer); - client.accept_issuer_transfer(&new_issuer, &issuer, &symbol_short!("def"), &token); - - // Old issuer should not be able to set holder shares - let result = - client.try_set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &5_000); - assert!(result.is_err()); -} - -#[test] -fn issuer_transfer_cancel_clears_pending() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let new_issuer = Address::generate(&env); - - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer); - client.cancel_issuer_transfer(&issuer, &symbol_short!("def"), &token); - - assert_eq!(client.get_pending_issuer_transfer(&issuer, &symbol_short!("def"), &token), None); -} - -#[test] -fn issuer_transfer_cancel_emits_event() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let new_issuer = Address::generate(&env); - - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer); - let before = legacy_events(&env).len(); - client.cancel_issuer_transfer(&issuer, &symbol_short!("def"), &token); - let after = legacy_events(&env).len(); - assert_eq!(after, before + 1); -} - -#[test] -fn testnet_mode_disabled_enforces_concentration() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - - // Testnet mode disabled (default) - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - client.set_concentration_limit(&issuer, &symbol_short!("def"), &token, &5000, &true); - client.report_concentration(&issuer, &symbol_short!("def"), &token, &8000); // Over limit - - // Should fail with concentration enforcement - let result = client.try_report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &1_000, - &1, - &false, - ); - assert!(result.is_err()); -} - -#[test] -fn testnet_mode_toggle_after_offerings_exist() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token1 = Address::generate(&env); - let token2 = Address::generate(&env); - let payout_asset1 = Address::generate(&env); - let payout_asset2 = Address::generate(&env); - - // Register offering in normal mode - client.register_offering(&issuer, &symbol_short!("def"), &token1, &5_000, &payout_asset1, &0); - - // Set admin and enable testnet mode - client.set_admin(&admin); - client.set_testnet_mode(&true); - - // Register offering with high bps in testnet mode - let result = client.try_register_offering( - &issuer, - &symbol_short!("def"), - &token2, - &20_000, - &payout_asset2, - &0, - ); - assert!(result.is_ok()); - - // Verify both offerings exist - assert_eq!(client.get_offering_count(&issuer, &symbol_short!("def")), 2); -} - -#[test] -fn testnet_mode_affects_only_validation_not_storage() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - - // Enable testnet mode - client.set_admin(&admin); - client.set_testnet_mode(&true); - - // Register with high bps - client.register_offering(&issuer, &symbol_short!("def"), &token, &25_000, &payout_asset, &0); - - // Disable testnet mode - client.set_testnet_mode(&false); - - // Offering should still exist with high bps value - let offering = client.get_offering(&issuer, &symbol_short!("def"), &token); - assert_eq!(offering.clone().clone().unwrap().revenue_share_bps, 25_000); -} - -#[test] -fn testnet_mode_multiple_offerings_with_varied_bps() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.set_admin(&admin); - client.set_testnet_mode(&true); - - // Register multiple offerings with various bps values - for i in 1..=5 { - let token = Address::generate(&env); - let bps = 10_000 + (i * 1_000); - let payout_asset = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &bps, &payout_asset, &0); - } - - assert_eq!(client.get_offering_count(&issuer, &symbol_short!("def")), 5); -} - -#[test] -fn testnet_mode_concentration_warning_still_emitted() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - - client.set_admin(&admin); - client.set_testnet_mode(&true); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - client.set_concentration_limit(&issuer, &symbol_short!("def"), &token, &5000, &false); - - // Warning should still be emitted in testnet mode - let before = legacy_events(&env).len(); - client.report_concentration(&issuer, &symbol_short!("def"), &token, &7000); - assert!(legacy_events(&env).len() > before); -} - -#[test] -fn issuer_transfer_cancel_then_can_propose_again() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let new_issuer_1 = Address::generate(&env); - let new_issuer_2 = Address::generate(&env); - - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer_1); - client.cancel_issuer_transfer(&issuer, &symbol_short!("def"), &token); - - // Should be able to propose to different address - let result = - client.try_propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer_2); - assert!(result.is_ok()); - assert_eq!( - client.get_pending_issuer_transfer(&issuer, &symbol_short!("def"), &token), - Some(new_issuer_2) - ); -} - -// ── Security and abuse prevention tests ────────────────────── - -#[test] -fn issuer_transfer_cannot_propose_for_nonexistent_offering() { - let (env, client, issuer, _token, _payment_token, _contract_id) = claim_setup(); - let unknown_token = Address::generate(&env); - let new_issuer = Address::generate(&env); - - let result = client.try_propose_issuer_transfer( - &issuer, - &symbol_short!("def"), - &unknown_token, - &new_issuer, - ); - assert!(result.is_err()); -} - -#[test] -fn issuer_transfer_cannot_propose_when_already_pending() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let new_issuer_1 = Address::generate(&env); - let new_issuer_2 = Address::generate(&env); - - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer_1); - - // Second proposal should fail - let result = - client.try_propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer_2); - assert!(result.is_err()); -} - -#[test] -fn issuer_transfer_cannot_accept_when_no_pending() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let any_caller = Address::generate(&env); - - let result = client.try_accept_issuer_transfer(&any_caller, &issuer, &symbol_short!("def"), &token); - assert!(result.is_err()); -} - -#[test] -fn issuer_transfer_cannot_cancel_when_no_pending() { - let (_env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - - let result = client.try_cancel_issuer_transfer(&issuer, &symbol_short!("def"), &token); - assert!(result.is_err()); -} - -#[test] -#[ignore] -#[should_panic] -#[ignore] -fn issuer_transfer_propose_requires_auth() { - let env = Env::default(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let _issuer = Address::generate(&env); - let token = Address::generate(&env); - let new_issuer = Address::generate(&env); - - // No mock_all_auths - should return error - let r = - client.try_propose_issuer_transfer(&_issuer, &symbol_short!("def"), &token, &new_issuer); - assert!(r.is_err()); -} - -#[test] -#[ignore] -#[should_panic] -#[ignore] -fn issuer_transfer_accept_requires_auth() { - let env = Env::default(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let token = Address::generate(&env); - - let _issuer = Address::generate(&env); - let _new_issuer = Address::generate(&env); - - // No mock_all_auths - should panic - client.accept_issuer_transfer(&_new_issuer, &_issuer, &symbol_short!("def"), &token); -} - -#[test] -#[ignore] -#[should_panic] -#[ignore] -fn issuer_transfer_cancel_requires_auth() { - let env = Env::default(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let token = Address::generate(&env); - - // No mock_all_auths - should return error - let issuer = Address::generate(&env); - let r = client.try_cancel_issuer_transfer(&issuer, &symbol_short!("def"), &token); - assert!(r.is_err()); -} - -#[test] -fn issuer_transfer_double_accept_fails() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let new_issuer = Address::generate(&env); - - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer); - client.accept_issuer_transfer(&new_issuer, &issuer, &symbol_short!("def"), &token); - - // Second accept should fail (no pending transfer) - let result = client.try_accept_issuer_transfer(&new_issuer, &issuer, &symbol_short!("def"), &token); - assert!(result.is_err()); -} - -// ── Edge case tests ─────────────────────────────────────────── - -#[test] -fn issuer_transfer_to_same_address() { - let (_env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - - // Transfer to self (issuer is used here) - let result = - client.try_propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &issuer); - assert!(result.is_ok()); - - let result = client.try_accept_issuer_transfer(&issuer, &issuer, &symbol_short!("def"), &token); - assert!(result.is_ok()); -} - -#[test] -fn issuer_transfer_multiple_offerings_isolation() { - let (env, client, issuer, token_a, _payment_token, _contract_id) = claim_setup(); - let token_b = Address::generate(&env); - let new_issuer_a = Address::generate(&env); - let new_issuer_b = Address::generate(&env); - - // Register second offering - client.register_offering(&issuer, &symbol_short!("def"), &token_b, &3_000, &token_b, &0); - - // Propose transfers for both (same issuer for both offerings) - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token_a, &new_issuer_a); - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token_b, &new_issuer_b); - - // Accept only token_a transfer - client.accept_issuer_transfer(&new_issuer_a, &issuer, &symbol_short!("def"), &token_a); - - // Verify token_a transferred but token_b still pending - assert_eq!(client.get_pending_issuer_transfer(&issuer, &symbol_short!("def"), &token_a), None); - assert_eq!( - client.get_pending_issuer_transfer(&issuer, &symbol_short!("def"), &token_b), - Some(new_issuer_b) - ); -} - -#[test] -fn issuer_transfer_blocked_when_frozen() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let new_issuer = Address::generate(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.set_admin(&admin); - client.freeze(); - let result = - client.try_propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer); - assert!(result.is_err()); -} - -// =========================================================================== -// Multisig admin pattern tests -// =========================================================================== -// -// Production recommendation note: -// The multisig pattern implemented here is a minimal on-chain approval tracker. -// It is suitable for low-frequency admin operations (fee changes, freeze, owner -// rotation). For high-security production use, consider: -// - Time-locks on execution (delay between threshold met and execution) -// - Proposal expiry to prevent stale proposals from being executed -// - Off-chain coordination tools (e.g. Gnosis Safe-style UX) -// - Audit of the threshold/owner management flows -// -// Soroban compatibility notes: -// - Soroban does not support multi-party auth in a single transaction. -// Each owner must call approve_action in separate transactions. -// - The proposer's vote is automatically counted as the first approval. -// - init_multisig only requires the caller (deployer) to authorize. -// - All proposal state is stored in persistent storage (survives ledger close). - -/// Helper: set up a 2-of-3 multisig environment. -fn multisig_setup() -> (Env, RevoraRevenueShareClient<'static>, Address, Address, Address, Address) -{ - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - - let caller = Address::generate(&env); - /// removed overwriting issuer - let owner1 = Address::generate(&env); - let owner2 = Address::generate(&env); - let owner3 = Address::generate(&env); - - let mut owners = Vec::new(&env); - owners.push_back(owner1.clone()); - owners.push_back(owner2.clone()); - owners.push_back(owner3.clone()); - - // 2-of-3 threshold - client.init_multisig(&caller, &owners, &2); - - (env, client, owner1, owner2, owner3, caller) -} - -#[test] -fn multisig_init_sets_owners_and_threshold() { - let (_env, client, owner1, owner2, owner3, _caller) = multisig_setup(); - - assert_eq!(client.get_multisig_threshold(), Some(2)); - let owners = client.get_multisig_owners(); - assert_eq!(owners.len(), 3); - assert_eq!(owners.get(0).unwrap(), owner1); - assert_eq!(owners.get(1).unwrap(), owner2); - assert_eq!(owners.get(2).unwrap(), owner3); -} - -#[test] -fn multisig_init_twice_fails() { - let (env, client, owner1, _owner2, _owner3, caller) = multisig_setup(); - - let mut owners2 = Vec::new(&env); - owners2.push_back(owner1.clone()); - let r = client.try_init_multisig(&caller, &owners2, &1); - assert!(r.is_err()); -} - -#[test] -fn multisig_init_zero_threshold_fails() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let caller = Address::generate(&env); - let issuer = caller.clone(); - - let owner = Address::generate(&env); - let issuer = owner.clone(); - - let mut owners = Vec::new(&env); - owners.push_back(owner.clone()); - let r = client.try_init_multisig(&caller, &owners, &0); - assert!(r.is_err()); -} - -#[test] -fn multisig_init_threshold_exceeds_owners_fails() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let caller = Address::generate(&env); - let issuer = caller.clone(); - - let owner = Address::generate(&env); - let issuer = owner.clone(); - - let mut owners = Vec::new(&env); - owners.push_back(owner.clone()); - // threshold=2 but only 1 owner - let r = client.try_init_multisig(&caller, &owners, &2); - assert!(r.is_err()); -} - -#[test] -fn multisig_init_empty_owners_fails() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let caller = Address::generate(&env); - let issuer = caller.clone(); - - let owners = Vec::new(&env); - let r = client.try_init_multisig(&caller, &owners, &1); - assert!(r.is_err()); -} - -#[test] -fn multisig_propose_action_emits_events_and_auto_approves_proposer() { - let (env, client, owner1, _owner2, _owner3, _caller) = multisig_setup(); - - let before = legacy_events(&env).len(); - let proposal_id = client.propose_action(&owner1, &ProposalAction::Freeze); - // Should emit prop_new + prop_app (auto-approval) - assert!(legacy_events(&env).len() >= before + 2); - - // Proposer's vote is counted automatically - let proposal = client.get_proposal(&proposal_id).unwrap(); - assert_eq!(proposal.approvals.len(), 1); - assert_eq!(proposal.approvals.get(0).unwrap(), owner1); - assert!(!proposal.executed); -} - -#[test] -fn multisig_non_owner_cannot_propose() { - let (env, client, _owner1, _owner2, _owner3, _caller) = multisig_setup(); - let outsider = Address::generate(&env); - let r = client.try_propose_action(&outsider, &ProposalAction::Freeze); - assert!(r.is_err()); -} - -#[test] -fn multisig_approve_action_records_approval_and_emits_event() { - let (env, client, owner1, owner2, owner3, _caller) = multisig_setup(); - - let proposal_id = client.propose_action(&owner1, &ProposalAction::Freeze); - let before = legacy_events(&env).len(); - client.approve_action(&owner2, &proposal_id); - assert!(legacy_events(&env).len() > before); - - let proposal = client.get_proposal(&proposal_id).unwrap(); - // owner1 auto-approved on propose, owner2 just approved = 2 total - assert_eq!(proposal.approvals.len(), 2); - assert_eq!(proposal.approvals.get(0).unwrap(), owner1); - assert_eq!(proposal.approvals.get(1).unwrap(), owner2); -} - -#[test] -fn multisig_duplicate_approval_is_idempotent() { - let (_env, client, owner1, _owner2, _owner3, _caller) = multisig_setup(); - - let proposal_id = client.propose_action(&owner1, &ProposalAction::Freeze); - // owner1 already approved (auto-approval from propose). - // A second approval by the same owner must be rejected with AlreadyApproved. - let r = client.try_approve_action(&owner1, &proposal_id); - assert_eq!(r, Err(Ok(RevoraError::AlreadyApproved))); - - // Approval list must remain a set — still exactly 1 entry. - let proposal = client.get_proposal(&proposal_id).unwrap(); - assert_eq!(proposal.approvals.len(), 1); -} - -#[test] -fn multisig_non_owner_cannot_approve() { - let (env, client, owner1, _owner2, _owner3, _caller) = multisig_setup(); - - let proposal_id = client.propose_action(&owner1, &ProposalAction::Freeze); - let outsider = Address::generate(&env); - let r = client.try_approve_action(&outsider, &proposal_id); - assert!(r.is_err()); -} - -#[test] -fn multisig_execute_fails_below_threshold() { - let (_env, client, owner1, _owner2, _owner3, _caller) = multisig_setup(); - - // Only 1 approval (proposer auto-approval), threshold is 2 - let proposal_id = client.propose_action(&owner1, &ProposalAction::Freeze); - let r = client.try_execute_action(&proposal_id); - assert!(r.is_err()); - assert!(!client.is_frozen()); -} - -#[test] -fn multisig_execute_freeze_succeeds_at_threshold() { - let (_env, client, owner1, owner2, _owner3, _caller) = multisig_setup(); - - let proposal_id = client.propose_action(&owner1, &ProposalAction::Freeze); - client.approve_action(&owner2, &proposal_id); - - // Now 2 approvals, threshold is 2 — should execute - let before_frozen = client.is_frozen(); - assert!(!before_frozen); - client.execute_action(&proposal_id); - assert!(client.is_frozen()); - - // Proposal marked as executed - let proposal = client.get_proposal(&proposal_id).unwrap(); - assert!(proposal.executed); -} - -#[test] -fn multisig_execute_emits_event() { - let (env, client, owner1, owner2, _owner3, _caller) = multisig_setup(); - - let proposal_id = client.propose_action(&owner1, &ProposalAction::Freeze); - client.approve_action(&owner2, &proposal_id); - let before = legacy_events(&env).len(); - client.execute_action(&proposal_id); - assert!(legacy_events(&env).len() > before); -} - -#[test] -fn multisig_execute_twice_fails() { - let (_env, client, owner1, owner2, _owner3, _caller) = multisig_setup(); - - let proposal_id = client.propose_action(&owner1, &ProposalAction::Freeze); - client.approve_action(&owner2, &proposal_id); - client.execute_action(&proposal_id); - - // Second execution should fail - let r = client.try_execute_action(&proposal_id); - assert!(r.is_err()); -} - -#[test] -fn multisig_approve_executed_proposal_fails() { - let (_env, client, owner1, owner2, owner3, _caller) = multisig_setup(); - - let proposal_id = client.propose_action(&owner1, &ProposalAction::Freeze); - client.approve_action(&owner2, &proposal_id); - client.execute_action(&proposal_id); - - // Approving an already-executed proposal should fail - let r = client.try_approve_action(&owner3, &proposal_id); - assert!(r.is_err()); -} - -#[test] -fn multisig_set_admin_action_updates_admin() { - let (env, client, owner1, owner2, _owner3, _caller) = multisig_setup(); - let new_admin = Address::generate(&env); - - let proposal_id = client.propose_action(&owner1, &ProposalAction::SetAdmin(new_admin.clone())); - client.approve_action(&owner2, &proposal_id); - client.execute_action(&proposal_id); - - assert_eq!(client.get_admin(), Some(new_admin)); -} - -#[test] -fn multisig_set_threshold_action_updates_threshold() { - let (_env, client, owner1, owner2, _owner3, _caller) = multisig_setup(); - - // Change threshold from 2 to 3 - let proposal_id = client.propose_action(&owner1, &ProposalAction::SetThreshold(3)); - client.approve_action(&owner2, &proposal_id); - client.execute_action(&proposal_id); - - assert_eq!(client.get_multisig_threshold(), Some(3)); -} - -#[test] -fn multisig_set_threshold_exceeding_owners_fails_on_execute() { - let (_env, client, owner1, owner2, _owner3, _caller) = multisig_setup(); - - // Try to set threshold to 4 (only 3 owners) - let proposal_id = client.propose_action(&owner1, &ProposalAction::SetThreshold(4)); - client.approve_action(&owner2, &proposal_id); - let r = client.try_execute_action(&proposal_id); - assert!(r.is_err()); - // Threshold unchanged - assert_eq!(client.get_multisig_threshold(), Some(2)); -} - -#[test] -fn multisig_add_owner_action_adds_owner() { - let (env, client, owner1, owner2, _owner3, _caller) = multisig_setup(); - let new_owner = Address::generate(&env); - - let proposal_id = client.propose_action(&owner1, &ProposalAction::AddOwner(new_owner.clone())); - client.approve_action(&owner2, &proposal_id); - client.execute_action(&proposal_id); - - let owners = client.get_multisig_owners(); - assert_eq!(owners.len(), 4); - assert_eq!(owners.get(3).unwrap(), new_owner); -} - -#[test] -fn multisig_remove_owner_action_removes_owner() { - let (_env, client, owner1, owner2, owner3, _caller) = multisig_setup(); - - // Remove owner3 (3 owners remain: owner1, owner2; threshold stays 2) - let proposal_id = client.propose_action(&owner1, &ProposalAction::RemoveOwner(owner3.clone())); - client.approve_action(&owner2, &proposal_id); - client.execute_action(&proposal_id); - - let owners = client.get_multisig_owners(); - assert_eq!(owners.len(), 2); - // owner3 should not be in the list - for i in 0..owners.len() { - assert_ne!(owners.get(i).unwrap(), owner3); - } -} - -#[test] -fn multisig_remove_owner_that_would_break_threshold_fails() { - let (_env, client, owner1, owner2, _owner3, _caller) = multisig_setup(); - - // Remove owner2 would leave 2 owners with threshold=2 (still valid) - // But remove owner1 AND owner2 would break it. Let's test removing to exactly threshold. - // First remove owner3 (leaves 2 owners, threshold=2 — still valid) - let p1 = client.propose_action(&owner1, &ProposalAction::RemoveOwner(owner2.clone())); - client.approve_action(&owner2, &p1); - client.execute_action(&p1); - - // Now 2 owners (owner1, owner3), threshold=2 - // Try to remove owner3 — would leave 1 owner < threshold=2 → should fail - let p2 = client.propose_action(&owner1, &ProposalAction::RemoveOwner(owner1.clone())); - // Need owner3 to approve (owner2 was removed) - let owners = client.get_multisig_owners(); - let remaining_owner2 = owners.get(1).unwrap(); - client.approve_action(&remaining_owner2, &p2); - let r = client.try_execute_action(&p2); - assert!(r.is_err()); -} - -#[test] -fn multisig_freeze_disables_direct_freeze_function() { - let (env, client, _owner1, _owner2, _owner3, _caller) = multisig_setup(); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - // set_admin and freeze are disabled when multisig is initialized - let r = client.try_set_admin(&admin); - assert!(r.is_err()); - - let r2 = client.try_freeze(); - assert!(r2.is_err()); -} - -#[test] -fn multisig_three_approvals_all_valid() { - let (_env, client, owner1, owner2, owner3, _caller) = multisig_setup(); - - // All 3 owners approve (threshold=2, so execution should succeed after 2) - let proposal_id = client.propose_action(&owner1, &ProposalAction::Freeze); - client.approve_action(&owner2, &proposal_id); - client.approve_action(&owner3, &proposal_id); - - let proposal = client.get_proposal(&proposal_id).unwrap(); - assert_eq!(proposal.approvals.len(), 3); - assert_eq!(proposal.approvals.get(0).unwrap(), owner1); - assert_eq!(proposal.approvals.get(1).unwrap(), owner2); - assert_eq!(proposal.approvals.get(2).unwrap(), owner3); - client.execute_action(&proposal_id); - assert!(client.is_frozen()); -} - -#[test] -fn multisig_multiple_proposals_independent() { - let (env, client, owner1, owner2, _owner3, _caller) = multisig_setup(); - let new_admin = Address::generate(&env); - - // Create two proposals - let p1 = client.propose_action(&owner1, &ProposalAction::Freeze); - let p2 = client.propose_action(&owner1, &ProposalAction::SetAdmin(new_admin.clone())); - - // Approve and execute only p2 - client.approve_action(&owner2, &p2); - client.execute_action(&p2); - - // p1 should still be pending - let proposal1 = client.get_proposal(&p1).unwrap(); - assert!(!proposal1.executed); - assert!(!client.is_frozen()); - - // p2 should be executed - let proposal2 = client.get_proposal(&p2).unwrap(); - assert!(proposal2.executed); - assert_eq!(client.get_admin(), Some(new_admin)); -} - -#[test] -fn multisig_get_proposal_nonexistent_returns_none() { - let (_env, client, _owner1, _owner2, _owner3, _caller) = multisig_setup(); - assert!(client.get_proposal(&9999).is_none()); -} - -// ── Duplicate-approval guard tests ──────────────────────────────────────────── -// -// Security assumption: the approval list is a set — each owner address appears -// at most once. Threshold enforcement counts list length, so any inflation of -// that count would allow a single owner to satisfy an N-of-M threshold alone. -// These tests validate every path through the guard. - -/// Proposer's auto-approval is counted; a second call by the proposer returns -/// AlreadyApproved and does NOT add a second entry. -#[test] -fn multisig_duplicate_approval_proposer_returns_already_approved() { - let (_env, client, owner1, _owner2, _owner3, _caller) = multisig_setup(); - - let pid = client.propose_action(&owner1, &ProposalAction::Freeze); - let r = client.try_approve_action(&owner1, &pid); - assert_eq!(r, Err(Ok(RevoraError::AlreadyApproved))); - - let proposal = client.get_proposal(&pid).unwrap(); - assert_eq!(proposal.approvals.len(), 1); -} - -/// A non-proposer owner who approves once cannot approve a second time. -#[test] -fn multisig_duplicate_approval_second_owner_returns_already_approved() { - let (_env, client, owner1, owner2, _owner3, _caller) = multisig_setup(); - - let pid = client.propose_action(&owner1, &ProposalAction::Freeze); - client.approve_action(&owner2, &pid); // first approval by owner2 — ok - let r = client.try_approve_action(&owner2, &pid); // second — must fail - assert_eq!(r, Err(Ok(RevoraError::AlreadyApproved))); - - let proposal = client.get_proposal(&pid).unwrap(); - // owner1 (auto) + owner2 = 2, no duplicates - assert_eq!(proposal.approvals.len(), 2); -} - -/// All three owners approve once each; no duplicates; count reaches 3. -#[test] -fn multisig_duplicate_approval_all_owners_approve_once_each() { - let (_env, client, owner1, owner2, owner3, _caller) = multisig_setup(); - - let pid = client.propose_action(&owner1, &ProposalAction::Freeze); - client.approve_action(&owner2, &pid); - client.approve_action(&owner3, &pid); - - let proposal = client.get_proposal(&pid).unwrap(); - assert_eq!(proposal.approvals.len(), 3); - // Each owner appears exactly once - assert_eq!(proposal.approvals.get(0).unwrap(), owner1); - assert_eq!(proposal.approvals.get(1).unwrap(), owner2); - assert_eq!(proposal.approvals.get(2).unwrap(), owner3); -} - -/// Duplicate approval does NOT emit a prop_app event (no side-effects on failure). -#[test] -fn multisig_duplicate_approval_emits_no_event_on_rejection() { - let (env, client, owner1, _owner2, _owner3, _caller) = multisig_setup(); - - let pid = client.propose_action(&owner1, &ProposalAction::Freeze); - let before = env.events().all().len(); - let _ = client.try_approve_action(&owner1, &pid); // duplicate — must fail - // No new events should have been emitted - assert_eq!(env.events().all().len(), before); -} - -/// Duplicate approval on an already-executed proposal returns LimitReached -/// (executed check fires before the duplicate guard). -#[test] -fn multisig_duplicate_approval_on_executed_proposal_returns_limit_reached() { - let (_env, client, owner1, owner2, _owner3, _caller) = multisig_setup(); - - let pid = client.propose_action(&owner1, &ProposalAction::Freeze); - client.approve_action(&owner2, &pid); - client.execute_action(&pid); - - // owner1 tries to approve again after execution - let r = client.try_approve_action(&owner1, &pid); - assert_eq!(r, Err(Ok(RevoraError::LimitReached))); -} - -/// A duplicate approval attempt does not advance the approval count and therefore -/// cannot push a proposal from below-threshold to at-threshold. -#[test] -fn multisig_duplicate_approval_cannot_satisfy_threshold() { - let (_env, client, owner1, _owner2, _owner3, _caller) = multisig_setup(); - - // threshold = 2; only owner1 has approved (auto) - let pid = client.propose_action(&owner1, &ProposalAction::Freeze); - - // owner1 tries to double-approve to reach threshold=2 alone - let _ = client.try_approve_action(&owner1, &pid); - - // Execution must still fail — threshold not met - let r = client.try_execute_action(&pid); - assert!(r.is_err()); - assert!(!client.is_frozen()); -} - -/// Duplicate approval on a non-existent proposal returns OfferingNotFound -/// (proposal lookup fires before the duplicate guard). -#[test] -fn multisig_duplicate_approval_nonexistent_proposal_returns_not_found() { - let (_env, client, owner1, _owner2, _owner3, _caller) = multisig_setup(); - - let r = client.try_approve_action(&owner1, &9999); - assert_eq!(r, Err(Ok(RevoraError::OfferingNotFound))); -} - -/// Duplicate approvals across independent proposals do not cross-contaminate. -#[test] -fn multisig_duplicate_approval_independent_proposals_isolated() { - let (_env, client, owner1, owner2, _owner3, _caller) = multisig_setup(); - - let p1 = client.propose_action(&owner1, &ProposalAction::Freeze); - let p2 = client.propose_action(&owner1, &ProposalAction::Freeze); - - // owner2 approves p1 — valid - client.approve_action(&owner2, &p1); - // owner2 approves p2 — also valid (different proposal) - client.approve_action(&owner2, &p2); - - // owner2 tries to approve p1 again — must fail - let r = client.try_approve_action(&owner2, &p1); - assert_eq!(r, Err(Ok(RevoraError::AlreadyApproved))); - - // p2 approval count unaffected - let proposal2 = client.get_proposal(&p2).unwrap(); - assert_eq!(proposal2.approvals.len(), 2); -} - -#[test] -fn issuer_transfer_accept_blocked_when_frozen() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let new_issuer = Address::generate(&env); - let admin = Address::generate(&env); - - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer); - - client.set_admin(&admin); - client.freeze(); - - let result = client.try_accept_issuer_transfer(&new_issuer, &issuer, &symbol_short!("def"), &token); - assert!(result.is_err()); -} - -#[test] -fn issuer_transfer_cancel_blocked_when_frozen() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let new_issuer = Address::generate(&env); - let admin = Address::generate(&env); - - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer); - - client.set_admin(&admin); - client.freeze(); - - let result = client.try_cancel_issuer_transfer(&issuer, &symbol_short!("def"), &token); - assert!(result.is_err()); -} - -// ── Integration tests with other features ───────────────────── - -#[test] -fn issuer_transfer_preserves_audit_summary() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let new_issuer = Address::generate(&env); - - // Report revenue before transfer - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &100_000, - &1, - &false, - ); - let summary_before = client.get_audit_summary(&issuer, &symbol_short!("def"), &token).unwrap(); - - // Transfer issuer - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer); - client.accept_issuer_transfer(&new_issuer, &issuer, &symbol_short!("def"), &token); - - // Audit summary should still be accessible - let summary_after = client.get_audit_summary(&issuer, &symbol_short!("def"), &token).unwrap(); - assert_eq!(summary_before.total_revenue, summary_after.total_revenue); - assert_eq!(summary_before.report_count, summary_after.report_count); -} - -#[test] -fn issuer_transfer_new_issuer_can_report_revenue() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let new_issuer = Address::generate(&env); - - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer); - client.accept_issuer_transfer(&new_issuer, &issuer, &symbol_short!("def"), &token); - - // New issuer can report revenue - let result = client.try_report_revenue( - &new_issuer, - &symbol_short!("def"), - &token, - &payment_token, - &200_000, - &2, - &false, - ); - assert!(result.is_ok()); -} - -#[test] -fn issuer_transfer_new_issuer_can_set_concentration_limit() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let new_issuer = Address::generate(&env); - - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer); - client.accept_issuer_transfer(&new_issuer, &issuer, &symbol_short!("def"), &token); - - // New issuer can set concentration limit - let result = client.try_set_concentration_limit( - &new_issuer, - &symbol_short!("def"), - &token, - &5_000, - &true, - ); - assert!(result.is_ok()); -} - -#[test] -fn issuer_transfer_new_issuer_can_set_rounding_mode() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let new_issuer = Address::generate(&env); - - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer); - client.accept_issuer_transfer(&new_issuer, &issuer, &symbol_short!("def"), &token); - - // New issuer can set rounding mode - let result = client.try_set_rounding_mode( - &new_issuer, - &symbol_short!("def"), - &token, - &RoundingMode::RoundHalfUp, - ); - assert!(result.is_ok()); -} - -#[test] -fn issuer_transfer_new_issuer_can_set_claim_delay() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let new_issuer = Address::generate(&env); - - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer); - client.accept_issuer_transfer(&new_issuer, &issuer, &symbol_short!("def"), &token); - - // New issuer can set claim delay - let result = client.try_set_claim_delay(&new_issuer, &symbol_short!("def"), &token, &3600); - assert!(result.is_ok()); -} - -#[test] -fn issuer_transfer_holders_can_still_claim() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - let new_issuer = Address::generate(&env); - - // Setup: deposit and set share before transfer - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &10_000); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100_000, &1); - - // Transfer issuer - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer); - client.accept_issuer_transfer(&new_issuer, &issuer, &symbol_short!("def"), &token); - - // Holder should still be able to claim - let payout = client.claim(&holder, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout, 100_000); -} - -#[test] -fn issuer_transfer_then_new_deposits_and_claims_work() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let holder = Address::generate(&env); - let new_issuer = Address::generate(&env); - - // Mint tokens to new issuer using mock_all_auths - token::StellarAssetClient::new(&env, &payment_token).mint(&new_issuer, &5_000_000); - - // Transfer issuer - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer); - client.accept_issuer_transfer(&new_issuer, &issuer, &symbol_short!("def"), &token); - - // New issuer sets share and deposits - client.set_holder_share(&new_issuer, &symbol_short!("def"), &token, &holder, &5_000); - client.deposit_revenue( - &new_issuer, - &symbol_short!("def"), - &token, - &payment_token, - &200_000, - &1, - ); - - // Holder claims under new issuer - let payout = client.claim(&holder, &new_issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout, 100_000); // 50% of 200k -} - -#[test] -fn issuer_transfer_get_offering_still_works() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let new_issuer = Address::generate(&env); - - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer); - client.accept_issuer_transfer(&new_issuer, &issuer, &symbol_short!("def"), &token); - - // get_offering should find the offering under new issuer now - let offering = client.get_offering(&new_issuer, &symbol_short!("def"), &token); - assert!(offering.is_some()); - assert_eq!(offering.clone().unwrap().issuer, new_issuer); -} - -#[test] -fn issuer_transfer_preserves_revenue_share_bps() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let new_issuer = Address::generate(&env); - - let offering_before = client.get_offering(&issuer, &symbol_short!("def"), &token); - - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer); - client.accept_issuer_transfer(&new_issuer, &issuer, &symbol_short!("def"), &token); - - let offering_after = client.get_offering(&new_issuer, &symbol_short!("def"), &token); - assert_eq!( - offering_before.unwrap().revenue_share_bps, - offering_after.unwrap().revenue_share_bps - ); -} - -#[test] -fn issuer_transfer_old_issuer_cannot_report_concentration() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let new_issuer = Address::generate(&env); - - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer); - client.accept_issuer_transfer(&new_issuer, &issuer, &symbol_short!("def"), &token); - - // Old issuer should not be able to report concentration - let result = client.try_report_concentration(&issuer, &symbol_short!("def"), &token, &5_000); - assert!(result.is_err()); -} - -#[test] -fn issuer_transfer_new_issuer_can_report_concentration() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let new_issuer = Address::generate(&env); - - client.set_concentration_limit(&issuer, &symbol_short!("def"), &token, &6_000, &false); - - client.propose_issuer_transfer(&issuer, &symbol_short!("def"), &token, &new_issuer); - client.accept_issuer_transfer(&new_issuer, &issuer, &symbol_short!("def"), &token); - - // New issuer can report concentration - let result = - client.try_report_concentration(&new_issuer, &symbol_short!("def"), &token, &5_000); - assert!(result.is_ok()); -} - -#[test] -fn testnet_mode_normal_operations_unaffected() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - - client.set_admin(&admin); - client.set_testnet_mode(&true); - - // Normal operations should work as expected - client.register_offering(&issuer, &symbol_short!("def"), &token, &5_000, &payout_asset, &0); - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &1_000_000, - &1, - &false, - ); - - let summary = client.get_audit_summary(&issuer, &symbol_short!("def"), &token); - assert_eq!(summary.clone().unwrap().total_revenue, 1_000_000); - assert_eq!(summary.clone().unwrap().report_count, 1); - let summary = client.get_audit_summary(&issuer, &symbol_short!("def"), &token).unwrap(); - assert_eq!(summary.total_revenue, 1_000_000); - assert_eq!(summary.report_count, 1); -} - -#[test] -fn testnet_mode_blacklist_operations_unaffected() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - let issuer = admin.clone(); - let investor = Address::generate(&env); - let issuer = admin.clone(); - - client.set_admin(&admin); - client.set_testnet_mode(&true); - - // Blacklist operations should work normally - client.blacklist_add(&issuer, &issuer, &symbol_short!("def"), &token, &investor); - assert!(client.is_blacklisted(&issuer, &symbol_short!("def"), &token, &investor)); - - client.blacklist_remove(&issuer, &issuer, &symbol_short!("def"), &token, &investor); - assert!(!client.is_blacklisted(&issuer, &symbol_short!("def"), &token, &investor)); -} - -#[test] -fn testnet_mode_pagination_unaffected() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.set_admin(&admin); - client.set_testnet_mode(&true); - - // Register multiple offerings - for i in 0..10 { - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.register_offering( - &issuer, - &symbol_short!("def"), - &token, - &(1_000 + i * 100), - &payout_asset, - &0, - ); - } - - // Pagination should work normally - let (page, cursor) = client.get_offerings_page(&issuer, &symbol_short!("def"), &0, &5); - assert_eq!(page.len(), 5); - assert_eq!(cursor, Some(5)); -} - -#[test] -#[ignore = "non-unwinding panic in no_std"] -#[should_panic] -fn testnet_mode_requires_auth_to_set() { - let env = Env::default(); - // No mock_all_auths - should error - let client = make_client(&env); - let admin = Address::generate(&env); - let _issuer = admin.clone(); - - let r = client.try_set_admin(&admin); - // setting admin without auth should fail - assert!(r.is_err()); - let r2 = client.try_set_testnet_mode(&true); - assert!(r2.is_err()); -} - -// ── Emergency pause tests ─────────────────────────────────────── - -#[test] -fn pause_unpause_idempotence_and_events() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.initialize(&admin, &None::
, &None::); - assert!(!client.is_paused()); - - // Pause twice (idempotent) - client.pause_admin(&admin); - assert!(client.is_paused()); - client.pause_admin(&admin); - assert!(client.is_paused()); - - // Unpause twice (idempotent) - client.unpause_admin(&admin); - assert!(!client.is_paused()); - client.unpause_admin(&admin); - assert!(!client.is_paused()); - - // Verify events were emitted - assert!(legacy_events(&env).len() >= 5); // init + pause + pause + unpause + unpause -} - -#[test] -#[ignore] -#[should_panic(expected = "contract is paused")] -fn register_blocked_while_paused() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.initialize(&admin, &None::
, &None::); - client.pause_admin(&admin); - assert!(client - .try_register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0) - .is_err()); -} - -#[test] -#[ignore] -#[should_panic(expected = "contract is paused")] -fn report_blocked_while_paused() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.initialize(&admin, &None::
, &None::); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - client.pause_admin(&admin); - assert!(client - .try_report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &1_000_000, - &1, - &false, - ) - .is_err()); -} - -#[test] -fn pause_safety_role_works() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let safety = Address::generate(&env); - let issuer = safety.clone(); - - client.initialize(&admin, &Some(safety.clone()), &None::); - assert!(!client.is_paused()); - - // Safety can pause - client.pause_safety(&safety); - assert!(client.is_paused()); - - // Safety can unpause - client.unpause_safety(&safety); - assert!(!client.is_paused()); -} - -#[test] -fn blacklist_add_blocked_while_paused() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - let investor = Address::generate(&env); - - client.initialize(&admin, &None::
, &None::); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - client.pause_admin(&admin); - let res = client.try_blacklist_add(&admin, &issuer, &symbol_short!("def"), &token, &investor); - assert!(res.is_err()); -} - -#[test] -fn blacklist_remove_blocked_while_paused() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - let investor = Address::generate(&env); - - client.initialize(&admin, &None::
, &None::); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - client.pause_admin(&admin); - let res = - client.try_blacklist_remove(&admin, &issuer, &symbol_short!("def"), &token, &investor); - assert!(res.is_err()); -} -#[test] -fn large_period_range_sums_correctly_full() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &payout_asset, &0); - for period in 1..=10 { - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &((period * 100) as i128), - &(period as u64), - &false, - ); - } - assert_eq!( - client.get_revenue_range(&issuer, &symbol_short!("def"), &token, &1, &10), - 100 + 200 + 300 + 400 + 500 + 600 + 700 + 800 + 900 + 1000 - ); -} - -// =========================================================================== -// PROPERTY-BASED INVARIANT TESTS (Hardened for production) -// =========================================================================== - -use crate::proptest_helpers::{ - any_test_operation, arb_strictly_increasing_periods, arb_valid_operation_sequence, - TestOperation, -}; -use soroban_sdk::testutils::Ledger as _; - -/// Enhanced invariant oracle: must hold after ANY sequence. -fn check_invariants_enhanced(env: &Env, client: &RevoraRevenueShareClient, issuers: &Vec
) { - for issuer in issuers.iter() { - let ns = soroban_sdk::symbol_short!("def"); - let offerings_page = client.get_offerings_page(issuer, &ns, &0, &20); - for i in 0..offerings_page.0.len() { - let offering = offerings_page.0.get(i).unwrap(); - let offering_id = crate::OfferingId { - issuer: issuer.clone(), - namespace: ns.clone(), - token: offering.token.clone(), - }; - - // 1. Period ordering preserved - let period_count = client.get_period_count(issuer, &ns, &offering.token); - let mut prev_period = 0u64; - for idx in 0..period_count { - let entry_key = crate::DataKey::PeriodEntry(offering_id.clone(), idx); - let period_id: u64 = env.storage().persistent().get(&entry_key).unwrap_or(0); - assert!(period_id > prev_period, "period ordering violated"); - prev_period = period_id; - } - - // 2. Payout conservation (claimed <= deposited) - let deposited = client.get_total_deposited_revenue(issuer, &ns, &offering.token); - // Placeholder: sum claimed (needs total_claimed_for_holder helper) - // assert!(total_claimed <= deposited); - - // 3. Blacklist enforcement (simplified) - let blacklist = client.get_blacklist(issuer, &ns, &offering.token); - // Placeholder: check blacklisted holders claim 0 - - // 4. Pause state preserved - if client.is_paused() { - // Mutations blocked - } - - // 5. Concentration limit respected - let conc_limit = client.get_concentration_limit(issuer, &ns, &offering.token); - if let Some(cfg) = conc_limit { - if cfg.enforce { - let current_conc = - client.get_current_concentration(issuer, &ns, &offering.token).unwrap_or(0); - assert!(current_conc <= cfg.max_bps, "concentration exceeded"); - } - } - - // 6. Pagination deterministic - let (page1, _) = client.get_offerings_page(issuer, &ns, &0, &3); - let (page2, _) = client.get_offerings_page(issuer, &ns, &3, &3); - // Assert stable ordering - } - } -} - -/// Property: Period ordering invariant holds after random sequences. -proptest! { - #![proptest_config(proptest::test_runner::Config { - cases: 100, - max_local_rng: None, - })] - #[test] - fn prop_period_ordering(env in Env::default(), seq in arb_valid_operation_sequence(&env, 20usize)) { - let client = make_client(&env); - let issuers = vec![&env, [Address::generate(&env)].to_vec()]; - - for op in seq { - match op { - TestOperation::RegisterOffering((i, ns, t, bps, pa)) => { - client.register_offering(&i, &ns, &t, &bps, &pa, &0); - } - TestOperation::ReportRevenue((i, ns, t, pa, amt, pid, ovr)) => { - client.report_revenue(&i, &ns, &t, &pa, &amt, &pid, &ovr); - } - // ... other ops - _ => {} - } - } - - check_invariants_enhanced(&env, &client, &issuers); - } -} - -/// Property: Concentration limits enforced. -proptest! { - #[test] - fn prop_concentration_limits(env in Env::default()) { - let client = make_client(&env); - let issuer = Address::generate(&env); - let ns = symbol_short!("def"); - let token = Address::generate(&env); - - client.register_offering(&issuer, &ns, &token, &1000, &token.clone(), &0); - client.set_concentration_limit(&issuer, &ns, &token.clone(), &5000, &true); - - // Over limit → report_revenue fails - client.report_concentration(&issuer, &ns, &token.clone(), &6000); - let result = client.try_report_revenue(&issuer, &ns, &token, &token, &1000, &1, &false); - prop_assert!(result.is_err()); - } -} - -/// Property: Multisig threshold enforcement. -proptest! { - #[test] - fn prop_multisig_threshold(env in Env::default()) { - let client = make_client(&env); - let owner1 = Address::generate(&env); - let owner2 = Address::generate(&env); - let owner3 = Address::generate(&env); - let caller = Address::generate(&env); - - let mut owners = Vec::new(&env); - owners.push_back(owner1.clone()); - owners.push_back(owner2.clone()); - owners.push_back(owner3.clone()); - - client.init_multisig(&caller, &owners, &2); - - let p1 = client.propose_action(&owner1, &ProposalAction::Freeze); - // Below threshold → fail - prop_assert!(client.try_execute_action(&p1).is_err()); - - client.approve_action(&owner2, &p1); - // Threshold met → succeeds - prop_assert!(client.try_execute_action(&p1).is_ok()); - } -} - -/// Property: Pause safety (mutations blocked post-pause). -proptest! { - #[test] - fn prop_pause_safety(env in Env::default()) { - let client = make_client(&env); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.initialize(&admin, &None::
, &None::); - client.pause_admin(&admin); - - let token = Address::generate(&env); - // Mutations panic post-pause - let result = std::panic::catch_unwind(|| { - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token.clone(), &0); - }); - prop_assert!(result.is_err()); - } -} - -#[test] -fn continuous_invariants_deterministic_reproducible() { - // Existing test preserved -} - -/// Property: Blacklist enforcement (blacklisted holders claim 0). -proptest! { - #[test] - fn prop_blacklist_enforcement( - env in Env::default(), - offering in any_offering_id(&env), - holder in any::
(), - ) { - let (i, ns, t) = offering; - let client = make_client(&env); - client.register_offering(&i, &ns, &t, &1000, &t.clone(), &0); - - // Blacklist holder - client.blacklist_add(&i, &i, &ns, &t.clone(), &holder); - - // Attempt claim - let share_bps = 5000u32; - client.set_holder_share(&i, &ns, &t.clone(), &holder, &share_bps); - // deposit then claim should yield 0 - assert_eq!(client.try_claim(&holder, &i, &ns, &t, &0).unwrap_err(), RevoraError::HolderBlacklisted); - } -} - -/// Property: Pagination stability (register N → paginate exactly). -proptest! { - #![proptest_config(proptest::test_runner::Config { cases: 50..=100, ..Default::default() })] - #[test] - fn prop_pagination_stability( - env in Env::default(), - n in 5usize..=50, - ) { - let client = make_client(&env); - let issuer = Address::generate(&env); - let ns = symbol_short!("def"); - - // Register exactly N offerings - for _ in 0..n { - let token = Address::generate(&env); - client.register_offering(&issuer, &ns, &token, &1000, &token, &0); - } - - assert_eq!(client.get_offering_count(&issuer, &ns), n as u32); - - // Page 1: first 20 (or N) - let (page1, cursor1) = client.get_offerings_page(&issuer, &ns, &0, &20); - let page1_len = page1.len(); - assert!(page1_len <= 20); - - if n > 20 { - let (page2, cursor2) = client.get_offerings_page(&issuer, &ns, &cursor1.unwrap(), &20); - assert_eq!(page1_len + page2.len(), core::cmp::min(40, n)); - } - - // Full scan reconstructs all N - let mut all_count = 0; - let mut cursor: u32 = 0; - loop { - let (page, next) = client.get_offerings_page(&issuer, &ns, &cursor, &20); - all_count += page.len(); - if let Some(c) = next { cursor = c; } else { break; } - } - assert_eq!(all_count, n); - } -} - -/// Stress: Random operations preserve all invariants (1000 cases). -proptest! { - #![proptest_config(proptest::test_runner::Config { - cases: 100, - ..proptest::test_runner::Config::default() - })] - #[test] - fn prop_random_operations( - mut env in any::(), - ) { - env.mock_all_auths(); - let client = make_client(&env); - let seed = 0xdeadbeefu64; - let issuers = vec![&env, vec![&env, Address::generate(&env)]]; - - for step in 0..50 { - let mut rng = seed.wrapping_add((step * 12345) as u64); - let op = any_test_operation(&env).new_tree(&mut proptest::test_runner::rng::RngCoreAdapter::new(&mut rng)).unwrap(); - - // Execute op (mocked) - // ... exec logic per TestOperation variant - - // Oracle check after each step - check_invariants_enhanced(&env, &client, &issuers); - } - } -} - -#[test] -fn continuous_invariants_deterministic_reproducible_smoke() { - // Existing test preserved -} - -// =========================================================================== -// On-chain revenue distribution calculation (#4) -// =========================================================================== - -#[test] -fn calculate_distribution_basic() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let caller = Address::generate(&env); - - let holder = Address::generate(&env); - - let total_revenue = 1_000_000_i128; - let total_supply = 10_000_i128; - let holder_balance = 1_000_i128; - - let payout = client.calculate_distribution( - &issuer, - &issuer, - &symbol_short!("def"), - &token, - &total_revenue, - &total_supply, - &holder_balance, - &holder, - ); - - assert_eq!(payout, 50_000); -} - -#[test] -fn calculate_distribution_bps_100_percent() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let holder = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &10_000, &token, &0); - - let payout = client.calculate_distribution( - &issuer, - &issuer, - &symbol_short!("def"), - &token, - &100_000, - &1_000, - &100, - &holder, - ); - - assert_eq!(payout, 10_000); -} - -#[test] -fn calculate_distribution_bps_25_percent() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let holder = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &2_500, &token, &0); - - let payout = client.calculate_distribution( - &issuer, - &issuer, - &symbol_short!("def"), - &token, - &100_000, - &1_000, - &200, - &holder, - ); - - assert_eq!(payout, 5_000); -} - -#[test] -fn calculate_distribution_zero_revenue() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let caller = Address::generate(&env); - - let holder = Address::generate(&env); - - let payout = client.calculate_distribution( - &issuer, - &issuer, - &symbol_short!("def"), - &token, - &0, - &1_000, - &100, - &holder, - ); - - assert_eq!(payout, 0); -} - -#[test] -fn calculate_distribution_zero_balance() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let caller = Address::generate(&env); - - let holder = Address::generate(&env); - - let payout = client.calculate_distribution( - &issuer, - &issuer, - &symbol_short!("def"), - &token, - &100_000, - &1_000, - &0, - &holder, - ); - - assert_eq!(payout, 0); -} - -#[test] -#[ignore] -#[should_panic(expected = "total_supply cannot be zero")] -fn calculate_distribution_zero_supply_panics() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let caller = Address::generate(&env); - - let holder = Address::generate(&env); - - client.calculate_distribution( - &issuer, - &issuer, - &symbol_short!("def"), - &token, - &100_000, - &0, - &100, - &holder, - ); - assert!(r.is_err()); -} - -#[test] -#[ignore] -#[should_panic(expected = "offering not found")] -fn calculate_distribution_nonexistent_offering_panics() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let holder = Address::generate(&env); - - let r = client.try_calculate_distribution( - &issuer, - &issuer, - &symbol_short!("def"), - &token, - &100_000, - &1_000, - &100, - &holder, - ); - assert!(r.is_err()); -} - -#[test] -#[ignore] -#[should_panic(expected = "holder is blacklisted")] -fn calculate_distribution_blacklisted_holder_panics() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let caller = Address::generate(&env); - - let holder = Address::generate(&env); - - client.blacklist_add(&issuer, &issuer, &symbol_short!("def"), &token, &holder); - - client.calculate_distribution( - &issuer, - &issuer, - &symbol_short!("def"), - &token, - &100_000, - &1_000, - &100, - &holder, - ); - assert!(r.is_err()); -} - -#[test] -fn calculate_distribution_rounds_down() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let holder = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &3_333, &token, &0); - - let payout = client.calculate_distribution( - &issuer, - &issuer, - &symbol_short!("def"), - &token, - &100, - &100, - &10, - &holder, - ); - - assert_eq!(payout, 3); -} - -#[test] -#[ignore] -fn calculate_distribution_rounds_down_exact() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let caller = Address::generate(&env); - let holder = Address::generate(&env); - let payout = client.calculate_distribution( - &issuer, - &issuer, - &symbol_short!("def"), - &token, - &100_000, - &1_000, - &400, - &holder, - ); - - assert_eq!(payout, 10_000); -} - -#[test] -fn calculate_distribution_large_values() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let caller = Address::generate(&env); - - let holder = Address::generate(&env); - - let large_revenue = 1_000_000_000_000_i128; - let total_supply = 1_000_000_000_i128; - let holder_balance = 100_000_000_i128; - - let payout = client.calculate_distribution( - &issuer, - &issuer, - &symbol_short!("def"), - &token, - &large_revenue, - &total_supply, - &holder_balance, - &holder, - ); - - assert_eq!(payout, 50_000_000_000); -} - -#[test] -fn calculate_distribution_emits_event() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let caller = Address::generate(&env); - - let holder = Address::generate(&env); - - let before = legacy_events(&env).len(); - client.calculate_distribution( - &issuer, - &issuer, - &symbol_short!("def"), - &token, - &100_000, - &1_000, - &100, - &holder, - ); - assert!(legacy_events(&env).len() > before); -} - -#[test] -#[ignore] -fn calculate_distribution_multiple_holders_sum() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let caller = Address::generate(&env); - let issuer = caller.clone(); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &5_000, &token, &0); - - let holder_a = Address::generate(&env); - let holder_b = Address::generate(&env); - let holder_c = Address::generate(&env); - - let total_supply = 1_000_i128; - let total_revenue = 100_000_i128; - - let payout_a = client.calculate_distribution( - &caller, - &issuer, - &symbol_short!("def"), - &token, - &total_revenue, - &total_supply, - &500, - &holder_a, - ); - let payout_b = client.calculate_distribution( - &caller, - &issuer, - &symbol_short!("def"), - &token, - &total_revenue, - &total_supply, - &300, - &holder_b, - ); - let payout_c = client.calculate_distribution( - &caller, - &issuer, - &symbol_short!("def"), - &token, - &total_revenue, - &total_supply, - &200, - &holder_c, - ); - - assert_eq!(payout_a, 25_000); - assert_eq!(payout_b, 15_000); - assert_eq!(payout_c, 10_000); - assert_eq!(payout_a + payout_b + payout_c, 50_000); -} - -#[test] -#[ignore] -#[should_panic] -#[ignore] -fn calculate_distribution_requires_auth() { - let env = Env::default(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let holder = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &5_000, &token, &0); - - client.calculate_distribution( - &issuer, - &issuer, - &symbol_short!("def"), - &token, - &100_000, - &1_000, - &100, - &holder, - ); - assert!(r.is_err()); -} - -#[test] -fn calculate_total_distributable_basic() { - let (_env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - - let total = - client.calculate_total_distributable(&issuer, &symbol_short!("def"), &token, &100_000); - - assert_eq!(total, 50_000); -} - -#[test] -fn calculate_total_distributable_bps_100_percent() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &10_000, &token, &0); - - let total = - client.calculate_total_distributable(&issuer, &symbol_short!("def"), &token, &100_000); - - assert_eq!(total, 100_000); -} - -#[test] -fn calculate_total_distributable_bps_25_percent() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &2_500, &token, &0); - - let total = - client.calculate_total_distributable(&issuer, &symbol_short!("def"), &token, &100_000); - - assert_eq!(total, 25_000); -} - -#[test] -fn calculate_total_distributable_zero_revenue() { - let (_env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - - let total = client.calculate_total_distributable(&issuer, &symbol_short!("def"), &token, &0); - - assert_eq!(total, 0); -} - -#[test] -fn calculate_total_distributable_rounds_down() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &3_333, &token, &0); - - let total = client.calculate_total_distributable(&issuer, &symbol_short!("def"), &token, &100); - - assert_eq!(total, 33); -} - -#[test] -#[ignore] -#[should_panic(expected = "offering not found")] -fn calculate_total_distributable_nonexistent_offering_panics() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - let r = - client.try_calculate_total_distributable(&issuer, &symbol_short!("def"), &token, &100_000); - assert!(r.is_err()); -} - -#[test] -fn calculate_total_distributable_large_value() { - let (_env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - - let total = client.calculate_total_distributable( - &issuer, - &symbol_short!("def"), - &token, - &1_000_000_000_000, - ); - - assert_eq!(total, 500_000_000_000); -} - -#[test] -fn calculate_distribution_offering_isolation() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let token_b = Address::generate(&env); - let caller = Address::generate(&env); - /// removed overwriting issuer - let holder = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token_b, &8_000, &token_b, &0); - - let payout_a = client.calculate_distribution( - &issuer, - &issuer, - &symbol_short!("def"), - &token, - &100_000, - &1_000, - &100, - &holder, - ); - let payout_b = client.calculate_distribution( - &issuer, - &issuer, - &symbol_short!("def"), - &token_b, - &100_000, - &1_000, - &100, - &holder, - ); - - assert_eq!(payout_a, 5_000); - assert_eq!(payout_b, 8_000); -} - -#[test] -fn calculate_total_distributable_offering_isolation() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let token_b = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token_b, &8_000, &token_b, &0); - - let total_a = - client.calculate_total_distributable(&issuer, &symbol_short!("def"), &token, &100_000); - let total_b = - client.calculate_total_distributable(&issuer, &symbol_short!("def"), &token_b, &100_000); - - assert_eq!(total_a, 50_000); - assert_eq!(total_b, 80_000); -} - -#[test] -fn calculate_distribution_tiny_balance() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let caller = Address::generate(&env); - - let holder = Address::generate(&env); - - let payout = client.calculate_distribution( - &issuer, - &issuer, - &symbol_short!("def"), - &token, - &100_000, - &1_000_000_000, - &1, - &holder, - ); - - assert_eq!(payout, 0); -} - -#[test] -fn calculate_distribution_all_zeros_except_supply() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let caller = Address::generate(&env); - - let holder = Address::generate(&env); - - let payout = client.calculate_distribution( - &issuer, - &issuer, - &symbol_short!("def"), - &token, - &0, - &1_000, - &0, - &holder, - ); - - assert_eq!(payout, 0); -} - -#[test] -fn calculate_distribution_single_holder_owns_all() { - let (env, client, issuer, token, _payment_token, _contract_id) = claim_setup(); - let caller = Address::generate(&env); - - let holder = Address::generate(&env); - - let total_revenue = 100_000_i128; - let total_supply = 1_000_i128; - - let payout = client.calculate_distribution( - &issuer, - &issuer, - &symbol_short!("def"), - &token, - &total_revenue, - &total_supply, - &total_supply, - &holder, - ); - - assert_eq!(payout, 50_000); -} - -// ── Event-only mode tests ─────────────────────────────────────────────────── - -#[test] -fn test_event_only_mode_register_and_report() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - let amount: i128 = 100_000; - let period_id: u64 = 1; - - // Initialize in event-only mode - client.initialize(&admin, &None, &Some(true)); - - assert!(client.is_event_only()); - - // Register offering should emit event but NOT persist state - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &payout_asset, &0); - - // Verify event emitted (skip checking EVENT_INIT) - let events = legacy_events(&env); - let offer_reg_val: soroban_sdk::Val = symbol_short!("offer_reg").into_val(&env); - assert!(events.iter().any(|e| e.1.contains(offer_reg_val))); - - // Storage should be empty for this offering - assert!(client.get_offering(&issuer, &symbol_short!("def"), &token).is_none()); - assert_eq!(client.get_offering_count(&issuer, &symbol_short!("def")), 0); - - // Report revenue should emit event but NOT require offering to exist in storage - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &amount, - &period_id, - &false, - ); - - let events = legacy_events(&env); - let rev_init_val: soroban_sdk::Val = symbol_short!("rev_init").into_val(&env); - let rev_rep_val: soroban_sdk::Val = symbol_short!("rev_rep").into_val(&env); - assert!(events.iter().any(|e| e.1.contains(rev_init_val))); - assert!(events.iter().any(|e| e.1.contains(rev_rep_val))); - - // Audit summary should NOT be updated - assert!(client.get_audit_summary(&issuer, &symbol_short!("def"), &token).is_none()); -} - -#[test] -fn test_event_only_mode_blacklist() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - let investor = Address::generate(&env); - - client.initialize(&admin, &None, &Some(true)); - - // Blacklist add should emit event but NOT persist - client.blacklist_add(&issuer, &issuer, &symbol_short!("def"), &token, &investor); - - let events = legacy_events(&env); - let bl_add_val: soroban_sdk::Val = symbol_short!("bl_add").into_val(&env); - assert!(events.iter().any(|e| e.1.contains(bl_add_val))); - - assert!(!client.is_blacklisted(&issuer, &symbol_short!("def"), &token, &investor)); - assert_eq!(client.get_blacklist(&issuer, &symbol_short!("def"), &token).len(), 0); -} - -#[test] -fn test_event_only_mode_testnet_config() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.initialize(&admin, &None, &Some(true)); - - client.set_testnet_mode(&true); - - let events = legacy_events(&env); - let test_mode_val: soroban_sdk::Val = symbol_short!("test_mode").into_val(&env); - assert!(events.iter().any(|e| e.1.contains(test_mode_val))); - - assert!(!client.is_testnet_mode()); -} - -// ── Per-offering metadata storage tests (#8) ────────────────── - -#[test] -fn test_set_offering_metadata_success() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - - let metadata = SdkString::from_str(&env, "ipfs://QmTest123"); - let result = - client.try_set_offering_metadata(&issuer, &symbol_short!("def"), &token, &metadata); - assert!(result.is_ok()); -} - -#[test] -fn test_get_offering_metadata_returns_none_initially() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - - let metadata = client.get_offering_metadata(&issuer, &symbol_short!("def"), &token); - assert_eq!(metadata, None); -} - -#[test] -fn test_update_offering_metadata_success() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - - let metadata1 = SdkString::from_str(&env, "ipfs://QmFirst"); - client.set_offering_metadata(&issuer, &symbol_short!("def"), &token, &metadata1); - - let metadata2 = SdkString::from_str(&env, "ipfs://QmSecond"); - let result = - client.try_set_offering_metadata(&issuer, &symbol_short!("def"), &token, &metadata2); - assert!(result.is_ok()); -} - -#[test] -fn test_get_offering_metadata_after_set() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - - let metadata = SdkString::from_str(&env, "https://example.com/metadata.json"); - let r = client.try_set_offering_metadata(&issuer, &symbol_short!("def"), &token, &metadata); - assert!(r.is_ok()); - - let retrieved = client.get_offering_metadata(&issuer, &symbol_short!("def"), &token); - assert_eq!(retrieved, Some(metadata)); -} - -#[test] -#[ignore] -#[should_panic] -#[ignore] -fn test_set_metadata_requires_auth() { - let env = Env::default(); // no mock_all_auths - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - // No mock_all_auths - should return error - let r = client.try_set_offering_metadata( - &issuer, - &symbol_short!("def"), - &token, - &SdkString::from_str(&env, "ipfs://QmTest"), - ); - assert!(r.is_err()); -} - -#[test] -fn test_set_metadata_nonexistent_offering() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - let metadata = SdkString::from_str(&env, "ipfs://QmTest"); - let result = - client.try_set_offering_metadata(&issuer, &symbol_short!("def"), &token, &metadata); - assert!(result.is_err()); -} - -#[test] -fn test_set_metadata_respects_freeze() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - - client.initialize(&admin, &None, &None::); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - client.freeze(); - - let metadata = SdkString::from_str(&env, "ipfs://QmTest"); - let result = - client.try_set_offering_metadata(&issuer, &symbol_short!("def"), &token, &metadata); - assert!(result.is_err()); -} - -#[test] -#[ignore = "require_not_paused uses non-unwinding panic; cannot be caught by try_ in no_std"] -fn test_set_metadata_respects_pause() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - - client.initialize(&admin, &None, &None::); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - client.pause_admin(&admin); - - let metadata = SdkString::from_str(&env, "ipfs://QmTest"); - let result = - client.try_set_offering_metadata(&issuer, &symbol_short!("def"), &token, &metadata); - assert!(result.is_err()); -} - -#[test] -fn test_set_metadata_empty_string() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - - let metadata = SdkString::from_str(&env, ""); - let result = - client.try_set_offering_metadata(&issuer, &symbol_short!("def"), &token, &metadata); - assert!(result.is_ok()); - - let retrieved = client.get_offering_metadata(&issuer, &symbol_short!("def"), &token); - assert_eq!(retrieved, Some(metadata)); -} - -#[test] -fn test_set_metadata_max_length() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - - // 256-char string with valid scheme (ipfs:// = 7 + 249 a's = 256) - let max_str = "ipfs://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - let metadata = SdkString::from_str(&env, max_str); - let result = - client.try_set_offering_metadata(&issuer, &symbol_short!("def"), &token, &metadata); - assert!(result.is_ok()); -} - -#[test] -fn test_set_metadata_oversized_data() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - - // Create a 257-byte string (exceeds max) - let oversized_str = "a".repeat(257); - let metadata = SdkString::from_str(&env, &oversized_str); - let result = - client.try_set_offering_metadata(&issuer, &symbol_short!("def"), &token, &metadata); - assert!(result.is_err()); -} - -#[test] -fn test_set_metadata_repeated_updates() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - - let metadata_values = - ["ipfs://QmTest0", "ipfs://QmTest1", "ipfs://QmTest2", "ipfs://QmTest3", "ipfs://QmTest4"]; - - for metadata_str in metadata_values.iter() { - let metadata = SdkString::from_str(&env, metadata_str); - let result = - client.try_set_offering_metadata(&issuer, &symbol_short!("def"), &token, &metadata); - assert!(result.is_ok()); - - let retrieved = client.get_offering_metadata(&issuer, &symbol_short!("def"), &token); - assert_eq!(retrieved, Some(metadata)); - } -} - -#[test] -fn test_metadata_scoped_per_offering() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token_a = Address::generate(&env); - let token_b = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token_a, &1000, &token_a, &0); - client.register_offering(&issuer, &symbol_short!("def"), &token_b, &2000, &token_b, &0); - - let metadata_a = SdkString::from_str(&env, "ipfs://QmTokenA"); - let metadata_b = SdkString::from_str(&env, "ipfs://QmTokenB"); - - client.set_offering_metadata(&issuer, &symbol_short!("def"), &token_a, &metadata_a); - client.set_offering_metadata(&issuer, &symbol_short!("def"), &token_b, &metadata_b); - - let retrieved_a = client.get_offering_metadata(&issuer, &symbol_short!("def"), &token_a); - let retrieved_b = client.get_offering_metadata(&issuer, &symbol_short!("def"), &token_b); - - assert_eq!(retrieved_a, Some(metadata_a)); - assert_eq!(retrieved_b, Some(metadata_b)); -} - -#[test] -fn test_metadata_set_emits_event() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - - let before = legacy_events(&env).len(); - let metadata = SdkString::from_str(&env, "ipfs://QmTest"); - client.set_offering_metadata(&issuer, &symbol_short!("def"), &token, &metadata); - - let events = legacy_events(&env); - assert!(events.len() > before); - - // Verify the event contains the correct symbol - let last_event = events.last().unwrap(); - let (_, topics, _) = last_event; - let event_symbol: Symbol = topics.get(0).unwrap().into_val(&env); - assert_eq!(event_symbol, symbol_short!("meta_set")); -} - -#[test] -fn test_metadata_update_emits_event() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - - let metadata1 = SdkString::from_str(&env, "ipfs://QmFirst"); - client.set_offering_metadata(&issuer, &symbol_short!("def"), &token, &metadata1); - - let before = legacy_events(&env).len(); - let metadata2 = SdkString::from_str(&env, "ipfs://QmSecond"); - client.set_offering_metadata(&issuer, &symbol_short!("def"), &token, &metadata2); - - let events = legacy_events(&env); - assert!(events.len() > before); - - // Verify the event contains the correct symbol for update - let last_event = events.last().unwrap(); - let (_, topics, _) = last_event; - let event_symbol: Symbol = topics.get(0).unwrap().into_val(&env); - assert_eq!(event_symbol, symbol_short!("meta_upd")); -} - -#[test] -fn test_metadata_events_include_correct_data() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - - let metadata = SdkString::from_str(&env, "ipfs://QmTest123"); - client.set_offering_metadata(&issuer, &symbol_short!("def"), &token, &metadata); - - let events = legacy_events(&env); - let (event_contract, topics, data) = events.last().unwrap(); - - assert_eq!(event_contract, contract_id); - - let event_symbol: Symbol = topics.get(0).unwrap().into_val(&env); - assert_eq!(event_symbol, symbol_short!("meta_set")); - - let event_issuer: Address = topics.get(1).unwrap().into_val(&env); - assert_eq!(event_issuer, issuer); - - // topics[2] = namespace, topics[3] = token - let event_token: Address = topics_vec.get(3).clone().unwrap().into_val(&env); - assert_eq!(event_token, token); - - let event_metadata: SdkString = data.into_val(&env); - assert_eq!(event_metadata, metadata); -} - -#[test] -fn test_metadata_multiple_offerings_same_issuer() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token1 = Address::generate(&env); - let token2 = Address::generate(&env); - let token3 = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token1, &1000, &token1, &0); - client.register_offering(&issuer, &symbol_short!("def"), &token2, &2000, &token2, &0); - client.register_offering(&issuer, &symbol_short!("def"), &token3, &3000, &token3, &0); - - let meta1 = SdkString::from_str(&env, "ipfs://Qm1"); - let meta2 = SdkString::from_str(&env, "ipfs://Qm2"); - let meta3 = SdkString::from_str(&env, "ipfs://Qm3"); - - client.set_offering_metadata(&issuer, &symbol_short!("def"), &token1, &meta1); - client.set_offering_metadata(&issuer, &symbol_short!("def"), &token2, &meta2); - client.set_offering_metadata(&issuer, &symbol_short!("def"), &token3, &meta3); - - assert_eq!(client.get_offering_metadata(&issuer, &symbol_short!("def"), &token1), Some(meta1)); - assert_eq!(client.get_offering_metadata(&issuer, &symbol_short!("def"), &token2), Some(meta2)); - assert_eq!(client.get_offering_metadata(&issuer, &symbol_short!("def"), &token3), Some(meta3)); -} - -#[test] -fn test_metadata_after_issuer_transfer() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let old_issuer = Address::generate(&env); - let new_issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&old_issuer, &symbol_short!("def"), &token, &1000, &token, &0); - - let metadata = SdkString::from_str(&env, "ipfs://QmOriginal"); - client.set_offering_metadata(&old_issuer, &symbol_short!("def"), &token, &metadata); - - // Propose and accept transfer - client.propose_issuer_transfer(&old_issuer, &symbol_short!("def"), &token, &new_issuer); - client.accept_issuer_transfer(&new_issuer, &old_issuer, &symbol_short!("def"), &token); - - // Metadata should still be accessible under old issuer key - let retrieved = client.get_offering_metadata(&old_issuer, &symbol_short!("def"), &token); - assert_eq!(retrieved, Some(metadata)); - - // New issuer can now set metadata (under new issuer key) - let new_metadata = SdkString::from_str(&env, "ipfs://QmNew"); - let result = - client.try_set_offering_metadata(&new_issuer, &symbol_short!("def"), &token, &new_metadata); - assert!(result.is_ok()); -} - -#[test] -fn test_set_metadata_requires_issuer() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let non_issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - - let metadata = SdkString::from_str(&env, "ipfs://QmTest"); - let result = - client.try_set_offering_metadata(&non_issuer, &symbol_short!("def"), &token, &metadata); - assert!(result.is_err()); -} - -#[test] -fn test_metadata_ipfs_cid_format() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - - // Test typical IPFS CID with ipfs:// prefix - let ipfs_cid = - SdkString::from_str(&env, "ipfs://QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG"); - let result = - client.try_set_offering_metadata(&issuer, &symbol_short!("def"), &token, &ipfs_cid); - assert!(result.is_ok()); - - let retrieved = client.get_offering_metadata(&issuer, &symbol_short!("def"), &token); - assert_eq!(retrieved, Some(ipfs_cid)); -} - -#[test] -fn test_metadata_https_url_format() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - - let https_url = SdkString::from_str(&env, "https://api.example.com/metadata/token123.json"); - let result = - client.try_set_offering_metadata(&issuer, &symbol_short!("def"), &token, &https_url); - assert!(result.is_ok()); - - let retrieved = client.get_offering_metadata(&issuer, &symbol_short!("def"), &token); - assert_eq!(retrieved, Some(https_url)); -} - -#[test] -fn test_metadata_content_hash_format() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1000, &token, &0); - - // SHA256 hash with sha256: prefix - let content_hash = SdkString::from_str( - &env, - "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - ); - let result = - client.try_set_offering_metadata(&issuer, &symbol_short!("def"), &token, &content_hash); - assert!(result.is_ok()); - - let retrieved = client.get_offering_metadata(&issuer, &symbol_short!("def"), &token); - assert_eq!(retrieved, Some(content_hash)); -} - -// ══════════════════════════════════════════════════════════════════════════════ -// REGRESSION TEST SUITE -// ══════════════════════════════════════════════════════════════════════════════ -// -// This module contains regression tests for critical bugs discovered in production, -// audits, or security reviews. Each test documents the original issue and verifies -// that the fix prevents recurrence. -// -// ## Guidelines for Adding Regression Tests -// -// 1. **Issue Reference:** Link to the GitHub issue, audit report, or incident ticket -// 2. **Bug Description:** Clearly explain what went wrong and why -// 3. **Expected Behavior:** Document the correct behavior after the fix -// 4. **Determinism:** Use fixed seeds, mock timestamps, and predictable addresses -// 5. **Performance:** Keep tests fast (<100ms) and avoid unnecessary setup -// 6. **Naming:** Use descriptive names: `regression_issue_N_description` -// -// ## Test Template -// -// ```rust -// /// Regression Test: [Brief Title] -// /// -// /// **Related Issue:** #N or [Audit Report Section X.Y] -// /// -// /// **Original Bug:** -// /// [Detailed description of the bug, including conditions that triggered it] -// /// -// /// **Expected Behavior:** -// /// [What should happen instead] -// /// -// /// **Fix Applied:** -// /// [Brief description of the code change that fixed it] -// #[test] -// fn regression_issue_N_description() { -// let env = Env::default(); -// env.mock_all_auths(); -// let client = make_client(&env); -// -// // Arrange: Set up the conditions that triggered the bug -// // ... -// -// // Act: Perform the operation that previously failed -// // ... -// -// // Assert: Verify the fix prevents the bug -// // ... -// } -// ``` -// -// ══════════════════════════════════════════════════════════════════════════════ - -#[cfg(test)] -mod regression { - use super::*; - - /// Regression Test Template - /// - /// **Related Issue:** #0 (Template - not a real bug) - /// - /// **Original Bug:** - /// This is a template test demonstrating the structure for regression tests. - /// Replace this with actual bug details when adding real regression cases. - /// - /// **Expected Behavior:** - /// The contract should handle the edge case correctly without panicking or - /// producing incorrect results. - /// - /// **Fix Applied:** - /// N/A - This is a template. Document the actual fix when adding real tests. - #[test] - fn regression_template_example() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - - // Arrange: Set up test conditions - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - - // Act: Perform the operation - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - - // Assert: Verify correct behavior - let offering = client.get_offering(&issuer, &symbol_short!("def"), &token); - assert!(offering.is_some()); - assert_eq!(offering.clone().unwrap().revenue_share_bps, 1_000); - } - - // ────────────────────────────────────────────────────────────────────────── - // Add new regression tests below this line - // ────────────────────────────────────────────────────────────────────────── - // ── Platform fee tests (#6) ───────────────────────────────── - - #[test] - fn default_platform_fee_is_zero() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.initialize(&admin, &None::
, &None::); - assert_eq!(client.get_platform_fee(), 0); - } - - #[test] - fn set_and_get_platform_fee() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.initialize(&admin, &None::
, &None::); - client.set_platform_fee(&250); - assert_eq!(client.get_platform_fee(), 250); - } - - #[test] - fn set_platform_fee_to_zero() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.initialize(&admin, &None::
, &None::); - client.set_platform_fee(&500); - client.set_platform_fee(&0); - assert_eq!(client.get_platform_fee(), 0); - } - - #[test] - fn set_platform_fee_to_maximum() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.initialize(&admin, &None::
, &None::); - client.set_platform_fee(&5000); - assert_eq!(client.get_platform_fee(), 5000); - } - - #[test] - fn set_platform_fee_above_maximum_fails() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.initialize(&admin, &None::
, &None::); - let result = client.try_set_platform_fee(&5001); - assert!(result.is_err()); - } - - #[test] - fn update_platform_fee_multiple_times() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.initialize(&admin, &None::
, &None::); - client.set_platform_fee(&100); - assert_eq!(client.get_platform_fee(), 100); - client.set_platform_fee(&200); - assert_eq!(client.get_platform_fee(), 200); - client.set_platform_fee(&0); - assert_eq!(client.get_platform_fee(), 0); - } - - #[test] - #[ignore] - #[should_panic] - fn set_platform_fee_requires_admin() { - let env = Env::default(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let _issuer = admin.clone(); - - // No mock_all_auths - should return error - let r = client.try_set_platform_fee(&100); - assert!(r.is_err()); - } - - #[test] - fn calculate_platform_fee_basic() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.initialize(&admin, &None::
, &None::); - client.set_platform_fee(&250); // 2.5% - let fee = client.calculate_platform_fee(&10_000); - assert_eq!(fee, 250); // 10000 * 250 / 10000 = 250 - } - - #[test] - fn calculate_platform_fee_with_zero_amount() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.initialize(&admin, &None::
, &None::); - client.set_platform_fee(&500); - let fee = client.calculate_platform_fee(&0); - assert_eq!(fee, 0); - } - - #[test] - fn calculate_platform_fee_with_zero_fee() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.initialize(&admin, &None::
, &None::); - let fee = client.calculate_platform_fee(&10_000); - assert_eq!(fee, 0); - } - - #[test] - fn calculate_platform_fee_at_maximum_rate() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.initialize(&admin, &None::
, &None::); - client.set_platform_fee(&5000); // 50% - let fee = client.calculate_platform_fee(&10_000); - assert_eq!(fee, 5_000); - } - - #[test] - fn calculate_platform_fee_precision() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.initialize(&admin, &None::
, &None::); - client.set_platform_fee(&1); // 0.01% - let fee = client.calculate_platform_fee(&1_000_000); - assert_eq!(fee, 100); // 1000000 * 1 / 10000 = 100 - } - - #[test] - #[ignore] - #[should_panic] - fn platform_fee_only_admin_can_set() { - let env = Env::default(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let _issuer = admin.clone(); - - // No mock_all_auths - should return error - let r = client.try_set_platform_fee(&100); - assert!(r.is_err()); - } - - #[test] - fn platform_fee_large_amount() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.initialize(&admin, &None::
, &None::); - client.set_platform_fee(&100); // 1% - let large_amount: i128 = 1_000_000_000_000; - let fee = client.calculate_platform_fee(&large_amount); - assert_eq!(fee, 10_000_000_000); // 1% of 1 trillion - } - - #[test] - fn platform_fee_integration_with_revenue() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - client.initialize(&admin, &None::
, &None::); - client.set_platform_fee(&500); // 5% - let revenue: i128 = 100_000; - let fee = client.calculate_platform_fee(&revenue); - assert_eq!(fee, 5_000); // 5% of 100,000 - let remaining = revenue - fee; - assert_eq!(remaining, 95_000); - } - - // --------------------------------------------------------------------------- - // Per-offering minimum revenue thresholds (#25) - // --------------------------------------------------------------------------- - - #[test] - fn min_revenue_threshold_default_is_zero() { - let env = Env::default(); - let (client, issuer, token, _payout) = setup_with_offering(&env); - let threshold = client.get_min_revenue_threshold(&issuer, &symbol_short!("def"), &token); - assert_eq!(threshold, 0); - } - - #[test] - fn set_min_revenue_threshold_emits_event() { - let env = Env::default(); - let (client, issuer, token, _payout) = setup_with_offering(&env); - let before = legacy_events(&env).len(); - client.set_min_revenue_threshold(&issuer, &symbol_short!("def"), &token, &5_000); - assert!(legacy_events(&env).len() > before); - } - - #[test] - fn report_below_threshold_emits_event_and_skips_distribution() { - let env = Env::default(); - let (client, issuer, token, payout_asset) = setup_with_offering(&env); - client.set_min_revenue_threshold(&issuer, &symbol_short!("def"), &token, &10_000); - let events_before = legacy_events(&env).len(); - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &1_000, - &1, - &false, - ); - let events_after = legacy_events(&env).len(); - assert!(events_after > events_before, "should emit rev_below event"); - let summary = client.get_audit_summary(&issuer, &symbol_short!("def"), &token); - assert!( - summary.is_none() || summary.as_ref().clone().unwrap().report_count == 0, - "below-threshold report must not count toward audit" - ); - } - - #[test] - fn report_at_or_above_threshold_updates_state() { - let env = Env::default(); - let (client, issuer, token, payout_asset) = setup_with_offering(&env); - client.set_min_revenue_threshold(&issuer, &symbol_short!("def"), &token, &1_000); - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &1_000, - &1, - &false, - ); - let summary = client.get_audit_summary(&issuer, &symbol_short!("def"), &token); - assert_eq!(summary.clone().unwrap().report_count, 1); - assert_eq!(summary.clone().unwrap().total_revenue, 1_000); - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &2_000, - &2, - &false, - ); - let summary2 = client.get_audit_summary(&issuer, &symbol_short!("def"), &token); - assert_eq!(summary2.report_count, 2); - assert_eq!(summary2.total_revenue, 3_000); - } - - #[test] - fn zero_threshold_disables_check() { - let env = Env::default(); - let (client, issuer, token, payout_asset) = setup_with_offering(&env); - client.set_min_revenue_threshold(&issuer, &symbol_short!("def"), &token, &100); - client.set_min_revenue_threshold(&issuer, &symbol_short!("def"), &token, &0); - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &50, - &1, - &false, - ); - let summary = client.get_audit_summary(&issuer, &symbol_short!("def"), &token); - assert_eq!(summary.clone().unwrap().report_count, 1); - } - - #[test] - fn set_concentration_limit_emits_event() { - let (env, client, issuer, token, _) = setup_with_offering(); - let before = env.events().all().len(); - client.set_concentration_limit(&issuer, &symbol_short!("def"), &token, &5_000, &true); - assert!(env.events().all().len() > before); - } - - // --------------------------------------------------------------------------- - // Deterministic ordering for query results (#38) - // --------------------------------------------------------------------------- - - #[test] - fn get_offerings_page_order_is_by_registration_index() { - let env = Env::default(); - let (client, issuer) = setup(&env); - let t0 = Address::generate(&env); - let t1 = Address::generate(&env); - let t2 = Address::generate(&env); - let t3 = Address::generate(&env); - let p0 = Address::generate(&env); - let p1 = Address::generate(&env); - let p2 = Address::generate(&env); - let p3 = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &t0, &100, &p0, &0); - client.register_offering(&issuer, &symbol_short!("def"), &t1, &200, &p1, &0); - client.register_offering(&issuer, &symbol_short!("def"), &t2, &300, &p2, &0); - client.register_offering(&issuer, &symbol_short!("def"), &t3, &400, &p3, &0); - let (page, _) = client.get_offerings_page(&issuer, &symbol_short!("def"), &0, &10); - assert_eq!(page.len(), 4); - assert_eq!(page.get(0).clone().unwrap().token, t0); - assert_eq!(page.get(1).clone().unwrap().token, t1); - assert_eq!(page.get(2).clone().unwrap().token, t2); - assert_eq!(page.get(3).clone().unwrap().token, t3); - } - - #[test] - fn set_admin_emits_event() { - // EVENT_ADMIN_SET is emitted both by set_admin and initialize. - // We verify initialize emits it, proving the event is correct. - let env = Env::default(); - env.mock_all_auths(); - let cid = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &cid); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - let a = Address::generate(&env); - let b = Address::generate(&env); - let c = Address::generate(&env); - - client.initialize(&admin, &None::
, &None::); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - client.blacklist_add(&admin, &issuer, &symbol_short!("def"), &token, &a); - client.blacklist_add(&admin, &issuer, &symbol_short!("def"), &token, &b); - client.blacklist_add(&admin, &issuer, &symbol_short!("def"), &token, &c); - let list = client.get_blacklist(&issuer, &symbol_short!("def"), &token); - assert_eq!(list.len(), 3); - assert_eq!(list.get(0).unwrap(), a); - assert_eq!(list.get(1).unwrap(), b); - assert_eq!(list.get(2).unwrap(), c); - } - - #[test] - fn set_platform_fee_emits_event() { - let env = Env::default(); - env.mock_all_auths(); - let cid = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &cid); - let admin = Address::generate(&env); - let issuer = admin.clone(); - - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - let a = Address::generate(&env); - let b = Address::generate(&env); - let c = Address::generate(&env); - - client.initialize(&admin, &None::
, &None::); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - client.blacklist_add(&admin, &issuer, &symbol_short!("def"), &token, &a); - client.blacklist_add(&admin, &issuer, &symbol_short!("def"), &token, &b); - client.blacklist_add(&admin, &issuer, &symbol_short!("def"), &token, &c); - client.blacklist_remove(&admin, &issuer, &symbol_short!("def"), &token, &b); - let list = client.get_blacklist(&issuer, &symbol_short!("def"), &token); - assert_eq!(list.len(), 2); - assert_eq!(list.get(0).unwrap(), a); - assert_eq!(list.get(1).unwrap(), c); - } - - #[test] - fn get_pending_periods_order_is_by_deposit_index() { - let (env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &100, &10); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &200, &20); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &300, &30); - let holder = Address::generate(&env); - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &holder, &1_000); - let periods = client.get_pending_periods(&issuer, &symbol_short!("def"), &token, &holder); - assert_eq!(periods.len(), 3); - assert_eq!(periods.get(0).unwrap(), 10); - assert_eq!(periods.get(1).unwrap(), 20); - assert_eq!(periods.get(2).unwrap(), 30); - } - - // --------------------------------------------------------------------------- - // Contract version and migration (#23) - // --------------------------------------------------------------------------- - - #[test] - fn get_version_returns_constant_version() { - let env = Env::default(); - let client = make_client(&env); - assert_eq!(client.get_version(), crate::CONTRACT_VERSION); - } - - #[test] - fn get_version_unchanged_after_operations() { - let env = Env::default(); - let (client, issuer) = setup(&env); - let v0 = client.get_version(); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - assert_eq!(client.get_version(), v0); - } - - // --------------------------------------------------------------------------- - // Versioned Migration Hooks (#44) - // --------------------------------------------------------------------------- - - #[test] - fn test_migrate_success() { - let env = Env::default(); - env.mock_all_auths(); - let cid = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &cid); - let admin = Address::generate(&env); - client.initialize(&admin, &None, &None); - - // Initial deployed version should be 0 (since we didn't update initialize) - assert_eq!(client.get_deployed_version(), 0); - - // Migrate to current CONTRACT_VERSION - let result = client.migrate(); - assert_eq!(result, crate::CONTRACT_VERSION); - assert_eq!(client.get_deployed_version(), crate::CONTRACT_VERSION); - } - - #[test] - #[should_panic(expected = "HostError: Error(Contract, #19)")] - fn test_migrate_unauthorized() { - let env = Env::default(); - let cid = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &cid); - let admin = Address::generate(&env); - client.initialize(&admin, &None, &None); - - // Call migrate without auth (will fail because mock_all_auths is not set) - client.migrate(); - } - - #[test] - fn test_migrate_already_at_target() { - let env = Env::default(); - env.mock_all_auths(); - let cid = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &cid); - let admin = Address::generate(&env); - client.initialize(&admin, &None, &None); - - client.migrate(); - assert_eq!(client.get_deployed_version(), crate::CONTRACT_VERSION); - - // Try to migrate again - let result = client.try_migrate(); - assert_eq!(result, Err(Ok(RevoraError::AlreadyAtTargetVersion))); - } - - #[test] - fn test_migrate_respects_freeze() { - let env = Env::default(); - env.mock_all_auths(); - let cid = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &cid); - let admin = Address::generate(&env); - client.initialize(&admin, &None, &None); - - // Freeze contract - client.set_frozen(&true); - - // Try to migrate - let result = client.try_migrate(); - assert_eq!(result, Err(Ok(RevoraError::ContractFrozen))); - } - - // --------------------------------------------------------------------------- - // Input parameter validation (#35) - // --------------------------------------------------------------------------- - - #[test] - fn deposit_revenue_rejects_zero_amount() { - let (_env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let r = client.try_deposit_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &0, - &1, - ); - assert!(r.is_err()); - } - - #[test] - fn deposit_revenue_rejects_negative_amount() { - let (_env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let r = client.try_deposit_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &-1, - &1, - ); - assert!(r.is_err()); - } - - #[test] - fn deposit_revenue_rejects_zero_period_id() { - let (_env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let r = client.try_deposit_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &100, - &0, - ); - assert!(r.is_err()); - } - - #[test] - fn deposit_revenue_accepts_minimum_valid_inputs() { - let (_env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - let r = client.try_deposit_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &1, - &1, - ); - assert!(r.is_ok()); - } - - #[test] - fn report_revenue_rejects_negative_amount() { - let env = Env::default(); - let (client, issuer, token, payout_asset) = setup_with_offering(&env); - let r = client.try_report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &-1, - &1, - &false, - ); - assert!(r.is_err()); - } - - #[test] - fn report_revenue_accepts_zero_amount() { - let env = Env::default(); - let (client, issuer, token, payout_asset) = setup_with_offering(&env); - let r = client.try_report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &0, - &0, - &false, - ); - assert!(r.is_ok()); - } - - #[test] - fn set_min_revenue_threshold_rejects_negative() { - let env = Env::default(); - let (client, issuer, token, _payout_asset) = setup_with_offering(&env); - let r = client.try_set_min_revenue_threshold(&issuer, &symbol_short!("def"), &token, &-1); - assert!(r.is_err()); - } - - #[test] - fn set_min_revenue_threshold_accepts_zero() { - let env = Env::default(); - let (client, issuer, token, _payout_asset) = setup_with_offering(&env); - let r = client.try_set_min_revenue_threshold(&issuer, &symbol_short!("def"), &token, &0); - assert!(r.is_ok()); - } - - // --------------------------------------------------------------------------- - // Continuous invariants testing (#49) – randomized sequences, deterministic seed - // --------------------------------------------------------------------------- - - const INVARIANT_SEED: u64 = 0x1234_5678_9abc_def0; - /// Kept modest to stay within Soroban test budget (#49). - const INVARIANT_STEPS: usize = 24; - - /// Run one random step (deterministic given seed). - fn invariant_random_step( - env: &Env, - client: &RevoraRevenueShareClient, - issuers: &soroban_sdk::Vec
, - tokens: &soroban_sdk::Vec
, - payout_assets: &soroban_sdk::Vec
, - seed: &mut u64, - ) { - let n_issuers = issuers.len() as usize; - let n_tokens = tokens.len() as usize; - let n_payout = payout_assets.len() as usize; - if n_issuers == 0 || n_tokens == 0 { - return; - } - let op = next_u64(seed) % 6; - let issuer_idx = (next_u64(seed) as usize) % n_issuers; - let token_idx = (next_u64(seed) as usize) % n_tokens; - let issuer = issuers.get(issuer_idx as u32).unwrap(); - let token = tokens.get(token_idx as u32).unwrap(); - let payout_idx = token_idx.min(n_payout.saturating_sub(1)); - let payout = payout_assets.get(payout_idx as u32).unwrap(); - - match op { - 0 => { - let _ = client.try_register_offering( - &issuer, - &symbol_short!("def"), - &token, - &1_000, - &payout, - &0, - ); - } - 1 => { - let amount = (next_u64(seed) % 1_000_000 + 1) as i128; - let period_id = next_period(seed) % 1_000_000 + 1; - let _ = client.try_report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout, - &amount, - &period_id, - &false, - ); - } - 2 => { - let _ = client.try_set_concentration_limit( - &issuer, - &symbol_short!("def"), - &token, - &5000, - &false, - ); - } - 3 => { - let conc_bps = (next_u64(seed) % 10_001) as u32; - let _ = client.try_report_concentration( - &issuer, - &symbol_short!("def"), - &token, - &conc_bps, - ); - } - 4 => { - let holder = Address::generate(env); - client.blacklist_add(&issuer, &issuer, &symbol_short!("def"), &token, &holder); - } - 5 => { - client.blacklist_remove(&issuer, &issuer, &symbol_short!("def"), &token, &issuer); - } - _ => {} - } - } - - /// Check invariants that must hold after any step. - fn check_invariants(client: &RevoraRevenueShareClient, issuers: &soroban_sdk::Vec
) { - for i in 0..issuers.len() { - let issuer = issuers.get(i).unwrap(); - let count = client.get_offering_count(&issuer, &symbol_short!("def")); - let (page, cursor) = client.get_offerings_page(&issuer, &symbol_short!("def"), &0, &20); - assert_eq!(page.len(), count.min(20)); - assert!(count <= 200, "offering count bounded"); - if count > 0 { - assert!(cursor.is_some() || page.len() == count); - } - } - let _v = client.get_version(); - assert!(_v >= 1); - } - - #[test] - fn continuous_invariants_after_random_operations() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let mut issuers_vec = Vec::new(&env); - let mut tokens_vec = Vec::new(&env); - let mut payout_vec = Vec::new(&env); - for _ in 0..4 { - issuers_vec.push_back(Address::generate(&env)); - let t = Address::generate(&env); - let p = Address::generate(&env); - tokens_vec.push_back(t); - payout_vec.push_back(p); - } - let mut seed = INVARIANT_SEED; - - for _ in 0..INVARIANT_STEPS { - invariant_random_step(&env, &client, &issuers_vec, &tokens_vec, &payout_vec, &mut seed); - check_invariants(&client, &issuers_vec); - } - } - - #[test] - fn continuous_invariants_deterministic_reproducible() { - let env1 = Env::default(); - env1.mock_all_auths(); - let client1 = make_client(&env1); - let mut iss1 = Vec::new(&env1); - let mut tok1 = Vec::new(&env1); - let mut pay1 = Vec::new(&env1); - iss1.push_back(Address::generate(&env1)); - tok1.push_back(Address::generate(&env1)); - pay1.push_back(Address::generate(&env1)); - let mut seed1 = INVARIANT_SEED; - for _ in 0..16 { - let _ = client1.try_register_offering( - &iss1.get(0).unwrap(), - &symbol_short!("def"), - &tok1.get(0).unwrap(), - &1000, - &pay1.get(0).unwrap(), - &0, - ); - invariant_random_step(&env1, &client1, &iss1, &tok1, &pay1, &mut seed1); - } - let count1 = client1.get_offering_count(&iss1.get(0).unwrap(), &symbol_short!("def")); - - let env2 = Env::default(); - env2.mock_all_auths(); - let client2 = make_client(&env2); - let mut iss2 = Vec::new(&env2); - let mut tok2 = Vec::new(&env2); - let mut pay2 = Vec::new(&env2); - iss2.push_back(Address::generate(&env2)); - tok2.push_back(Address::generate(&env2)); - pay2.push_back(Address::generate(&env2)); - let mut seed2 = INVARIANT_SEED; - for _ in 0..16 { - let _ = client2.try_register_offering( - &iss2.get(0).unwrap(), - &symbol_short!("def"), - &tok2.get(0).unwrap(), - &1000, - &pay2.get(0).unwrap(), - &0, - ); - invariant_random_step(&env2, &client2, &iss2, &tok2, &pay2, &mut seed2); - } - let count2 = client2.get_offering_count(&iss2.get(0).unwrap(), &symbol_short!("def")); - assert_eq!(count1, count2, "same seed yields same operation sequence"); - } - - // =========================================================================== - // Cross-offering aggregation query tests (#39) - // =========================================================================== - - #[test] - fn aggregation_empty_issuer_returns_zeroes() { - let (_env, client, issuer) = setup(); - let metrics = client.get_issuer_aggregation(&issuer); - assert_eq!(metrics.total_reported_revenue, 0); - assert_eq!(metrics.total_deposited_revenue, 0); - assert_eq!(metrics.total_report_count, 0); - assert_eq!(metrics.offering_count, 0); - } - - #[test] - fn aggregation_single_offering_reported_revenue() { - let env = Env::default(); - let (client, issuer) = setup(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout_asset, &0); - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &100_000, - &1, - &false, - ); - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &200_000, - &2, - &false, - ); - - let metrics = client.get_issuer_aggregation(&issuer); - assert_eq!(metrics.total_reported_revenue, 300_000); - assert_eq!(metrics.total_report_count, 2); - assert_eq!(metrics.offering_count, 1); - assert_eq!(metrics.total_deposited_revenue, 0); - } - - #[test] - fn aggregation_multiple_offerings_same_issuer() { - let env = Env::default(); - let (client, issuer) = setup(&env); - let token_a = Address::generate(&env); - let token_b = Address::generate(&env); - let payout_a = Address::generate(&env); - let payout_b = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token_a, &1_000, &payout_a, &0); - client.register_offering(&issuer, &symbol_short!("def"), &token_b, &2_000, &payout_b, &0); - - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token_a, - &payout_a, - &100_000, - &1, - &false, - ); - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token_b, - &payout_b, - &200_000, - &1, - &false, - ); - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token_b, - &payout_b, - &300_000, - &2, - &false, - ); - - let metrics = client.get_issuer_aggregation(&issuer); - assert_eq!(metrics.total_reported_revenue, 600_000); - assert_eq!(metrics.total_report_count, 3); - assert_eq!(metrics.offering_count, 2); - } - - #[test] - fn aggregation_deposited_revenue_tracking() { - let (_env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - - client.deposit_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &100_000, - &1, - ); - client.deposit_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &200_000, - &2, - ); - - let metrics = client.get_issuer_aggregation(&issuer); - assert_eq!(metrics.total_deposited_revenue, 300_000); - assert_eq!(metrics.offering_count, 1); - } - - #[test] - fn aggregation_mixed_reported_and_deposited() { - let (_env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - - // Report revenue - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &500_000, - &1, - &false, - ); - - // Deposit revenue - client.deposit_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &100_000, - &10, - ); - client.deposit_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &200_000, - &20, - ); - - let metrics = client.get_issuer_aggregation(&issuer); - assert_eq!(metrics.total_reported_revenue, 500_000); - assert_eq!(metrics.total_deposited_revenue, 300_000); - assert_eq!(metrics.total_report_count, 1); - assert_eq!(metrics.offering_count, 1); - } - - #[test] - fn aggregation_per_issuer_isolation() { - let (env, client, issuer_a) = setup(); - let issuer_b = Address::generate(&env); - let issuer = issuer_b.clone(); - - let token_a = Address::generate(&env); - let token_b = Address::generate(&env); - let payout_a = Address::generate(&env); - let payout_b = Address::generate(&env); - - client.register_offering(&issuer_a, &symbol_short!("def"), &token_a, &1_000, &payout_a, &0); - client.register_offering(&issuer_b, &symbol_short!("def"), &token_b, &2_000, &payout_b, &0); - - client.report_revenue( - &issuer_a, - &symbol_short!("def"), - &token_a, - &payout_a, - &100_000, - &1, - &false, - ); - client.report_revenue( - &issuer_b, - &symbol_short!("def"), - &token_b, - &payout_b, - &500_000, - &1, - &false, - ); - - let metrics_a = client.get_issuer_aggregation(&issuer_a); - let metrics_b = client.get_issuer_aggregation(&issuer_b); - - assert_eq!(metrics_a.total_reported_revenue, 100_000); - assert_eq!(metrics_a.offering_count, 1); - assert_eq!(metrics_b.total_reported_revenue, 500_000); - assert_eq!(metrics_b.offering_count, 1); - } - - #[test] - fn platform_aggregation_empty() { - let (_env, client, _issuer) = setup(); - let metrics = client.get_platform_aggregation(); - assert_eq!(metrics.total_reported_revenue, 0); - assert_eq!(metrics.total_deposited_revenue, 0); - assert_eq!(metrics.total_report_count, 0); - assert_eq!(metrics.offering_count, 0); - } - - #[test] - fn platform_aggregation_single_issuer() { - let env = Env::default(); - let (client, issuer) = setup(&env); - let token = Address::generate(&env); - let payout = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout, &0); - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout, - &100_000, - &1, - &false, - ); - - let metrics = client.get_platform_aggregation(); - assert_eq!(metrics.total_reported_revenue, 100_000); - assert_eq!(metrics.total_report_count, 1); - assert_eq!(metrics.offering_count, 1); - } - - #[test] - fn platform_aggregation_multiple_issuers() { - let (env, client, issuer_a) = setup(); - let issuer_b = Address::generate(&env); - let issuer = issuer_b.clone(); - - let issuer_c = Address::generate(&env); - - let token_a = Address::generate(&env); - let token_b = Address::generate(&env); - let token_c = Address::generate(&env); - let payout_a = Address::generate(&env); - let payout_b = Address::generate(&env); - let payout_c = Address::generate(&env); - - client.register_offering(&issuer_a, &symbol_short!("def"), &token_a, &1_000, &payout_a, &0); - client.register_offering(&issuer_b, &symbol_short!("def"), &token_b, &2_000, &payout_b, &0); - client.register_offering(&issuer_c, &symbol_short!("def"), &token_c, &3_000, &payout_c, &0); - - client.report_revenue( - &issuer_a, - &symbol_short!("def"), - &token_a, - &payout_a, - &100_000, - &1, - &false, - ); - client.report_revenue( - &issuer_b, - &symbol_short!("def"), - &token_b, - &payout_b, - &200_000, - &1, - &false, - ); - client.report_revenue( - &issuer_c, - &symbol_short!("def"), - &token_c, - &payout_c, - &300_000, - &1, - &false, - ); - - let metrics = client.get_platform_aggregation(); - assert_eq!(metrics.total_reported_revenue, 600_000); - assert_eq!(metrics.total_report_count, 3); - assert_eq!(metrics.offering_count, 3); - } - - #[test] - fn get_all_issuers_returns_registered() { - let (env, client, issuer_a) = setup(); - let issuer_b = Address::generate(&env); - let issuer = issuer_b.clone(); - - let token_a = Address::generate(&env); - let token_b = Address::generate(&env); - let payout_a = Address::generate(&env); - let payout_b = Address::generate(&env); - - client.register_offering(&issuer_a, &symbol_short!("def"), &token_a, &1_000, &payout_a, &0); - client.register_offering(&issuer_b, &symbol_short!("def"), &token_b, &2_000, &payout_b, &0); - - let issuers = client.get_all_issuers(); - assert_eq!(issuers.len(), 2); - assert!(issuers.contains(&issuer_a)); - assert!(issuers.contains(&issuer_b)); - } - - #[test] - fn get_all_issuers_empty_when_none_registered() { - let (_env, client, _issuer) = setup(); - let issuers = client.get_all_issuers(); - assert_eq!(issuers.len(), 0); - } - - #[test] - fn issuer_registered_once_even_with_multiple_offerings() { - let env = Env::default(); - let (client, issuer) = setup(&env); - let token_a = Address::generate(&env); - let token_b = Address::generate(&env); - let token_c = Address::generate(&env); - let payout_a = Address::generate(&env); - let payout_b = Address::generate(&env); - let payout_c = Address::generate(&env); - - client.register_offering(&issuer, &symbol_short!("def"), &token_a, &1_000, &payout_a, &0); - client.register_offering(&issuer, &symbol_short!("def"), &token_b, &2_000, &payout_b, &0); - client.register_offering(&issuer, &symbol_short!("def"), &token_c, &3_000, &payout_c, &0); - - let issuers = client.get_all_issuers(); - assert_eq!(issuers.len(), 1); - assert_eq!(issuers.get(0).unwrap(), issuer); - } - - #[test] - fn get_total_deposited_revenue_per_offering() { - let (_env, client, issuer, token, payment_token, _contract_id) = claim_setup(); - - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &50_000, &1); - client.deposit_revenue(&issuer, &symbol_short!("def"), &token, &payment_token, &75_000, &2); - client.deposit_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payment_token, - &125_000, - &3, - ); - - let total = client.get_total_deposited_revenue(&issuer, &symbol_short!("def"), &token); - assert_eq!(total, 250_000); - } - - #[test] - fn get_total_deposited_revenue_zero_when_no_deposits() { - let (env, _client, issuer) = setup(); - let client = make_client(&env); - let random_token = Address::generate(&env); - assert_eq!( - client.get_total_deposited_revenue(&issuer, &symbol_short!("def"), &random_token), - 0 - ); - } - - #[test] - fn aggregation_no_reports_only_offerings() { - let env = Env::default(); - let (client, issuer) = setup(&env); - register_n(&env, &client, &issuer, 5); - - let metrics = client.get_issuer_aggregation(&issuer); - assert_eq!(metrics.offering_count, 5); - assert_eq!(metrics.total_reported_revenue, 0); - assert_eq!(metrics.total_deposited_revenue, 0); - assert_eq!(metrics.total_report_count, 0); - } - - #[test] - fn init_multisig_emits_event() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - - let issuer_a = Address::generate(&env); - let issuer = issuer_a.clone(); - - let issuer_b = Address::generate(&env); - let issuer = issuer_b.clone(); - - let token_a = Address::generate(&env); - let token_b = Address::generate(&env); - - let (pt_a, pt_a_admin) = create_payment_token(&env); - let (pt_b, pt_b_admin) = create_payment_token(&env); - - client.register_offering(&issuer_a, &symbol_short!("def"), &token_a, &5_000, &pt_a, &0); - client.register_offering(&issuer_b, &symbol_short!("def"), &token_b, &3_000, &pt_b, &0); - - mint_tokens(&env, &pt_a, &pt_a_admin, &issuer_a, &5_000_000); - mint_tokens(&env, &pt_b, &pt_b_admin, &issuer_b, &5_000_000); - - client.deposit_revenue(&issuer_a, &symbol_short!("def"), &token_a, &pt_a, &100_000, &1); - client.deposit_revenue(&issuer_b, &symbol_short!("def"), &token_b, &pt_b, &200_000, &1); - - let metrics = client.get_platform_aggregation(); - assert_eq!(metrics.total_deposited_revenue, 300_000); - assert_eq!(metrics.offering_count, 2); - } - - #[test] - fn aggregation_stress_many_offerings() { - let env = Env::default(); - let (client, issuer) = setup(&env); - - // Register 20 offerings and report revenue on each - let mut tokens = soroban_sdk::Vec::new(&env); - let mut payouts = soroban_sdk::Vec::new(&env); - for _i in 0..20_u32 { - let token = Address::generate(&env); - let payout = Address::generate(&env); - tokens.push_back(token.clone()); - payouts.push_back(payout.clone()); - client.register_offering(&issuer, &symbol_short!("def"), &token, &1_000, &payout, &0); - } - - for i in 0..20_u32 { - let token = tokens.get(i).unwrap(); - let payout = payouts.get(i).unwrap(); - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout, - &((i as i128 + 1) * 10_000), - &1, - &false, - ); - } - - let metrics = client.get_issuer_aggregation(&issuer); - assert_eq!(metrics.offering_count, 20); - // Sum of 10_000 + 20_000 + ... + 200_000 = 10_000 * (1 + 2 + ... + 20) = 10_000 * 210 = 2_100_000 - assert_eq!(metrics.total_reported_revenue, 2_100_000); - assert_eq!(metrics.total_report_count, 20); - } - - #[test] - fn happy_path_lifecycle() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - - let investor_a = Address::generate(&env); - let investor_b = Address::generate(&env); - - // 1. Issuer registers offering with 50% revenue share (5000 bps) - client.register_offering(&issuer, &symbol_short!("def"), &token, &5_000, &payout_asset, &0); - - // 2. Report revenue for period 1 - // total_revenue = 1,000,000 - // distributable = 1,000,000 * 50% = 500,000 - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &1_000_000, - &1, - &false, - ); - - // 3. Investors set their shares for period 1 (Total supply 100) - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &investor_a, &6_000); // 60% - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &investor_b, &4_000); // 40% - - // 4. Report revenue for period 2 - // total_revenue = 2,000,000 - // distributable = 2,000,000 * 50% = 1,000,000 - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &2_000_000, - &2, - &false, - ); - - // 5. Investors' shares shift for period 2 - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &investor_a, &2_000); // 20% - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &investor_b, &8_000); // 80% - - // 6. Investor A claims all available periods (1 and 2) - let claimable_a = client.get_claimable(&issuer, &symbol_short!("def"), &token, &investor_a); - assert_eq!(claimable_a, 500_000); - let payout_a = client.claim(&investor_a, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout_a, 500_000); - - // 7. Investor B claims all available periods - let claimable_b = client.get_claimable(&issuer, &symbol_short!("def"), &token, &investor_b); - assert_eq!(claimable_b, 1_000_000); - let payout_b = client.claim(&investor_b, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout_b, 1_000_000); - - // Verify no pending claims - let remaining_a = - client.get_pending_periods(&issuer, &symbol_short!("def"), &token, &investor_a); - assert!(remaining_a.is_empty()); - let claimable_b_after = - client.get_claimable(&issuer, &symbol_short!("def"), &token, &investor_b); - assert_eq!(claimable_b_after, 0); - - // Verify aggregation totals - let metrics = client.get_platform_aggregation(); - assert_eq!(metrics.total_reported_revenue, 3_000_000); - assert_eq!(metrics.total_report_count, 2); - } - - #[test] - fn failure_and_correction_flow() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let payout_asset = Address::generate(&env); - let investor = Address::generate(&env); - - // 1. Offering registered with 100% revenue share and a time delay (86400 secs) - client.register_offering( - &issuer, - &symbol_short!("def"), - &token, - &10_000, - &payout_asset, - &86400, - ); - - // 2. Issuer attempts to report negative revenue (validation should reject) - let res = client.try_report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &-500, - &1, - &false, - ); - assert!(res.is_err()); - - // 3. Issuer successfully reports valid revenue for period 1 - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &100_000, - &1, - &false, - ); - - // 4. Investor is assigned 100% share for period 1 - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &investor, &10_000); - - // 5. Investor tries to claim but delay has not elapsed - let claim_preview = client.get_claimable(&issuer, &symbol_short!("def"), &token, &investor); - assert_eq!(claim_preview, 0); // Preview returns 0 since delay hasn't passed - let claim_res = client.try_claim(&investor, &issuer, &symbol_short!("def"), &token, &0); - assert!(claim_res.is_err(), "Claim should fail due to delay not elapsed"); - - // 6. Fast forward time by 2 days - env.ledger().with_mut(|li| li.timestamp += 2 * 86400); - - // 7. Issuer corrects the revenue report for period 1 via override (changes to 50_000) - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &50_000, - &1, - &true, - ); - - // 8. Investor successfully claims after delay and override - let claim_preview_after = - client.get_claimable(&issuer, &symbol_short!("def"), &token, &investor); - assert_eq!( - claim_preview_after, 50_000, - "Preview should reflect overridden amount and passed delay" - ); - - let payout = client.claim(&investor, &issuer, &symbol_short!("def"), &token, &0); - assert_eq!(payout, 50_000); - - // 6. Issuer blacklists investor to prevent future claims - client.blacklist_add(&issuer, &issuer, &symbol_short!("def"), &token, &investor); - - // 10. Issuer reports revenue for period 2 - client.report_revenue( - &issuer, - &symbol_short!("def"), - &token, - &payout_asset, - &200_000, - &2, - &false, - ); - client.set_holder_share(&issuer, &symbol_short!("def"), &token, &investor, &10_000); - - // 11. Investor attempts claim but is blocked by blacklist - env.ledger().with_mut(|li| li.timestamp += 2 * 86400); // pass delay - let claim_res_blocked = - client.try_claim(&investor, &issuer, &symbol_short!("def"), &token, &0); - assert!(claim_res_blocked.is_err(), "Claim should fail due to blacklist"); - } -} - -// ── Per-offering pause tests ───────────────────────────────────────────────── - -#[test] -fn test_per_offering_pause_authorized() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - - let admin = Address::generate(&env); - let safety = Address::generate(&env); - client.initialize(&admin, &Some(safety.clone()), &Some(false)); - - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let namespace = symbol_short!("def"); - - client.register_offering(&issuer, &namespace, &token, &1000, &token, &0); - - // Safety role should be able to pause - client.pause_offering(&safety, &issuer, &namespace, &token); - assert!(client.is_offering_paused(&issuer, &namespace, &token)); - - // Safety role should be able to unpause - client.unpause_offering(&safety, &issuer, &namespace, &token); - assert!(!client.is_offering_paused(&issuer, &namespace, &token)); - - // Issuer should be able to pause - client.pause_offering(&issuer, &issuer, &namespace, &token); - assert!(client.is_offering_paused(&issuer, &namespace, &token)); - - // Admin should be able to unpause - client.unpause_offering(&admin, &issuer, &namespace, &token); - assert!(!client.is_offering_paused(&issuer, &namespace, &token)); -} - -#[test] -fn test_blocked_by_offering_pause() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let admin = Address::generate(&env); - client.initialize(&admin, &None, &Some(false)); - - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let namespace = symbol_short!("def"); - - client.register_offering(&issuer, &namespace, &token, &1000, &token, &0); - - // Pause the offering - client.pause_offering(&issuer, &issuer, &namespace, &token); - - // report_revenue should fail - let res = client.try_report_revenue(&issuer, &namespace, &token, &token, &1000, &1, &false); - assert!(res.is_err()); - - // claim should fail - let holder = Address::generate(&env); - let res = client.try_claim(&holder, &issuer, &namespace, &token, &0); - assert!(res.is_err()); - - // set_offering_metadata should fail - let res = client.try_set_offering_metadata( - &issuer, - &namespace, - &token, - &soroban_sdk::String::from_str(&env, "ipfs://..."), - ); - assert!(res.is_err()); - - // Unpause - client.unpause_offering(&issuer, &issuer, &namespace, &token); - - // report_revenue should now succeed - let res = client.try_report_revenue(&issuer, &namespace, &token, &token, &1000, &1, &false); - assert!(res.is_ok()); -} - -#[test] -fn test_per_offering_pause_persistence() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - let admin = Address::generate(&env); - client.initialize(&admin, &None, &Some(false)); - - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let namespace = symbol_short!("def"); - - client.register_offering(&issuer, &namespace, &token, &1000, &token, &0); - client.pause_offering(&issuer, &issuer, &namespace, &token); - - // In a real blockchain, we'd check ledger state, here we just verify it stays paused - // across multiple calls and within the same block state simulation. - assert!(client.is_offering_paused(&issuer, &namespace, &token)); - - // Verify it doesn't affect OTHER tokens - let token2 = Address::generate(&env); - client.register_offering(&issuer, &namespace, &token2, &1000, &token2, &0); - assert!(!client.is_offering_paused(&issuer, &namespace, &token2)); -} - -// ── Offering Pagination Stability tests (#24) ──────────────────────────────── - -#[test] -fn test_pagination_caps_at_max() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let namespace = symbol_short!("ns"); - - // Register 25 offerings (exceeds MAX_PAGE_LIMIT=20) - for _ in 0..25 { - let token = Address::generate(&env); - client.register_offering(&issuer, &namespace, &token, &1000, &token, &0); - } - - // Default limit (0) should be MAX_PAGE_LIMIT - let (page, cursor) = client.get_offerings_page(&issuer, &namespace, &0, &0); - assert_eq!(page.len(), 20); // MAX_PAGE_LIMIT - assert_eq!(cursor, Some(20)); - - // Requested limit 50 should be capped to 20 - let (page50, cursor50) = client.get_offerings_page(&issuer, &namespace, &0, &50); - assert_eq!(page50.len(), 20); - assert_eq!(cursor50, Some(20)); -} - -#[test] -fn test_get_issuers_page_stability() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - - let mut issuers = Vec::new(&env); - for _ in 0..10 { - let issuer = Address::generate(&env); - issuers.push_back(issuer.clone()); - let token = Address::generate(&env); - client.register_offering(&issuer, &symbol_short!("ns"), &token, &1000, &token, &0); - } - - let (page1, cursor1) = client.get_issuers_page(&0, &4); - assert_eq!(page1.len(), 4); - assert_eq!(cursor1, Some(4)); - for i in 0..4 { - assert_eq!(page1.get(i).unwrap(), issuers.get(i).unwrap()); - } - - let (page2, cursor2) = client.get_issuers_page(&4, &4); - assert_eq!(page2.len(), 4); - assert_eq!(cursor2, Some(8)); - for i in 0..4 { - assert_eq!(page2.get(i).unwrap(), issuers.get(i + 4).unwrap()); - } - - let (page3, cursor3) = client.get_issuers_page(&8, &10); - assert_eq!(page3.len(), 2); - assert_eq!(cursor3, None); - for i in 0..2 { - assert_eq!(page3.get(i).unwrap(), issuers.get(i + 8).unwrap()); - } -} - -#[test] -fn test_get_namespaces_page_stability() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - - let mut namespaces = Vec::new(&env); - namespaces.push_back(symbol_short!("ns0")); - namespaces.push_back(symbol_short!("ns1")); - namespaces.push_back(symbol_short!("ns2")); - namespaces.push_back(symbol_short!("ns3")); - namespaces.push_back(symbol_short!("ns4")); - - for ns in namespaces.iter() { - let token = Address::generate(&env); - client.register_offering(&issuer, &ns, &token, &1000, &token, &0); - } - - let (page, cursor) = client.get_namespaces_page(&issuer, &0, &3); - assert_eq!(page.len(), 3); - assert_eq!(cursor, Some(3)); - for i in 0..3 { - assert_eq!(page.get(i).unwrap(), namespaces.get(i).unwrap()); - } - - let (page_end, cursor_end) = client.get_namespaces_page(&issuer, &3, &10); - assert_eq!(page_end.len(), 2); - assert_eq!(cursor_end, None); -} - -#[test] -fn test_get_blacklist_page_stability() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - client.initialize(&admin, &None, &Some(false)); - - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let namespace = symbol_short!("ns"); - client.register_offering(&issuer, &namespace, &token, &1000, &token, &0); - - let mut investors = Vec::new(&env); - for _ in 0..15 { - let investor = Address::generate(&env); - investors.push_back(investor.clone()); - client.blacklist_add(&issuer, &issuer, &namespace, &token, &investor); - } - - let (page, cursor) = client.get_blacklist_page(&issuer, &namespace, &token, &0, &10); - assert_eq!(page.len(), 10); - assert_eq!(cursor, Some(10)); - for i in 0..10 { - assert_eq!(page.get(i).unwrap(), investors.get(i).unwrap()); - } - - #[test] - fn min_revenue_threshold_zero_accepted_integration() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - -#[test] -fn test_get_whitelist_page_stability_lexicographical() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let admin = Address::generate(&env); - client.initialize(&admin, &None, &Some(false)); - - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let namespace = symbol_short!("ns"); - client.register_offering(&issuer, &namespace, &token, &1000, &token, &0); - - for _ in 0..10 { - let investor = Address::generate(&env); - client.whitelist_add(&issuer, &issuer, &namespace, &token, &investor); - } - - let (page, _) = client.get_whitelist_page(&issuer, &namespace, &token, &0, &100); - assert_eq!(page.len(), 10); - - // Verify ordering is stable (lexicographical for Addresses as per Soroban Map keys) - for i in 0..9 { - assert!(page.get(i).unwrap() < page.get(i + 1).unwrap()); - } -} - -#[test] -fn test_get_periods_page_stability() { - let env = Env::default(); - env.mock_all_auths(); - let client = make_client(&env); - let issuer = Address::generate(&env); - let token = Address::generate(&env); - let namespace = symbol_short!("ns"); - client.register_offering(&issuer, &namespace, &token, &1000, &token, &0); - - for i in 1..=12 { - client.test_insert_period(&issuer, &namespace, &token, &(i as u64), &1000); - } - - let (page, cursor) = client.get_periods_page(&issuer, &namespace, &token, &0, &5); - assert_eq!(page.len(), 5); - assert_eq!(cursor, Some(5)); - for i in 0..5 { - assert_eq!(page.get(i).unwrap(), (i + 1) as u64); - } - - let (page2, cursor2) = client.get_periods_page(&issuer, &namespace, &token, &10, &10); - assert_eq!(page2.len(), 2); - assert_eq!(cursor2, None); - assert_eq!(page2.get(0).unwrap(), 11); - assert_eq!(page2.get(1).unwrap(), 12); -} - -#[test] -fn test_pagination_out_of_bounds() { - let env = Env::default(); - let contract_id = env.register_contract(None, RevoraRevenueShare); - let client = RevoraRevenueShareClient::new(&env, &contract_id); - - let (page, cursor) = client.get_issuers_page(&100, &10); - assert_eq!(page.len(), 0); - assert_eq!(cursor, None); -} -} // mod regression diff --git a/test_errors.txt b/test_errors.txt index fd0eb7c8..e69de29b 100644 Binary files a/test_errors.txt and b/test_errors.txt differ diff --git a/test_snapshots/test/add_marks_investor_as_blacklisted.1.json b/test_snapshots/test/add_marks_investor_as_blacklisted.1.json index 70cb34b1..e69de29b 100644 --- a/test_snapshots/test/add_marks_investor_as_blacklisted.1.json +++ b/test_snapshots/test/add_marks_investor_as_blacklisted.1.json @@ -1,1903 +0,0 @@ -{ - "generators": { - "address": 5, - "nonce": 0 - }, - "auth": [ - [], - [ - [ - "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", - { - "function": { - "contract_fn": { - "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "function_name": "register_offering", - "args": [ - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "symbol": "def" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - }, - { - "u32": 1000 - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" - }, - { - "i128": { - "hi": 0, - "lo": 0 - } - } - ] - } - }, - "sub_invocations": [] - } - ] - ], - [], - [ - [ - "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", - { - "function": { - "contract_fn": { - "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "function_name": "blacklist_add", - "args": [ - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "symbol": "def" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" - } - ] - } - }, - "sub_invocations": [] - } - ] - ], - [] - ], - "ledger": { - "protocol_version": 20, - "sequence_number": 0, - "timestamp": 0, - "network_id": "0000000000000000000000000000000000000000000000000000000000000000", - "base_reserve": 0, - "min_persistent_entry_ttl": 4096, - "min_temp_entry_ttl": 16, - "max_entry_ttl": 6312000, - "ledger_entries": [ - [ - { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "Admin" - } - ] - }, - "durability": "persistent" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "Admin" - } - ] - }, - "durability": "persistent", - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - } - }, - "ext": "v0" - }, - 4095 - ] - ], - [ - { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "Blacklist" - }, - { - "map": [ - { - "key": { - "symbol": "issuer" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - }, - { - "key": { - "symbol": "namespace" - }, - "val": { - "symbol": "def" - } - }, - { - "key": { - "symbol": "token" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - } - } - ] - } - ] - }, - "durability": "persistent" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "Blacklist" - }, - { - "map": [ - { - "key": { - "symbol": "issuer" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - }, - { - "key": { - "symbol": "namespace" - }, - "val": { - "symbol": "def" - } - }, - { - "key": { - "symbol": "token" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - } - } - ] - } - ] - }, - "durability": "persistent", - "val": { - "map": [ - { - "key": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" - }, - "val": { - "bool": true - } - } - ] - } - } - }, - "ext": "v0" - }, - 4095 - ] - ], - [ - { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "EventOnlyMode" - } - ] - }, - "durability": "persistent" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "EventOnlyMode" - } - ] - }, - "durability": "persistent", - "val": { - "bool": false - } - } - }, - "ext": "v0" - }, - 4095 - ] - ], - [ - { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "IssuerCount" - } - ] - }, - "durability": "persistent" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "IssuerCount" - } - ] - }, - "durability": "persistent", - "val": { - "u32": 1 - } - } - }, - "ext": "v0" - }, - 4095 - ] - ], - [ - { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "IssuerItem" - }, - { - "map": [ - { - "key": { - "symbol": "issuer" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - }, - { - "key": { - "symbol": "namespace" - }, - "val": { - "symbol": "def" - } - }, - { - "key": { - "symbol": "token" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - } - } - ] - } - ] - }, - "durability": "persistent" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "IssuerItem" - }, - { - "map": [ - { - "key": { - "symbol": "issuer" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - }, - { - "key": { - "symbol": "namespace" - }, - "val": { - "symbol": "def" - } - }, - { - "key": { - "symbol": "token" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - } - } - ] - } - ] - }, - "durability": "persistent", - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - } - }, - "ext": "v0" - }, - 4095 - ] - ], - [ - { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "IssuerRegistered" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - ] - }, - "durability": "persistent" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" - } - ] - }, - "durability": "persistent", - "val": { - "bool": true - } - } - }, - "ext": "v0" - }, - 4095 - ] - ], - [ - { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "EventOnlyMode" - } - ] - }, - "durability": "persistent" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "EventOnlyMode" - } - ] - }, - "durability": "persistent", - "val": { - "bool": false - } - } - }, - "ext": "v0" - }, - 4095 - ] - ], - [ - { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "IssuerCount" - } - ] - }, - "durability": "persistent" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "IssuerCount" - } - ] - }, - "durability": "persistent", - "val": { - "u32": 1 - } - } - }, - "ext": "v0" - }, - 4095 - ] - ], - [ - { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "IssuerItem" - }, - { - "u32": 0 - } - ] - }, - "durability": "persistent" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "IssuerItem" - }, - { - "u32": 0 - } - ] - }, - "durability": "persistent", - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - } - }, - "ext": "v0" - }, - 15 - ] - ], - [ - { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "IssuerRegistered" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - ] - }, - "durability": "persistent" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "IssuerRegistered" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - ] - }, - "durability": "persistent", - "val": { - "bool": true - } - } - }, - "ext": "v0" - }, - 4095 - ] - ], - [ - { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "NamespaceCount" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - ] - }, - "durability": "persistent" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "NamespaceCount" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - ] - }, - "durability": "persistent", - "val": { - "u32": 1 - } - } - }, - "ext": "v0" - }, - 4095 - ] - ], - [ - { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "NamespaceItem" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "u32": 0 - } - ] - }, - "durability": "persistent" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "NamespaceItem" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "u32": 0 - } - ] - }, - "durability": "persistent", - "val": { - "symbol": "def" - } - } - }, - "ext": "v0" - }, - 4095 - ] - ], - [ - { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "NamespaceRegistered" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "symbol": "def" - } - ] - }, - "durability": "persistent" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "NamespaceRegistered" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "symbol": "def" - } - ] - }, - "durability": "persistent", - "val": { - "bool": true - } - } - }, - "ext": "v0" - }, - 4095 - ] - ], - [ - { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "OfferCount" - }, - { - "map": [ - { - "key": { - "symbol": "issuer" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - }, - { - "key": { - "symbol": "namespace" - }, - "val": { - "symbol": "def" - } - } - ] - } - ] - }, - "durability": "persistent" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "OfferCount" - }, - { - "map": [ - { - "key": { - "symbol": "issuer" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - }, - { - "key": { - "symbol": "namespace" - }, - "val": { - "symbol": "def" - } - } - ] - } - ] - }, - "durability": "persistent", - "val": { - "u32": 1 - } - } - }, - "ext": "v0" - }, - 4095 - ] - ], - [ - { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "OfferItem" - }, - { - "map": [ - { - "key": { - "symbol": "issuer" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - }, - { - "key": { - "symbol": "namespace" - }, - "val": { - "symbol": "def" - } - } - ] - }, - { - "u32": 0 - } - ] - }, - "durability": "persistent" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "OfferItem" - }, - { - "map": [ - { - "key": { - "symbol": "issuer" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - }, - { - "key": { - "symbol": "namespace" - }, - "val": { - "symbol": "def" - } - } - ] - }, - { - "u32": 0 - } - ] - }, - "durability": "persistent", - "val": { - "map": [ - { - "key": { - "symbol": "issuer" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - }, - { - "key": { - "symbol": "namespace" - }, - "val": { - "symbol": "def" - } - }, - { - "key": { - "symbol": "payout_asset" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" - } - }, - { - "key": { - "symbol": "revenue_share_bps" - }, - "val": { - "u32": 1000 - } - }, - { - "key": { - "symbol": "token" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - } - } - ] - } - } - }, - "ext": "v0" - }, - 4095 - ] - ], - [ - { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "OfferingIssuer" - }, - { - "map": [ - { - "key": { - "symbol": "issuer" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - }, - { - "key": { - "symbol": "namespace" - }, - "val": { - "symbol": "def" - } - }, - { - "key": { - "symbol": "token" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - } - } - ] - } - ] - }, - "durability": "persistent" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "OfferingIssuer" - }, - { - "map": [ - { - "key": { - "symbol": "issuer" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - }, - { - "key": { - "symbol": "namespace" - }, - "val": { - "symbol": "def" - } - }, - { - "key": { - "symbol": "token" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - } - } - ] - } - ] - }, - "durability": "persistent", - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - } - }, - "ext": "v0" - }, - 4095 - ] - ], - [ - { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "Paused" - } - ] - }, - "durability": "persistent" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "vec": [ - { - "symbol": "Paused" - } - ] - }, - "durability": "persistent", - "val": { - "bool": false - } - } - }, - "ext": "v0" - }, - 4095 - ] - ], - [ - { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": "ledger_key_contract_instance", - "durability": "persistent" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": "ledger_key_contract_instance", - "durability": "persistent", - "val": { - "contract_instance": { - "executable": { - "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - }, - "storage": null - } - } - } - }, - "ext": "v0" - }, - 4095 - ] - ], - [ - { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", - "key": { - "ledger_key_nonce": { - "nonce": 801925984706572462 - } - }, - "durability": "temporary" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", - "key": { - "ledger_key_nonce": { - "nonce": 801925984706572462 - } - }, - "durability": "temporary", - "val": "void" - } - }, - "ext": "v0" - }, - 15 - ] - ], - [ - { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": { - "ledger_key_nonce": { - "nonce": 5541220902715666415 - } - }, - "durability": "temporary" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", - "key": { - "ledger_key_nonce": { - "nonce": 5541220902715666415 - } - }, - "durability": "temporary", - "val": "void" - } - }, - "ext": "v0" - }, - 15 - ] - ], - [ - { - "contract_code": { - "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_code": { - "ext": "v0", - "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "code": "" - } - }, - "ext": "v0" - }, - 4095 - ] - ] - ] - }, - "events": [ - { - "event": { - "ext": "v0", - "contract_id": null, - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_call" - }, - { - "bytes": "0000000000000000000000000000000000000000000000000000000000000001" - }, - { - "symbol": "initialize" - } - ], - "data": { - "vec": [ - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - "void", - "void" - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", - "type_": "contract", - "body": { - "v0": { - "topics": [ - { - "symbol": "init" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - ], - "data": { - "vec": [ - "void", - { - "bool": false - } - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_return" - }, - { - "symbol": "initialize" - } - ], - "data": "void" - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": null, - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_call" - }, - { - "bytes": "0000000000000000000000000000000000000000000000000000000000000001" - }, - { - "symbol": "register_offering" - } - ], - "data": { - "vec": [ - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "symbol": "def" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - }, - { - "u32": 1000 - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" - }, - { - "i128": { - "hi": 0, - "lo": 0 - } - } - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", - "type_": "contract", - "body": { - "v0": { - "topics": [ - { - "symbol": "offer_reg" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "symbol": "def" - } - ], - "data": { - "vec": [ - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - "void", - "void" - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", - "type_": "contract", - "body": { - "v0": { - "topics": [ - { - "symbol": "init" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - ], - "data": { - "vec": [ - "void", - { - "u32": 1000 - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" - } - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", - "type_": "contract", - "body": { - "v0": { - "topics": [ - { - "symbol": "ev_idx2" - }, - { - "map": [ - { - "key": { - "symbol": "event_type" - }, - "val": { - "symbol": "offer" - } - }, - { - "key": { - "symbol": "issuer" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - }, - { - "key": { - "symbol": "namespace" - }, - "val": { - "symbol": "def" - } - }, - { - "key": { - "symbol": "period_id" - }, - "val": { - "u64": 0 - } - }, - { - "key": { - "symbol": "token" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - } - }, - { - "key": { - "symbol": "version" - }, - "val": { - "u32": 2 - } - } - ] - } - ], - "data": { - "vec": [ - { - "u32": 1000 - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" - } - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_return" - }, - { - "symbol": "register_offering" - } - ], - "data": "void" - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": null, - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_call" - }, - { - "bytes": "0000000000000000000000000000000000000000000000000000000000000001" - }, - { - "symbol": "is_blacklisted" - } - ], - "data": { - "vec": [ - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "symbol": "def" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" - } - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_return" - }, - { - "symbol": "is_blacklisted" - } - ], - "data": "void" - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": null, - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_call" - }, - { - "bytes": "0000000000000000000000000000000000000000000000000000000000000001" - }, - { - "symbol": "register_offering" - } - ], - "data": { - "vec": [ - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "symbol": "def" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" - } - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", - "type_": "contract", - "body": { - "v0": { - "topics": [ - { - "symbol": "offer_reg" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "symbol": "def" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - } - ], - "data": { - "vec": [ - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" - } - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_return" - }, - { - "symbol": "register_offering" - } - ], - "data": "void" - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": null, - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_call" - }, - { - "bytes": "0000000000000000000000000000000000000000000000000000000000000001" - }, - { - "symbol": "is_blacklisted" - } - ], - "data": { - "vec": [ - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "symbol": "def" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" - } - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_return" - }, - { - "symbol": "is_blacklisted" - } - ], - "data": { - "bool": false - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": null, - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_call" - }, - { - "bytes": "0000000000000000000000000000000000000000000000000000000000000001" - }, - { - "symbol": "blacklist_add" - } - ], - "data": { - "vec": [ - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "symbol": "def" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" - } - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "bl_add" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "symbol": "def" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - } - ], - "data": { - "vec": [ - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" - } - ] - } - } - } - }, - "failed_call": true - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "error" - }, - { - "error": { - "contract": 4 - } - } - ], - "data": { - "string": "escalating Ok(ScErrorType::Contract) frame-exit to Err" - } - } - } - }, - "failed_call": true - }, - { - "event": { - "ext": "v0", - "contract_id": null, - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "error" - }, - { - "error": { - "contract": 4 - } - } - ], - "data": { - "vec": [ - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "symbol": "def" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" - } - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": null, - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "error" - }, - { - "error": { - "contract": 4 - } - } - ], - "data": { - "string": "escalating error to panic" - } - } - } - }, - "failed_call": false - } - ] -} \ No newline at end of file