From 98b9ecbfe20503b7beb528909a04213aaa2992da Mon Sep 17 00:00:00 2001 From: Ajadu-Saviour <4r2hkdjfhf@privaterelay.appleid.com> Date: Fri, 26 Jun 2026 01:37:13 +0100 Subject: [PATCH] Revert "test(events): verify deterministic event emission order across contract operations" --- .github/workflows/ci.yml | 22 - contracts/chainsetttle/src/benchmarks.rs | 8 +- contracts/chainsetttle/src/lib.rs | 521 +----- contracts/chainsetttle/src/property_tests.rs | 319 ---- contracts/chainsetttle/src/test.rs | 326 +--- contracts/chainsetttle/src/test_boundaries.rs | 4 +- contracts/chainsetttle/src/test_chaos.rs | 2 +- .../src/test_concurrent_disputes.rs | 2 +- contracts/chainsetttle/src/test_errors.rs | 4 - contracts/chainsetttle/src/test_oracle.rs | 594 ------ .../chainsetttle/src/test_permissions.rs | 1608 ----------------- contracts/chainsetttle/src/test_regression.rs | 345 ---- contracts/chainsetttle/src/test_upgrade.rs | 2 +- 13 files changed, 65 insertions(+), 3692 deletions(-) delete mode 100644 contracts/chainsetttle/src/test_oracle.rs delete mode 100644 contracts/chainsetttle/src/test_permissions.rs delete mode 100644 contracts/chainsetttle/src/test_regression.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f6225d..1f8c331 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,25 +52,3 @@ jobs: - name: Run tests run: cargo test --all - - regression: - name: Regression Tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - - name: Cache cargo registry - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: ${{ runner.os }}-cargo- - - - name: Run regression tests - run: cargo test --all -- --ignored diff --git a/contracts/chainsetttle/src/benchmarks.rs b/contracts/chainsetttle/src/benchmarks.rs index 0ede920..ccab851 100644 --- a/contracts/chainsetttle/src/benchmarks.rs +++ b/contracts/chainsetttle/src/benchmarks.rs @@ -4,8 +4,8 @@ extern crate std; use super::*; use soroban_sdk::{ - testutils::{Address as _, Ledger as _}, - token, vec, Address, Env, String as SorobanString, Symbol, + testutils::{Address as _, Ledger as _, Symbol}, + token, vec, Address, Env, String as SorobanString, }; use std::fs; use std::path::Path; @@ -108,10 +108,6 @@ fn default_options(_env: &Env) -> ShipmentOptions { late_penalty_bps_per_ledger: 0, auto_confirm_ledgers: 0, dispute_bond_amount: 0, - arbiter_fee_bps: 0, - logistics_fee_bps: 0, - supplier_collateral: 0, - expires_at_ledger: None, } } diff --git a/contracts/chainsetttle/src/lib.rs b/contracts/chainsetttle/src/lib.rs index f77c6a9..55e528c 100644 --- a/contracts/chainsetttle/src/lib.rs +++ b/contracts/chainsetttle/src/lib.rs @@ -1,41 +1,9 @@ #![no_std] use soroban_sdk::{ - contract, contractclient, contractimpl, contracttype, token, Address, BytesN, Env, String, Vec, Symbol, + contract, contractimpl, contracttype, token, Address, BytesN, Env, String, Vec, Symbol, }; -// ============================================================ -// YIELD PROTOCOL INTERFACE -// ============================================================ - -mod yield_protocol { - use soroban_sdk::{contractclient, Address, Env}; - - /// Minimal interface expected of an external yield protocol. - /// The ChainSettle contract deposits idle escrow tokens here to earn - /// yield while milestones are pending confirmation. - /// - /// Deposit flow: - /// 1. ChainSettle transfers `amount` tokens to the protocol contract. - /// 2. ChainSettle calls `deposit` so the protocol records the principal. - /// - /// Withdraw flow: - /// 1. ChainSettle calls `withdraw`; the protocol transfers principal + - /// accrued yield back to `to` and returns the total amount. - #[contractclient(name = "YieldProtocolClient")] - pub trait YieldProtocol { - /// Record a deposit of `amount` units of `token` on behalf of `depositor`. - /// The caller must have already transferred the tokens to this contract. - fn deposit(env: Env, depositor: Address, token: Address, amount: i128); - /// Withdraw all funds (principal + yield) for `depositor`/`token` to `to`. - /// Returns the total amount transferred. - fn withdraw(env: Env, depositor: Address, token: Address, to: Address) -> i128; - /// Current balance (principal + accrued yield) for `depositor` and `token`. - fn balance_of(env: Env, depositor: Address, token: Address) -> i128; - } -} -use yield_protocol::YieldProtocolClient; - // ============================================================ // DATA TYPES // ============================================================ @@ -137,10 +105,6 @@ pub struct Shipment { pub dispute_bond_amount: i128, /// Basis points of disputed payment sent to arbiter on resolution (0 = no arbiter fee). pub arbiter_fee_bps: u32, - /// Basis points deducted from each milestone payment for logistics provider (0 = no fee). - pub logistics_fee_bps: u32, - /// Ledger at which the shipment expires (None = no expiry). - pub expires_at_ledger: Option, } /// Cancellation policy stored separately (keeps Shipment within the contracttype field limit). @@ -203,28 +167,6 @@ pub struct ShipmentOptions { pub dispute_bond_amount: i128, /// Basis points of disputed payment sent to arbiter on resolution (0 = no arbiter fee). pub arbiter_fee_bps: u32, - /// Basis points deducted from each milestone payment for logistics provider (0 = no fee). - pub logistics_fee_bps: u32, - /// Supplier collateral required at creation (0 = no collateral required). - pub supplier_collateral: i128, - /// Ledger at which shipment expires (None = no expiry). - pub expires_at_ledger: Option, -} - -/// All parameters needed to create a single shipment in a batch call. -/// Mirrors the individual `create_shipment` parameters without the `Env`. -#[contracttype] -#[derive(Clone)] -pub struct BatchShipmentParams { - pub shipment_id: String, - pub buyers: Vec
, - pub supplier: Address, - pub logistics: Address, - pub arbiter: Address, - pub token: Address, - pub total_amount: i128, - pub milestones: Vec, - pub options: ShipmentOptions, } /// Contract-level statistics for analytics and monitoring. @@ -359,13 +301,6 @@ pub enum DataKey { /// Contested percentage stored when a partial dispute is raised: (shipment_id, milestone_index) -> u32. /// Absence of this key means the associated dispute covers 100% of the milestone value. DisputeContestedPercent(String, u32), - /// Address of the external yield protocol contract (admin-configured; optional). - YieldProtocol, - /// Cumulative amount deposited to the yield protocol per token address. - /// Cleared to 0 on each full withdrawal. - YieldDeposited(Address), - /// Supplier collateral amount for a shipment. - SupplierCollateral(String), } // ============================================================ @@ -407,10 +342,6 @@ pub enum ChainSettleError { ProofTypeNotAllowed = 30, RebalanceNotAllowed = 31, InvalidContestedPercent = 32, - ShipmentNotExpired = 33, - ExpiredShipment = 34, - InvalidExpiryLedger = 35, - LogisticsFeeExceedsPayment = 36, } // ============================================================ @@ -493,7 +424,24 @@ impl ChainSettleContract { // UPGRADE // ---------------------------------------------------------- - // upgrade is defined in admin.rs + /// Replace the contract WASM in-place. Only callable by admin. + pub fn upgrade(env: Env, admin: Address, new_wasm_hash: BytesN<32>) { + admin.require_auth(); + let stored_admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .unwrap_or_else(|| panic!("unauthorized")); + if admin != stored_admin { + panic!("unauthorized"); + } + env.deployer() + .update_current_contract_wasm(new_wasm_hash.clone()); + env.events().publish( + (Symbol::new(&env, "contract_upgraded"),), + (new_wasm_hash, env.ledger().sequence()), + ); + } /// Migration stub — call once after upgrade to perform any data-model changes. pub fn migrate(_env: Env) { @@ -915,9 +863,6 @@ impl ChainSettleContract { let late_penalty_bps_per_ledger = options.late_penalty_bps_per_ledger; let auto_confirm_ledgers = options.auto_confirm_ledgers; let dispute_bond_amount = options.dispute_bond_amount; - let logistics_fee_bps = options.logistics_fee_bps; - let supplier_collateral = options.supplier_collateral; - let expires_at_ledger = options.expires_at_ledger; if buyers.is_empty() { panic!("at least one buyer is required"); @@ -1007,18 +952,6 @@ impl ChainSettleContract { panic!("shipment already exists"); } - // Validate logistics_fee_bps (must be <= 10000 basis points). - if logistics_fee_bps > 10_000 { - panic!("logistics fee cannot exceed 100%"); - } - - // Validate expires_at_ledger if provided. - if let Some(expiry) = expires_at_ledger { - if expiry <= env.ledger().sequence() { - panic!("expires_at_ledger must be greater than current ledger"); - } - } - // Transfer total_amount from the primary buyer (index 0). let primary_buyer = buyers.get(0).unwrap(); let token_client = token::Client::new(&env, &token); @@ -1034,14 +967,6 @@ impl ChainSettleContract { token_client.transfer(&primary_buyer, &env.current_contract_address(), &bond_total); } - // Lock supplier collateral if required. - if supplier_collateral > 0 { - token_client.transfer(&supplier, &env.current_contract_address(), &supplier_collateral); - env.storage() - .persistent() - .set(&DataKey::SupplierCollateral(shipment_id.clone()), &supplier_collateral); - } - // Normalise milestones: clear any caller-supplied state. let mut clean_milestones: Vec = Vec::new(&env); for i in 0..milestones.len() { @@ -1078,8 +1003,6 @@ impl ChainSettleContract { open_dispute_count: 0, dispute_bond_amount, arbiter_fee_bps: options.arbiter_fee_bps, - logistics_fee_bps, - expires_at_ledger, }; Self::append_audit_entry( @@ -1692,15 +1615,6 @@ impl ChainSettleContract { ), ); } else { - // Deduct logistics fee if configured. - let mut logistics_amount: i128 = 0; - if shipment.logistics_fee_bps > 0 { - logistics_amount = (payment * shipment.logistics_fee_bps as i128) / 10_000; - if logistics_amount > 0 { - payment -= logistics_amount; - } - } - let mut fee_amount: i128 = 0; let net_payment = Self::deduct_fee(&env, payment, &shipment.token, &mut fee_amount); @@ -1711,19 +1625,9 @@ impl ChainSettleContract { shipment.milestones.set(milestone_index, milestone); shipment.released_amount += payment; - let token_client = token::Client::new(&env, &shipment.token); - - // Transfer logistics fee if any. - if logistics_amount > 0 { - token_client.transfer( - &env.current_contract_address(), - &shipment.logistics.clone(), - &logistics_amount, - ); - } - // Transfer the net payment minus any advance already sent. let actual_transfer = net_payment - advance_deducted; + let token_client = token::Client::new(&env, &shipment.token); if actual_transfer > 0 { token_client.transfer( &env.current_contract_address(), @@ -1767,16 +1671,6 @@ impl ChainSettleContract { ShipmentStatus::Completed, &shipment_id, ); - - // Return supplier collateral on completion. - let collateral: i128 = env - .storage() - .persistent() - .get(&DataKey::SupplierCollateral(shipment_id.clone())) - .unwrap_or(0); - if collateral > 0 { - token_client.transfer(&env.current_contract_address(), &shipment.supplier, &collateral); - } } // Decrement total escrowed value (net of any advance already deducted). @@ -1812,18 +1706,6 @@ impl ChainSettleContract { remaining_amount, ), ); - - // Publish logistics payment event if any. - if logistics_amount > 0 { - env.events().publish( - (Symbol::new(&env, "logistics_paid"), shipment_id.clone()), - ( - milestone_index, - logistics_amount, - shipment.logistics.clone(), - ), - ); - } } } @@ -1851,16 +1733,7 @@ impl ChainSettleContract { panic!("holdback period not yet expired"); } - let mut payment = (shipment.total_amount * milestone.payment_percent as i128) / 100; - - // Deduct logistics fee if configured. - let mut logistics_amount: i128 = 0; - if shipment.logistics_fee_bps > 0 { - logistics_amount = (payment * shipment.logistics_fee_bps as i128) / 10_000; - if logistics_amount > 0 { - payment -= logistics_amount; - } - } + let payment = (shipment.total_amount * milestone.payment_percent as i128) / 100; // Deduct any approved advance for this milestone. let advance_deducted = @@ -1877,18 +1750,8 @@ impl ChainSettleContract { shipment.milestones.set(milestone_index, milestone); shipment.released_amount += payment; - let token_client = token::Client::new(&env, &shipment.token); - - // Transfer logistics fee if any. - if logistics_amount > 0 { - token_client.transfer( - &env.current_contract_address(), - &shipment.logistics.clone(), - &logistics_amount, - ); - } - let actual_transfer = net_payment - advance_deducted; + let token_client = token::Client::new(&env, &shipment.token); if actual_transfer > 0 { token_client.transfer( &env.current_contract_address(), @@ -1922,16 +1785,6 @@ impl ChainSettleContract { ShipmentStatus::Completed, &shipment_id, ); - - // Return supplier collateral on completion. - let collateral: i128 = env - .storage() - .persistent() - .get(&DataKey::SupplierCollateral(shipment_id.clone())) - .unwrap_or(0); - if collateral > 0 { - token_client.transfer(&env.current_contract_address(), &shipment.supplier, &collateral); - } } // Decrement total escrowed value (net of any advance already deducted). @@ -2664,23 +2517,12 @@ impl ChainSettleContract { } let refund = shipment.total_amount - shipment.released_amount - shipment.total_advanced_amount; - let primary_buyer = shipment.buyers.get(0).unwrap(); - let token_client = token::Client::new(&env, &shipment.token); - if refund > 0 { + let primary_buyer = shipment.buyers.get(0).unwrap(); + let token_client = token::Client::new(&env, &shipment.token); token_client.transfer(&env.current_contract_address(), &primary_buyer, &refund); } - // Forfeit supplier collateral to buyer on buyer cancellation. - let collateral: i128 = env - .storage() - .persistent() - .get(&DataKey::SupplierCollateral(shipment_id.clone())) - .unwrap_or(0); - if collateral > 0 { - token_client.transfer(&env.current_contract_address(), &primary_buyer, &collateral); - } - shipment.status = ShipmentStatus::Cancelled; Self::increment_reputation_internal(&env, &shipment.supplier, 0, 0, 1); @@ -2854,85 +2696,6 @@ impl ChainSettleContract { ); } - // ---------------------------------------------------------- - // EXPIRE SHIPMENT - // ---------------------------------------------------------- - - /// Anyone can call this after expires_at_ledger has passed. - /// Refunds the unreleased escrow to the buyer and marks shipment as Cancelled. - pub fn expire_shipment(env: Env, shipment_id: String) { - Self::assert_not_paused(&env); - - let mut shipment = Self::get_shipment_internal(&env, &shipment_id); - - if shipment.status != ShipmentStatus::Active { - panic!("shipment is not active"); - } - - // Check if expiry is configured. - if let Some(expiry_ledger) = shipment.expires_at_ledger { - if env.ledger().sequence() < expiry_ledger { - panic!("shipment has not expired yet"); - } - } else { - panic!("shipment has no expiry configured"); - } - - let refund = shipment.total_amount - shipment.released_amount - shipment.total_advanced_amount; - if refund > 0 { - let primary_buyer = shipment.buyers.get(0).unwrap(); - let token_client = token::Client::new(&env, &shipment.token); - token_client.transfer(&env.current_contract_address(), &primary_buyer, &refund); - } - - shipment.status = ShipmentStatus::Cancelled; - - // Move from Active to Cancelled status index. - Self::move_shipment_status_index( - &env, - ShipmentStatus::Active, - ShipmentStatus::Cancelled, - &shipment_id, - ); - - env.storage() - .persistent() - .set(&DataKey::Shipment(shipment_id.clone()), &shipment); - - // Decrement total escrowed value. - let current_escrowed: i128 = env - .storage() - .persistent() - .get(&DataKey::TotalEscrowed(shipment.token.clone())) - .unwrap_or(0); - env.storage().persistent().set( - &DataKey::TotalEscrowed(shipment.token.clone()), - &(current_escrowed - refund).max(0), - ); - - // Remove any disputes for this shipment. - let disputes: Vec = env - .storage() - .persistent() - .get(&DataKey::ActiveDisputes) - .unwrap_or_else(|| Vec::new(&env)); - let mut new_disputes: Vec = Vec::new(&env); - for i in 0..disputes.len() { - let d = disputes.get(i).unwrap(); - if d.shipment_id != shipment_id { - new_disputes.push_back(d); - } - } - env.storage() - .persistent() - .set(&DataKey::ActiveDisputes, &new_disputes); - - env.events().publish( - (Symbol::new(&env, "shipment_expired"), shipment_id.clone()), - (refund, env.ledger().sequence()), - ); - } - // ---------------------------------------------------------- // PROPOSE AMENDMENT // ---------------------------------------------------------- @@ -3468,9 +3231,21 @@ impl ChainSettleContract { } // ---------------------------------------------------------- - // UPGRADE (defined in admin.rs) + // UPGRADE // ---------------------------------------------------------- + /// Admin upgrades the contract WASM. Persistent storage is preserved + /// across upgrades; only the executing code changes. + pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) { + let admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .unwrap_or_else(|| panic!("not initialised")); + admin.require_auth(); + env.deployer().update_current_contract_wasm(new_wasm_hash); + } + // ---------------------------------------------------------- // READ-ONLY QUERIES // ---------------------------------------------------------- @@ -3644,214 +3419,6 @@ impl ChainSettleContract { seen.len() as u32 } - // ---------------------------------------------------------- - // YIELD PROTOCOL INTEGRATION - // ---------------------------------------------------------- - - /// Configure the external yield protocol contract address. Admin only. - /// Once set, `deposit_idle_to_yield` and `withdraw_from_yield` become callable. - pub fn set_yield_protocol(env: Env, admin: Address, protocol: Address) { - admin.require_auth(); - Self::assert_admin(&env, &admin); - env.storage() - .instance() - .set(&DataKey::YieldProtocol, &protocol); - env.events() - .publish((Symbol::new(&env, "yield_protocol_set"),), protocol); - } - - /// Deposit `amount` idle escrow tokens for `token` into the external yield protocol. - /// - /// The admin is responsible for ensuring the contract retains sufficient - /// liquidity to cover upcoming milestone payments; no automatic check is - /// enforced here beyond "don't deposit more than un-deposited escrow balance". - /// - /// Admin only. - pub fn deposit_idle_to_yield(env: Env, admin: Address, token: Address, amount: i128) { - Self::assert_not_paused(&env); - admin.require_auth(); - Self::assert_admin(&env, &admin); - - if amount <= 0 { - panic!("amount must be greater than zero"); - } - - let protocol: Address = env - .storage() - .instance() - .get(&DataKey::YieldProtocol) - .unwrap_or_else(|| panic!("yield protocol not configured")); - - let total_escrowed: i128 = env - .storage() - .persistent() - .get(&DataKey::TotalEscrowed(token.clone())) - .unwrap_or(0); - - let already_deposited: i128 = env - .storage() - .persistent() - .get(&DataKey::YieldDeposited(token.clone())) - .unwrap_or(0); - - let available = total_escrowed - already_deposited; - if amount > available { - panic!("amount exceeds available idle escrow balance"); - } - - // Push tokens to the yield protocol, then record the deposit. - let token_client = token::Client::new(&env, &token); - token_client.transfer(&env.current_contract_address(), &protocol, &amount); - - let yield_client = YieldProtocolClient::new(&env, &protocol); - yield_client.deposit(&env.current_contract_address(), &token, &amount); - - let new_deposited = already_deposited + amount; - env.storage() - .persistent() - .set(&DataKey::YieldDeposited(token.clone()), &new_deposited); - - env.events().publish( - (Symbol::new(&env, "yield_deposited"),), - (token, amount, new_deposited), - ); - } - - /// Withdraw all deposited funds (principal + accrued yield) from the external - /// yield protocol back to this contract. - /// - /// Any yield earned above the deposited principal is added to `TotalEscrowed` - /// so that it can be distributed to suppliers via normal milestone payments. - /// - /// Returns the total amount received from the protocol (principal + yield). - /// Admin only. - pub fn withdraw_from_yield(env: Env, admin: Address, token: Address) -> i128 { - Self::assert_not_paused(&env); - admin.require_auth(); - Self::assert_admin(&env, &admin); - - let protocol: Address = env - .storage() - .instance() - .get(&DataKey::YieldProtocol) - .unwrap_or_else(|| panic!("yield protocol not configured")); - - let deposited: i128 = env - .storage() - .persistent() - .get(&DataKey::YieldDeposited(token.clone())) - .unwrap_or(0); - - if deposited == 0 { - panic!("no yield deposit to withdraw"); - } - - let yield_client = YieldProtocolClient::new(&env, &protocol); - let withdrawn = yield_client.withdraw( - &env.current_contract_address(), - &token, - &env.current_contract_address(), - ); - - // Any amount above the original principal is accrued yield — add it to - // TotalEscrowed so escrow accounting stays correct. - let yield_earned = if withdrawn > deposited { - withdrawn - deposited - } else { - 0 - }; - - if yield_earned > 0 { - let current_escrowed: i128 = env - .storage() - .persistent() - .get(&DataKey::TotalEscrowed(token.clone())) - .unwrap_or(0); - env.storage().persistent().set( - &DataKey::TotalEscrowed(token.clone()), - &(current_escrowed + yield_earned), - ); - } - - env.storage() - .persistent() - .set(&DataKey::YieldDeposited(token.clone()), &0i128); - - env.events().publish( - (Symbol::new(&env, "yield_withdrawn"),), - (token, withdrawn, yield_earned), - ); - - withdrawn - } - - /// Returns the amount currently tracked as deposited to the yield protocol for `token`. - pub fn get_yield_deposited(env: Env, token: Address) -> i128 { - env.storage() - .persistent() - .get(&DataKey::YieldDeposited(token)) - .unwrap_or(0) - } - - /// Query the live balance (principal + accrued yield) directly from the - /// external yield protocol. Returns 0 if no protocol is configured. - pub fn get_yield_balance(env: Env, token: Address) -> i128 { - if let Some(protocol) = env - .storage() - .instance() - .get::(&DataKey::YieldProtocol) - { - let yield_client = YieldProtocolClient::new(&env, &protocol); - yield_client.balance_of(&env.current_contract_address(), &token) - } else { - 0 - } - } - - // ---------------------------------------------------------- - // BATCH CREATE SHIPMENTS - // ---------------------------------------------------------- - - /// Create multiple independent shipments in a single atomic transaction. - /// - /// Every entry in `params` is validated and created in order. Because - /// Soroban transactions are atomic, any validation failure reverts all - /// preceding creations in the batch. - /// - /// Returns the ordered list of created shipment IDs. - pub fn batch_create_shipments( - env: Env, - params: Vec, - ) -> Vec { - Self::assert_not_paused(&env); - - let mut created: Vec = Vec::new(&env); - - for i in 0..params.len() { - let p = params.get(i).unwrap(); - let shipment_id = Self::create_shipment( - env.clone(), - p.shipment_id, - p.buyers, - p.supplier, - p.logistics, - p.arbiter, - p.token, - p.total_amount, - p.milestones, - p.options, - ); - created.push_back(shipment_id); - } - - env.events().publish( - (Symbol::new(&env, "batch_shipments_created"),), - created.len() as u32, - ); - - created - } - // ---------------------------------------------------------- // INTERNAL HELPERS // ---------------------------------------------------------- @@ -4174,22 +3741,10 @@ impl ChainSettleContract { } } -// NOTE: benchmarks, property_tests, test, test_upgrade, test_concurrent_disputes, -// test_boundaries, test_chaos have pre-existing compile errors (missing fields, -// duplicate upgrade fn, missing crates, wrong API). Commented out so test_permissions -// can compile and run cleanly. -// mod benchmarks; +mod benchmarks; pub mod constants; -// mod property_tests; -// mod test; -mod test_permissions; -// mod test_upgrade; -// mod test_concurrent_disputes; -// mod test_boundaries; -// mod test_chaos; mod property_tests; mod test; -mod test_oracle; mod test_upgrade; mod test_concurrent_disputes; mod test_boundaries; diff --git a/contracts/chainsetttle/src/property_tests.rs b/contracts/chainsetttle/src/property_tests.rs index a917230..793ccc4 100644 --- a/contracts/chainsetttle/src/property_tests.rs +++ b/contracts/chainsetttle/src/property_tests.rs @@ -313,10 +313,6 @@ mod contract_prop_tests { late_penalty_bps_per_ledger: 0, auto_confirm_ledgers: 0, dispute_bond_amount: 0, - arbiter_fee_bps: 0, - logistics_fee_bps: 0, - supplier_collateral: 0, - expires_at_ledger: None, }, ); } @@ -397,318 +393,3 @@ mod contract_prop_tests { } } } - -// ============================================================ -// MILESTONE PERCENTAGE FUZZ TESTS -// -// Pure-Rust model: validates the percentage constraint logic that -// the contract enforces in `create_shipment` and `rebalance_milestones`. -// -// Rules under test: -// 1. sum(percents) must equal 100 -// 2. every individual percent must be >= MIN_PERCENT (default 5) -// ============================================================ - -#[cfg(test)] -mod milestone_percent_fuzz { - use proptest::prelude::*; - - const MIN_PERCENT: u32 = 5; - - fn is_valid_percent_set(percents: &[u32]) -> bool { - if percents.is_empty() { - return false; - } - let sum: u32 = percents.iter().sum(); - sum == 100 && percents.iter().all(|&p| p >= MIN_PERCENT) - } - - proptest! { - #![proptest_config(proptest::test_runner::Config { - cases: 10_000, - ..Default::default() - })] - - /// Validity is determined solely by (sum == 100) AND (all >= MIN_PERCENT). - #[test] - fn prop_percent_validity_is_sum_and_min_only( - percents in prop::collection::vec(0u32..=100u32, 1..=10usize), - ) { - let sum: u32 = percents.iter().sum(); - let all_above_min = percents.iter().all(|&p| p >= MIN_PERCENT); - let model_valid = sum == 100 && all_above_min; - prop_assert_eq!( - is_valid_percent_set(&percents), - model_valid, - "validity mismatch for {:?} (sum={}, all_above_min={})", - percents, sum, all_above_min - ); - } - - /// A two-element split (first, 100-first) is valid iff both halves >= MIN_PERCENT. - #[test] - fn prop_two_milestone_split_validity(first in 0u32..=100u32) { - let second = 100u32.saturating_sub(first); - let percents = [first, second]; - let expected = first >= MIN_PERCENT && second >= MIN_PERCENT; - prop_assert_eq!( - is_valid_percent_set(&percents), - expected, - "two-split ({}, {}) validity mismatch", first, second - ); - } - - /// Swapping any two entries does not change validity (order-independent). - #[test] - fn prop_validity_is_order_independent( - percents in prop::collection::vec(5u32..=50u32, 2..=8usize), - a in 0usize..8, - b in 0usize..8, - ) { - let n = percents.len(); - let mut reordered = percents.clone(); - reordered.swap(a % n, b % n); - prop_assert_eq!( - is_valid_percent_set(&percents), - is_valid_percent_set(&reordered), - "swap must not change validity" - ); - } - - /// Any set containing a zero-valued entry is always invalid (0 < MIN_PERCENT). - #[test] - fn prop_zero_percent_entry_always_invalid( - rest in prop::collection::vec(5u32..=50u32, 1..=9usize), - ) { - let mut percents = rest; - percents.push(0); - prop_assert!( - !is_valid_percent_set(&percents), - "set with a 0-valued entry must be invalid" - ); - } - - /// A set with any entry > 100 cannot sum to exactly 100 with positive siblings. - #[test] - fn prop_entry_over_100_is_invalid( - excess in 101u32..=200u32, - ) { - let sum = excess; - prop_assert!( - sum != 100, - "single-entry sum {} must not equal 100", sum - ); - } - - /// Adding a positive delta to a sum-100 set breaks the invariant. - #[test] - fn prop_augmented_sum_invalidates_set( - first in 5u32..=90u32, - delta in 1u32..=10u32, - ) { - let second = 100u32.saturating_sub(first); - if second < MIN_PERCENT { - return Ok(()); - } - // (first, second) is a valid baseline. - prop_assert!(is_valid_percent_set(&[first, second])); - // Augmenting the first entry pushes the sum past 100. - prop_assert!( - !is_valid_percent_set(&[first + delta, second]), - "augmented set (sum={}) must be invalid", first + delta + second - ); - } - - /// For n milestones, the "all equal, with remainder on last" distribution - /// is valid iff every per-entry value >= MIN_PERCENT. - #[test] - fn prop_uniform_milestone_distribution(n in 1usize..=20usize) { - let per: u32 = 100 / n as u32; - let remainder = 100 % n as u32; - let percents: Vec = (0..n) - .map(|i| if i == n - 1 { per + remainder } else { per }) - .collect(); - let sum: u32 = percents.iter().sum(); - prop_assert_eq!(sum, 100u32, "uniform distribution must sum to 100 for n={}", n); - let all_above_min = percents.iter().all(|&p| p >= MIN_PERCENT); - prop_assert_eq!( - is_valid_percent_set(&percents), - all_above_min, - "uniform n={} validity should match all_above_min={}", n, all_above_min - ); - } - - /// An empty list is always invalid (sum = 0 ≠ 100). - #[test] - fn prop_empty_list_is_invalid(_seed in 0u32..1u32) { - let empty: Vec = vec![]; - prop_assert!(!is_valid_percent_set(&empty)); - } - - /// Splitting 100 into exactly MIN_PERCENT-sized blocks leaves - /// 100 / MIN_PERCENT = 20 milestones, each at exactly 5 — valid. - #[test] - fn prop_minimum_percent_exact_split(_seed in 0u32..1u32) { - let n = (100 / MIN_PERCENT) as usize; // 20 - let percents: Vec = (0..n).map(|_| MIN_PERCENT).collect(); - prop_assert!(is_valid_percent_set(&percents)); - } - } - - // -------------------------------------------------------- - // CONTRACT-BACKED PERCENTAGE VALIDATION TESTS - // These spin up a real Soroban test environment and call - // the contract to verify the on-chain checks match the model. - // -------------------------------------------------------- - #[cfg(test)] - mod contract_percent_tests { - use crate::{ - ChainSettleContract, ChainSettleContractClient, Milestone, MilestoneMode, - MilestoneStatus, ShipmentOptions, - }; - use proptest::prelude::*; - use soroban_sdk::{testutils::Address as _, token, vec, Address, Env, String}; - - fn make_test_env() -> (Env, Address, Address, Address, Address, Address, Address) { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register(ChainSettleContract, ()); - let token_admin = Address::generate(&env); - let token_id = env - .register_stellar_asset_contract_v2(token_admin.clone()) - .address(); - let buyer = Address::generate(&env); - let supplier = Address::generate(&env); - let logistics = Address::generate(&env); - let arbiter = Address::generate(&env); - token::StellarAssetClient::new(&env, &token_id) - .mint(&buyer, &100_000_000_000i128); - ChainSettleContractClient::new(&env, &contract_id).init(&buyer); - (env, contract_id, token_id, buyer, supplier, logistics, arbiter) - } - - fn default_options() -> ShipmentOptions { - ShipmentOptions { - response_deadline: 0, - penalty_bps: 0, - milestone_mode: MilestoneMode::Parallel, - holdback_ledgers: 0, - dispute_cooldown_ledgers: 0, - late_penalty_bps_per_ledger: 0, - auto_confirm_ledgers: 0, - dispute_bond_amount: 0, - arbiter_fee_bps: 0, - } - } - - fn milestone(env: &Env, name: &str, pct: u32) -> Milestone { - Milestone { - name: String::from_str(env, name), - payment_percent: pct, - proof_hash: String::from_str(env, ""), - status: MilestoneStatus::Pending, - release_after_ledger: 0, - proof_submitted_ledger: None, - dispute_opened_ledger: None, - } - } - - proptest! { - #![proptest_config(proptest::test_runner::Config { - cases: 200, - ..Default::default() - })] - - /// Valid two-milestone splits (both >= 5, sum == 100) are accepted by the contract. - #[test] - fn prop_contract_accepts_valid_two_split(first in 5u32..=90u32) { - let second = 100u32 - first; - if second < 5 { - return Ok(()); - } - let (env, cid, tid, buyer, supplier, logistics, arbiter) = make_test_env(); - let client = ChainSettleContractClient::new(&env, &cid); - let sid = String::from_str(&env, "PCT-VALID"); - let ms = vec![&env, milestone(&env, "M0", first), milestone(&env, "M1", second)]; - let result = client.try_create_shipment( - &sid, - &vec![&env, buyer.clone()], - &supplier, &logistics, &arbiter, &tid, - &1_000_000i128, - &ms, - &default_options(), - ); - prop_assert!( - result.is_ok(), - "valid split ({}, {}) must be accepted", first, second - ); - } - - /// Any milestone percentage below 5 (the default minimum) is rejected. - #[test] - fn prop_contract_rejects_below_min_percent(below in 1u32..=4u32) { - let (env, cid, tid, buyer, supplier, logistics, arbiter) = make_test_env(); - let client = ChainSettleContractClient::new(&env, &cid); - let sid = String::from_str(&env, "PCT-LOW"); - let above = 100 - below; - let ms = vec![&env, milestone(&env, "M0", below), milestone(&env, "M1", above)]; - let result = client.try_create_shipment( - &sid, - &vec![&env, buyer.clone()], - &supplier, &logistics, &arbiter, &tid, - &1_000_000i128, - &ms, - &default_options(), - ); - prop_assert!( - result.is_err(), - "percent {} below minimum must be rejected", below - ); - } - - /// Two-milestone sets whose percentages sum to something other than 100 are rejected. - #[test] - fn prop_contract_rejects_non_100_sum( - first in 5u32..=45u32, - second in 5u32..=45u32, - ) { - let sum = first + second; - if sum == 100 { return Ok(()); } - let (env, cid, tid, buyer, supplier, logistics, arbiter) = make_test_env(); - let client = ChainSettleContractClient::new(&env, &cid); - let sid = String::from_str(&env, "PCT-BADSUM"); - let ms = vec![&env, milestone(&env, "M0", first), milestone(&env, "M1", second)]; - let result = client.try_create_shipment( - &sid, - &vec![&env, buyer.clone()], - &supplier, &logistics, &arbiter, &tid, - &1_000_000i128, - &ms, - &default_options(), - ); - prop_assert!( - result.is_err(), - "sum {} != 100 must be rejected", sum - ); - } - - /// A single 100% milestone (at or above the minimum) is always valid. - #[test] - fn prop_contract_accepts_single_100_milestone(_seed in 0u32..10u32) { - let (env, cid, tid, buyer, supplier, logistics, arbiter) = make_test_env(); - let client = ChainSettleContractClient::new(&env, &cid); - let sid = String::from_str(&env, "PCT-SINGLE"); - let ms = vec![&env, milestone(&env, "M0", 100)]; - let result = client.try_create_shipment( - &sid, - &vec![&env, buyer.clone()], - &supplier, &logistics, &arbiter, &tid, - &1_000_000i128, - &ms, - &default_options(), - ); - prop_assert!(result.is_ok(), "single 100% milestone must be accepted"); - } - } - } -} diff --git a/contracts/chainsetttle/src/test.rs b/contracts/chainsetttle/src/test.rs index a465dbc..ee04a6d 100644 --- a/contracts/chainsetttle/src/test.rs +++ b/contracts/chainsetttle/src/test.rs @@ -4,8 +4,8 @@ extern crate std; use super::*; use soroban_sdk::{ - testutils::{Address as _, Ledger as _}, - token, vec, Address, BytesN, Env, String, Symbol, + testutils::{Address as _, Ledger as _, Symbol}, + token, vec, Address, BytesN, Env, String, }; use std::format; @@ -25,37 +25,27 @@ struct TestSetup { treasury: Address, } -/// Create a mock SAC (Stellar Asset Contract) for testing. -/// Returns the token address and mints initial funds to addresses. -fn mock_token(env: &Env, admin: &Address, addresses: &Vec
, initial_balance: i128) -> Address { - let token_id = env - .register_stellar_asset_contract_v2(admin.clone()) - .address(); - let token_client = token::StellarAssetClient::new(env, &token_id); - - for i in 0..addresses.len() { - token_client.mint(addresses.get(i).unwrap(), &initial_balance); - } - - token_id -} - fn setup() -> TestSetup { let env = Env::default(); env.mock_all_auths(); let contract_id = env.register(ChainSettleContract, ()); + let token_admin = Address::generate(&env); + let token_id = env + .register_stellar_asset_contract_v2(token_admin.clone()) + .address(); + let token_client = token::StellarAssetClient::new(&env, &token_id); + let buyer = Address::generate(&env); let buyer2 = Address::generate(&env); let supplier = Address::generate(&env); let logistics = Address::generate(&env); let arbiter = Address::generate(&env); let treasury = Address::generate(&env); - let token_admin = Address::generate(&env); - let addresses = vec![&env, buyer.clone(), buyer2.clone()]; - let token_id = mock_token(&env, &token_admin, &addresses, 10_000_000_000); + token_client.mint(&buyer, &10_000_000_000); + token_client.mint(&buyer2, &10_000_000_000); let client = ChainSettleContractClient::new(&env, &contract_id); client.init(&buyer); @@ -120,10 +110,6 @@ fn default_options(_env: &Env) -> ShipmentOptions { late_penalty_bps_per_ledger: 0, auto_confirm_ledgers: 0, dispute_bond_amount: 0, - arbiter_fee_bps: 0, - logistics_fee_bps: 0, - supplier_collateral: 0, - expires_at_ledger: None, } } @@ -1262,11 +1248,7 @@ fn test_dispute_cooldown_enforced() { late_penalty_bps_per_ledger: 0, auto_confirm_ledgers: 0, dispute_bond_amount: 0, - arbiter_fee_bps: 0, - logistics_fee_bps: 0, - supplier_collateral: 0, - expires_at_ledger: None, - }, + }, ); // First dispute on milestone 0. @@ -1331,11 +1313,7 @@ fn test_dispute_cooldown_blocks_early_redispute() { late_penalty_bps_per_ledger: 0, auto_confirm_ledgers: 0, dispute_bond_amount: 0, - arbiter_fee_bps: 0, - logistics_fee_bps: 0, - supplier_collateral: 0, - expires_at_ledger: None, - }, + }, ); client.submit_proof( @@ -1430,11 +1408,7 @@ fn test_cooldown_updated_on_resolve() { late_penalty_bps_per_ledger: 0, auto_confirm_ledgers: 0, dispute_bond_amount: 0, - arbiter_fee_bps: 0, - logistics_fee_bps: 0, - supplier_collateral: 0, - expires_at_ledger: None, - }, + }, ); client.submit_proof( @@ -1821,11 +1795,7 @@ fn test_non_whitelisted_token_rejected() { late_penalty_bps_per_ledger: 0, auto_confirm_ledgers: 0, dispute_bond_amount: 0, - arbiter_fee_bps: 0, - logistics_fee_bps: 0, - supplier_collateral: 0, - expires_at_ledger: None, - }, + }, ); } @@ -1947,11 +1917,7 @@ fn test_holdback_happy_path() { late_penalty_bps_per_ledger: 0, auto_confirm_ledgers: 0, dispute_bond_amount: 0, - arbiter_fee_bps: 0, - logistics_fee_bps: 0, - supplier_collateral: 0, - expires_at_ledger: None, - }, + }, ); client.submit_proof( @@ -2038,11 +2004,7 @@ fn test_holdback_early_dispute_cancels_hold() { late_penalty_bps_per_ledger: 0, auto_confirm_ledgers: 0, dispute_bond_amount: 0, - arbiter_fee_bps: 0, - logistics_fee_bps: 0, - supplier_collateral: 0, - expires_at_ledger: None, - }, + }, ); client.submit_proof( @@ -2254,11 +2216,7 @@ fn test_multisig_both_buyers_must_confirm() { late_penalty_bps_per_ledger: 0, auto_confirm_ledgers: 0, dispute_bond_amount: 0, - arbiter_fee_bps: 0, - logistics_fee_bps: 0, - supplier_collateral: 0, - expires_at_ledger: None, - }, + }, ); client.submit_proof( @@ -2334,11 +2292,7 @@ fn test_multisig_minority_veto_dispute() { late_penalty_bps_per_ledger: 0, auto_confirm_ledgers: 0, dispute_bond_amount: 0, - arbiter_fee_bps: 0, - logistics_fee_bps: 0, - supplier_collateral: 0, - expires_at_ledger: None, - }, + }, ); client.submit_proof( @@ -2713,11 +2667,7 @@ fn test_deadline_cancellation_success() { late_penalty_bps_per_ledger: 0, auto_confirm_ledgers: 0, dispute_bond_amount: 0, - arbiter_fee_bps: 0, - logistics_fee_bps: 0, - supplier_collateral: 0, - expires_at_ledger: None, - }, + }, ); client.submit_proof( @@ -2772,11 +2722,7 @@ fn test_deadline_cancellation_too_early() { late_penalty_bps_per_ledger: 0, auto_confirm_ledgers: 0, dispute_bond_amount: 0, - arbiter_fee_bps: 0, - logistics_fee_bps: 0, - supplier_collateral: 0, - expires_at_ledger: None, - }, + }, ); client.submit_proof( @@ -4418,235 +4364,3 @@ fn test_ttl_documentation_in_comments() { "TTL_MAX_LEDGERS = 31,500,000 seconds" ); } - -// ============================================================ -// NEW FEATURES TESTS -// ============================================================ - -#[test] -fn test_logistics_fee_deduction() { - let t = setup(); - let client = ChainSettleContractClient::new(&t.env, &t.contract_id); - let token_client = token::Client::new(&t.env, &t.token_id); - - let shipment_id = String::from_str(&t.env, "logistics-fee-1"); - let mut opts = default_options(&t.env); - opts.logistics_fee_bps = 500; // 5% logistics fee - - client.create_shipment( - &shipment_id, - &single_buyer_vec(&t.env, &t.buyer), - &t.supplier, - &t.logistics, - &t.arbiter, - &t.token_id, - &1_000_000_000i128, - &build_milestones(&t.env), - &opts, - ); - - // Submit and confirm proof for first milestone (25% = 250,000,000) - client.submit_proof( - &t.supplier, - &shipment_id, - &0, - &String::from_str(&t.env, "proof_hash_0"), - &Symbol::new(&t.env, "ipfs"), - ); - client.confirm_milestone(&t.buyer, &shipment_id, &0); - - // Logistics fee = 250,000,000 * 500 / 10,000 = 12,500,000 - // Supplier should receive: 250,000,000 - 12,500,000 = 237,500,000 - let supplier_balance = token_client.balance(&t.supplier); - assert_eq!(supplier_balance, 237_500_000, "Supplier should receive payment minus logistics fee"); - - let logistics_balance = token_client.balance(&t.logistics); - assert_eq!(logistics_balance, 12_500_000, "Logistics provider should receive logistics fee"); -} - -#[test] -fn test_logistics_fee_zero() { - let t = setup(); - let client = ChainSettleContractClient::new(&t.env, &t.contract_id); - let token_client = token::Client::new(&t.env, &t.token_id); - - let shipment_id = String::from_str(&t.env, "no-logistics-fee"); - let opts = default_options(&t.env); - - client.create_shipment( - &shipment_id, - &single_buyer_vec(&t.env, &t.buyer), - &t.supplier, - &t.logistics, - &t.arbiter, - &t.token_id, - &1_000_000_000i128, - &build_milestones(&t.env), - &opts, - ); - - client.submit_proof( - &t.supplier, - &shipment_id, - &0, - &String::from_str(&t.env, "proof_hash_0"), - &Symbol::new(&t.env, "ipfs"), - ); - client.confirm_milestone(&t.buyer, &shipment_id, &0); - - // With zero logistics fee, full payment goes to supplier - let supplier_balance = token_client.balance(&t.supplier); - assert_eq!(supplier_balance, 250_000_000, "Supplier should receive full payment"); - - let logistics_balance = token_client.balance(&t.logistics); - assert_eq!(logistics_balance, 0, "Logistics provider should receive nothing"); -} - -#[test] -fn test_supplier_collateral_forfeiture_on_buyer_cancel() { - let t = setup(); - let client = ChainSettleContractClient::new(&t.env, &t.contract_id); - let token_client = token::Client::new(&t.env, &t.token_id); - - let shipment_id = String::from_str(&t.env, "collateral-1"); - let mut opts = default_options(&t.env); - opts.supplier_collateral = 50_000_000; // Supplier must lock 50M - - // Mint additional funds for supplier - token_client.mint(&t.supplier, &100_000_000); - - client.create_shipment( - &shipment_id, - &single_buyer_vec(&t.env, &t.buyer), - &t.supplier, - &t.logistics, - &t.arbiter, - &t.token_id, - &1_000_000_000i128, - &build_milestones(&t.env), - &opts, - ); - - // Buyer cancels, collateral should be forfeited to buyer - client.cancel_shipment(&t.buyer, &shipment_id); - - let buyer_balance = token_client.balance(&t.buyer); - // Original 10B - 1B (shipment) + 1B (full refund) + 50M (collateral forfeiture) - assert_eq!( - buyer_balance, 10_050_000_000, - "Buyer should receive collateral forfeiture on cancellation" - ); -} - -#[test] -fn test_supplier_collateral_return_on_completion() { - let t = setup(); - let client = ChainSettleContractClient::new(&t.env, &t.contract_id); - let token_client = token::Client::new(&t.env, &t.token_id); - - let shipment_id = String::from_str(&t.env, "collateral-complete"); - let mut opts = default_options(&t.env); - opts.supplier_collateral = 50_000_000; - - token_client.mint(&t.supplier, &100_000_000); - - client.create_shipment( - &shipment_id, - &single_buyer_vec(&t.env, &t.buyer), - &t.supplier, - &t.logistics, - &t.arbiter, - &t.token_id, - &1_000_000_000i128, - &build_milestones(&t.env), - &opts, - ); - - let initial_supplier_balance = token_client.balance(&t.supplier); - - // Complete all milestones - for i in 0..3 { - client.submit_proof( - &t.supplier, - &shipment_id, - &i, - &String::from_str(&t.env, &format!("proof_hash_{}", i)), - &Symbol::new(&t.env, "ipfs"), - ); - client.confirm_milestone(&t.buyer, &shipment_id, &i); - } - - let final_supplier_balance = token_client.balance(&t.supplier); - // Supplier should have: initial + all milestone payments + returned collateral - // All milestones: 1B total (no deductions from original supply in this test) - assert!( - final_supplier_balance > initial_supplier_balance, - "Supplier should receive collateral return on completion" - ); -} - -#[test] -fn test_shipment_expiry_success() { - let t = setup(); - let client = ChainSettleContractClient::new(&t.env, &t.contract_id); - let token_client = token::Client::new(&t.env, &t.token_id); - - let shipment_id = String::from_str(&t.env, "expiry-1"); - let mut opts = default_options(&t.env); - opts.expires_at_ledger = Some(t.env.ledger().sequence() + 10); - - client.create_shipment( - &shipment_id, - &single_buyer_vec(&t.env, &t.buyer), - &t.supplier, - &t.logistics, - &t.arbiter, - &t.token_id, - &1_000_000_000i128, - &build_milestones(&t.env), - &opts, - ); - - // Try to expire before deadline (should fail) - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - client.expire_shipment(&shipment_id); - })); - assert!(result.is_err(), "Should not allow expiry before deadline"); - - // Jump to after expiry ledger - t.env.ledger().set_sequence(t.env.ledger().sequence() + 11); - - // Now expiry should succeed - client.expire_shipment(&shipment_id); - - let buyer_balance = token_client.balance(&t.buyer); - // Full refund: 10B (original) - 1B (locked) + 1B (refund) = 10B - assert_eq!(buyer_balance, 10_000_000_000, "Buyer should receive full escrow refund on expiry"); -} - -#[test] -fn test_shipment_no_expiry() { - let t = setup(); - let client = ChainSettleContractClient::new(&t.env, &t.contract_id); - - let shipment_id = String::from_str(&t.env, "no-expiry"); - let opts = default_options(&t.env); - - client.create_shipment( - &shipment_id, - &single_buyer_vec(&t.env, &t.buyer), - &t.supplier, - &t.logistics, - &t.arbiter, - &t.token_id, - &1_000_000_000i128, - &build_milestones(&t.env), - &opts, - ); - - // Try to expire when no expiry is set (should fail) - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - client.expire_shipment(&shipment_id); - })); - assert!(result.is_err(), "Should not allow expiry when not configured"); -} diff --git a/contracts/chainsetttle/src/test_boundaries.rs b/contracts/chainsetttle/src/test_boundaries.rs index 4d6ff69..7dd8578 100644 --- a/contracts/chainsetttle/src/test_boundaries.rs +++ b/contracts/chainsetttle/src/test_boundaries.rs @@ -21,8 +21,8 @@ use super::*; use soroban_sdk::{ - testutils::{Address as _, Ledger as _}, - token, vec, Address, Env, String, Symbol, + testutils::{Address as _, Ledger as _, Symbol}, + token, vec, Address, Env, String, }; // ---- shared env setup ------------------------------------------------------- diff --git a/contracts/chainsetttle/src/test_chaos.rs b/contracts/chainsetttle/src/test_chaos.rs index f2ce6fb..be0853c 100644 --- a/contracts/chainsetttle/src/test_chaos.rs +++ b/contracts/chainsetttle/src/test_chaos.rs @@ -15,7 +15,7 @@ extern crate std; use super::*; use soroban_sdk::{ testutils::Address as _, - token, vec, Address, Env, String, Symbol}; + token, vec, Address, Env, String,, Symbol}; use proptest::prelude::*; // Fixed slot IDs so we can reference shipments without heap allocation gymnastics diff --git a/contracts/chainsetttle/src/test_concurrent_disputes.rs b/contracts/chainsetttle/src/test_concurrent_disputes.rs index ca226a8..82c48c9 100644 --- a/contracts/chainsetttle/src/test_concurrent_disputes.rs +++ b/contracts/chainsetttle/src/test_concurrent_disputes.rs @@ -7,7 +7,7 @@ use super::*; use soroban_sdk::{ testutils::Address as _, - token, vec, Address, Env, String, Symbol}; + token, vec, Address, Env, String,, Symbol}; // Resolution order: deliberately non-sequential to catch cross-shipment contamination. const RESOLUTION_ORDER: [usize; 10] = [5, 2, 8, 1, 6, 0, 9, 3, 7, 4]; diff --git a/contracts/chainsetttle/src/test_errors.rs b/contracts/chainsetttle/src/test_errors.rs index 6c586cd..9132206 100644 --- a/contracts/chainsetttle/src/test_errors.rs +++ b/contracts/chainsetttle/src/test_errors.rs @@ -92,10 +92,6 @@ fn opts(env: &Env) -> ShipmentOptions { late_penalty_bps_per_ledger: 0, auto_confirm_ledgers: 0, dispute_bond_amount: 0, - arbiter_fee_bps: 0, - logistics_fee_bps: 0, - supplier_collateral: 0, - expires_at_ledger: None, } } diff --git a/contracts/chainsetttle/src/test_oracle.rs b/contracts/chainsetttle/src/test_oracle.rs deleted file mode 100644 index 935ba6c..0000000 --- a/contracts/chainsetttle/src/test_oracle.rs +++ /dev/null @@ -1,594 +0,0 @@ -#![cfg(test)] - -use soroban_sdk::{token, Address, Env, String, Symbol, Vec}; - -use crate::{ - ChainSettleContract, Milestone, MilestoneMode, MilestoneStatus, ShipmentOptions, ShipmentStatus, -}; - -// ============================================================ -// TEST SETUP & HELPERS -// ============================================================ - -struct OracleTestSetup { - env: Env, - chainsettle: ChainSettleContract, - buyer: Address, - supplier: Address, - logistics: Address, - arbiter: Address, - admin: Address, - token_id: Address, -} - -fn oracle_setup() -> OracleTestSetup { - let env = Env::default(); - env.mock_all_auths(); - env.budget().reset_unlimited(); - - let buyer = Address::random(&env); - let supplier = Address::random(&env); - let logistics = Address::random(&env); - let arbiter = Address::random(&env); - let admin = Address::random(&env); - let token_admin = Address::random(&env); - - // Deploy mock token - let token_id = env.register_stellar_asset_contract_v2(token_admin.clone()); - let token_client = token::Client::new(&env, &token_id); - - // Mint tokens - token_client.mint(&buyer, &(100_000 * 10i128.pow(18))); - token_client.mint(&supplier, &(10_000 * 10i128.pow(18))); - - // Deploy ChainSettle contract - let client = ChainSettleContract; - env.register_contract(None, ChainSettleContract); - - // Initialize ChainSettle - ChainSettleContract::init(env.clone(), admin.clone()); - - OracleTestSetup { - env, - chainsettle: client, - buyer, - supplier, - logistics, - arbiter, - admin, - token_id, - } -} - -fn build_single_milestone(env: &Env) -> Vec { - let mut v = Vec::new(&env); - v.push_back(Milestone { - name: String::from_str(&env, "Delivery"), - payment_percent: 100, - proof_hash: String::from_str(&env, ""), - status: MilestoneStatus::Pending, - release_after_ledger: 0, - proof_submitted_ledger: None, - dispute_opened_ledger: None, - }); - v -} - -fn build_three_milestones(env: &Env) -> Vec { - let mut v = Vec::new(&env); - v.push_back(Milestone { - name: String::from_str(&env, "Dispatch"), - payment_percent: 33, - proof_hash: String::from_str(&env, ""), - status: MilestoneStatus::Pending, - release_after_ledger: 0, - proof_submitted_ledger: None, - dispute_opened_ledger: None, - }); - v.push_back(Milestone { - name: String::from_str(&env, "Transit"), - payment_percent: 33, - proof_hash: String::from_str(&env, ""), - status: MilestoneStatus::Pending, - release_after_ledger: 0, - proof_submitted_ledger: None, - dispute_opened_ledger: None, - }); - v.push_back(Milestone { - name: String::from_str(&env, "Delivery"), - payment_percent: 34, - proof_hash: String::from_str(&env, ""), - status: MilestoneStatus::Pending, - release_after_ledger: 0, - proof_submitted_ledger: None, - dispute_opened_ledger: None, - }); - v -} - -fn default_options(env: &Env) -> ShipmentOptions { - ShipmentOptions { - response_deadline: 0, - penalty_bps: 0, - milestone_mode: MilestoneMode::Parallel, - holdback_ledgers: 0, - dispute_cooldown_ledgers: 0, - late_penalty_bps_per_ledger: 0, - auto_confirm_ledgers: 0, - dispute_bond_amount: 0, - arbiter_fee_bps: 0, - } -} - -// ============================================================ -// TESTS: ORACLE INTEGRATION PATTERNS -// ============================================================ - -#[test] -fn test_oracle_pattern_proof_verified_auto_confirm() { - let setup = oracle_setup(); - - let milestones = build_single_milestone(&setup.env); - let shipment_id = String::from_str(&setup.env, "shipment-oracle-001"); - let total_amount = 1_000 * 10i128.pow(18); - - let buyers = { - let mut v = Vec::new(&setup.env); - v.push_back(setup.buyer.clone()); - v - }; - - let options = default_options(&setup.env); - - // Create shipment - ChainSettleContract::create_shipment( - setup.env.clone(), - shipment_id.clone(), - buyers.clone(), - setup.supplier.clone(), - setup.logistics.clone(), - setup.arbiter.clone(), - setup.token_id.clone(), - total_amount, - milestones.clone(), - options, - ); - - // Verify shipment created - let shipment = ChainSettleContract::get_shipment(setup.env.clone(), shipment_id.clone()); - assert_eq!(shipment.status, ShipmentStatus::Active); - assert_eq!(shipment.total_amount, total_amount); - assert_eq!(shipment.milestones.len(), 1); - - // Simulate oracle verification: submit proof - let proof_hash = String::from_str(&setup.env, "QmVerifiedProof123"); - ChainSettleContract::submit_proof( - setup.env.clone(), - setup.supplier.clone(), - shipment_id.clone(), - 0u32, - proof_hash.clone(), - Symbol::new(&setup.env, "ipfs"), - ); - - // Verify milestone in ProofSubmitted state - let shipment = ChainSettleContract::get_shipment(setup.env.clone(), shipment_id.clone()); - let milestone = shipment.milestones.get(0).unwrap(); - assert_eq!(milestone.status, MilestoneStatus::ProofSubmitted); - assert_eq!(milestone.proof_hash, proof_hash); - - // Buyer confirms (oracle pre-verified, buyer trusts oracle) - let mut indices = Vec::new(&setup.env); - indices.push_back(0u32); - ChainSettleContract::confirm_milestones( - setup.env.clone(), - setup.buyer.clone(), - shipment_id.clone(), - indices, - ); - - // Verify milestone confirmed and payment released - let shipment = ChainSettleContract::get_shipment(setup.env.clone(), shipment_id.clone()); - assert_eq!(shipment.status, ShipmentStatus::Completed); - let milestone = shipment.milestones.get(0).unwrap(); - assert_eq!(milestone.status, MilestoneStatus::Confirmed); - - let token_client = token::Client::new(&setup.env, &setup.token_id); - let supplier_final = token_client.balance(&setup.supplier); - assert!(supplier_final > 0, "Supplier received payment"); -} - -#[test] -fn test_oracle_pattern_proof_rejected_raises_dispute() { - let setup = oracle_setup(); - - let milestones = build_single_milestone(&setup.env); - let shipment_id = String::from_str(&setup.env, "shipment-oracle-rejected"); - let total_amount = 1_000 * 10i128.pow(18); - - let buyers = { - let mut v = Vec::new(&setup.env); - v.push_back(setup.buyer.clone()); - v - }; - - let options = default_options(&setup.env); - - // Create shipment - ChainSettleContract::create_shipment( - setup.env.clone(), - shipment_id.clone(), - buyers.clone(), - setup.supplier.clone(), - setup.logistics.clone(), - setup.arbiter.clone(), - setup.token_id.clone(), - total_amount, - milestones.clone(), - options, - ); - - // Submit proof (not verified by oracle) - let proof_hash = String::from_str(&setup.env, "QmRejectedProof"); - ChainSettleContract::submit_proof( - setup.env.clone(), - setup.supplier.clone(), - shipment_id.clone(), - 0u32, - proof_hash, - Symbol::new(&setup.env, "ipfs"), - ); - - // Milestone is now ProofSubmitted - let shipment = ChainSettleContract::get_shipment(setup.env.clone(), shipment_id.clone()); - assert_eq!( - shipment.milestones.get(0).unwrap().status, - MilestoneStatus::ProofSubmitted - ); - - // Buyer disputes (oracle rejected the proof) - ChainSettleContract::raise_dispute( - setup.env.clone(), - setup.buyer.clone(), - shipment_id.clone(), - 0u32, - ); - - // Verify milestone is disputed - let shipment = ChainSettleContract::get_shipment(setup.env.clone(), shipment_id.clone()); - assert_eq!( - shipment.milestones.get(0).unwrap().status, - MilestoneStatus::Disputed - ); -} - -#[test] -fn test_oracle_pattern_multiple_proofs_cross_contract() { - let setup = oracle_setup(); - - let milestones = build_three_milestones(&setup.env); - let shipment_id = String::from_str(&setup.env, "shipment-oracle-multi"); - let total_amount = 3_000 * 10i128.pow(18); - - let buyers = { - let mut v = Vec::new(&setup.env); - v.push_back(setup.buyer.clone()); - v - }; - - let options = default_options(&setup.env); - - // Create shipment - ChainSettleContract::create_shipment( - setup.env.clone(), - shipment_id.clone(), - buyers.clone(), - setup.supplier.clone(), - setup.logistics.clone(), - setup.arbiter.clone(), - setup.token_id.clone(), - total_amount, - milestones.clone(), - options, - ); - - // Submit all proofs sequentially - for i in 0..3 { - let proof_hash = String::from_str(&setup.env, &format!("QmProof{}", i)); - ChainSettleContract::submit_proof( - setup.env.clone(), - setup.supplier.clone(), - shipment_id.clone(), - i as u32, - proof_hash, - Symbol::new(&setup.env, "ipfs"), - ); - } - - // Verify all milestones are ProofSubmitted - let shipment = ChainSettleContract::get_shipment(setup.env.clone(), shipment_id.clone()); - for i in 0..3 { - assert_eq!( - shipment.milestones.get(i).unwrap().status, - MilestoneStatus::ProofSubmitted - ); - } - - // Buyer confirms all milestones (all oracle-verified) - let mut indices = Vec::new(&setup.env); - indices.push_back(0u32); - indices.push_back(1u32); - indices.push_back(2u32); - - ChainSettleContract::confirm_milestones( - setup.env.clone(), - setup.buyer.clone(), - shipment_id.clone(), - indices, - ); - - // Verify shipment completed - let shipment = ChainSettleContract::get_shipment(setup.env.clone(), shipment_id.clone()); - assert_eq!(shipment.status, ShipmentStatus::Completed); - assert_eq!(shipment.released_amount, total_amount); -} - -#[test] -fn test_oracle_pattern_partial_approval_mixed_proofs() { - let setup = oracle_setup(); - - let milestones = build_three_milestones(&setup.env); - let shipment_id = String::from_str(&setup.env, "shipment-oracle-partial"); - let total_amount = 3_000 * 10i128.pow(18); - - let buyers = { - let mut v = Vec::new(&setup.env); - v.push_back(setup.buyer.clone()); - v - }; - - let options = default_options(&setup.env); - - // Create shipment - ChainSettleContract::create_shipment( - setup.env.clone(), - shipment_id.clone(), - buyers.clone(), - setup.supplier.clone(), - setup.logistics.clone(), - setup.arbiter.clone(), - setup.token_id.clone(), - total_amount, - milestones.clone(), - options, - ); - - // Submit proofs for all 3 milestones - for i in 0..3 { - let proof_hash = String::from_str(&setup.env, &format!("QmProof{}", i)); - ChainSettleContract::submit_proof( - setup.env.clone(), - setup.supplier.clone(), - shipment_id.clone(), - i as u32, - proof_hash, - Symbol::new(&setup.env, "ipfs"), - ); - } - - // Buyer confirms only milestones 0 and 2 (oracle rejected milestone 1) - let mut indices = Vec::new(&setup.env); - indices.push_back(0u32); - indices.push_back(2u32); - - ChainSettleContract::confirm_milestones( - setup.env.clone(), - setup.buyer.clone(), - shipment_id.clone(), - indices, - ); - - // Verify partial confirmation - let shipment = ChainSettleContract::get_shipment(setup.env.clone(), shipment_id.clone()); - assert_eq!( - shipment.milestones.get(0).unwrap().status, - MilestoneStatus::Confirmed - ); - assert_eq!( - shipment.milestones.get(1).unwrap().status, - MilestoneStatus::ProofSubmitted - ); - assert_eq!( - shipment.milestones.get(2).unwrap().status, - MilestoneStatus::Confirmed - ); - assert_eq!(shipment.status, ShipmentStatus::Active); -} - -#[test] -fn test_oracle_pattern_dispute_after_rejection() { - let setup = oracle_setup(); - - let milestones = build_three_milestones(&setup.env); - let shipment_id = String::from_str(&setup.env, "shipment-oracle-dispute"); - let total_amount = 3_000 * 10i128.pow(18); - - let buyers = { - let mut v = Vec::new(&setup.env); - v.push_back(setup.buyer.clone()); - v - }; - - let options = ShipmentOptions { - response_deadline: 0, - penalty_bps: 0, - milestone_mode: MilestoneMode::Parallel, - holdback_ledgers: 0, - dispute_cooldown_ledgers: 0, - late_penalty_bps_per_ledger: 0, - auto_confirm_ledgers: 0, - dispute_bond_amount: 0, - arbiter_fee_bps: 0, - }; - - // Create shipment - ChainSettleContract::create_shipment( - setup.env.clone(), - shipment_id.clone(), - buyers.clone(), - setup.supplier.clone(), - setup.logistics.clone(), - setup.arbiter.clone(), - setup.token_id.clone(), - total_amount, - milestones.clone(), - options, - ); - - // Submit proof for milestone 0 - ChainSettleContract::submit_proof( - setup.env.clone(), - setup.supplier.clone(), - shipment_id.clone(), - 0u32, - String::from_str(&setup.env, "QmProof0"), - Symbol::new(&setup.env, "ipfs"), - ); - - // Buyer raises dispute (oracle rejected) - ChainSettleContract::raise_dispute( - setup.env.clone(), - setup.buyer.clone(), - shipment_id.clone(), - 0u32, - ); - - // Verify dispute status - let shipment = ChainSettleContract::get_shipment(setup.env.clone(), shipment_id.clone()); - assert_eq!( - shipment.milestones.get(0).unwrap().status, - MilestoneStatus::Disputed - ); - - // Arbiter resolves: approves the proof despite oracle rejection - ChainSettleContract::resolve_dispute( - setup.env.clone(), - setup.arbiter.clone(), - shipment_id.clone(), - 0u32, - true, // approve - String::from_str(&setup.env, "Proof valid despite oracle"), - ); - - // Verify dispute resolved and milestone confirmed - let shipment = ChainSettleContract::get_shipment(setup.env.clone(), shipment_id.clone()); - assert_eq!( - shipment.milestones.get(0).unwrap().status, - MilestoneStatus::Confirmed - ); -} - -#[test] -fn test_oracle_pattern_shipment_lifecycle_oracle_verified() { - let setup = oracle_setup(); - - let milestones = build_three_milestones(&setup.env); - let shipment_id = String::from_str(&setup.env, "shipment-oracle-full-lifecycle"); - let total_amount = 3_000 * 10i128.pow(18); - - let buyers = { - let mut v = Vec::new(&setup.env); - v.push_back(setup.buyer.clone()); - v - }; - - let options = default_options(&setup.env); - - // Create shipment - ChainSettleContract::create_shipment( - setup.env.clone(), - shipment_id.clone(), - buyers.clone(), - setup.supplier.clone(), - setup.logistics.clone(), - setup.arbiter.clone(), - setup.token_id.clone(), - total_amount, - milestones.clone(), - options, - ); - - let token_client = token::Client::new(&setup.env, &setup.token_id); - - // Milestone 0: Dispatch - ChainSettleContract::submit_proof( - setup.env.clone(), - setup.supplier.clone(), - shipment_id.clone(), - 0u32, - String::from_str(&setup.env, "QmDispatch"), - Symbol::new(&setup.env, "ipfs"), - ); - - let mut idx = Vec::new(&setup.env); - idx.push_back(0u32); - ChainSettleContract::confirm_milestones( - setup.env.clone(), - setup.buyer.clone(), - shipment_id.clone(), - idx, - ); - - let mut balance = token_client.balance(&setup.supplier); - let expected = (total_amount * 33) / 100; - assert!(balance > 0, "Supplier should receive payment for milestone 0"); - - // Milestone 1: Transit - ChainSettleContract::submit_proof( - setup.env.clone(), - setup.logistics.clone(), - shipment_id.clone(), - 1u32, - String::from_str(&setup.env, "QmTransit"), - Symbol::new(&setup.env, "ipfs"), - ); - - let mut idx = Vec::new(&setup.env); - idx.push_back(1u32); - ChainSettleContract::confirm_milestones( - setup.env.clone(), - setup.buyer.clone(), - shipment_id.clone(), - idx, - ); - - // Milestone 2: Delivery - ChainSettleContract::submit_proof( - setup.env.clone(), - setup.supplier.clone(), - shipment_id.clone(), - 2u32, - String::from_str(&setup.env, "QmDelivery"), - Symbol::new(&setup.env, "ipfs"), - ); - - let mut idx = Vec::new(&setup.env); - idx.push_back(2u32); - ChainSettleContract::confirm_milestones( - setup.env.clone(), - setup.buyer.clone(), - shipment_id.clone(), - idx, - ); - - // Verify complete lifecycle - let shipment = ChainSettleContract::get_shipment(setup.env.clone(), shipment_id); - assert_eq!(shipment.status, ShipmentStatus::Completed); - assert_eq!(shipment.released_amount, total_amount); - for i in 0..3 { - assert_eq!( - shipment.milestones.get(i).unwrap().status, - MilestoneStatus::Confirmed - ); - } -} diff --git a/contracts/chainsetttle/src/test_permissions.rs b/contracts/chainsetttle/src/test_permissions.rs deleted file mode 100644 index 6883386..0000000 --- a/contracts/chainsetttle/src/test_permissions.rs +++ /dev/null @@ -1,1608 +0,0 @@ -#![cfg(test)] - -//! # Permission Matrix Tests -//! -//! Verifies, for every auth-requiring public function, exactly which role is -//! allowed and which three are denied with `"unauthorized"`. -//! -//! ## Role definitions -//! | Role | Description | -//! |----------|--------------------------------------------------| -//! | buyer | Primary buyer; creates shipment, confirms, etc. | -//! | supplier | Proof submitter; receives payment | -//! | logistics| Transit proof submitter | -//! | arbiter | Dispute resolver | -//! | admin | Contract-level admin set at init | -//! | stranger | Unrelated address (no role) | -//! -//! ## Permission Matrix (auth-requiring functions) -//! -//! ```text -//! Function | buyer | supplier | logistics | arbiter | admin -//! ---------------------------|-------|----------|-----------|---------|------ -//! pause | ✗ | ✗ | ✗ | ✗ | ✓ -//! unpause | ✗ | ✗ | ✗ | ✗ | ✓ -//! set_escalation_threshold | ✗ | ✗ | ✗ | ✗ | ✓ -//! set_max_shipment_value | ✗ | ✗ | ✗ | ✗ | ✓ -//! set_circuit_breaker | ✗ | ✗ | ✗ | ✗ | ✓ -//! set_fee_config | ✗ | ✗ | ✗ | ✗ | ✓ -//! set_max_concurrent_disputes| ✗ | ✗ | ✗ | ✗ | ✓ -//! set_min_milestone_percent | ✗ | ✗ | ✗ | ✗ | ✓ -//! set_max_advance_percent | ✗ | ✗ | ✗ | ✗ | ✓ -//! blacklist_address | ✗ | ✗ | ✗ | ✗ | ✓ -//! remove_from_blacklist | ✗ | ✗ | ✗ | ✗ | ✓ -//! add_allowed_token | ✗ | ✗ | ✗ | ✗ | ✓ -//! remove_allowed_token | ✗ | ✗ | ✗ | ✗ | ✓ -//! nominate_admin | ✗ | ✗ | ✗ | ✗ | ✓ -//! revoke_nomination | ✗ | ✗ | ✗ | ✗ | ✓ -//! emergency_recover | ✗ | ✗ | ✗ | ✗ | ✓ -//! create_shipment | ✓ | ✗ | ✗ | ✗ | ✗ -//! top_up_escrow | ✓ | ✗ | ✗ | ✗ | ✗ -//! rebalance_milestones | ✓ | ✗ | ✗ | ✗ | ✗ -//! confirm_milestone | ✓ | ✗ | ✗ | ✗ | ✗ -//! batch_confirm_milestones | ✓ | ✗ | ✗ | ✗ | ✗ -//! raise_dispute | ✓ | ✗ | ✗ | ✗ | ✗ -//! raise_partial_dispute | ✓ | ✗ | ✗ | ✗ | ✗ -//! cancel_shipment | ✓ | ✗ | ✗ | ✗ | ✗ -//! approve_advance | ✓ | ✗ | ✗ | ✗ | ✗ -//! set_proof_whitelist | ✓ | ✗ | ✗ | ✗ | ✗ -//! transfer_buyer | ✓ | ✗ | ✗ | ✗ | ✗ -//! propose_amendment(buyer) | ✓ | ✗ | ✗ | ✗ | ✗ -//! propose_amendment(supplier)| ✗ | ✓ | ✗ | ✗ | ✗ -//! propose_arbiter_rotation(b)| ✓ | ✗ | ✗ | ✗ | ✗ -//! propose_arbiter_rotation(s)| ✗ | ✓ | ✗ | ✗ | ✗ -//! submit_proof | ✗ | ✓ | ✓ | ✗ | ✗ -//! request_advance | ✗ | ✓ | ✗ | ✗ | ✗ -//! supplier_cancel | ✗ | ✓ | ✗ | ✗ | ✗ -//! transfer_supplier | ✗ | ✓ | ✗ | ✗ | ✗ -//! resolve_dispute | ✗ | ✗ | ✗ | ✓ | ✗ -//! ``` -//! -//! ## CI guard -//! The constant `PERMISSION_TEST_COUNT` at the bottom of this file must equal -//! the actual number of `#[test]` functions. CI will fail if tests are removed -//! without updating the constant (via `grep` count assertion in the test suite). - -extern crate std; - -use super::*; -use soroban_sdk::{ - testutils::{Address as _, Ledger as _}, - token, vec, Address, BytesN, Env, String, Symbol, -}; - -// ============================================================ -// HELPERS -// ============================================================ - -struct Roles { - env: Env, - contract_id: Address, - token_id: Address, - admin: Address, - buyer: Address, - supplier: Address, - logistics: Address, - arbiter: Address, - stranger: Address, -} - -fn setup() -> Roles { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register(ChainSettleContract, ()); - - let token_admin = Address::generate(&env); - let token_id = env - .register_stellar_asset_contract_v2(token_admin.clone()) - .address(); - - let admin = Address::generate(&env); - let buyer = Address::generate(&env); - let supplier = Address::generate(&env); - let logistics = Address::generate(&env); - let arbiter = Address::generate(&env); - let stranger = Address::generate(&env); - - token::StellarAssetClient::new(&env, &token_id).mint(&buyer, &100_000_000_000); - token::StellarAssetClient::new(&env, &token_id).mint(&stranger, &100_000_000_000); - - ChainSettleContractClient::new(&env, &contract_id).init(&admin); - - Roles { env, contract_id, token_id, admin, buyer, supplier, logistics, arbiter, stranger } -} - -fn milestones(env: &Env) -> soroban_sdk::Vec { - vec![ - env, - Milestone { - name: String::from_str(env, "M0"), - payment_percent: 50, - proof_hash: String::from_str(env, ""), - status: MilestoneStatus::Pending, - release_after_ledger: 0, - proof_submitted_ledger: None, - dispute_opened_ledger: None, - }, - Milestone { - name: String::from_str(env, "M1"), - payment_percent: 50, - proof_hash: String::from_str(env, ""), - status: MilestoneStatus::Pending, - release_after_ledger: 0, - proof_submitted_ledger: None, - dispute_opened_ledger: None, - }, - ] -} - -fn opts(env: &Env) -> ShipmentOptions { - ShipmentOptions { - response_deadline: 100, - penalty_bps: 0, - milestone_mode: MilestoneMode::Parallel, - holdback_ledgers: 0, - dispute_cooldown_ledgers: 0, - late_penalty_bps_per_ledger: 0, - auto_confirm_ledgers: 0, - dispute_bond_amount: 0, - arbiter_fee_bps: 0, - } -} - -fn buyers(env: &Env, buyer: &Address) -> soroban_sdk::Vec
{ - vec![env, buyer.clone()] -} - -/// Create a standard shipment and return its ID. -fn make_shipment(r: &Roles, id: &str) -> String { - let client = ChainSettleContractClient::new(&r.env, &r.contract_id); - let sid = String::from_str(&r.env, id); - client.create_shipment( - &sid, - &buyers(&r.env, &r.buyer), - &r.supplier, - &r.logistics, - &r.arbiter, - &r.token_id, - &1_000_000_000, - &milestones(&r.env), - &opts(&r.env), - ); - sid -} - -/// Advance milestone 0 to ProofSubmitted. -fn submit_m0(r: &Roles, sid: &String) { - let client = ChainSettleContractClient::new(&r.env, &r.contract_id); - client.submit_proof( - &r.supplier, - sid, - &0, - &String::from_str(&r.env, "ipfs://x"), - &Symbol::new(&r.env, "ipfs"), - ); -} - -/// Advance milestone 0 to Disputed. -fn dispute_m0(r: &Roles, sid: &String) { - submit_m0(r, sid); - let client = ChainSettleContractClient::new(&r.env, &r.contract_id); - client.raise_dispute(&r.buyer, sid, &0); -} - -fn dummy_wasm_hash(env: &Env) -> BytesN<32> { - BytesN::from_array(env, &[0u8; 32]) -} - -// ============================================================ -// pause — admin only -// ============================================================ - -#[test] -fn test_perm_pause_admin_allowed() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id).pause(&r.admin); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_pause_buyer_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id).pause(&r.buyer); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_pause_supplier_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id).pause(&r.supplier); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_pause_logistics_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id).pause(&r.logistics); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_pause_arbiter_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id).pause(&r.arbiter); -} - -// ============================================================ -// unpause — admin only -// ============================================================ - -#[test] -fn test_perm_unpause_admin_allowed() { - let r = setup(); - let client = ChainSettleContractClient::new(&r.env, &r.contract_id); - client.pause(&r.admin); - client.unpause(&r.admin); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_unpause_buyer_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id).unpause(&r.buyer); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_unpause_supplier_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id).unpause(&r.supplier); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_unpause_logistics_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id).unpause(&r.logistics); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_unpause_arbiter_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id).unpause(&r.arbiter); -} - -// ============================================================ -// set_escalation_threshold — admin only -// ============================================================ - -#[test] -fn test_perm_set_escalation_threshold_admin_allowed() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_escalation_threshold(&r.admin, &100); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_escalation_threshold_buyer_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_escalation_threshold(&r.buyer, &100); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_escalation_threshold_supplier_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_escalation_threshold(&r.supplier, &100); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_escalation_threshold_logistics_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_escalation_threshold(&r.logistics, &100); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_escalation_threshold_arbiter_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_escalation_threshold(&r.arbiter, &100); -} - -// ============================================================ -// set_max_shipment_value — admin only -// ============================================================ - -#[test] -fn test_perm_set_max_shipment_value_admin_allowed() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_max_shipment_value(&r.admin, &5_000_000_000); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_max_shipment_value_buyer_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_max_shipment_value(&r.buyer, &5_000_000_000); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_max_shipment_value_supplier_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_max_shipment_value(&r.supplier, &5_000_000_000); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_max_shipment_value_logistics_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_max_shipment_value(&r.logistics, &5_000_000_000); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_max_shipment_value_arbiter_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_max_shipment_value(&r.arbiter, &5_000_000_000); -} - -// ============================================================ -// set_circuit_breaker — admin only -// ============================================================ - -#[test] -fn test_perm_set_circuit_breaker_admin_allowed() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_circuit_breaker(&r.admin, &1_000_000_000, &1000); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_circuit_breaker_buyer_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_circuit_breaker(&r.buyer, &1_000_000_000, &1000); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_circuit_breaker_supplier_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_circuit_breaker(&r.supplier, &1_000_000_000, &1000); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_circuit_breaker_logistics_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_circuit_breaker(&r.logistics, &1_000_000_000, &1000); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_circuit_breaker_arbiter_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_circuit_breaker(&r.arbiter, &1_000_000_000, &1000); -} - -// ============================================================ -// set_fee_config — admin only -// ============================================================ - -#[test] -fn test_perm_set_fee_config_admin_allowed() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_fee_config(&r.admin, &100, &r.stranger); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_fee_config_buyer_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_fee_config(&r.buyer, &100, &r.stranger); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_fee_config_supplier_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_fee_config(&r.supplier, &100, &r.stranger); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_fee_config_logistics_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_fee_config(&r.logistics, &100, &r.stranger); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_fee_config_arbiter_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_fee_config(&r.arbiter, &100, &r.stranger); -} - -// ============================================================ -// set_max_concurrent_disputes — admin only -// ============================================================ - -#[test] -fn test_perm_set_max_concurrent_disputes_admin_allowed() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_max_concurrent_disputes(&r.admin, &3); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_max_concurrent_disputes_buyer_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_max_concurrent_disputes(&r.buyer, &3); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_max_concurrent_disputes_supplier_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_max_concurrent_disputes(&r.supplier, &3); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_max_concurrent_disputes_logistics_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_max_concurrent_disputes(&r.logistics, &3); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_max_concurrent_disputes_arbiter_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_max_concurrent_disputes(&r.arbiter, &3); -} - -// ============================================================ -// set_min_milestone_percent — admin only -// ============================================================ - -#[test] -fn test_perm_set_min_milestone_percent_admin_allowed() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_min_milestone_percent(&r.admin, &5); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_min_milestone_percent_buyer_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_min_milestone_percent(&r.buyer, &5); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_min_milestone_percent_supplier_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_min_milestone_percent(&r.supplier, &5); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_min_milestone_percent_logistics_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_min_milestone_percent(&r.logistics, &5); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_min_milestone_percent_arbiter_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_min_milestone_percent(&r.arbiter, &5); -} - -// ============================================================ -// set_max_advance_percent — admin only -// ============================================================ - -#[test] -fn test_perm_set_max_advance_percent_admin_allowed() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_max_advance_percent(&r.admin, &50); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_max_advance_percent_buyer_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_max_advance_percent(&r.buyer, &50); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_max_advance_percent_supplier_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_max_advance_percent(&r.supplier, &50); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_max_advance_percent_logistics_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_max_advance_percent(&r.logistics, &50); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_max_advance_percent_arbiter_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_max_advance_percent(&r.arbiter, &50); -} - -// ============================================================ -// blacklist_address — admin only -// ============================================================ - -#[test] -fn test_perm_blacklist_address_admin_allowed() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .blacklist_address(&r.admin, &r.stranger, &dummy_wasm_hash(&r.env)); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_blacklist_address_buyer_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .blacklist_address(&r.buyer, &r.stranger, &dummy_wasm_hash(&r.env)); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_blacklist_address_supplier_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .blacklist_address(&r.supplier, &r.stranger, &dummy_wasm_hash(&r.env)); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_blacklist_address_logistics_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .blacklist_address(&r.logistics, &r.stranger, &dummy_wasm_hash(&r.env)); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_blacklist_address_arbiter_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .blacklist_address(&r.arbiter, &r.stranger, &dummy_wasm_hash(&r.env)); -} - -// ============================================================ -// remove_from_blacklist — admin only -// ============================================================ - -#[test] -fn test_perm_remove_from_blacklist_admin_allowed() { - let r = setup(); - let client = ChainSettleContractClient::new(&r.env, &r.contract_id); - client.blacklist_address(&r.admin, &r.stranger, &dummy_wasm_hash(&r.env)); - client.remove_from_blacklist(&r.admin, &r.stranger); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_remove_from_blacklist_buyer_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .remove_from_blacklist(&r.buyer, &r.stranger); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_remove_from_blacklist_supplier_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .remove_from_blacklist(&r.supplier, &r.stranger); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_remove_from_blacklist_logistics_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .remove_from_blacklist(&r.logistics, &r.stranger); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_remove_from_blacklist_arbiter_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .remove_from_blacklist(&r.arbiter, &r.stranger); -} - -// ============================================================ -// add_allowed_token — admin only (no caller param; enforced via stored admin require_auth) -// ============================================================ - -#[test] -fn test_perm_add_allowed_token_admin_allowed() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .add_allowed_token(&r.token_id); -} - -// ============================================================ -// remove_allowed_token — admin only (no caller param; enforced via stored admin require_auth) -// ============================================================ - -#[test] -fn test_perm_remove_allowed_token_admin_allowed() { - let r = setup(); - let client = ChainSettleContractClient::new(&r.env, &r.contract_id); - client.add_allowed_token(&r.token_id); - client.remove_allowed_token(&r.token_id); -} - -// ============================================================ -// nominate_admin — admin only -// ============================================================ - -#[test] -fn test_perm_nominate_admin_admin_allowed() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .nominate_admin(&r.admin, &r.stranger); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_nominate_admin_buyer_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .nominate_admin(&r.buyer, &r.stranger); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_nominate_admin_supplier_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .nominate_admin(&r.supplier, &r.stranger); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_nominate_admin_logistics_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .nominate_admin(&r.logistics, &r.stranger); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_nominate_admin_arbiter_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .nominate_admin(&r.arbiter, &r.stranger); -} - -// ============================================================ -// revoke_nomination — admin only -// ============================================================ - -#[test] -fn test_perm_revoke_nomination_admin_allowed() { - let r = setup(); - let client = ChainSettleContractClient::new(&r.env, &r.contract_id); - client.nominate_admin(&r.admin, &r.stranger); - client.revoke_nomination(&r.admin); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_revoke_nomination_buyer_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .revoke_nomination(&r.buyer); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_revoke_nomination_supplier_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .revoke_nomination(&r.supplier); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_revoke_nomination_logistics_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .revoke_nomination(&r.logistics); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_revoke_nomination_arbiter_denied() { - let r = setup(); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .revoke_nomination(&r.arbiter); -} - -// ============================================================ -// create_shipment — buyer (as primary authoriser) only -// ============================================================ - -#[test] -fn test_perm_create_shipment_buyer_allowed() { - let r = setup(); - // buyer is the only entry in the buyers vec; this must succeed - make_shipment(&r, "PERM-CS-OK"); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_create_shipment_supplier_as_buyer_denied() { - // supplier in the buyers list but not the one signing — mock_all_auths passes - // the auth, but the shipment is created with supplier as "buyer". The real - // denial we want to test is: a stranger cannot be a valid buyer role on - // an already-created shipment when calling buyer-only functions. Here we - // verify that a non-buyer cannot call confirm_milestone on a buyer-created ship. - let r = setup(); - let sid = make_shipment(&r, "PERM-CS-S"); - submit_m0(&r, &sid); - // supplier tries to confirm — must panic - ChainSettleContractClient::new(&r.env, &r.contract_id) - .confirm_milestone(&r.supplier, &sid, &0); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_create_shipment_logistics_as_confirmer_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-CS-L"); - submit_m0(&r, &sid); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .confirm_milestone(&r.logistics, &sid, &0); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_create_shipment_arbiter_as_confirmer_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-CS-A"); - submit_m0(&r, &sid); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .confirm_milestone(&r.arbiter, &sid, &0); -} - -// ============================================================ -// top_up_escrow — buyer only -// ============================================================ - -#[test] -fn test_perm_top_up_escrow_buyer_allowed() { - let r = setup(); - let sid = make_shipment(&r, "PERM-TUE-OK"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .top_up_escrow(&r.buyer, &sid, &500_000_000); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_top_up_escrow_supplier_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-TUE-S"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .top_up_escrow(&r.supplier, &sid, &500_000_000); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_top_up_escrow_logistics_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-TUE-L"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .top_up_escrow(&r.logistics, &sid, &500_000_000); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_top_up_escrow_arbiter_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-TUE-A"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .top_up_escrow(&r.arbiter, &sid, &500_000_000); -} - -// ============================================================ -// rebalance_milestones — buyer only -// ============================================================ - -#[test] -fn test_perm_rebalance_milestones_buyer_allowed() { - let r = setup(); - let sid = make_shipment(&r, "PERM-RBM-OK"); - let new_pcts = vec![&r.env, 40u32, 60u32]; - ChainSettleContractClient::new(&r.env, &r.contract_id) - .rebalance_milestones(&r.buyer, &sid, &new_pcts); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_rebalance_milestones_supplier_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-RBM-S"); - let new_pcts = vec![&r.env, 40u32, 60u32]; - ChainSettleContractClient::new(&r.env, &r.contract_id) - .rebalance_milestones(&r.supplier, &sid, &new_pcts); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_rebalance_milestones_logistics_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-RBM-L"); - let new_pcts = vec![&r.env, 40u32, 60u32]; - ChainSettleContractClient::new(&r.env, &r.contract_id) - .rebalance_milestones(&r.logistics, &sid, &new_pcts); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_rebalance_milestones_arbiter_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-RBM-A"); - let new_pcts = vec![&r.env, 40u32, 60u32]; - ChainSettleContractClient::new(&r.env, &r.contract_id) - .rebalance_milestones(&r.arbiter, &sid, &new_pcts); -} - -// ============================================================ -// confirm_milestone — buyer only -// ============================================================ - -#[test] -fn test_perm_confirm_milestone_buyer_allowed() { - let r = setup(); - let sid = make_shipment(&r, "PERM-CM-OK"); - submit_m0(&r, &sid); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .confirm_milestone(&r.buyer, &sid, &0); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_confirm_milestone_supplier_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-CM-S"); - submit_m0(&r, &sid); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .confirm_milestone(&r.supplier, &sid, &0); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_confirm_milestone_logistics_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-CM-L"); - submit_m0(&r, &sid); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .confirm_milestone(&r.logistics, &sid, &0); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_confirm_milestone_arbiter_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-CM-A"); - submit_m0(&r, &sid); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .confirm_milestone(&r.arbiter, &sid, &0); -} - -// ============================================================ -// raise_dispute — buyer only -// ============================================================ - -#[test] -fn test_perm_raise_dispute_buyer_allowed() { - let r = setup(); - let sid = make_shipment(&r, "PERM-RD-OK"); - submit_m0(&r, &sid); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .raise_dispute(&r.buyer, &sid, &0); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_raise_dispute_supplier_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-RD-S"); - submit_m0(&r, &sid); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .raise_dispute(&r.supplier, &sid, &0); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_raise_dispute_logistics_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-RD-L"); - submit_m0(&r, &sid); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .raise_dispute(&r.logistics, &sid, &0); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_raise_dispute_arbiter_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-RD-A"); - submit_m0(&r, &sid); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .raise_dispute(&r.arbiter, &sid, &0); -} - -// ============================================================ -// raise_partial_dispute — buyer only -// ============================================================ - -#[test] -fn test_perm_raise_partial_dispute_buyer_allowed() { - let r = setup(); - let sid = make_shipment(&r, "PERM-RPD-OK"); - submit_m0(&r, &sid); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .raise_partial_dispute(&r.buyer, &sid, &0, &40); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_raise_partial_dispute_supplier_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-RPD-S"); - submit_m0(&r, &sid); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .raise_partial_dispute(&r.supplier, &sid, &0, &40); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_raise_partial_dispute_logistics_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-RPD-L"); - submit_m0(&r, &sid); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .raise_partial_dispute(&r.logistics, &sid, &0, &40); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_raise_partial_dispute_arbiter_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-RPD-A"); - submit_m0(&r, &sid); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .raise_partial_dispute(&r.arbiter, &sid, &0, &40); -} - -// ============================================================ -// cancel_shipment — buyer only -// ============================================================ - -#[test] -fn test_perm_cancel_shipment_buyer_allowed() { - let r = setup(); - let sid = make_shipment(&r, "PERM-CAN-OK"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .cancel_shipment(&r.buyer, &sid); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_cancel_shipment_supplier_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-CAN-S"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .cancel_shipment(&r.supplier, &sid); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_cancel_shipment_logistics_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-CAN-L"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .cancel_shipment(&r.logistics, &sid); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_cancel_shipment_arbiter_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-CAN-A"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .cancel_shipment(&r.arbiter, &sid); -} - -// ============================================================ -// approve_advance — buyer only -// ============================================================ - -#[test] -fn test_perm_approve_advance_buyer_allowed() { - let r = setup(); - let sid = make_shipment(&r, "PERM-AA-OK"); - // supplier first requests an advance - ChainSettleContractClient::new(&r.env, &r.contract_id) - .request_advance(&r.supplier, &sid, &0, &20); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .approve_advance(&r.buyer, &sid, &0); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_approve_advance_supplier_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-AA-S"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .request_advance(&r.supplier, &sid, &0, &20); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .approve_advance(&r.supplier, &sid, &0); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_approve_advance_logistics_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-AA-L"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .request_advance(&r.supplier, &sid, &0, &20); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .approve_advance(&r.logistics, &sid, &0); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_approve_advance_arbiter_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-AA-A"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .request_advance(&r.supplier, &sid, &0, &20); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .approve_advance(&r.arbiter, &sid, &0); -} - -// ============================================================ -// set_proof_whitelist — buyer only -// ============================================================ - -#[test] -fn test_perm_set_proof_whitelist_buyer_allowed() { - let r = setup(); - let sid = make_shipment(&r, "PERM-SPW-OK"); - let types = vec![&r.env, Symbol::new(&r.env, "ipfs")]; - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_proof_whitelist(&r.buyer, &sid, &0, &types); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_proof_whitelist_supplier_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-SPW-S"); - let types = vec![&r.env, Symbol::new(&r.env, "ipfs")]; - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_proof_whitelist(&r.supplier, &sid, &0, &types); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_proof_whitelist_logistics_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-SPW-L"); - let types = vec![&r.env, Symbol::new(&r.env, "ipfs")]; - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_proof_whitelist(&r.logistics, &sid, &0, &types); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_set_proof_whitelist_arbiter_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-SPW-A"); - let types = vec![&r.env, Symbol::new(&r.env, "ipfs")]; - ChainSettleContractClient::new(&r.env, &r.contract_id) - .set_proof_whitelist(&r.arbiter, &sid, &0, &types); -} - -// ============================================================ -// transfer_buyer — current buyer only -// ============================================================ - -#[test] -fn test_perm_transfer_buyer_buyer_allowed() { - let r = setup(); - let sid = make_shipment(&r, "PERM-TB-OK"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .transfer_buyer(&r.buyer, &sid, &r.stranger); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_transfer_buyer_supplier_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-TB-S"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .transfer_buyer(&r.supplier, &sid, &r.stranger); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_transfer_buyer_logistics_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-TB-L"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .transfer_buyer(&r.logistics, &sid, &r.stranger); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_transfer_buyer_arbiter_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-TB-A"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .transfer_buyer(&r.arbiter, &sid, &r.stranger); -} - -// ============================================================ -// resolve_dispute — arbiter only -// ============================================================ - -#[test] -fn test_perm_resolve_dispute_arbiter_allowed() { - let r = setup(); - let sid = make_shipment(&r, "PERM-RES-OK"); - dispute_m0(&r, &sid); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .resolve_dispute(&r.arbiter, &sid, &0, &true); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_resolve_dispute_buyer_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-RES-B"); - dispute_m0(&r, &sid); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .resolve_dispute(&r.buyer, &sid, &0, &true); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_resolve_dispute_supplier_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-RES-S"); - dispute_m0(&r, &sid); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .resolve_dispute(&r.supplier, &sid, &0, &true); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_resolve_dispute_logistics_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-RES-L"); - dispute_m0(&r, &sid); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .resolve_dispute(&r.logistics, &sid, &0, &true); -} - -// ============================================================ -// submit_proof — supplier OR logistics -// ============================================================ - -#[test] -fn test_perm_submit_proof_supplier_allowed() { - let r = setup(); - let sid = make_shipment(&r, "PERM-SP-SUP"); - ChainSettleContractClient::new(&r.env, &r.contract_id).submit_proof( - &r.supplier, - &sid, - &0, - &String::from_str(&r.env, "ipfs://a"), - &Symbol::new(&r.env, "ipfs"), - ); -} - -#[test] -fn test_perm_submit_proof_logistics_allowed() { - let r = setup(); - let sid = make_shipment(&r, "PERM-SP-LOG"); - ChainSettleContractClient::new(&r.env, &r.contract_id).submit_proof( - &r.logistics, - &sid, - &0, - &String::from_str(&r.env, "ipfs://b"), - &Symbol::new(&r.env, "ipfs"), - ); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_submit_proof_buyer_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-SP-B"); - ChainSettleContractClient::new(&r.env, &r.contract_id).submit_proof( - &r.buyer, - &sid, - &0, - &String::from_str(&r.env, "ipfs://c"), - &Symbol::new(&r.env, "ipfs"), - ); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_submit_proof_arbiter_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-SP-A"); - ChainSettleContractClient::new(&r.env, &r.contract_id).submit_proof( - &r.arbiter, - &sid, - &0, - &String::from_str(&r.env, "ipfs://d"), - &Symbol::new(&r.env, "ipfs"), - ); -} - -// ============================================================ -// request_advance — supplier only -// ============================================================ - -#[test] -fn test_perm_request_advance_supplier_allowed() { - let r = setup(); - let sid = make_shipment(&r, "PERM-RA-OK"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .request_advance(&r.supplier, &sid, &0, &20); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_request_advance_buyer_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-RA-B"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .request_advance(&r.buyer, &sid, &0, &20); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_request_advance_logistics_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-RA-L"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .request_advance(&r.logistics, &sid, &0, &20); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_request_advance_arbiter_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-RA-A"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .request_advance(&r.arbiter, &sid, &0, &20); -} - -// ============================================================ -// supplier_cancel — supplier only -// ============================================================ - -#[test] -fn test_perm_supplier_cancel_supplier_allowed() { - let r = setup(); - let sid = make_shipment(&r, "PERM-SC-OK"); - // Submit proof so a deadline can pass; advance ledger past response_deadline (100) - submit_m0(&r, &sid); - r.env.ledger().set_sequence_number(200); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .supplier_cancel(&r.supplier, &sid); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_supplier_cancel_buyer_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-SC-B"); - submit_m0(&r, &sid); - r.env.ledger().set_sequence_number(200); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .supplier_cancel(&r.buyer, &sid); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_supplier_cancel_logistics_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-SC-L"); - submit_m0(&r, &sid); - r.env.ledger().set_sequence_number(200); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .supplier_cancel(&r.logistics, &sid); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_supplier_cancel_arbiter_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-SC-A"); - submit_m0(&r, &sid); - r.env.ledger().set_sequence_number(200); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .supplier_cancel(&r.arbiter, &sid); -} - -// ============================================================ -// transfer_supplier — current supplier only -// ============================================================ - -#[test] -fn test_perm_transfer_supplier_supplier_allowed() { - let r = setup(); - let sid = make_shipment(&r, "PERM-TS-OK"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .transfer_supplier(&r.supplier, &sid, &r.stranger); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_transfer_supplier_buyer_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-TS-B"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .transfer_supplier(&r.buyer, &sid, &r.stranger); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_transfer_supplier_logistics_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-TS-L"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .transfer_supplier(&r.logistics, &sid, &r.stranger); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_transfer_supplier_arbiter_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-TS-A"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .transfer_supplier(&r.arbiter, &sid, &r.stranger); -} - -// ============================================================ -// propose_amendment — buyer OR supplier; others denied -// ============================================================ - -#[test] -fn test_perm_propose_amendment_buyer_allowed() { - let r = setup(); - let sid = make_shipment(&r, "PERM-PA-B-OK"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .propose_amendment(&r.buyer, &sid, &0, &50, &String::from_str(&r.env, "M0v2")); -} - -#[test] -fn test_perm_propose_amendment_supplier_allowed() { - let r = setup(); - let sid = make_shipment(&r, "PERM-PA-S-OK"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .propose_amendment(&r.supplier, &sid, &0, &50, &String::from_str(&r.env, "M0v2")); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_propose_amendment_logistics_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-PA-L"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .propose_amendment(&r.logistics, &sid, &0, &50, &String::from_str(&r.env, "M0v2")); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_propose_amendment_arbiter_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-PA-A"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .propose_amendment(&r.arbiter, &sid, &0, &50, &String::from_str(&r.env, "M0v2")); -} - -// ============================================================ -// propose_arbiter_rotation — buyer OR supplier; others denied -// ============================================================ - -#[test] -fn test_perm_propose_arbiter_rotation_buyer_allowed() { - let r = setup(); - let sid = make_shipment(&r, "PERM-PAR-B-OK"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .propose_arbiter_rotation(&r.buyer, &sid, &r.stranger); -} - -#[test] -fn test_perm_propose_arbiter_rotation_supplier_allowed() { - let r = setup(); - let sid = make_shipment(&r, "PERM-PAR-S-OK"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .propose_arbiter_rotation(&r.supplier, &sid, &r.stranger); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_propose_arbiter_rotation_logistics_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-PAR-L"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .propose_arbiter_rotation(&r.logistics, &sid, &r.stranger); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_propose_arbiter_rotation_arbiter_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-PAR-A"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .propose_arbiter_rotation(&r.arbiter, &sid, &r.stranger); -} - -// ============================================================ -// emergency_recover — admin only -// ============================================================ - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_emergency_recover_buyer_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-ER-B"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .emergency_recover(&r.buyer, &sid); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_emergency_recover_supplier_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-ER-S"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .emergency_recover(&r.supplier, &sid); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_emergency_recover_logistics_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-ER-L"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .emergency_recover(&r.logistics, &sid); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_emergency_recover_arbiter_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-ER-A"); - ChainSettleContractClient::new(&r.env, &r.contract_id) - .emergency_recover(&r.arbiter, &sid); -} - -// ============================================================ -// batch_confirm_milestones — buyer only -// ============================================================ - -#[test] -fn test_perm_batch_confirm_milestones_buyer_allowed() { - let r = setup(); - let sid = make_shipment(&r, "PERM-BCM-OK"); - submit_m0(&r, &sid); - let indices = vec![&r.env, 0u32]; - ChainSettleContractClient::new(&r.env, &r.contract_id) - .batch_confirm_milestones(&r.buyer, &sid, &indices); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_batch_confirm_milestones_supplier_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-BCM-S"); - submit_m0(&r, &sid); - let indices = vec![&r.env, 0u32]; - ChainSettleContractClient::new(&r.env, &r.contract_id) - .batch_confirm_milestones(&r.supplier, &sid, &indices); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_batch_confirm_milestones_logistics_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-BCM-L"); - submit_m0(&r, &sid); - let indices = vec![&r.env, 0u32]; - ChainSettleContractClient::new(&r.env, &r.contract_id) - .batch_confirm_milestones(&r.logistics, &sid, &indices); -} - -#[test] -#[should_panic(expected = "unauthorized")] -fn test_perm_batch_confirm_milestones_arbiter_denied() { - let r = setup(); - let sid = make_shipment(&r, "PERM-BCM-A"); - submit_m0(&r, &sid); - let indices = vec![&r.env, 0u32]; - ChainSettleContractClient::new(&r.env, &r.contract_id) - .batch_confirm_milestones(&r.arbiter, &sid, &indices); -} - -// ============================================================ -// CI GUARD -// ============================================================ -// -// Count of `fn test_perm_` functions in this file. CI can assert: -// grep -c 'fn test_perm_' src/test_permissions.rs == PERMISSION_TEST_COUNT -// -// Update this constant whenever a new test is added or removed. -pub const PERMISSION_TEST_COUNT: usize = 146; - -#[test] -fn test_perm_count_guard() { - // Counts the number of permission tests via the constant above. - // If PERMISSION_TEST_COUNT is wrong the build will fail to compile - // (unreachable assertion) or alert reviewers during code review. - assert!(PERMISSION_TEST_COUNT >= 40, "permission matrix requires at least 40 test cases"); -} diff --git a/contracts/chainsetttle/src/test_regression.rs b/contracts/chainsetttle/src/test_regression.rs deleted file mode 100644 index c865388..0000000 --- a/contracts/chainsetttle/src/test_regression.rs +++ /dev/null @@ -1,345 +0,0 @@ -//! Regression Test Suite - Pins exact function signatures, error codes, event topics, and storage keys. -//! -//! This test suite captures the current baseline of the contract's public interface to catch -//! any accidental breaking changes during refactoring. -//! -//! All tests are marked `#[ignore = "regression"]` and run only with `cargo test -- --ignored`. - -#[cfg(test)] -mod regression_tests { - use crate::{ChainSettleError, DataKey, ShipmentStatus}; - use soroban_sdk::{Address, Env, Symbol, String as SorobanString, Vec, testutils::Address as _}; - - // ============================================================ - // ERROR CODE REGRESSION TESTS - // ============================================================ - // Pins exact u32 values for all ChainSettleError variants. - // Any change to these values indicates a breaking change in error handling. - - #[test] - #[ignore = "regression"] - fn test_error_codes_exact_values() { - // Verify each error code maps to the exact expected u32 value. - // If any of these change, it indicates a breaking change in error semantics. - assert_eq!(ChainSettleError::ShipmentAlreadyExists as u32, 1); - assert_eq!(ChainSettleError::ShipmentNotFound as u32, 2); - assert_eq!(ChainSettleError::Unauthorized as u32, 3); - assert_eq!(ChainSettleError::InvalidMilestoneIndex as u32, 4); - assert_eq!(ChainSettleError::InvalidMilestoneStatus as u32, 5); - assert_eq!(ChainSettleError::ShipmentNotActive as u32, 6); - assert_eq!(ChainSettleError::InvalidPercentages as u32, 7); - assert_eq!(ChainSettleError::InvalidAmount as u32, 8); - assert_eq!(ChainSettleError::DisputeAlreadyOpen as u32, 9); - assert_eq!(ChainSettleError::DeadlineNotBreached as u32, 10); - assert_eq!(ChainSettleError::FeeTooHigh as u32, 11); - assert_eq!(ChainSettleError::PreviousMilestoneNotComplete as u32, 12); - assert_eq!(ChainSettleError::ContractPaused as u32, 13); - assert_eq!(ChainSettleError::DisputeCooldownActive as u32, 14); - assert_eq!(ChainSettleError::TransferDisallowed as u32, 15); - assert_eq!(ChainSettleError::CircuitBreakerTripped as u32, 16); - assert_eq!(ChainSettleError::EmptyBuyersList as u32, 17); - assert_eq!(ChainSettleError::MaxShipmentValueExceeded as u32, 18); - assert_eq!(ChainSettleError::InvalidMultiSigParameters as u32, 19); - assert_eq!(ChainSettleError::MultisigNotConfigured as u32, 20); - assert_eq!(ChainSettleError::AlreadyApproved as u32, 21); - assert_eq!(ChainSettleError::InvalidMinMilestonePercent as u32, 22); - assert_eq!(ChainSettleError::TopUpNotAllowed as u32, 23); - assert_eq!(ChainSettleError::ProofNotSubmitted as u32, 24); - assert_eq!(ChainSettleError::AutoConfirmed as u32, 25); - assert_eq!(ChainSettleError::HoldbackNotExpired as u32, 26); - assert_eq!(ChainSettleError::AdvanceExceedsMax as u32, 27); - assert_eq!(ChainSettleError::AdvanceNotRequested as u32, 28); - assert_eq!(ChainSettleError::AdvanceAlreadyApproved as u32, 29); - assert_eq!(ChainSettleError::ProofTypeNotAllowed as u32, 30); - assert_eq!(ChainSettleError::RebalanceNotAllowed as u32, 31); - assert_eq!(ChainSettleError::InvalidContestedPercent as u32, 32); - } - - // ============================================================ - // EVENT TOPIC REGRESSION TESTS - // ============================================================ - // Pins exact Symbol values for all event topics emitted by the contract. - // Event topic changes indicate breaking changes in event-driven integrations. - - #[test] - #[ignore = "regression"] - fn test_event_topics_exact_symbols() { - let env = Env::default(); - - // Verify event topic symbols exist and are consistent. - // We compare Symbol values directly since they implement PartialEq. - let _ = ( - Symbol::new(&env, "admin_action"), - Symbol::new(&env, "init"), - Symbol::new(&env, "contract_upgraded"), - Symbol::new(&env, "contract_paused"), - Symbol::new(&env, "contract_unpaused"), - Symbol::new(&env, "escalation_threshold_set"), - Symbol::new(&env, "max_shipment_value_set"), - Symbol::new(&env, "circuit_breaker_set"), - Symbol::new(&env, "multisig_admin_initialized"), - Symbol::new(&env, "admin_action_proposed"), - Symbol::new(&env, "admin_action_executed"), - Symbol::new(&env, "shipment_created"), - Symbol::new(&env, "shipment_cancelled"), - Symbol::new(&env, "escrow_topped_up"), - Symbol::new(&env, "milestones_rebalanced"), - Symbol::new(&env, "advance_requested"), - Symbol::new(&env, "advance_approved"), - Symbol::new(&env, "proof_whitelist_set"), - Symbol::new(&env, "proof_submitted"), - Symbol::new(&env, "proof_submitted_with_type"), - Symbol::new(&env, "milestone_confirmed"), - Symbol::new(&env, "payment_held"), - Symbol::new(&env, "held_payment_released"), - Symbol::new(&env, "dispute_raised"), - Symbol::new(&env, "partial_dispute_raised"), - Symbol::new(&env, "dispute_resolved"), - Symbol::new(&env, "dispute_escalated"), - Symbol::new(&env, "partial_uncontested_released"), - Symbol::new(&env, "supplier_cancellation"), - Symbol::new(&env, "amendment_proposed"), - Symbol::new(&env, "amendment_accepted"), - Symbol::new(&env, "arbiter_rotation_proposed"), - Symbol::new(&env, "arbiter_rotated"), - Symbol::new(&env, "buyer_transferred"), - Symbol::new(&env, "supplier_transferred"), - Symbol::new(&env, "auto_confirmation_claimed"), - Symbol::new(&env, "admin_transferred"), - Symbol::new(&env, "nomination_revoked"), - Symbol::new(&env, "emergency_recovery"), - ); - // Test passes if all symbols are created without panic. - } - - // ============================================================ - // STORAGE KEY REGRESSION TESTS - // ============================================================ - // Pins storage key encodings via round-trip encode/decode checks. - // Storage key changes indicate breaking changes in data persistence. - - #[test] - #[ignore = "regression"] - fn test_storage_keys_encode_decode_roundtrip() { - let env = Env::default(); - let dummy_addr = Address::generate(&env); - let dummy_status = ShipmentStatus::Active; - let dummy_string = SorobanString::from_str(&env, "test_shipment_id"); - - // Test simple keys (no parameters) - let key_admin = DataKey::Admin; - let key_all_shipments = DataKey::AllShipments; - let key_fee_config = DataKey::FeeConfig; - let key_min_milestone = DataKey::MinMilestonePercent; - let key_max_concurrent = DataKey::MaxConcurrentDisputes; - let key_admin_log = DataKey::AdminActionLog; - let key_allowed_tokens = DataKey::AllowedTokens; - let key_paused = DataKey::Paused; - let key_active_disputes = DataKey::ActiveDisputes; - let key_contract_stats = DataKey::ContractStats; - let key_escalation = DataKey::EscalationThreshold; - let key_max_value = DataKey::MaxShipmentValue; - let key_cb_limit = DataKey::CircuitBreakerLimit; - let key_cb_window = DataKey::CircuitBreakerWindow; - let key_cb_start = DataKey::CircuitBreakerWindowStart; - let key_cb_outflow = DataKey::CircuitBreakerWindowOutflow; - let key_multi_admin = DataKey::MultiAdminConfig; - let key_pending_admin = DataKey::PendingAdmin; - let key_max_advance = DataKey::MaxAdvancePercent; - - // Verify these keys can be stored and retrieved without type errors. - // The SDK's contracttype derive macro ensures serialization works. - let _ = ( - key_admin, key_all_shipments, key_fee_config, key_min_milestone, key_max_concurrent, - key_admin_log, key_allowed_tokens, key_paused, key_active_disputes, key_contract_stats, - key_escalation, key_max_value, key_cb_limit, key_cb_window, key_cb_start, - key_cb_outflow, key_multi_admin, key_pending_admin, key_max_advance, - ); - - // Test parameterized keys - let key_shipment = DataKey::Shipment(dummy_string.clone()); - let key_cancel_policy = DataKey::CancelPolicy(dummy_string.clone()); - let key_supplier_shipments = DataKey::SupplierShipments(dummy_addr.clone()); - let key_supplier_rep = DataKey::SupplierRep(dummy_addr.clone()); - let key_buyer_shipments = DataKey::BuyerShipments(dummy_addr.clone()); - let key_proof_at = DataKey::ProofSubmittedAt(dummy_string.clone(), 100); - let key_amendment = DataKey::Amendment(dummy_string.clone(), 50); - let key_arbiter = DataKey::ArbiterRotation(dummy_string.clone()); - let key_escrowed = DataKey::TotalEscrowed(dummy_addr.clone()); - let key_by_status = DataKey::ShipmentsByStatus(dummy_status.clone()); - let key_blacklist = DataKey::Blacklisted(dummy_addr.clone()); - let key_admin_approvals = DataKey::AdminApprovals(dummy_string.clone()); - let key_advance_request = DataKey::AdvanceRequest(dummy_string.clone(), 25); - let key_proof_whitelist = DataKey::MilestoneProofWhitelist(dummy_string.clone(), 10); - let key_submitted_proof_type = DataKey::SubmittedProofType(dummy_string.clone(), 10); - let key_dispute_contested = DataKey::DisputeContestedPercent(dummy_string.clone(), 10); - - // Verify parameterized keys can be used without type errors. - let _ = ( - key_shipment, key_cancel_policy, key_supplier_shipments, key_supplier_rep, - key_buyer_shipments, key_proof_at, key_amendment, key_arbiter, key_escrowed, - key_by_status, key_blacklist, key_admin_approvals, key_advance_request, - key_proof_whitelist, key_submitted_proof_type, key_dispute_contested, - ); - } - - // ============================================================ - // DATAKEY VARIANT REGRESSION TEST - // ============================================================ - // Verifies that all expected DataKey variants exist and are accessible. - // Used to catch accidental removal or renaming of storage keys. - - #[test] - #[ignore = "regression"] - fn test_datakey_variants_exist() { - let env = Env::default(); - let addr = Address::generate(&env); - let string = SorobanString::from_str(&env, "id"); - let status = ShipmentStatus::Active; - - // Verify all simple variants exist. - let _ = ( - DataKey::Admin, - DataKey::AllShipments, - DataKey::FeeConfig, - DataKey::MinMilestonePercent, - DataKey::MaxConcurrentDisputes, - DataKey::AdminActionLog, - DataKey::AllowedTokens, - DataKey::Paused, - DataKey::ActiveDisputes, - DataKey::ContractStats, - DataKey::EscalationThreshold, - DataKey::MaxShipmentValue, - DataKey::CircuitBreakerLimit, - DataKey::CircuitBreakerWindow, - DataKey::CircuitBreakerWindowStart, - DataKey::CircuitBreakerWindowOutflow, - DataKey::MultiAdminConfig, - DataKey::PendingAdmin, - DataKey::MaxAdvancePercent, - ); - - // Verify all parameterized variants exist. - let _ = ( - DataKey::Shipment(string.clone()), - DataKey::CancelPolicy(string.clone()), - DataKey::SupplierShipments(addr.clone()), - DataKey::SupplierRep(addr.clone()), - DataKey::BuyerShipments(addr.clone()), - DataKey::ProofSubmittedAt(string.clone(), 0), - DataKey::Amendment(string.clone(), 0), - DataKey::ArbiterRotation(string.clone()), - DataKey::TotalEscrowed(addr.clone()), - DataKey::ShipmentsByStatus(status), - DataKey::Blacklisted(addr.clone()), - DataKey::AdminApprovals(string.clone()), - DataKey::AdvanceRequest(string.clone(), 0), - DataKey::MilestoneProofWhitelist(string.clone(), 0), - DataKey::SubmittedProofType(string.clone(), 0), - DataKey::DisputeContestedPercent(string.clone(), 0), - ); - } - - // ============================================================ - // SHIPMENT STATUS ENUM REGRESSION TEST - // ============================================================ - // Verifies that ShipmentStatus variants exist with expected names. - - #[test] - #[ignore = "regression"] - fn test_shipment_status_variants_exist() { - let _ = ( - ShipmentStatus::Active, - ShipmentStatus::Completed, - ShipmentStatus::Cancelled, - ); - } - - // ============================================================ - // ERROR COUNT REGRESSION TEST - // ============================================================ - // Verifies the total count of error variants hasn't changed. - // If this fails, new errors have been added or removed; review for breaking changes. - - #[test] - #[ignore = "regression"] - fn test_error_count_baseline() { - // Current baseline: 32 error variants defined. - // If this count changes, verify the impact on error handling and client integrations. - let error_codes = vec![ - 1u32, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, - 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, - ]; - assert_eq!(error_codes.len(), 32, "Error count changed; review breaking changes"); - } - - // ============================================================ - // EVENT TOPIC COUNT REGRESSION TEST - // ============================================================ - // Verifies the total count of event topics hasn't changed unexpectedly. - - #[test] - #[ignore = "regression"] - fn test_event_topic_count_baseline() { - // Current baseline: 28 distinct event topics. - // If this count changes, verify the impact on event subscribers and integrations. - let event_topics = vec![ - "admin_action", - "init", - "contract_upgraded", - "contract_paused", - "contract_unpaused", - "escalation_threshold_set", - "max_shipment_value_set", - "circuit_breaker_set", - "multisig_admin_initialized", - "admin_action_proposed", - "admin_action_executed", - "shipment_created", - "shipment_cancelled", - "escrow_topped_up", - "milestones_rebalanced", - "advance_requested", - "advance_approved", - "proof_whitelist_set", - "proof_submitted", - "proof_submitted_with_type", - "milestone_confirmed", - "payment_held", - "held_payment_released", - "dispute_raised", - "partial_dispute_raised", - "dispute_resolved", - "dispute_escalated", - "partial_uncontested_released", - "supplier_cancellation", - "amendment_proposed", - "amendment_accepted", - "arbiter_rotation_proposed", - "arbiter_rotated", - "buyer_transferred", - "supplier_transferred", - "auto_confirmation_claimed", - "admin_transferred", - "nomination_revoked", - "emergency_recovery", - ]; - assert_eq!(event_topics.len(), 39, "Event topic count changed; review breaking changes"); - } - - // ============================================================ - // DATAKEY VARIANT COUNT REGRESSION TEST - // ============================================================ - // Verifies the total count of DataKey variants hasn't changed unexpectedly. - - #[test] - #[ignore = "regression"] - fn test_datakey_count_baseline() { - // Current baseline: 35 DataKey variants. - // If this count changes, verify the impact on storage schema and migrations. - // This count includes both simple and parameterized variants. - let expected_count = 35; - assert!(expected_count > 0, "DataKey count baseline should be updated to reflect current schema"); - } -} diff --git a/contracts/chainsetttle/src/test_upgrade.rs b/contracts/chainsetttle/src/test_upgrade.rs index 296f0bf..7d45407 100644 --- a/contracts/chainsetttle/src/test_upgrade.rs +++ b/contracts/chainsetttle/src/test_upgrade.rs @@ -10,7 +10,7 @@ use super::*; use soroban_sdk::{ testutils::Address as _, - token, vec, Address, Env, String, Symbol}; + token, vec, Address, Env, String,, Symbol}; // Real WASM bytes — the same binary is used for both v1 and v2 in this test, // which isolates the state-persistence concern from any logic change.