From 5fa0a5773b87d6c36401266403f27f2e828402c2 Mon Sep 17 00:00:00 2001 From: Justice Date: Tue, 30 Jun 2026 08:03:45 +0100 Subject: [PATCH 1/3] test: add fee bps boundary coverage --- contracts/escrow/src/lib.rs | 106 +++++----- contracts/escrow/src/test.rs | 186 +++++++++++------- contracts/escrow/src/test_accumulated_fees.rs | 19 +- contracts/escrow/src/test_admin_rotation.rs | 2 +- contracts/escrow/src/test_amount_limits.rs | 31 ++- contracts/escrow/src/test_arbitration_fee.rs | 21 +- contracts/escrow/src/test_auth_ordering.rs | 16 +- contracts/escrow/src/test_auto_release.rs | 25 ++- contracts/escrow/src/test_batch_create.rs | 2 +- .../escrow/src/test_cancel_emits_event.rs | 2 +- contracts/escrow/src/test_cancel_escrow.rs | 2 +- .../escrow/src/test_cancel_restrictions.rs | 16 +- .../src/test_concurrent_vendor_escrows.rs | 60 +++--- contracts/escrow/src/test_contract_config.rs | 16 +- .../escrow/src/test_create_escrow_boundary.rs | 103 ++++++++++ contracts/escrow/src/test_delivery.rs | 20 +- contracts/escrow/src/test_dispute.rs | 49 +++-- .../src/test_dispute_deadline_fallback.rs | 30 ++- contracts/escrow/src/test_dispute_flow.rs | 16 +- contracts/escrow/src/test_dispute_window.rs | 16 +- contracts/escrow/src/test_double_funding.rs | 22 ++- contracts/escrow/src/test_edge_cases.rs | 26 ++- contracts/escrow/src/test_escrow_id.rs | 101 ++++++---- contracts/escrow/src/test_escrow_states.rs | 2 +- contracts/escrow/src/test_events.rs | 121 ++++++++++-- .../src/test_fee_calculation_accuracy.rs | 10 +- contracts/escrow/src/test_fee_config.rs | 2 +- contracts/escrow/src/test_fee_minimum.rs | 31 ++- contracts/escrow/src/test_fee_snapshot.rs | 47 +++-- contracts/escrow/src/test_fee_update.rs | 2 +- .../escrow/src/test_get_escrows_by_buyer.rs | 20 +- .../escrow/src/test_get_escrows_by_vendor.rs | 31 ++- contracts/escrow/src/test_helpers.rs | 16 +- contracts/escrow/src/test_initialize_twice.rs | 2 +- .../escrow/src/test_initialize_zero_admin.rs | 2 +- .../escrow/src/test_minimum_amount_guard.rs | 31 ++- contracts/escrow/src/test_mutual_cancel.rs | 2 +- contracts/escrow/src/test_overflow.rs | 64 +++--- contracts/escrow/src/test_pause.rs | 69 ++++--- .../escrow/src/test_pause_all_blocked.rs | 19 +- contracts/escrow/src/test_pause_cancel.rs | 13 +- contracts/escrow/src/test_refund_flow.rs | 2 +- .../src/test_resolve_dispute_no_buyer.rs | 2 +- .../escrow/src/test_resolver_rotation.rs | 25 ++- contracts/escrow/src/test_sep41.rs | 88 ++++++++- .../escrow/src/test_storage_collision.rs | 124 ++++++++++-- contracts/escrow/src/test_string_length.rs | 2 +- contracts/escrow/src/test_unauthorized.rs | 24 ++- contracts/escrow/src/test_withdraw_fees.rs | 38 ++-- contracts/escrow/src/types.rs | 2 + contracts/escrow/tests/auth_audit.rs | 14 +- contracts/escrow/tests/cancel_events.rs | 17 +- contracts/escrow/tests/edge_cases.rs | 14 +- contracts/escrow/tests/happy_path.rs | 13 +- 54 files changed, 1282 insertions(+), 454 deletions(-) create mode 100644 contracts/escrow/src/test_create_escrow_boundary.rs diff --git a/contracts/escrow/src/lib.rs b/contracts/escrow/src/lib.rs index ba21c1aa..abdaf372 100644 --- a/contracts/escrow/src/lib.rs +++ b/contracts/escrow/src/lib.rs @@ -31,7 +31,7 @@ pub use crate::events::{ }; pub use crate::types::{ ContractConfig, ContractStats, DataKey, DisputeData, DisputeStatus, EscrowData, EscrowState, - FeeConfig, PublicContractConfig, ResolutionType, EscrowInput, + FeeConfig, PublicContractConfig, ResolutionType, EscrowInput, Payee, }; /// Maximum escrow fee in basis points (300 = 3%). @@ -221,27 +221,6 @@ fn write_treasury(env: &Env, treasury: &Address) { } -#[contracttype] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct EscrowData { - pub payees: Vec, - pub buyer: Option
, - pub resolver: Address, - pub token: Address, - pub amount: i128, - pub fee_bps: u32, - pub resolver_fee_bps: u32, - pub shipping_window: u64, - pub funded_at: u64, - pub dispute_deadline: u64, - pub shipped_at: u64, - pub delivered_at: Option, - pub tracking_id: Option, - pub state: EscrowState, - env.storage() - .instance() - .set(&DataKey::FeeConfig, fee_config); -} fn validate_escrow_fee_bps(fee_bps: u32) -> Result<(), ContractError> { if fee_bps > MAX_ESCROW_FEE_BPS { @@ -509,15 +488,18 @@ fn increment_counter(env: &Env, key: &DataKey) -> Result<(), ContractError> { fn create_escrow_internal( env: &Env, - seller: Address, + payees: Vec, buyer: Option
, resolver: Address, token: Address, amount: i128, fee_bps: u32, + resolver_fee_bps: u32, shipping_window: u64, notes: Option, ) -> Result { + let first_payee = payees.get(0).unwrap(); + let seller = first_payee.address.clone(); seller.require_auth(); ensure_not_paused(env)?; @@ -571,12 +553,14 @@ fn create_escrow_internal( env.storage().instance().extend_ttl(ext / 2, ext); let escrow = EscrowData { + payees, seller, buyer, resolver, token, amount, fee_bps, + resolver_fee_bps, shipping_window, funded_at: 0, dispute_deadline: 0, @@ -584,7 +568,6 @@ fn create_escrow_internal( shipped_at: 0, delivered_at: None, tracking_id: None, - }; save_escrow(env, escrow_id, &escrow); @@ -603,6 +586,7 @@ fn create_escrow_internal( escrow.token.clone(), escrow.amount, escrow.fee_bps, + escrow.resolver_fee_bps, escrow.shipping_window, ); Ok(escrow_id) @@ -833,7 +817,7 @@ impl Escrow { return Err(ContractError::InvalidAddress); } let first_payee = payees.get(0).unwrap(); - first_payee.address.require_auth(); + // SECURITY: Auth is handled in create_escrow_internal ensure_not_paused(&env)?; @@ -868,27 +852,9 @@ impl Escrow { // Token allowlist check is_token_allowed(&env, &token)?; - let escrow_id: u64 = env - .storage() - .instance() - .get(&DataKey::EscrowCounter) - .expect("counter initialized"); - let next_id = escrow_id - .checked_add(1) - .ok_or(ContractError::ArithmeticError)?; - env.storage() - .instance() - .set(&DataKey::EscrowCounter, &next_id); - - // Extend instance storage TTL on every counter access so the counter key - // cannot expire between a read and the subsequent write. - let ext = get_ttl_extension(&env); - env.storage().instance().extend_ttl(ext / 2, ext); - - let escrow = EscrowData { create_escrow_internal( &env, - seller, + payees, buyer, resolver, token, @@ -902,23 +868,56 @@ impl Escrow { pub fn create_escrow_with_notes( env: Env, - seller: Address, + payees: Vec, buyer: Option
, resolver: Address, token: Address, amount: i128, fee_bps: u32, + resolver_fee_bps: u32, shipping_window: u64, notes: Option, ) -> Result { + if payees.is_empty() { + return Err(ContractError::InvalidAddress); + } + // SECURITY: Auth is handled in create_escrow_internal + ensure_not_paused(&env)?; + + if amount <= 0 || amount < MIN_ESCROW_AMOUNT { + return Err(ContractError::InvalidAmount); + } + if amount > MAX_ESCROW_AMOUNT { + return Err(ContractError::AmountExceedsMaximum); + } + + validate_escrow_fee_bps(fee_bps)?; + validate_resolver_fee_bps(resolver_fee_bps)?; + validate_payees(&env, &payees)?; + + for i in 0..payees.len() { + let payee = payees.get(i).unwrap(); + if resolver == payee.address { + return Err(ContractError::ConflictingRoles); + } + if let Some(ref b) = buyer { + if b == &payee.address { + return Err(ContractError::ConflictingRoles); + } + } + } + + is_token_allowed(&env, &token)?; + create_escrow_internal( &env, - seller, + payees, buyer, resolver, token, amount, fee_bps, + resolver_fee_bps, shipping_window, notes, ) @@ -1209,7 +1208,7 @@ impl Escrow { .clone() .unwrap_or(String::from_str(&env, "")); save_escrow(&env, escrow_id, &escrow); - emit_escrow_shipped(&env, escrow_id, first_payee.address.clone(), tracking); + emit_escrow_shipped(&env, escrow_id, escrow.seller.clone(), tracking); Ok(()) } @@ -1299,7 +1298,7 @@ impl Escrow { emit_escrow_completed( &env, escrow_id, - first_payee.address.clone(), + escrow.seller.clone(), escrow.amount, escrow.fee_bps, ); @@ -1617,6 +1616,7 @@ impl Escrow { recipient, escrow.amount, 0, // arbitration fee already deducted + 0, // resolver_fee ); Ok(()) } @@ -1891,13 +1891,17 @@ impl Escrow { let primary_amount = amounts.get(0).ok_or(ContractError::InvalidAmount)?; let primary_token = tokens.get(0).ok_or(ContractError::InvalidAmount)?; + let mut primary_payees: Vec = Vec::new(&env); + primary_payees.push_back(Payee { address: seller.clone(), bps: 10_000 }); let escrow = EscrowData { + payees: primary_payees, seller: seller.clone(), buyer: buyer.clone(), resolver: resolver.clone(), token: primary_token, amount: primary_amount, fee_bps, + resolver_fee_bps: 0, shipping_window, funded_at: 0, dispute_deadline: 0, @@ -2159,14 +2163,17 @@ impl Escrow { let mut escrow_ids = Vec::new(&env); for input in escrows.into_iter() { + let mut payees: Vec = Vec::new(&env); + payees.push_back(Payee { address: seller.clone(), bps: 10_000 }); let id = create_escrow_internal( &env, - seller.clone(), + payees, input.buyer, input.resolver, input.token, input.amount, input.fee_bps, + 0_u32, input.shipping_window, input.notes, )?; @@ -2211,6 +2218,7 @@ mod test_auto_release; mod test_cancel_restrictions; mod test_concurrent_vendor_escrows; mod test_contract_config; +mod test_create_escrow_boundary; mod test_delivery; mod test_dispute; mod test_dispute_flow; diff --git a/contracts/escrow/src/test.rs b/contracts/escrow/src/test.rs index 645450af..60941720 100644 --- a/contracts/escrow/src/test.rs +++ b/contracts/escrow/src/test.rs @@ -82,9 +82,12 @@ where }) } -fn single_payee(env: &Env, address: &Address) -> Vec { - let mut payees = Vec::new(env); - payees.push_back(Payee { address: address.clone(), bps: 10_000 }); +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); payees } @@ -350,13 +353,14 @@ fn test_raise_dispute_only_once() { client.initialize(&admin, &fee_collector, &0_u32); mint_tokens(&env, &token, &buyer, 1000); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&id, &buyer); client.mark_shipped( @@ -389,22 +393,24 @@ fn test_multiple_escrows() { client.initialize(&admin, &fee_collector, &0_u32); mint_tokens(&env, &token, &buyer, 2000); let id1 = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &200_u32, - &3600_u64, + &0_u32, + &3600_u64 ); let id2 = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &200_i128, &200_u32, - &7200_u64, + &0_u32, + &7200_u64 ); assert_eq!(id1, 1u64); assert_eq!(id2, 2u64); @@ -418,13 +424,14 @@ fn test_create_escrow_with_non_usdc_token() { let client = EscrowClient::new(&env, &contract_id); client.initialize(&admin, &fee_collector, &0_u32); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &alt_token, &500_i128, &0_u32, - &7200_u64, + &0_u32, + &7200_u64 ); assert_eq!(id, 1u64); @@ -446,13 +453,14 @@ fn test_fund_and_confirm_delivery_with_non_usdc_token() { client.set_protocol_fee(&admin, &100_u32); mint_tokens(&env, &alt_token, &buyer, 1000); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &alt_token, &300_i128, &100_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&id, &buyer); @@ -477,13 +485,14 @@ fn test_dispute_resolved_to_seller_with_non_usdc_token() { mint_tokens(&env, &alt_token, &buyer, 1_000); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &alt_token, &400_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&id, &buyer); client.mark_shipped( @@ -521,13 +530,14 @@ fn test_dispute_refunded_to_buyer_with_non_usdc_token() { mint_tokens(&env, &alt_token, &buyer, 1_000); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &alt_token, &400_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&id, &buyer); client.mark_shipped( @@ -563,13 +573,14 @@ fn test_auto_release_with_non_usdc_token() { let shipping_window: u64 = 86_400; let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &alt_token, &250_i128, &0_u32, - &shipping_window, + &0_u32, + &shipping_window ); client.fund_escrow(&id, &buyer); client.mark_shipped( @@ -609,22 +620,24 @@ fn test_multi_asset_concurrent_escrows_different_tokens() { mint_tokens(&env, &token_b, &buyer_b, 2_000); let id1 = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token_a, &150_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); let id2 = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token_b, &500_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); assert_eq!(id1, 1u64); @@ -681,14 +694,15 @@ fn test_sequential_escrows_same_non_usdc_token() { for (i, amount) in [100_i128, 200_i128, 300_i128].iter().enumerate() { let expected_id = (i as u64) + 1; let id = client.create_escrow( - &seller, - &None::
, - &resolver, - &alt_token, - amount, - &0_u32, - &3600_u64, - ); + &single_payee(&env, &seller), + &None::
, + &resolver, + &alt_token, + amount, + &0_u32, + &0_u32, + &3600_u64 + ); assert_eq!(id, expected_id); client.fund_escrow(&id, &buyer); @@ -715,13 +729,14 @@ fn test_zero_fee_no_collector_transfer() { client.initialize(&admin, &fee_collector, &0_u32); mint_tokens(&env, &token, &buyer, 1000); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1000_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&id, &buyer); @@ -755,13 +770,14 @@ fn test_fee_exceeds_max_bps_fails() { let admin = Address::generate(&env); client.initialize(&admin, &fee_collector, &0_u32); let res = client.try_create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1000_i128, &301_u32, - &3600_u64, + &0_u32, + &3600_u64 ); assert!(matches!(res, Err(Ok(ContractError::FeeExceedsMax)))); } @@ -776,13 +792,14 @@ fn test_dispute_after_shipping_succeeds() { mint_tokens(&env, &token, &buyer, 1000); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1000_i128, &200_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&id, &buyer); client.mark_shipped( @@ -813,13 +830,14 @@ fn test_dispute_requires_shipped_state() { mint_tokens(&env, &token, &buyer, 1000); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1000_i128, &200_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&id, &buyer); @@ -849,13 +867,14 @@ fn test_auto_release_after_dispute_deadline() { mint_tokens(&env, &token, &buyer, 1000); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1000_i128, &200_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&id, &buyer); client.mark_shipped(&seller, &id, &SorobanString::from_str(&env, "TRACK-AUTO-3")); @@ -886,13 +905,14 @@ fn test_fee_change_does_not_affect_funded_escrow() { let escrow_amount = 1_000_000_i128; let escrow_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &escrow_amount, &100_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&escrow_id, &buyer); @@ -927,13 +947,14 @@ fn test_event_integrity_escrow_created() { client.initialize(&admin, &fee_collector, &0_u32); let escrow_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &500_i128, &150_u32, - &7200_u64, + &0_u32, + &7200_u64 ); assert!(has_event::( @@ -961,13 +982,14 @@ fn test_event_integrity_escrow_funded() { mint_tokens(&env, &token, &buyer, 1000); let escrow_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &500_i128, &150_u32, - &7200_u64, + &0_u32, + &7200_u64 ); client.fund_escrow(&escrow_id, &buyer); @@ -988,13 +1010,14 @@ fn test_event_integrity_escrow_completed_via_confirm_delivery() { mint_tokens(&env, &token, &buyer, 1000); let escrow_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1000_i128, &200_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&escrow_id, &buyer); client.mark_shipped( @@ -1029,13 +1052,14 @@ fn test_event_integrity_dispute_raised() { mint_tokens(&env, &token, &buyer, 1000); let escrow_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1000_i128, &200_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&escrow_id, &buyer); client.mark_shipped( @@ -1072,13 +1096,14 @@ fn test_event_integrity_dispute_resolved_release_to_seller() { mint_tokens(&env, &token, &buyer, 1000); let escrow_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1000_i128, &200_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&escrow_id, &buyer); client.mark_shipped( @@ -1112,13 +1137,14 @@ fn test_event_integrity_dispute_resolved_refund_buyer() { mint_tokens(&env, &token, &buyer, 1000); let escrow_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1000_i128, &200_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&escrow_id, &buyer); client.mark_shipped( @@ -1152,13 +1178,14 @@ fn test_event_integrity_auto_released() { mint_tokens(&env, &token, &buyer, 1000); let escrow_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1000_i128, &200_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&escrow_id, &buyer); client.mark_shipped( @@ -1196,13 +1223,14 @@ fn test_event_integrity_full_lifecycle_all_events_decoded() { mint_tokens(&env, &token, &buyer, 2000); let id1 = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &500_i128, &200_u32, - &3600_u64, + &0_u32, + &3600_u64 ); assert!(has_event::( &env, @@ -1212,13 +1240,14 @@ fn test_event_integrity_full_lifecycle_all_events_decoded() { )); let id2 = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1000_i128, &200_u32, - &7200_u64, + &0_u32, + &7200_u64 ); assert!(has_event::( &env, @@ -1292,13 +1321,14 @@ fn test_cancel_escrow_by_buyer_refunds_full_amount() { mint_tokens(&env, &token, &buyer, 1000); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &500_i128, &200_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&id, &buyer); @@ -1326,13 +1356,14 @@ fn test_cancel_escrow_state_transitions_correctly() { mint_tokens(&env, &token, &buyer, 1000); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &300_i128, &100_u32, - &7200_u64, + &0_u32, + &7200_u64 ); let escrow = client.get_escrow(&id); assert_eq!(escrow.state, EscrowState::Pending); @@ -1357,13 +1388,14 @@ fn test_cancel_escrow_pending_escrow_fails() { client.initialize(&admin, &_fee_collector, &0_u32); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &500_i128, &200_u32, - &3600_u64, + &0_u32, + &3600_u64 ); let res = client.try_cancel_escrow(&buyer, &id); assert!(matches!(res, Err(Ok(ContractError::InvalidState)))); @@ -1379,13 +1411,14 @@ fn test_cancel_escrow_completed_escrow_fails() { mint_tokens(&env, &token, &buyer, 1000); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1000_i128, &200_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&id, &buyer); client.mark_shipped(&seller, &id, &SorobanString::from_str(&env, "TRK-CANCEL")); @@ -1407,13 +1440,14 @@ fn test_cancel_escrow_already_cancelled_fails() { mint_tokens(&env, &token, &buyer, 1000); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &500_i128, &200_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&id, &buyer); client.cancel_escrow(&buyer, &id); @@ -1432,13 +1466,14 @@ fn test_cancel_escrow_with_zero_fee() { mint_tokens(&env, &token, &buyer, 500); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &500_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&id, &buyer); client.cancel_escrow(&buyer, &id); @@ -1460,13 +1495,14 @@ fn test_cancel_escrow_preserves_escrow_metadata() { mint_tokens(&env, &token, &buyer, 2000); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1500_i128, &250_u32, - &86400_u64, + &0_u32, + &86400_u64 ); client.fund_escrow(&id, &buyer); client.cancel_escrow(&seller, &id); diff --git a/contracts/escrow/src/test_accumulated_fees.rs b/contracts/escrow/src/test_accumulated_fees.rs index 77c93ea5..42efb15c 100644 --- a/contracts/escrow/src/test_accumulated_fees.rs +++ b/contracts/escrow/src/test_accumulated_fees.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use crate::{ContractError, EscrowClient, ResolutionType}; +use crate::{Payee, ContractError, EscrowClient, ResolutionType}; use soroban_sdk::testutils::{Address as _, Events, Ledger as _}; use soroban_sdk::{token, Address, Env}; @@ -40,13 +40,15 @@ fn test_accumulated_fees() { let amount = 1000; let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &Some(buyer.clone()), &resolver, &token, &amount, - &100_u32, // Escrow fee 1% - &3600, + &100_u32, + &0_u32, + // Escrow fee 1% + &3600 ); let sac = token::StellarAssetClient::new(&env, &token); @@ -75,3 +77,12 @@ fn test_accumulated_fees() { let fees = client.get_accumulated_fees(&token); assert_eq!(fees, 30); } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_admin_rotation.rs b/contracts/escrow/src/test_admin_rotation.rs index 819dc38d..47cc2874 100644 --- a/contracts/escrow/src/test_admin_rotation.rs +++ b/contracts/escrow/src/test_admin_rotation.rs @@ -2,7 +2,7 @@ //! `set_admin` rotates the admin key (#13). The new admin is allowed to call //! admin-gated functions; the old admin is not. -use crate::{Escrow, EscrowClient}; +use crate::{Payee, Escrow, EscrowClient}; use soroban_sdk::{testutils::Address as _, Address, Env}; fn setup() -> (Env, EscrowClient<'static>, Address) { diff --git a/contracts/escrow/src/test_amount_limits.rs b/contracts/escrow/src/test_amount_limits.rs index 561d1699..39f3be45 100644 --- a/contracts/escrow/src/test_amount_limits.rs +++ b/contracts/escrow/src/test_amount_limits.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use crate::{ContractError, EscrowClient}; +use crate::{Payee, ContractError, EscrowClient}; use soroban_sdk::testutils::{Address as _, Events, Ledger as _}; use soroban_sdk::{token, Address, Env}; @@ -43,49 +43,53 @@ fn test_amount_limits_enforced() { // Test below minimum let res = client.try_create_escrow( - &seller, + &single_payee(&env, &seller), &Some(buyer.clone()), &resolver, &token, &499, &100, - &3600, + &0_u32, + &3600 ); assert_eq!(res, Err(Ok(ContractError::AmountBelowMinimum))); // Test exactly minimum let id1 = client.create_escrow( - &seller, + &single_payee(&env, &seller), &Some(buyer.clone()), &resolver, &token, &500, &100, - &3600, + &0_u32, + &3600 ); assert_eq!(id1, 1); // Test exactly maximum let id2 = client.create_escrow( - &seller, + &single_payee(&env, &seller), &Some(buyer.clone()), &resolver, &token, &5000, &100, - &3600, + &0_u32, + &3600 ); assert_eq!(id2, 2); // Test above maximum let res = client.try_create_escrow( - &seller, + &single_payee(&env, &seller), &Some(buyer.clone()), &resolver, &token, &5001, &100, - &3600, + &0_u32, + &3600 ); assert_eq!(res, Err(Ok(ContractError::AmountExceedsMaximum))); } @@ -102,3 +106,12 @@ fn test_set_amount_limits_auth() { let res = client.try_set_amount_limits(&seller, &500, &5000); assert_eq!(res, Err(Ok(ContractError::NotAuthorized))); } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_arbitration_fee.rs b/contracts/escrow/src/test_arbitration_fee.rs index 89816808..304c7b20 100644 --- a/contracts/escrow/src/test_arbitration_fee.rs +++ b/contracts/escrow/src/test_arbitration_fee.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use crate::{DisputeResolved, Escrow, EscrowClient, ResolutionType}; +use crate::{Payee, DisputeResolved, Escrow, EscrowClient, ResolutionType}; use soroban_sdk::{ testutils::{Address as _, Events as _, Ledger}, token, Address, Env, IntoVal, String as SorobanString, Symbol, TryFromVal, Val, @@ -40,13 +40,14 @@ fn test_arbitration_fee_deduction_on_resolve_release() { let fee_bps = 200; // 2% let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &amount, &fee_bps, - &3600_u64, + &0_u32, + &3600_u64 ); mint(&env, &token, &buyer, amount); @@ -103,13 +104,14 @@ fn test_arbitration_fee_deduction_on_resolve_refund() { let fee_bps = 300; // 3% let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &amount, &fee_bps, - &3600_u64, + &0_u32, + &3600_u64 ); mint(&env, &token, &buyer, amount); @@ -189,3 +191,12 @@ fn test_set_and_get_arbitration_fee() { client.set_arbitration_fee(&admin, &150_u32); assert_eq!(client.get_arbitration_fee(), 150); } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_auth_ordering.rs b/contracts/escrow/src/test_auth_ordering.rs index b5063e73..7819f382 100644 --- a/contracts/escrow/src/test_auth_ordering.rs +++ b/contracts/escrow/src/test_auth_ordering.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use crate::{Escrow, EscrowClient}; +use crate::{Payee, Escrow, EscrowClient}; use soroban_sdk::{testutils::Address as _, Address, Env, String}; #[test] @@ -47,13 +47,14 @@ fn test_unauthorized_create_escrow_fails_early() { // Will panic on `seller.require_auth()` instead of `ensure_not_paused` client.create_escrow( - &fake_seller, + &single_payee(&env, &fake_seller), &None::
, &resolver, &token, &1000, &100, - &86400, + &0_u32, + &86400 ); } @@ -69,3 +70,12 @@ fn test_unauthorized_cancel_escrow_fails_early() { // Will panic on `caller.require_auth()` instead of `load_escrow` client.cancel_escrow(&fake_caller, &1); } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_auto_release.rs b/contracts/escrow/src/test_auto_release.rs index ae3c8a04..cc8745b0 100644 --- a/contracts/escrow/src/test_auto_release.rs +++ b/contracts/escrow/src/test_auto_release.rs @@ -6,7 +6,7 @@ //! succeeds afterward, and that funds end up at the seller with the escrow //! advanced to Completed. -use crate::{ContractError, Escrow, EscrowClient, EscrowState}; +use crate::{Payee, ContractError, Escrow, EscrowClient, EscrowState}; use soroban_sdk::{ testutils::{Address as _, Ledger as _}, token, Address, BytesN, Env, String as SorobanString, Symbol, @@ -43,13 +43,14 @@ fn setup_funded_and_shipped() -> Fx { let amount: i128 = 1_000; // shipping_window=0 isolates the dispute-window assertion the issue cares about. let escrow_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token_addr, &amount, &0_u32, - &0_u64, + &0_u32, + &0_u64 ); token::StellarAssetClient::new(&env, &token_addr).mint(&buyer, &amount); client.fund_escrow(&escrow_id, &buyer); @@ -61,7 +62,7 @@ fn setup_funded_and_shipped() -> Fx { env.ledger().set_timestamp(1_700_000_000); client.record_delivery(&admin, &escrow_id); - use crate::{DataKey, EscrowData}; + use crate::{Payee, DataKey, EscrowData}; let data: EscrowData = env .as_contract(&client.address, || { env.storage().persistent().get(&DataKey::Escrow(escrow_id)) @@ -107,7 +108,7 @@ fn auto_release_after_48_hours_succeeds_and_pays_the_seller() { assert_eq!(token_client.balance(&fx.seller), 1_000); // State advanced to Completed. - use crate::{DataKey, EscrowData}; + use crate::{Payee, DataKey, EscrowData}; let after: EscrowData = fx .env .as_contract(&fx.client.address, || { @@ -141,13 +142,14 @@ fn auto_release_fails_when_dispute_is_active() { client.initialize(&admin, &fee_collector, &0_u32); let escrow_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token_addr, &500_i128, &0_u32, - &100_u64, + &0_u32, + &100_u64 ); client.fund_escrow(&escrow_id, &buyer); @@ -178,3 +180,12 @@ fn auto_release_fails_when_dispute_is_active() { assert_eq!(balance_before, balance_after); assert_eq!(balance_after, 500); } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_batch_create.rs b/contracts/escrow/src/test_batch_create.rs index 0e6fcc10..e6d7081a 100644 --- a/contracts/escrow/src/test_batch_create.rs +++ b/contracts/escrow/src/test_batch_create.rs @@ -1,7 +1,7 @@ #![cfg(test)] use crate::test_helpers::setup_contract; -use crate::types::{EscrowState, EscrowInput}; +use crate::types::{Payee, EscrowState, EscrowInput}; use crate::{ContractError, EscrowClient}; use soroban_sdk::testutils::{Address as _, Events, Ledger as _}; use soroban_sdk::{token, Address, Env}; diff --git a/contracts/escrow/src/test_cancel_emits_event.rs b/contracts/escrow/src/test_cancel_emits_event.rs index 37fe0abd..7434afcb 100644 --- a/contracts/escrow/src/test_cancel_emits_event.rs +++ b/contracts/escrow/src/test_cancel_emits_event.rs @@ -1,7 +1,7 @@ #![cfg(test)] use soroban_sdk::{testutils::Address as _, Address, Env, Symbol}; -use crate::{test_helpers::setup_contract, DataKey, EscrowState}; +use crate::{Payee, test_helpers::setup_contract, DataKey, EscrowState}; fn register_token(env: &Env) -> Address { let token_admin = Address::generate(env); diff --git a/contracts/escrow/src/test_cancel_escrow.rs b/contracts/escrow/src/test_cancel_escrow.rs index c012a93d..cb7361bc 100644 --- a/contracts/escrow/src/test_cancel_escrow.rs +++ b/contracts/escrow/src/test_cancel_escrow.rs @@ -1,7 +1,7 @@ #![cfg(test)] use soroban_sdk::{testutils::Address as _, Address, Env}; -use crate::{ContractError, DataKey, EscrowData, EscrowState}; +use crate::{Payee, ContractError, DataKey, EscrowData, EscrowState}; use crate::test_helpers::{setup_contract, mint_token}; fn register_token(env: &Env) -> Address { diff --git a/contracts/escrow/src/test_cancel_restrictions.rs b/contracts/escrow/src/test_cancel_restrictions.rs index f36f705e..2639b151 100644 --- a/contracts/escrow/src/test_cancel_restrictions.rs +++ b/contracts/escrow/src/test_cancel_restrictions.rs @@ -2,7 +2,7 @@ //! `cancel_escrow` is only legal while the escrow is `Pending` (#21). From //! any other state it must reject with `InvalidState`. -use crate::{ContractError, DataKey, Escrow, EscrowClient, EscrowData, EscrowState}; +use crate::{Payee, ContractError, DataKey, Escrow, EscrowClient, EscrowData, EscrowState}; use soroban_sdk::{ testutils::{Address as _, Ledger as _}, token, Address, BytesN, Env, String, Symbol, @@ -35,13 +35,14 @@ fn setup() -> Fx { client.initialize(&admin, &fee_collector, &0_u32); let amount: i128 = 1_000; let escrow_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token_addr, &amount, &0_u32, - &0_u64, + &0_u32, + &0_u64 ); token::StellarAssetClient::new(&env, &token_addr).mint(&buyer, &amount); Fx { @@ -143,3 +144,12 @@ fn cancel_fails_in_disputed_state() { let _ = fx.resolver; let _ = fx.token_addr; } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_concurrent_vendor_escrows.rs b/contracts/escrow/src/test_concurrent_vendor_escrows.rs index 81eebbff..307275d0 100644 --- a/contracts/escrow/src/test_concurrent_vendor_escrows.rs +++ b/contracts/escrow/src/test_concurrent_vendor_escrows.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use crate::{Escrow, EscrowClient, EscrowState}; +use crate::{Payee, Escrow, EscrowClient, EscrowState}; use soroban_sdk::{ testutils::{Address as _, Ledger as _}, Address, Env, @@ -37,14 +37,15 @@ fn same_vendor_can_create_multiple_escrows_without_collision() { // Vary the amount slightly for each escrow to ensure isolated data let amount = 100_i128 + ((i + 1) as i128); let id = client.create_escrow( - &seller, - &None::
, - &resolver, - &token, - &amount, - &0_u32, - &3600_u64, - ); + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &amount, + &0_u32, + &0_u32, + &3600_u64 + ); // IDs should be strictly monotonic assert_eq!(id, (i + 1) as u64); @@ -83,31 +84,34 @@ fn escrow_storage_entries_remain_isolated() { // Create multiple escrows let id1 = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); let id2 = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &200_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); let id3 = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &300_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); // Mutate one escrow @@ -135,14 +139,24 @@ fn escrow_counter_remains_monotonic_under_rapid_creation() { for i in 1..=50 { let id = client.create_escrow( - &seller, - &None::
, - &resolver, - &token, - &100_i128, - &0_u32, - &3600_u64, - ); + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &100_i128, + &0_u32, + &0_u32, + &3600_u64 + ); assert_eq!(id, i as u64); } } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_contract_config.rs b/contracts/escrow/src/test_contract_config.rs index 13b9642d..9da751b0 100644 --- a/contracts/escrow/src/test_contract_config.rs +++ b/contracts/escrow/src/test_contract_config.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use crate::{Escrow, EscrowClient}; +use crate::{Payee, Escrow, EscrowClient}; use soroban_sdk::{testutils::Address as _, Address, Env}; #[test] @@ -37,13 +37,14 @@ fn test_get_public_config() { let token = Address::generate(&env); client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1000_0000000, &100, - &86400, + &0_u32, + &86400 ); public = client.get_public_config(); @@ -83,3 +84,12 @@ fn test_get_contract_config_requires_admin() { config = client.get_contract_config(); assert_eq!(config.admin, new_admin); } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_create_escrow_boundary.rs b/contracts/escrow/src/test_create_escrow_boundary.rs new file mode 100644 index 00000000..9600ea4a --- /dev/null +++ b/contracts/escrow/src/test_create_escrow_boundary.rs @@ -0,0 +1,103 @@ +#![cfg(test)] +//! Boundary tests for `create_escrow` fee_bps (#26). +//! +//! Covers: +//! - fee_bps = 0 (accepted) +//! - fee_bps = 300 (MAX_ESCROW_FEE_BPS, accepted) +//! - fee_bps = 301 (rejected with FeeExceedsMax) + +use crate::{ContractError, Escrow, EscrowClient, Payee}; +use soroban_sdk::{testutils::Address as _, Address, Env}; + +fn setup(env: &Env) -> (EscrowClient<'static>, Address, Address, Address, Address) { + let admin = Address::generate(env); + let seller = Address::generate(env); + let resolver = Address::generate(env); + let token = env.register_stellar_asset_contract(Address::generate(env)); + let fee_collector = Address::generate(env); + + let contract_id = env.register(Escrow, ()); + let client = EscrowClient::new(env, &contract_id); + client.initialize(&admin, &fee_collector, &0_u32); + + (client, seller, resolver, token, contract_id) +} + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} + +#[test] +fn test_create_escrow_fee_bps_zero() { + let env = Env::default(); + env.mock_all_auths(); + let (client, seller, resolver, token, _) = setup(&env); + let payees = single_payee(&env, &seller); + + // fee_bps = 0 should be accepted + let id = client.create_escrow( + &payees, + &None::
, + &resolver, + &token, + &1000_i128, + &0_u32, // fee_bps + &0_u32, // resolver_fee_bps + &3600_u64, + ); + assert_eq!(id, 1); + + let escrow = client.get_escrow(&id); + assert_eq!(escrow.fee_bps, 0); +} + +#[test] +fn test_create_escrow_fee_bps_max() { + let env = Env::default(); + env.mock_all_auths(); + let (client, seller, resolver, token, _) = setup(&env); + let payees = single_payee(&env, &seller); + + // fee_bps = 300 (MAX) should be accepted + let id = client.create_escrow( + &payees, + &None::
, + &resolver, + &token, + &1000_i128, + &300_u32, // fee_bps (MAX) + &0_u32, // resolver_fee_bps + &3600_u64, + ); + assert_eq!(id, 1); + + let escrow = client.get_escrow(&id); + assert_eq!(escrow.fee_bps, 300); +} + +#[test] +fn test_create_escrow_fee_bps_above_max() { + let env = Env::default(); + env.mock_all_auths(); + let (client, seller, resolver, token, _) = setup(&env); + let payees = single_payee(&env, &seller); + + // fee_bps = 301 should be rejected + let res = client.try_create_escrow( + &payees, + &None::
, + &resolver, + &token, + &1000_i128, + &301_u32, // fee_bps (MAX + 1) + &0_u32, // resolver_fee_bps + &3600_u64, + ); + + assert_eq!(res, Err(Ok(ContractError::FeeExceedsMax))); +} diff --git a/contracts/escrow/src/test_delivery.rs b/contracts/escrow/src/test_delivery.rs index e951da7a..19516626 100644 --- a/contracts/escrow/src/test_delivery.rs +++ b/contracts/escrow/src/test_delivery.rs @@ -1,7 +1,7 @@ #![cfg(test)] use crate::test_helpers::{advance_time, create_funded_escrow, setup_contract}; -use crate::{ContractError, DeliveryRecorded, EscrowState}; +use crate::{Payee, ContractError, DeliveryRecorded, EscrowState}; use soroban_sdk::{ testutils::{Address as _, Events as _, Ledger}, vec, Address, Env, IntoVal, String as SorobanString, Symbol, TryFromVal, Val, @@ -417,13 +417,14 @@ fn test_confirm_delivery_from_pending_state_fails() { // Create escrow with an explicit buyer so authorization passes. let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &Some(buyer.clone()), &resolver, &token, &1000_i128, &100_u32, - &3600_u64, + &0_u32, + &3600_u64 ); let res = client.try_confirm_delivery(&buyer, &id); @@ -492,13 +493,14 @@ fn test_confirm_delivery_from_canceled_state_fails() { // Create escrow with an explicit buyer. let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &Some(buyer.clone()), &resolver, &token, &1000_i128, &100_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.cancel_escrow(&seller, &id); @@ -534,3 +536,11 @@ fn test_confirm_delivery_from_completed_state_fails() { assert_eq!(res, Err(Ok(ContractError::InvalidState))); } +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_dispute.rs b/contracts/escrow/src/test_dispute.rs index 297083a4..c9862b99 100644 --- a/contracts/escrow/src/test_dispute.rs +++ b/contracts/escrow/src/test_dispute.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use crate::{ContractError, DisputeStatus, Escrow, EscrowClient, ResolutionType}; +use crate::{Payee, ContractError, DisputeStatus, Escrow, EscrowClient, ResolutionType}; use soroban_sdk::{ testutils::{Address as _, Ledger as _}, token, Address, BytesN, Env, String, Symbol, @@ -40,13 +40,14 @@ fn test_get_dispute_returns_accurate_data_after_raise() { let amount = 1000_i128; let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &amount, &100_u32, - &3600_u64, + &0_u32, + &3600_u64 ); let sac = token::StellarAssetClient::new(&env, &token); @@ -96,13 +97,14 @@ fn test_dispute_allowed_after_shipping() { let amount = 1000_i128; let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &amount, &100_u32, - &3600_u64, + &0_u32, + &3600_u64 ); let sac = soroban_sdk::token::StellarAssetClient::new(&env, &token); @@ -136,13 +138,14 @@ fn test_dispute_allowed_on_late_shipped_escrow() { let amount = 1000_i128; let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &amount, &100_u32, - &3600_u64, + &0_u32, + &3600_u64 ); let sac = soroban_sdk::token::StellarAssetClient::new(&env, &token); @@ -174,13 +177,14 @@ fn test_dispute_requires_shipped_state() { let amount = 1000_i128; let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &amount, &100_u32, - &3600_u64, + &0_u32, + &3600_u64 ); let sac = soroban_sdk::token::StellarAssetClient::new(&env, &token); @@ -218,13 +222,14 @@ fn test_dispute_rejected_after_48h_deadline() { let amount = 1000_i128; let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &amount, &100_u32, - &3600_u64, + &0_u32, + &3600_u64 ); let sac = soroban_sdk::token::StellarAssetClient::new(&env, &token); @@ -265,13 +270,14 @@ fn test_dispute_from_funded_state() { let amount = 1000_i128; let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &amount, &100_u32, - &3600_u64, + &0_u32, + &3600_u64 ); let sac = soroban_sdk::token::StellarAssetClient::new(&env, &token); @@ -311,7 +317,7 @@ fn test_dispute_from_pending_state() { client.initialize(&admin, &fee_collector, &0_u32); let amount = 1000_i128; - let id = client.create_escrow(&seller, &Some(buyer.clone()), &resolver, &token, &amount, &100_u32, &3600_u64); + let id = client.create_escrow(&single_payee(&env, &seller), &Some(buyer.clone()), &resolver, &token, &amount, &100_u32, &0_u32, &3600_u64); let reason = soroban_sdk::Symbol::new(&env, "reason"); let description = soroban_sdk::String::from_str(&env, "desc"); @@ -330,7 +336,7 @@ fn test_dispute_from_canceled_state() { client.initialize(&admin, &fee_collector, &0_u32); let amount = 1000_i128; - let id = client.create_escrow(&seller, &Some(buyer.clone()), &resolver, &token, &amount, &100_u32, &3600_u64); + let id = client.create_escrow(&single_payee(&env, &seller), &Some(buyer.clone()), &resolver, &token, &amount, &100_u32, &0_u32, &3600_u64); client.cancel_escrow(&seller, &id); @@ -351,7 +357,7 @@ fn test_dispute_from_completed_state() { client.initialize(&admin, &fee_collector, &0_u32); let amount = 1000_i128; - let id = client.create_escrow(&seller, &Some(buyer.clone()), &resolver, &token, &amount, &100_u32, &3600_u64); + let id = client.create_escrow(&single_payee(&env, &seller), &Some(buyer.clone()), &resolver, &token, &amount, &100_u32, &0_u32, &3600_u64); let sac = soroban_sdk::token::StellarAssetClient::new(&env, &token); sac.mint(&buyer, &amount); @@ -379,7 +385,7 @@ fn test_dispute_from_refunded_state() { client.initialize(&admin, &fee_collector, &0_u32); let amount = 1000_i128; - let id = client.create_escrow(&seller, &Some(buyer.clone()), &resolver, &token, &amount, &100_u32, &3600_u64); + let id = client.create_escrow(&single_payee(&env, &seller), &Some(buyer.clone()), &resolver, &token, &amount, &100_u32, &0_u32, &3600_u64); let sac = soroban_sdk::token::StellarAssetClient::new(&env, &token); sac.mint(&buyer, &amount); @@ -397,3 +403,12 @@ fn test_dispute_from_refunded_state() { let result = client.try_raise_dispute(&buyer, &id, &reason, &description, &evidence_hash); assert_eq!(result, Err(Ok(crate::ContractError::InvalidStateTransition))); } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_dispute_deadline_fallback.rs b/contracts/escrow/src/test_dispute_deadline_fallback.rs index 00bece7d..f792507d 100644 --- a/contracts/escrow/src/test_dispute_deadline_fallback.rs +++ b/contracts/escrow/src/test_dispute_deadline_fallback.rs @@ -5,7 +5,7 @@ mod tests { Address, BytesN, Env, String, }; - use crate::{ + use crate::{Payee, errors::ContractError, types::EscrowState, Escrow, EscrowClient, @@ -49,14 +49,15 @@ mod tests { token: &Address, ) -> u64 { let escrow_id = client.create_escrow( - seller, - buyer, - resolver, - token, - &100_000_000_i128, - &0_u32, - &SHIPPING_WINDOW, - ); + &single_payee(&env, &seller), + buyer, + resolver, + token, + &100_000_000_i128, + &0_u32, + &0_u32, + &SHIPPING_WINDOW + ); client.fund_escrow(&escrow_id, buyer); client.mark_shipped( seller, @@ -173,4 +174,13 @@ mod tests { "[REGRESSION] Infinite dispute window bug must be fixed" ); } -} \ No newline at end of file +} + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_dispute_flow.rs b/contracts/escrow/src/test_dispute_flow.rs index 1e0a7c35..97a53b9e 100644 --- a/contracts/escrow/src/test_dispute_flow.rs +++ b/contracts/escrow/src/test_dispute_flow.rs @@ -6,7 +6,7 @@ //! receive `amount - arbitration_fee`, the buyer must not be refunded, and //! the on-chain dispute record must be marked `Resolved`. -use crate::{ +use crate::{Payee, DataKey, DisputeData, DisputeStatus, Escrow, EscrowClient, EscrowData, EscrowState, ResolutionType, }; @@ -40,13 +40,14 @@ fn full_dispute_release_to_vendor() { // fee_bps = 0 isolates the arbitration-fee accounting the issue specifies // (a non-zero protocol fee would further reduce the seller's payout). let escrow_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token_address, &amount, &0_u32, - &0_u64, + &0_u32, + &0_u64 ); // Fund the buyer and the escrow. @@ -108,3 +109,12 @@ fn full_dispute_release_to_vendor() { .expect("dispute exists"); assert_eq!(dispute.status, DisputeStatus::Resolved); } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_dispute_window.rs b/contracts/escrow/src/test_dispute_window.rs index 47fc9536..5bb44550 100644 --- a/contracts/escrow/src/test_dispute_window.rs +++ b/contracts/escrow/src/test_dispute_window.rs @@ -2,7 +2,7 @@ //! Regression tests for dispute handling after shipping and for the //! admin-triggered auto-release path (#4). -use crate::{ +use crate::{Payee, ContractError, DataKey, DisputeData, DisputeStatus, Escrow, EscrowClient, EscrowData, EscrowState, }; @@ -41,13 +41,14 @@ fn setup_funded_and_shipped() -> Fx { let amount: i128 = 1_000; let escrow_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token_addr, &amount, &0_u32, - &0_u64, + &0_u32, + &0_u64 ); token::StellarAssetClient::new(&env, &token_addr).mint(&buyer, &amount); env.ledger().set_timestamp(1_700_000_000); @@ -128,3 +129,12 @@ fn auto_release_rejects_when_dispute_exists() { let _ = fx.admin; let _ = fx.seller; } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_double_funding.rs b/contracts/escrow/src/test_double_funding.rs index cc5d5f7c..8aa8b182 100644 --- a/contracts/escrow/src/test_double_funding.rs +++ b/contracts/escrow/src/test_double_funding.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use crate::{ContractError, EscrowState}; +use crate::{Payee, ContractError, EscrowState}; use crate::test_helpers::{setup_contract, mint_token}; use soroban_sdk::{testutils::Address as _, token, Address, Env}; @@ -20,7 +20,16 @@ fn test_double_fund_reverts_with_invalid_state() { // Mint enough for two potential fundings to detect double-deduction mint_token(&env, &token, &buyer, 200); - let id = client.create_escrow(&seller, &None::
, &resolver, &token, &100_i128, &0_u32, &3600_u64); + let id = client.create_escrow( + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &100_i128, + &0_u32, + &0_u32, + &3600_u64 + ); // First funding succeeds client.fund_escrow(&id, &buyer); @@ -36,3 +45,12 @@ fn test_double_fund_reverts_with_invalid_state() { assert_eq!(token::Client::new(&env, &token).balance(&buyer), 100); assert_eq!(token::Client::new(&env, &token).balance(&contract_id), 100); } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_edge_cases.rs b/contracts/escrow/src/test_edge_cases.rs index 088585a2..56963367 100644 --- a/contracts/escrow/src/test_edge_cases.rs +++ b/contracts/escrow/src/test_edge_cases.rs @@ -2,7 +2,7 @@ use crate::helpers::payout::calculate_protocol_fee; use crate::test_helpers::{advance_time, create_funded_escrow, setup_contract}; -use crate::{ContractError, Escrow, EscrowClient, MIN_ESCROW_AMOUNT}; +use crate::{Payee, ContractError, Escrow, EscrowClient, MIN_ESCROW_AMOUNT}; use soroban_sdk::{ testutils::{Address as _, Ledger as _}, token, Address, BytesN, Env, String as SorobanString, Symbol, @@ -87,13 +87,14 @@ fn test_buyer_index_populated_on_cancel_by_buyer() { // Create a Pending escrow that names the buyer up front. let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &Some(buyer.clone()), &resolver, &token, &1000_i128, &100_u32, - &3600_u64, + &0_u32, + &3600_u64 ); // The buyer cancels the still-Pending escrow. @@ -173,25 +174,27 @@ fn test_min_escrow_amount_rejects_dust_prone_amount() { // 99 stroops, 1% fee — the exact case from the bug report. // MIN_ESCROW_AMOUNT = 1, so 99 is above the minimum and should succeed for creation. let result = client.try_create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &99_i128, &100_u32, - &3600_u64, + &0_u32, + &3600_u64 ); assert!(result.is_ok()); // One stroop below the minimum is still rejected. let result = client.try_create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &0_i128, &100_u32, - &3600_u64, + &0_u32, + &3600_u64 ); assert_eq!(result, Err(Ok(ContractError::InvalidAmount))); } @@ -372,3 +375,12 @@ fn test_counter_survives_near_ttl_expiry() { assert_eq!(client.get_escrow(&id1).amount, 1000); assert_eq!(client.get_escrow(&id2).amount, 1000); } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_escrow_id.rs b/contracts/escrow/src/test_escrow_id.rs index 42df93bc..406de0a3 100644 --- a/contracts/escrow/src/test_escrow_id.rs +++ b/contracts/escrow/src/test_escrow_id.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use crate::{Escrow, EscrowCancelled, EscrowClient}; +use crate::{Payee, Escrow, EscrowCancelled, EscrowClient}; use soroban_sdk::{ testutils::{Address as _, Events as _}, Address, Env, IntoVal, Symbol, TryFromVal, Val, Vec, @@ -61,14 +61,15 @@ fn test_escrow_ids_monotonic_and_unique() { let mut ids = Vec::new(&env); for i in 1..=10 { let id = client.create_escrow( - &seller, - &None::
, - &resolver, - &token, - &100_i128, - &0_u32, - &3600_u64, - ); + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &100_i128, + &0_u32, + &0_u32, + &3600_u64 + ); assert_eq!(id, i as u64); ids.push_back(id); } @@ -76,12 +77,13 @@ fn test_escrow_ids_monotonic_and_unique() { // Verify persistence: new client instance sees counter at 11 let client2 = EscrowClient::new(&env, &contract_id); let next_id = client2.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, + &0_u32, &3600_u64, ); assert_eq!(next_id, 11); @@ -96,31 +98,34 @@ fn test_escrow_ids_increment_sequentially() { client.initialize(&admin, &fee_collector, &0_u32); let id1 = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); let id2 = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); let id3 = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); assert_eq!(id1, 1); @@ -137,22 +142,24 @@ fn test_cancelled_escrow_does_not_reset_counter() { client.initialize(&admin, &fee_collector, &0_u32); let id1 = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); let id2 = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); // Ensure cancellation of #1 doesn't reset counter to 1 or 2 @@ -161,13 +168,14 @@ fn test_cancelled_escrow_does_not_reset_counter() { // Create a new escrow after cancellation let next_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); assert_eq!(next_id, 3); } @@ -181,43 +189,47 @@ fn test_escrow_counter_does_not_skip_after_cancellation() { client.initialize(&admin, &fee_collector, &0_u32); let id1 = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); let id2 = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.cancel_escrow(&seller, &id1); let id3 = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); let id4 = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); assert_eq!(id1, 1); @@ -239,44 +251,57 @@ fn test_multiple_cancellations() { client.initialize(&admin, &fee_collector, &0_u32); let id1 = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); let id2 = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); let id3 = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.cancel_escrow(&seller, &id1); client.cancel_escrow(&seller, &id2); let next_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); assert_eq!(next_id, 4); } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_escrow_states.rs b/contracts/escrow/src/test_escrow_states.rs index ee13c856..2fb2b438 100644 --- a/contracts/escrow/src/test_escrow_states.rs +++ b/contracts/escrow/src/test_escrow_states.rs @@ -3,7 +3,7 @@ //! matrix (#9): all 7 states exist, every legal edge is accepted, every illegal //! edge is rejected with `InvalidStateTransition`, and self-loops are illegal. -use crate::{transition_state, ContractError, EscrowState}; +use crate::{Payee, transition_state, ContractError, EscrowState}; #[test] fn all_seven_states_are_defined() { diff --git a/contracts/escrow/src/test_events.rs b/contracts/escrow/src/test_events.rs index 14ac48e8..b5338614 100644 --- a/contracts/escrow/src/test_events.rs +++ b/contracts/escrow/src/test_events.rs @@ -2,7 +2,7 @@ #![cfg(test)] -use crate::{ +use crate::{Payee, EscrowClient, Escrow, ContractError, ResolutionType, ContractInitialized, FeeUpdated, ProtocolFeeUpdated, ArbitrationFeeUpdated, FeesWithdrawn, EscrowCreated, EscrowCancelled, EscrowShipped, DeliveryRecorded, EscrowCompleted, DisputeRaised, DisputeResolved, @@ -114,7 +114,16 @@ fn test_withdraw_fees_emits_event() { let (env, admin, _seller, _buyer, _resolver, token, contract_id) = setup_env(); // Populate some accumulated fees first via a dispute resolution let client = EscrowClient::new(&env, &contract_id); - let id = client.create_escrow(&admin, &None::
, &admin, &token, &100_i128, &0_u32, &6000_u64); + let id = client.create_escrow( + &single_payee(&env, &admin), + &None::
, + &admin, + &token, + &100_i128, + &0_u32, + &0_u32, + &6000_u64 + ); client.fund_escrow(&id, &admin); client.mark_shipped(&admin, &id, &SorobanString::from_str(&env, "TRACK")); client.pause_contract(&admin); @@ -133,7 +142,16 @@ fn test_withdraw_fees_emits_event() { fn test_create_escrow_emits_event() { let (env, _admin, seller, _buyer, resolver, token, contract_id) = setup_env(); let client = EscrowClient::new(&env, &contract_id); - client.create_escrow(&seller, &None::
, &resolver, &token, &100_i128, &0_u32, &6000_u64); + client.create_escrow( + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &100_i128, + &0_u32, + &0_u32, + &6000_u64 + ); let symbol = last_event_symbol(&env); assert_eq!(symbol, Symbol::new(&env, "escrow_created")); } @@ -142,7 +160,16 @@ fn test_create_escrow_emits_event() { fn test_cancel_escrow_emits_event() { let (env, admin, seller, _buyer, _resolver, _token, contract_id) = setup_env(); let client = EscrowClient::new(&env, &contract_id); - let id = client.create_escrow(&seller, &None::
, &admin, &admin, &100_i128, &0_u32, &6000_u64); + let id = client.create_escrow( + &single_payee(&env, &seller), + &None::
, + &admin, + &admin, + &100_i128, + &0_u32, + &0_u32, + &6000_u64 + ); client.cancel_escrow(&seller, &id); let symbol = last_event_symbol(&env); assert_eq!(symbol, Symbol::new(&env, "escrow_cancelled")); @@ -152,7 +179,16 @@ fn test_cancel_escrow_emits_event() { fn test_mark_shipped_emits_event() { let (env, admin, seller, buyer, resolver, token, contract_id) = setup_env(); let client = EscrowClient::new(&env, &contract_id); - let id = client.create_escrow(&seller, &None::
, &resolver, &token, &100_i128, &0_u32, &6000_u64); + let id = client.create_escrow( + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &100_i128, + &0_u32, + &0_u32, + &6000_u64 + ); mint_tokens(&env, &token, &buyer, 100); client.fund_escrow(&id, &buyer); client.mark_shipped(&seller, &id, &SorobanString::from_str(&env, "TRACK123")); @@ -164,7 +200,16 @@ fn test_mark_shipped_emits_event() { fn test_record_delivery_emits_event() { let (env, admin, seller, buyer, resolver, token, contract_id) = setup_env(); let client = EscrowClient::new(&env, &contract_id); - let id = client.create_escrow(&seller, &None::
, &resolver, &token, &100_i128, &0_u32, &6000_u64); + let id = client.create_escrow( + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &100_i128, + &0_u32, + &0_u32, + &6000_u64 + ); mint_tokens(&env, &token, &buyer, 100); client.fund_escrow(&id, &buyer); client.mark_shipped(&seller, &id, &SorobanString::from_str(&env, "TRACK")); @@ -177,7 +222,16 @@ fn test_record_delivery_emits_event() { fn test_confirm_delivery_emits_event() { let (env, admin, seller, buyer, resolver, token, contract_id) = setup_env(); let client = EscrowClient::new(&env, &contract_id); - let id = client.create_escrow(&seller, &None::
, &resolver, &token, &100_i128, &0_u32, &6000_u64); + let id = client.create_escrow( + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &100_i128, + &0_u32, + &0_u32, + &6000_u64 + ); mint_tokens(&env, &token, &buyer, 100); client.fund_escrow(&id, &buyer); client.mark_shipped(&seller, &id, &SorobanString::from_str(&env, "TRACK")); @@ -191,7 +245,16 @@ fn test_confirm_delivery_emits_event() { fn test_raise_dispute_emits_event() { let (env, admin, seller, buyer, resolver, token, contract_id) = setup_env(); let client = EscrowClient::new(&env, &contract_id); - let id = client.create_escrow(&seller, &None::
, &resolver, &token, &100_i128, &0_u32, &6000_u64); + let id = client.create_escrow( + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &100_i128, + &0_u32, + &0_u32, + &6000_u64 + ); mint_tokens(&env, &token, &buyer, 100); client.fund_escrow(&id, &buyer); client.mark_shipped(&seller, &id, &SorobanString::from_str(&env, "TRACK")); @@ -205,7 +268,16 @@ fn test_raise_dispute_emits_event() { fn test_resolve_dispute_emits_event() { let (env, admin, seller, buyer, resolver, token, contract_id) = setup_env(); let client = EscrowClient::new(&env, &contract_id); - let id = client.create_escrow(&seller, &None::
, &resolver, &token, &100_i128, &0_u32, &6000_u64); + let id = client.create_escrow( + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &100_i128, + &0_u32, + &0_u32, + &6000_u64 + ); mint_tokens(&env, &token, &buyer, 100); client.fund_escrow(&id, &buyer); client.mark_shipped(&seller, &id, &SorobanString::from_str(&env, "TRACK")); @@ -221,7 +293,16 @@ fn test_auto_release_emits_event() { let (env, admin, seller, buyer, resolver, token, contract_id) = setup_env(); let client = EscrowClient::new(&env, &contract_id); // escrow with no buyer; auto release after dispute window - let id = client.create_escrow(&seller, &None::
, &resolver, &token, &100_i128, &0_u32, &6000_u64); + let id = client.create_escrow( + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &100_i128, + &0_u32, + &0_u32, + &6000_u64 + ); env.ledger().set_timestamp(DISPUTE_WINDOW + 10); client.auto_release(&id); let symbol = last_event_symbol(&env); @@ -232,9 +313,27 @@ fn test_auto_release_emits_event() { fn test_rotate_resolver_emits_event() { let (env, admin, seller, buyer, resolver, token, contract_id) = setup_env(); let client = EscrowClient::new(&env, &contract_id); - let id = client.create_escrow(&seller, &None::
, &resolver, &token, &100_i128, &0_u32, &6000_u64); + let id = client.create_escrow( + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &100_i128, + &0_u32, + &0_u32, + &6000_u64 + ); let new_resolver = Address::generate(&env); client.rotate_resolver(&admin, &id, &new_resolver); let symbol = last_event_symbol(&env); assert_eq!(symbol, Symbol::new(&env, "resolver_rotated")); } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_fee_calculation_accuracy.rs b/contracts/escrow/src/test_fee_calculation_accuracy.rs index 5bf95150..d3ad2de5 100644 --- a/contracts/escrow/src/test_fee_calculation_accuracy.rs +++ b/contracts/escrow/src/test_fee_calculation_accuracy.rs @@ -422,7 +422,7 @@ fn test_fee_calculation_invalid_amount() { #[test] fn test_dispute_allocations_include_protocol_fee() { use crate::helpers::payout::calculate_dispute_allocations; - use crate::{EscrowData, EscrowState, ResolutionType}; + use crate::{Payee, EscrowData, EscrowState, ResolutionType}; use soroban_sdk::{testutils::Address as _, Address, Env}; let env = Env::default(); @@ -434,12 +434,14 @@ fn test_dispute_allocations_include_protocol_fee() { // Create mock escrow with 1,000,000 stroops and 100 bps (1%) fee let escrow = EscrowData { + payees: soroban_sdk::Vec::new(&env), // Not used in this specific helper call seller: seller.clone(), buyer: Some(buyer.clone()), resolver: resolver.clone(), token: token.clone(), amount: 1_000_000_i128, fee_bps: 100_u32, // 1% + resolver_fee_bps: 0, state: EscrowState::Disputed, shipping_window: 3600, funded_at: 0, @@ -447,7 +449,6 @@ fn test_dispute_allocations_include_protocol_fee() { shipped_at: 0, delivered_at: None, tracking_id: None, - }; let arbitration_fee = 50_000_i128; // 5% arbitration fee @@ -487,7 +488,7 @@ fn test_dispute_allocations_include_protocol_fee() { #[test] fn test_dispute_allocations_zero_fee_no_fee_transfer() { use crate::helpers::payout::calculate_dispute_allocations; - use crate::{EscrowData, EscrowState, ResolutionType}; + use crate::{Payee, EscrowData, EscrowState, ResolutionType}; use soroban_sdk::{testutils::Address as _, Address, Env}; let env = Env::default(); @@ -498,12 +499,14 @@ fn test_dispute_allocations_zero_fee_no_fee_transfer() { let fee_collector = Address::generate(&env); let escrow = EscrowData { + payees: soroban_sdk::Vec::new(&env), seller: seller.clone(), buyer: Some(buyer.clone()), resolver: resolver.clone(), token: token.clone(), amount: 1_000_000_i128, fee_bps: 0_u32, // 0% fee + resolver_fee_bps: 0, state: EscrowState::Disputed, shipping_window: 3600, funded_at: 0, @@ -511,7 +514,6 @@ fn test_dispute_allocations_zero_fee_no_fee_transfer() { shipped_at: 0, delivered_at: None, tracking_id: None, - }; let arbitration_fee = 50_000_i128; diff --git a/contracts/escrow/src/test_fee_config.rs b/contracts/escrow/src/test_fee_config.rs index 6de31759..b0717571 100644 --- a/contracts/escrow/src/test_fee_config.rs +++ b/contracts/escrow/src/test_fee_config.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use crate::{test_helpers::setup_contract, ContractError, EscrowClient, FeeConfig}; +use crate::{Payee, test_helpers::setup_contract, ContractError, EscrowClient, FeeConfig}; use soroban_sdk::Env; #[test] diff --git a/contracts/escrow/src/test_fee_minimum.rs b/contracts/escrow/src/test_fee_minimum.rs index 2d9cbcd1..0408a5ed 100644 --- a/contracts/escrow/src/test_fee_minimum.rs +++ b/contracts/escrow/src/test_fee_minimum.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use crate::{Escrow, EscrowClient, ResolutionType}; +use crate::{Payee, Escrow, EscrowClient, ResolutionType}; use soroban_sdk::{ testutils::{Address as _, Ledger}, token, Address, Env, String as SorobanString, Symbol, @@ -41,13 +41,14 @@ fn test_fee_rounds_to_zero_on_one_stroop_confirm_delivery() { // MAX_FEE_BPS = 300 (3%) — still rounds to 0 on 1 stroop let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1_i128, &300_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&id, &buyer); client.mark_shipped(&seller, &id, &SorobanString::from_str(&env, "TRACK-ONE")); @@ -75,13 +76,14 @@ fn test_fee_rounds_to_zero_on_one_stroop_auto_release() { mint(&env, &token, &buyer, 1); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1_i128, &300_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&id, &buyer); client.mark_shipped( @@ -114,13 +116,14 @@ fn test_fee_rounds_to_zero_on_one_stroop_resolve_dispute_release() { mint(&env, &token, &buyer, 1); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1_i128, &300_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&id, &buyer); client.mark_shipped( @@ -153,13 +156,14 @@ fn test_fee_rounds_to_zero_on_one_stroop_resolve_dispute_refund() { mint(&env, &token, &buyer, 1); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1_i128, &300_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&id, &buyer); client.mark_shipped( @@ -180,3 +184,12 @@ fn test_fee_rounds_to_zero_on_one_stroop_resolve_dispute_refund() { assert_eq!(balance(&env, &token, &buyer), 1); assert_eq!(balance(&env, &token, &contract_id), 0); } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_fee_snapshot.rs b/contracts/escrow/src/test_fee_snapshot.rs index 6898c4d3..8f2d09e6 100644 --- a/contracts/escrow/src/test_fee_snapshot.rs +++ b/contracts/escrow/src/test_fee_snapshot.rs @@ -4,7 +4,7 @@ mod tests { testutils::{Address as _, Ledger}, Address, Env, }; - use crate::{Escrow, EscrowClient}; + use crate::{Payee, Escrow, EscrowClient}; const DISPUTE_WINDOW: u64 = 172_800; @@ -52,14 +52,15 @@ mod tests { // Create escrow at 1% fee — this snapshots fee_bps = 100 into EscrowData. let escrow_id = client.create_escrow( - &seller, - &buyer, - &resolver, - &token_id, - &amount, - &100_u32, - &604_800_u64, - ); + &single_payee(&env, &seller), + &buyer, + &resolver, + &token_id, + &amount, + &100_u32, + &0_u32, + &604_800_u64 + ); client.fund_escrow(&escrow_id, &buyer); @@ -115,14 +116,15 @@ mod tests { let shipping_window: u64 = 604_800; let escrow_id = client.create_escrow( - &seller, - &buyer, - &resolver, - &token_id, - &amount, - &100_u32, - &shipping_window, - ); + &single_payee(&env, &seller), + &buyer, + &resolver, + &token_id, + &amount, + &100_u32, + &0_u32, + &shipping_window + ); client.fund_escrow(&escrow_id, &buyer); client.mark_shipped(&seller, &escrow_id, &soroban_sdk::String::from_str(&env, "TRACK-001")); @@ -151,4 +153,13 @@ mod tests { "fee collector should receive snapshotted 1% fee on auto_release" ); } -} \ No newline at end of file +} + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_fee_update.rs b/contracts/escrow/src/test_fee_update.rs index a5e70fb0..0d4cdd72 100644 --- a/contracts/escrow/src/test_fee_update.rs +++ b/contracts/escrow/src/test_fee_update.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use crate::{test_helpers::setup_contract, ContractError, DataKey, FeeConfig, ProtocolFeeUpdated}; +use crate::{Payee, test_helpers::setup_contract, ContractError, DataKey, FeeConfig, ProtocolFeeUpdated}; use soroban_sdk::{testutils::{Address as _, Events as _}, Address, Env, IntoVal, Symbol, TryFromVal, Val}; /// Test: set_protocol_fee with 0 bps (minimum) diff --git a/contracts/escrow/src/test_get_escrows_by_buyer.rs b/contracts/escrow/src/test_get_escrows_by_buyer.rs index f2cfbcae..a82d40c5 100644 --- a/contracts/escrow/src/test_get_escrows_by_buyer.rs +++ b/contracts/escrow/src/test_get_escrows_by_buyer.rs @@ -1,5 +1,6 @@ #![cfg(test)] +use crate::Payee; use crate::test_helpers::{create_funded_escrow, setup_contract}; use soroban_sdk::{testutils::Address as _, Address, Env}; @@ -42,13 +43,14 @@ fn test_get_escrows_by_buyer() { // Create 1 pending escrow (no buyer yet) let _id4 = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &4000_i128, &100_u32, - &3600_u64, + &0_u32, + &3600_u64 ); // Check escrows for buyer 1 @@ -83,13 +85,14 @@ fn test_buyer_index_populated_on_fund() { mint_tokens(&env, &token, &buyer, 1000); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1000_i128, &100_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&id, &buyer); @@ -97,3 +100,12 @@ fn test_buyer_index_populated_on_fund() { assert_eq!(escrows.len(), 1); assert_eq!(escrows.get(0).unwrap(), id); } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_get_escrows_by_vendor.rs b/contracts/escrow/src/test_get_escrows_by_vendor.rs index a9624bc9..28f0395f 100644 --- a/contracts/escrow/src/test_get_escrows_by_vendor.rs +++ b/contracts/escrow/src/test_get_escrows_by_vendor.rs @@ -1,7 +1,7 @@ #![cfg(test)] use crate::test_helpers::setup_contract; -use crate::{EscrowData, EscrowState}; +use crate::{Payee, EscrowData, EscrowState}; use soroban_sdk::{ testutils::{Address as _, Ledger as _}, Address, Env, @@ -41,33 +41,36 @@ fn test_get_escrows_by_vendor_multiple() { // Create escrows for vendor 1 let id1 = client.create_escrow( - &vendor_1, + &single_payee(&env, &vendor_1), &None::
, &resolver, &token, &1000_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); let id2 = client.create_escrow( - &vendor_1, + &single_payee(&env, &vendor_1), &None::
, &resolver, &token, &2000_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); // Create escrow for vendor 2 let id3 = client.create_escrow( - &vendor_2, + &single_payee(&env, &vendor_2), &None::
, &resolver, &token, &3000_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); // Check escrows for vendor 1 @@ -96,13 +99,14 @@ fn test_vendor_escrow_data_integrity_and_state_transitions() { // Create let id = client.create_escrow( - &vendor, + &single_payee(&env, &vendor), &None::
, &resolver, &token, &1000_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); // Assert initial state and data integrity @@ -144,3 +148,12 @@ fn test_vendor_escrow_data_integrity_and_state_transitions() { assert_eq!(escrows.len(), 1); assert_eq!(escrows.get(0).unwrap(), id); } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_helpers.rs b/contracts/escrow/src/test_helpers.rs index f41c8187..dd90169a 100644 --- a/contracts/escrow/src/test_helpers.rs +++ b/contracts/escrow/src/test_helpers.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use crate::{Escrow, EscrowClient}; +use crate::{Payee, Escrow, EscrowClient}; use soroban_sdk::{ testutils::{Address as _, Ledger}, token, Address, Env, @@ -37,14 +37,24 @@ pub fn create_funded_escrow( ) -> u64 { mint_token(env, token, buyer, amount); let id = client.create_escrow( - seller, + &single_payee(&env, &seller), &None::
, resolver, token, &amount, &fee_bps, - &shipping_window, + &0_u32, + &shipping_window ); client.fund_escrow(&id, buyer); id } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_initialize_twice.rs b/contracts/escrow/src/test_initialize_twice.rs index c05e1821..131c05ab 100644 --- a/contracts/escrow/src/test_initialize_twice.rs +++ b/contracts/escrow/src/test_initialize_twice.rs @@ -2,7 +2,7 @@ //! Calling `initialize` a second time must return `ContractError::AlreadyInitialized` //! and leave the storage values from the first call intact (#14). -use crate::{DataKey, Escrow, EscrowClient}; +use crate::{Payee, DataKey, Escrow, EscrowClient}; use soroban_sdk::{testutils::Address as _, Address, Env}; fn deploy_and_init(env: &Env) -> (EscrowClient, Address, Address) { diff --git a/contracts/escrow/src/test_initialize_zero_admin.rs b/contracts/escrow/src/test_initialize_zero_admin.rs index 2ef1364d..0680071f 100644 --- a/contracts/escrow/src/test_initialize_zero_admin.rs +++ b/contracts/escrow/src/test_initialize_zero_admin.rs @@ -6,7 +6,7 @@ //! * Setup requests throw immediate, clear errors on empty address parameters. //! * Contract state configuration remains uninitialized on failed setup calls. -use crate::{ContractError, DataKey, Escrow, EscrowClient}; +use crate::{Payee, ContractError, DataKey, Escrow, EscrowClient}; use soroban_sdk::{testutils::Address as _, Address, Env, String}; /// Strkey for the all-zero ed25519 public key — the canonical "empty" Stellar diff --git a/contracts/escrow/src/test_minimum_amount_guard.rs b/contracts/escrow/src/test_minimum_amount_guard.rs index 241e72f2..f8e20d4d 100644 --- a/contracts/escrow/src/test_minimum_amount_guard.rs +++ b/contracts/escrow/src/test_minimum_amount_guard.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use crate::{ContractError, Escrow, EscrowClient, MIN_ESCROW_AMOUNT}; +use crate::{Payee, ContractError, Escrow, EscrowClient, MIN_ESCROW_AMOUNT}; use soroban_sdk::{testutils::Address as _, token, Address, Env}; fn setup(env: &Env) -> (Address, Address, Address, Address, Address, Address) { @@ -18,6 +18,15 @@ fn mint(env: &Env, token: &Address, to: &Address, amount: i128) { token::StellarAssetClient::new(env, token).mint(to, &amount); } +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} + /// Verify that creating an escrow with zero amount throws an error. #[test] fn test_create_escrow_zero_amount_fails() { @@ -29,13 +38,14 @@ fn test_create_escrow_zero_amount_fails() { client.initialize(&admin, &fee_collector, &0_u32); let result = client.try_create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &0_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); assert_eq!(result, Err(Ok(ContractError::InvalidAmount))); } @@ -52,13 +62,14 @@ fn test_create_escrow_below_minimum_fails() { let below_minimum = MIN_ESCROW_AMOUNT - 1; let result = client.try_create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &below_minimum, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); assert_eq!(result, Err(Ok(ContractError::InvalidAmount))); } @@ -76,13 +87,14 @@ fn test_create_escrow_at_minimum_succeeds() { mint(&env, &token, &buyer, MIN_ESCROW_AMOUNT); let result = client.try_create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &MIN_ESCROW_AMOUNT, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); assert!(matches!(result, Ok(_))); } @@ -101,13 +113,14 @@ fn test_create_escrow_above_minimum_succeeds() { mint(&env, &token, &buyer, above_minimum); let result = client.try_create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &above_minimum, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); assert!(matches!(result, Ok(_))); } diff --git a/contracts/escrow/src/test_mutual_cancel.rs b/contracts/escrow/src/test_mutual_cancel.rs index 139ed934..dc2ec20f 100644 --- a/contracts/escrow/src/test_mutual_cancel.rs +++ b/contracts/escrow/src/test_mutual_cancel.rs @@ -8,7 +8,7 @@ use soroban_sdk::{ }; use crate::test_helpers::{create_funded_escrow, setup_contract}; -use crate::{ContractError, EscrowState}; +use crate::{Payee, ContractError, EscrowState}; fn register_token(env: &Env) -> Address { let token_admin = Address::generate(env); diff --git a/contracts/escrow/src/test_overflow.rs b/contracts/escrow/src/test_overflow.rs index 75554eb6..9681238d 100644 --- a/contracts/escrow/src/test_overflow.rs +++ b/contracts/escrow/src/test_overflow.rs @@ -48,13 +48,14 @@ fn test_fee_calculation_max_escrow_amount() { let fee_bps = 300; // 3% let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &amount, &fee_bps, - &3600_u64, + &0_u32, + &3600_u64 ); mint_tokens(&env, &token, &buyer, amount); @@ -95,24 +96,26 @@ fn test_create_escrow_amount_exceeds_maximum() { let amount = MAX_ESCROW_AMOUNT + 1; let res = client.try_create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &amount, &300, - &3600_u64, + &0_u32, + &3600_u64 ); assert_eq!(res, Err(Ok(ContractError::AmountExceedsMaximum))); let res2 = client.try_create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &i128::MAX, &300, - &3600_u64, + &0_u32, + &3600_u64 ); assert_eq!(res2, Err(Ok(ContractError::AmountExceedsMaximum))); } @@ -127,24 +130,26 @@ fn test_create_escrow_invalid_amount() { client.initialize(&admin, &fee_collector, &0_u32); let res = client.try_create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &0, &200, - &3600, + &0_u32, + &3600 ); assert!(matches!(res, Err(Ok(ContractError::InvalidAmount)))); let res2 = client.try_create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &-1, &200, - &3600, + &0_u32, + &3600 ); assert!(matches!(res2, Err(Ok(ContractError::InvalidAmount)))); } @@ -159,22 +164,24 @@ fn test_fee_exceeds_max_clean_error() { client.initialize(&admin, &fee_collector, &0_u32); let res = client.try_create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1000, &301, - &3600, + &0_u32, + &3600 ); let res = client.try_create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1000, &10_001, - &3600, + &0_u32, + &3600 ); assert!(matches!(res, Err(Ok(ContractError::FeeExceedsMax)))); } @@ -194,23 +201,25 @@ fn test_addition_overflow_escrow_counter() { }); let res = client.try_create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1000, &300, - &3600, + &0_u32, + &3600 ); assert_eq!(res, Err(Ok(ContractError::ArithmeticError))); let res = client.try_create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1000, &300, - &3600, + &0_u32, + &3600 ); assert_eq!(res, Err(Ok(ContractError::ArithmeticError))); } @@ -227,23 +236,25 @@ fn test_addition_overflow_shipping_window() { mint_tokens(&env, &token, &buyer, amount); let escrow_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &amount, &300, - &u64::MAX, + &0_u32, + &u64::MAX ); env.ledger().set_timestamp(1000); let escrow_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &amount, &300, - &u64::MAX, + &0_u32, + &u64::MAX ); client.fund_escrow(&escrow_id, &buyer); client.mark_shipped( @@ -288,3 +299,12 @@ fn test_division_by_zero_safety() { let res = amount.checked_div(0); assert_eq!(res, None); } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_pause.rs b/contracts/escrow/src/test_pause.rs index bf619cd7..5dd36328 100644 --- a/contracts/escrow/src/test_pause.rs +++ b/contracts/escrow/src/test_pause.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use crate::{ContractError, Escrow, EscrowClient, EscrowState, ResolutionType}; +use crate::{Payee, ContractError, Escrow, EscrowClient, EscrowState, ResolutionType}; use soroban_sdk::{ testutils::{Address as _, Ledger as _}, token, Address, Env, String as SorobanString, Symbol, @@ -47,13 +47,14 @@ fn test_create_escrow_blocked_when_paused() { let client = EscrowClient::new(&env, &contract_id); client.pause_contract(&admin); let result = client.try_create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); assert!(matches!(result, Err(Ok(ContractError::ContractPaused)))); } @@ -63,13 +64,14 @@ fn test_fund_escrow_blocked_when_paused() { let (env, admin, seller, buyer, resolver, token, contract_id) = setup_env(); let client = EscrowClient::new(&env, &contract_id); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.pause_contract(&admin); let result = client.try_fund_escrow(&id, &buyer); @@ -81,13 +83,14 @@ fn test_pause_blocks_mutations_but_keeps_views_available() { let (env, admin, seller, buyer, resolver, token, contract_id) = setup_env(); let client = EscrowClient::new(&env, &contract_id); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); mint_tokens(&env, &token, &buyer, 100); client.fund_escrow(&id, &buyer); @@ -105,13 +108,14 @@ fn test_confirm_delivery_blocked_when_paused() { let (env, admin, seller, buyer, resolver, token, contract_id) = setup_env(); let client = EscrowClient::new(&env, &contract_id); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); mint_tokens(&env, &token, &buyer, 100); client.fund_escrow(&id, &buyer); @@ -126,13 +130,14 @@ fn test_raise_dispute_blocked_when_paused() { let (env, admin, seller, buyer, resolver, token, contract_id) = setup_env(); let client = EscrowClient::new(&env, &contract_id); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); mint_tokens(&env, &token, &buyer, 100); client.fund_escrow(&id, &buyer); @@ -154,13 +159,14 @@ fn test_resolve_dispute_blocked_when_paused() { let (env, admin, seller, buyer, resolver, token, contract_id) = setup_env(); let client = EscrowClient::new(&env, &contract_id); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); mint_tokens(&env, &token, &buyer, 100); client.fund_escrow(&id, &buyer); @@ -183,13 +189,14 @@ fn test_auto_release_blocked_when_paused() { let (env, admin, seller, buyer, resolver, token, contract_id) = setup_env(); let client = EscrowClient::new(&env, &contract_id); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &1_u64, + &0_u32, + &1_u64 ); mint_tokens(&env, &token, &buyer, 100); client.fund_escrow(&id, &buyer); @@ -215,13 +222,14 @@ fn test_read_only_views_work_while_paused() { let (env, admin, seller, _buyer, resolver, token, contract_id) = setup_env(); let client = EscrowClient::new(&env, &contract_id); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.pause_contract(&admin); let _ = client.get_escrow(&id); @@ -250,13 +258,14 @@ fn test_unpause_resumes_operations() { client.pause_contract(&admin); client.unpause_contract(&admin); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); mint_tokens(&env, &token, &buyer, 100); client.fund_escrow(&id, &buyer); @@ -264,13 +273,14 @@ fn test_unpause_resumes_operations() { mint_tokens(&env, &token, &buyer, 1_000); let escrow_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &100_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.pause_contract(&admin); @@ -283,12 +293,13 @@ fn test_unpause_resumes_operations() { .is_err()); assert!(client .try_create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &100_u32, + &0_u32, &3600_u64 ) .is_err()); @@ -311,13 +322,23 @@ fn test_unpause_resumes_operations() { client.unpause_contract(&admin); mint_tokens(&env, &token, &buyer, 100); let second_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &50_i128, &50_u32, - &3600_u64, + &0_u32, + &3600_u64 ); assert_eq!(second_id, 3); } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_pause_all_blocked.rs b/contracts/escrow/src/test_pause_all_blocked.rs index 2aeea5b7..3dee1598 100644 --- a/contracts/escrow/src/test_pause_all_blocked.rs +++ b/contracts/escrow/src/test_pause_all_blocked.rs @@ -43,26 +43,28 @@ fn test_pause_blocks_all_mutations() { // 1. try_create_escrow should fail let create_res = client.try_create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &36_00_u64, + &0_u32, + &36_00_u64 ); assert!(matches!(create_res, Err(Ok(ContractError::ContractPaused)))); // Need a valid escrow for subsequent tests; create without pause client.unpause_contract(&admin); let escrow_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &36_00_u64, + &0_u32, + &36_00_u64 ); client.pause_contract(&admin); @@ -118,3 +120,12 @@ fn test_pause_blocks_all_mutations() { let _ = client.get_fee_config(); assert!(client.is_paused()); } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_pause_cancel.rs b/contracts/escrow/src/test_pause_cancel.rs index fe32f490..188da7e1 100644 --- a/contracts/escrow/src/test_pause_cancel.rs +++ b/contracts/escrow/src/test_pause_cancel.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use crate::{ContractError, EscrowClient}; +use crate::{Payee, ContractError, EscrowClient}; use soroban_sdk::{testutils::{Address as _, Ledger as _}, token, Address, Env, String as SorobanString, Symbol}; fn setup_env() -> (Env, Address, Address, Address, Address, Address, Address) { @@ -28,10 +28,19 @@ fn test_cancel_escrow_blocked_when_paused() { let (env, admin, seller, _buyer, _resolver, _token, contract_id) = setup_env(); let client = EscrowClient::new(&env, &contract_id); // create escrow in pending state - let id = client.create_escrow(&seller, &None::
, &admin, &env.register_stellar_asset_contract(admin.clone()), &100_i128, &0_u32, &3600_u64); + let id = client.create_escrow(&single_payee(&env, &seller), &None::
, &admin, &env.register_stellar_asset_contract(admin.clone()), &100_i128, &0_u32, &0_u32, &3600_u64); // pause contract client.pause_contract(&admin); // attempt to cancel escrow should fail with ContractPaused let result = client.try_cancel_escrow(&seller, &id); assert!(matches!(result, Err(Ok(ContractError::ContractPaused)))); } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_refund_flow.rs b/contracts/escrow/src/test_refund_flow.rs index 85c73f0f..12f8c0d5 100644 --- a/contracts/escrow/src/test_refund_flow.rs +++ b/contracts/escrow/src/test_refund_flow.rs @@ -2,7 +2,7 @@ use crate::test_helpers::{setup_contract, mint_token}; use crate::types::EscrowState; -use crate::{ContractError, EscrowClient}; +use crate::{Payee, ContractError, EscrowClient}; use soroban_sdk::testutils::{Address as _, Events}; use soroban_sdk::{token, Address, Env}; diff --git a/contracts/escrow/src/test_resolve_dispute_no_buyer.rs b/contracts/escrow/src/test_resolve_dispute_no_buyer.rs index 48a65225..4bdb4c16 100644 --- a/contracts/escrow/src/test_resolve_dispute_no_buyer.rs +++ b/contracts/escrow/src/test_resolve_dispute_no_buyer.rs @@ -1,7 +1,7 @@ #![cfg(test)] use crate::test_helpers::setup_contract; -use crate::{DataKey, DisputeData, DisputeStatus, EscrowData, EscrowState, ResolutionType, ContractError}; +use crate::{Payee, DataKey, DisputeData, DisputeStatus, EscrowData, EscrowState, ResolutionType, ContractError}; use soroban_sdk::{testutils::Address as _, Address, Env, Symbol, String as SorobanString, BytesN}; #[test] diff --git a/contracts/escrow/src/test_resolver_rotation.rs b/contracts/escrow/src/test_resolver_rotation.rs index 7c879521..a28d03b7 100644 --- a/contracts/escrow/src/test_resolver_rotation.rs +++ b/contracts/escrow/src/test_resolver_rotation.rs @@ -2,7 +2,7 @@ //! Tests for `rotate_resolver`: seller and admin can rotate, buyer cannot, //! same-address is rejected, and terminal states are rejected. -use crate::{ContractError, Escrow, EscrowClient, EscrowState, ResolutionType, ResolverRotated}; +use crate::{Payee, ContractError, Escrow, EscrowClient, EscrowState, ResolutionType, ResolverRotated}; use soroban_sdk::{ testutils::{Address as _, Events as _}, token, Address, BytesN, Env, String as SorobanString, Symbol, TryFromVal, Val, @@ -38,13 +38,14 @@ fn setup() -> Fx { client.initialize(&admin, &fee_collector, &0_u32); let escrow_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token_addr, &500_i128, &0_u32, - &0_u64, + &0_u32, + &0_u64 ); Fx { @@ -66,7 +67,7 @@ fn seller_can_rotate_resolver() { fx.client .rotate_resolver(&fx.seller, &fx.escrow_id, &new_resolver); - use crate::{DataKey, EscrowData}; + use crate::{Payee, DataKey, EscrowData}; let escrow: EscrowData = fx .env .as_contract(&fx.client.address, || { @@ -87,7 +88,7 @@ fn admin_can_rotate_resolver() { fx.client .rotate_resolver(&fx.admin, &fx.escrow_id, &new_resolver); - use crate::{DataKey, EscrowData}; + use crate::{Payee, DataKey, EscrowData}; let escrow: EscrowData = fx .env .as_contract(&fx.client.address, || { @@ -161,13 +162,14 @@ fn terminal_state_rejected() { client.initialize(&admin, &fee_collector, &0_u32); let escrow_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token_addr, &100_i128, &0_u32, - &0_u64, + &0_u32, + &0_u64 ); // Cancel moves to Canceled (terminal) @@ -272,3 +274,12 @@ fn rotation_emits_resolver_rotated_event() { "expected a resolver_rotated event with the old and new resolver", ); } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_sep41.rs b/contracts/escrow/src/test_sep41.rs index 0197b8d8..8c0c597f 100644 --- a/contracts/escrow/src/test_sep41.rs +++ b/contracts/escrow/src/test_sep41.rs @@ -7,7 +7,9 @@ //! payout path (`deduct_and_transfer`). These tests verify that the full //! lifecycle works correctly with a generic SEP-41 token that is not USDC. +use crate::Payee; use crate::EscrowState; +use crate::Payee; use crate::test_helpers::setup_contract; use soroban_sdk::{ testutils::{Address as _, Events as _, Ledger}, @@ -79,7 +81,16 @@ fn test_sep41_fund_and_confirm_delivery() { mint(&env, &token, &buyer, 500); - let id = client.create_escrow(&seller, &None::
, &resolver, &token, &500_i128, &100_u32, &3600_u64); + let id = client.create_escrow( + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &500_i128, + &100_u32, + &0_u32, + &3600_u64 + ); client.fund_escrow(&id, &buyer); client.mark_shipped(&seller, &id, &SorobanString::from_str(&env, "TRACK001")); @@ -124,7 +135,16 @@ fn test_sep41_auto_release() { mint(&env, &token, &buyer, 1000); - let id = client.create_escrow(&seller, &None::
, &resolver, &token, &1000_i128, &0_u32, &3600_u64); + let id = client.create_escrow( + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &1000_i128, + &0_u32, + &0_u32, + &3600_u64 + ); client.fund_escrow(&id, &buyer); client.mark_shipped(&seller, &id, &SorobanString::from_str(&env, "TRACK-AUTO")); env.ledger().set_timestamp(1_700_000_000); @@ -158,7 +178,16 @@ fn test_sep41_dispute_and_refund() { mint(&env, &token, &buyer, 800); - let id = client.create_escrow(&seller, &None::
, &resolver, &token, &800_i128, &0_u32, &3600_u64); + let id = client.create_escrow( + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &800_i128, + &0_u32, + &0_u32, + &3600_u64 + ); client.fund_escrow(&id, &buyer); client.mark_shipped(&seller, &id, &SorobanString::from_str(&env, "TRACK-DISPUTE")); @@ -197,7 +226,16 @@ fn test_sep41_token_address_stored_in_escrow() { let seller = Address::generate(&env); let resolver = Address::generate(&env); - let id = client.create_escrow(&seller, &None::
, &resolver, &token, &100_i128, &0_u32, &3600_u64); + let id = client.create_escrow( + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &100_i128, + &0_u32, + &0_u32, + &3600_u64 + ); // Verify the stored token address matches what was passed in assert_eq!(client.get_escrow(&id).token, token); } @@ -217,7 +255,16 @@ fn test_sep41_cancel_escrow() { mint(&env, &token, &buyer, 1000); // Create escrow (starts in Pending state) - let id = client.create_escrow(&seller, &None::
, &resolver, &token, &1000_i128, &0_u32, &3600_u64); + let id = client.create_escrow( + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &1000_i128, + &0_u32, + &0_u32, + &3600_u64 + ); let escrow_before = client.get_escrow(&id); assert_eq!(escrow_before.state, EscrowState::Pending); @@ -256,7 +303,16 @@ fn test_sep41_dispute_and_release() { mint(&env, &token, &buyer, 1000); // Create escrow with 1000 amount, 100 BPS (1.0%) fee - let id = client.create_escrow(&seller, &None::
, &resolver, &token, &1000_i128, &100_u32, &3600_u64); + let id = client.create_escrow( + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &1000_i128, + &100_u32, + &0_u32, + &3600_u64 + ); client.fund_escrow(&id, &buyer); client.mark_shipped(&seller, &id, &SorobanString::from_str(&env, "TRACK-RELEASE")); @@ -318,7 +374,16 @@ fn test_sep41_auto_release_with_fees() { mint(&env, &token, &buyer, 1000); - let id = client.create_escrow(&seller, &None::
, &resolver, &token, &1000_i128, &0_u32, &3600_u64); + let id = client.create_escrow( + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &1000_i128, + &0_u32, + &0_u32, + &3600_u64 + ); client.fund_escrow(&id, &buyer); client.mark_shipped(&seller, &id, &SorobanString::from_str(&env, "TRACK-AUTO-FEES")); env.ledger().set_timestamp(1_700_000_000); @@ -342,3 +407,12 @@ fn test_sep41_auto_release_with_fees() { assert_eq!(balance(&env, &token, &contract_id), 0); assert_eq!(client.get_escrow(&id).state, EscrowState::Completed); } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_storage_collision.rs b/contracts/escrow/src/test_storage_collision.rs index 918d6a8a..bdfe46f0 100644 --- a/contracts/escrow/src/test_storage_collision.rs +++ b/contracts/escrow/src/test_storage_collision.rs @@ -7,7 +7,7 @@ //! `DataKey::Escrow(id)` produces independent persistent storage slots: a //! write to slot N must never bleed into slot M where N ≠ M. -use crate::{Escrow, EscrowClient, EscrowState}; +use crate::{Payee, Escrow, EscrowClient, EscrowState}; use soroban_sdk::{ testutils::Address as _, token, Address, Env, String, @@ -49,10 +49,24 @@ fn cancel_escrow1_does_not_affect_escrow2() { let (env, client, _admin, _fee_collector, seller, resolver, token) = setup(); let id1 = client.create_escrow( - &seller, &None::
, &resolver, &token, &500_i128, &0_u32, &3600_u64, + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &500_i128, + &0_u32, + &0_u32, + &3600_u64 ); let id2 = client.create_escrow( - &seller, &None::
, &resolver, &token, &750_i128, &0_u32, &3600_u64, + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &750_i128, + &0_u32, + &0_u32, + &3600_u64 ); // Snapshot escrow 2 before the mutation. @@ -83,10 +97,24 @@ fn fund_escrow2_does_not_affect_escrow1() { let buyer = Address::generate(&env); let id1 = client.create_escrow( - &seller, &None::
, &resolver, &token, &100_i128, &0_u32, &3600_u64, + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &100_i128, + &0_u32, + &0_u32, + &3600_u64 ); let id2 = client.create_escrow( - &seller, &Some(buyer.clone()), &resolver, &token, &200_i128, &0_u32, &3600_u64, + &single_payee(&env, &seller), + &Some(buyer.clone()), + &resolver, + &token, + &200_i128, + &0_u32, + &0_u32, + &3600_u64 ); let before = client.get_escrow(&id1); @@ -110,10 +138,24 @@ fn mark_shipped_escrow1_does_not_affect_escrow2() { let buyer2 = Address::generate(&env); let id1 = client.create_escrow( - &seller, &Some(buyer1.clone()), &resolver, &token, &300_i128, &0_u32, &0_u64, + &single_payee(&env, &seller), + &Some(buyer1.clone()), + &resolver, + &token, + &300_i128, + &0_u32, + &0_u32, + &0_u64 ); let id2 = client.create_escrow( - &seller, &Some(buyer2.clone()), &resolver, &token, &400_i128, &0_u32, &0_u64, + &single_payee(&env, &seller), + &Some(buyer2.clone()), + &resolver, + &token, + &400_i128, + &0_u32, + &0_u32, + &0_u64 ); fund(&env, &client, &token, &buyer1, &id1); @@ -138,13 +180,34 @@ fn modifying_middle_escrow_leaves_neighbors_unchanged() { let (_env, client, _admin, _fee_collector, seller, resolver, token) = setup(); let id1 = client.create_escrow( - &seller, &None::
, &resolver, &token, &100_i128, &0_u32, &3600_u64, + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &100_i128, + &0_u32, + &0_u32, + &3600_u64 ); let id2 = client.create_escrow( - &seller, &None::
, &resolver, &token, &200_i128, &0_u32, &3600_u64, + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &200_i128, + &0_u32, + &0_u32, + &3600_u64 ); let id3 = client.create_escrow( - &seller, &None::
, &resolver, &token, &300_i128, &0_u32, &3600_u64, + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &300_i128, + &0_u32, + &0_u32, + &3600_u64 ); let before1 = client.get_escrow(&id1); @@ -176,18 +239,26 @@ fn independent_escrows_store_correct_fields() { // Escrow 1: open buyer, amount=111, fee=50, window=1800 let id1 = client.create_escrow( - &seller, &None::
, &resolver, &token, &111_i128, &50_u32, &1800_u64, + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &111_i128, + &50_u32, + &0_u32, + &1800_u64 ); // Escrow 2: locked buyer, amount=999, fee=100, window=7200 let id2 = client.create_escrow( - &seller2, + &single_payee(&env, &seller2), &Some(buyer2.clone()), &resolver2, &token, &999_i128, &100_u32, - &7200_u64, + &0_u32, + &7200_u64 ); let e1 = client.get_escrow(&id1); @@ -221,10 +292,24 @@ fn funded_at_of_escrow2_unchanged_after_cancelling_escrow1() { let buyer = Address::generate(&env); let id1 = client.create_escrow( - &seller, &None::
, &resolver, &token, &50_i128, &0_u32, &3600_u64, + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &50_i128, + &0_u32, + &0_u32, + &3600_u64 ); let id2 = client.create_escrow( - &seller, &Some(buyer.clone()), &resolver, &token, &50_i128, &0_u32, &3600_u64, + &single_payee(&env, &seller), + &Some(buyer.clone()), + &resolver, + &token, + &50_i128, + &0_u32, + &0_u32, + &3600_u64 ); fund(&env, &client, &token, &buyer, &id2); @@ -240,3 +325,12 @@ fn funded_at_of_escrow2_unchanged_after_cancelling_escrow1() { assert_eq!(funded_at_after, funded_at_before); assert_eq!(client.get_escrow(&id2).state, EscrowState::Funded); } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_string_length.rs b/contracts/escrow/src/test_string_length.rs index 4839e07d..ee8b2126 100644 --- a/contracts/escrow/src/test_string_length.rs +++ b/contracts/escrow/src/test_string_length.rs @@ -1,7 +1,7 @@ #![cfg(test)] use crate::test_helpers::{create_funded_escrow, setup_contract}; -use crate::{ContractError, MAX_DESCRIPTION_LEN, MAX_TRACKING_ID_LEN}; +use crate::{Payee, ContractError, MAX_DESCRIPTION_LEN, MAX_TRACKING_ID_LEN}; use soroban_sdk::{ testutils::{Address as _, Ledger}, Address, Bytes, BytesN, Env, String as SorobanString, Symbol, diff --git a/contracts/escrow/src/test_unauthorized.rs b/contracts/escrow/src/test_unauthorized.rs index ea96fdaf..a1408588 100644 --- a/contracts/escrow/src/test_unauthorized.rs +++ b/contracts/escrow/src/test_unauthorized.rs @@ -7,7 +7,7 @@ //! `caller != admin → NotAuthorized` guard, not just the host's `require_auth` //! reject path. -use crate::{ContractError, Escrow, EscrowClient}; +use crate::{Payee, ContractError, Escrow, EscrowClient}; use soroban_sdk::{testutils::Address as _, Address, Env}; /// Fresh contract with admin/fee_collector initialised. All auths are mocked @@ -128,12 +128,13 @@ fn create_escrow_rejects_resolver_equal_to_seller() { assert_eq!( client.try_create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &seller, // resolver == seller &token, &100_i128, &0_u32, + &0_u32, &3600_u64, ), Err(Ok(ContractError::ConflictingRoles)), @@ -153,13 +154,14 @@ fn fund_escrow_rejects_buyer_equal_to_seller() { soroban_sdk::token::StellarAssetClient::new(&env, &token).mint(&seller, &1000_i128); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); assert_eq!( @@ -181,13 +183,14 @@ fn fund_escrow_rejects_buyer_equal_to_resolver() { soroban_sdk::token::StellarAssetClient::new(&env, &token).mint(&resolver, &1000_i128); let id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &100_i128, &0_u32, - &3600_u64, + &0_u32, + &3600_u64 ); assert_eq!( @@ -195,3 +198,12 @@ fn fund_escrow_rejects_buyer_equal_to_resolver() { Err(Ok(ContractError::ConflictingRoles)), ); } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/test_withdraw_fees.rs b/contracts/escrow/src/test_withdraw_fees.rs index 8bbda093..9cd3f2f1 100644 --- a/contracts/escrow/src/test_withdraw_fees.rs +++ b/contracts/escrow/src/test_withdraw_fees.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use crate::{ContractError, Escrow, EscrowClient}; +use crate::{Payee, ContractError, Escrow, EscrowClient}; use soroban_sdk::{ testutils::{Address as _, Ledger}, token, Address, Env, @@ -48,14 +48,15 @@ fn test_withdraw_fees_after_multiple_escrows() { // Complete 3 escrows that each accrue 1% fees via dispute release. for _ in 0..3 { let id = client.create_escrow( - &seller, - &None::
, - &resolver, - &token, - &1000_i128, - &100_u32, - &3600_u64, - ); + &single_payee(&env, &seller), + &None::
, + &resolver, + &token, + &1000_i128, + &100_u32, + &0_u32, + &3600_u64 + ); client.fund_escrow(&id, &buyer); client.mark_shipped( &seller, @@ -104,13 +105,14 @@ fn test_withdraw_fees_multiple_tokens() { // Accrue fees for Token A (1000 amount, 1% fee = 10) mint_tokens(&env, &token_a, &buyer, 1000); let id_a = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token_a, &1000_i128, &100_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&id_a, &buyer); client.mark_shipped( @@ -130,13 +132,14 @@ fn test_withdraw_fees_multiple_tokens() { // Accrue fees for Token B (2000 amount, 2% fee = 40) mint_tokens(&env, &token_b, &buyer, 2000); let id_b = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token_b, &2000_i128, &200_u32, - &3600_u64, + &0_u32, + &3600_u64 ); client.fund_escrow(&id_b, &buyer); client.mark_shipped( @@ -171,3 +174,12 @@ fn test_withdraw_fees_multiple_tokens() { assert_eq!(token::Client::new(&env, &token_b).balance(&to), 40); assert_eq!(token::Client::new(&env, &token_b).balance(&contract_id), 0); } + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} diff --git a/contracts/escrow/src/types.rs b/contracts/escrow/src/types.rs index ba48141a..4478cee4 100644 --- a/contracts/escrow/src/types.rs +++ b/contracts/escrow/src/types.rs @@ -85,12 +85,14 @@ pub struct ContractConfig { #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct EscrowData { + pub payees: Vec, pub seller: Address, pub buyer: Option
, pub resolver: Address, pub token: Address, pub amount: i128, pub fee_bps: u32, + pub resolver_fee_bps: u32, pub shipping_window: u64, pub funded_at: u64, pub dispute_deadline: u64, diff --git a/contracts/escrow/tests/auth_audit.rs b/contracts/escrow/tests/auth_audit.rs index ed7d6193..1a2db6a7 100644 --- a/contracts/escrow/tests/auth_audit.rs +++ b/contracts/escrow/tests/auth_audit.rs @@ -1,7 +1,16 @@ #![cfg(test)] use soroban_sdk::{testutils::Address as _, token, Address, Env}; -use trustlink_escrow::{ContractError, Escrow, EscrowClient, EscrowData, EscrowState}; +use trustlink_escrow::{ContractError, Escrow, EscrowClient, EscrowData, EscrowState, Payee}; + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} #[test] fn test_unauthorized_attacker_cannot_fund_escrow() { @@ -27,12 +36,13 @@ fn test_unauthorized_attacker_cannot_fund_escrow() { // Create the escrow. let escrow_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token_addr, &amount, &100_u32, + &0_u32, &3600_u64, ); diff --git a/contracts/escrow/tests/cancel_events.rs b/contracts/escrow/tests/cancel_events.rs index 02d704e3..3c47b5e1 100644 --- a/contracts/escrow/tests/cancel_events.rs +++ b/contracts/escrow/tests/cancel_events.rs @@ -4,7 +4,16 @@ use soroban_sdk::{ testutils::{Address as _, Events as _}, token, Address, Env, Symbol, TryFromVal, }; -use trustlink_escrow::{DataKey, Escrow, EscrowClient, EscrowData, EscrowState}; +use trustlink_escrow::{DataKey, Escrow, EscrowClient, EscrowData, EscrowState, Payee}; + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} fn has_cancel_event(env: &Env, contract_id: &Address) -> bool { let expected_topic = Symbol::new(env, "escrow_cancelled"); @@ -46,12 +55,13 @@ fn seller_pending_cancel_emits_event() { client.initialize(&admin, &fee_collector, &0_u32); let escrow_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1_000_i128, &0_u32, + &0_u32, &3_600_u64, ); @@ -84,12 +94,13 @@ fn buyer_funded_cancel_emits_event() { sac.mint(&buyer, &1_000_i128); let escrow_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token, &1_000_i128, &0_u32, + &0_u32, &3_600_u64, ); diff --git a/contracts/escrow/tests/edge_cases.rs b/contracts/escrow/tests/edge_cases.rs index c9617426..1a93e047 100644 --- a/contracts/escrow/tests/edge_cases.rs +++ b/contracts/escrow/tests/edge_cases.rs @@ -1,7 +1,16 @@ #![cfg(test)] use soroban_sdk::{testutils::Address as _, token, Address, Env, String as SorobanString}; -use trustlink_escrow::{ContractError, Escrow, EscrowClient, EscrowState}; +use trustlink_escrow::{ContractError, Escrow, EscrowClient, EscrowState, Payee}; + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} #[test] fn test_auto_release_before_record_delivery_reverts() { @@ -27,12 +36,13 @@ fn test_auto_release_before_record_delivery_reverts() { // 1. Create Escrow let escrow_id = client.create_escrow( - &seller, + &single_payee(&env, &seller), &None::
, &resolver, &token_addr, &amount, &100_u32, + &0_u32, &3600_u64, ); diff --git a/contracts/escrow/tests/happy_path.rs b/contracts/escrow/tests/happy_path.rs index 70c95860..15691a30 100644 --- a/contracts/escrow/tests/happy_path.rs +++ b/contracts/escrow/tests/happy_path.rs @@ -6,6 +6,7 @@ use soroban_sdk::{ }; use trustlink_escrow::{ Escrow, EscrowClient, EscrowCompleted, EscrowCreated, EscrowFunded, EscrowShipped, EscrowState, + Payee, }; struct Fx { @@ -49,6 +50,15 @@ fn setup() -> Fx { } } +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} + fn has_event(env: &Env, contract_id: &Address, topic: &str, predicate: F) -> bool where T: TryFromVal, @@ -99,12 +109,13 @@ fn test_happy_path_escrow_lifecycle() { // 1. Create Escrow let escrow_id = fx.client.create_escrow( - &fx.seller, + &single_payee(&fx.env, &fx.seller), &None::, &fx.resolver, &fx.token_addr, &amount, &100_u32, // 1% escrow fee + &0_u32, &3600_u64, // shipping window ); From 6b1b38bfbe31f5c6eb25f31ea38a4cc181a6cc63 Mon Sep 17 00:00:00 2001 From: Justice Date: Tue, 30 Jun 2026 08:39:30 +0100 Subject: [PATCH 2/3] feat: add escrow state history view --- contracts/escrow/src/lib.rs | 48 ++++++++ contracts/escrow/src/test_state_history.rs | 121 +++++++++++++++++++++ contracts/escrow/src/types.rs | 1 + 3 files changed, 170 insertions(+) create mode 100644 contracts/escrow/src/test_state_history.rs diff --git a/contracts/escrow/src/lib.rs b/contracts/escrow/src/lib.rs index abdaf372..aeae4ed8 100644 --- a/contracts/escrow/src/lib.rs +++ b/contracts/escrow/src/lib.rs @@ -344,8 +344,18 @@ fn get_ttl_extension(env: &Env) -> u32 { fn save_escrow(env: &Env, id: u64, escrow: &EscrowData) { let key = DataKey::Escrow(id); let ext = get_ttl_extension(env); + let previous: Option = env.storage().persistent().get(&key); + let state_changed = previous + .as_ref() + .map(|existing| existing.state != escrow.state) + .unwrap_or(true); + env.storage().persistent().set(&key, escrow); env.storage().persistent().extend_ttl(&key, ext / 2, ext); + + if state_changed { + append_state_history(env, id, &escrow.state); + } } fn load_escrow(env: &Env, id: u64) -> Result { @@ -360,6 +370,35 @@ fn load_escrow(env: &Env, id: u64) -> Result { Ok(escrow) } +fn append_state_history(env: &Env, id: u64, state: &EscrowState) { + let key = DataKey::EscrowStateHistory(id); + let ext = get_ttl_extension(env); + let mut history: Vec<(EscrowState, u64)> = env + .storage() + .persistent() + .get(&key) + .unwrap_or_else(|| Vec::new(env)); + + history.push_back((state.clone(), env.ledger().timestamp())); + env.storage().persistent().set(&key, &history); + env.storage().persistent().extend_ttl(&key, ext / 2, ext); +} + +fn load_state_history(env: &Env, id: u64) -> Vec<(EscrowState, u64)> { + let key = DataKey::EscrowStateHistory(id); + let ext = get_ttl_extension(env); + let history = env + .storage() + .persistent() + .get(&key) + .unwrap_or_else(|| Vec::new(env)); + + if !history.is_empty() { + env.storage().persistent().extend_ttl(&key, ext / 2, ext); + } + history +} + fn save_dispute(env: &Env, id: u64, dispute: &DisputeData) { let key = DataKey::Dispute(id); let ext = get_ttl_extension(env); @@ -1927,6 +1966,14 @@ impl Escrow { load_escrow(&env, escrow_id) } + pub fn get_state_history( + env: Env, + escrow_id: u64, + ) -> Result, ContractError> { + load_escrow(&env, escrow_id)?; + Ok(load_state_history(&env, escrow_id)) + } + /// Retrieves the dispute data for a specific escrow, if any. pub fn get_dispute(env: Env, escrow_id: u64) -> Option { load_dispute(&env, escrow_id).ok() @@ -2242,6 +2289,7 @@ mod test_resolution; mod test_resolver_rotation; mod test_mutual_cancel; mod test_set_fee_boundary; +mod test_state_history; mod test_string_length; mod test_ttl; mod test_unauthorized; diff --git a/contracts/escrow/src/test_state_history.rs b/contracts/escrow/src/test_state_history.rs new file mode 100644 index 00000000..34d722a8 --- /dev/null +++ b/contracts/escrow/src/test_state_history.rs @@ -0,0 +1,121 @@ +#![cfg(test)] + +use crate::{ContractError, Escrow, EscrowClient, EscrowState, Payee}; +use soroban_sdk::{ + testutils::{Address as _, Ledger as _}, + token, Address, Env, String as SorobanString, +}; + +fn setup(env: &Env) -> (EscrowClient<'static>, Address, Address, Address, Address) { + let admin = Address::generate(env); + let seller = Address::generate(env); + let resolver = Address::generate(env); + let token = env.register_stellar_asset_contract(Address::generate(env)); + let fee_collector = Address::generate(env); + + let contract_id = env.register(Escrow, ()); + let client = EscrowClient::new(env, &contract_id); + client.initialize(&admin, &fee_collector, &0_u32); + + (client, seller, resolver, token, admin) +} + +fn single_payee(env: &Env, address: &Address) -> soroban_sdk::Vec { + let mut payees = soroban_sdk::Vec::new(env); + payees.push_back(Payee { + address: address.clone(), + bps: 10_000, + }); + payees +} + +#[test] +fn state_history_records_refund_transitions_with_timestamps() { + let env = Env::default(); + env.mock_all_auths(); + let (client, seller, resolver, token, _admin) = setup(&env); + let buyer = Address::generate(&env); + let amount = 1_000_i128; + token::StellarAssetClient::new(&env, &token).mint(&buyer, &amount); + + env.ledger().set_timestamp(100); + let escrow_id = client.create_escrow( + &single_payee(&env, &seller), + &Some(buyer.clone()), + &resolver, + &token, + &amount, + &0_u32, + &0_u32, + &3_600_u64, + ); + + env.ledger().set_timestamp(200); + client.fund_escrow(&escrow_id, &buyer); + + env.ledger().set_timestamp(300); + client.request_refund(&buyer, &escrow_id); + + env.ledger().set_timestamp(400); + client.approve_refund(&seller, &escrow_id); + + let history = client.get_state_history(&escrow_id); + assert_eq!(history.len(), 4); + assert_eq!(history.get(0).unwrap(), (EscrowState::Pending, 100)); + assert_eq!(history.get(1).unwrap(), (EscrowState::Funded, 200)); + assert_eq!(history.get(2).unwrap(), (EscrowState::RefundRequested, 300)); + assert_eq!(history.get(3).unwrap(), (EscrowState::Refunded, 400)); +} + +#[test] +fn state_history_ignores_non_state_updates() { + let env = Env::default(); + env.mock_all_auths(); + let (client, seller, resolver, token, admin) = setup(&env); + let buyer = Address::generate(&env); + let amount = 1_000_i128; + token::StellarAssetClient::new(&env, &token).mint(&buyer, &amount); + + env.ledger().set_timestamp(1_000); + let escrow_id = client.create_escrow( + &single_payee(&env, &seller), + &Some(buyer.clone()), + &resolver, + &token, + &amount, + &0_u32, + &0_u32, + &3_600_u64, + ); + + env.ledger().set_timestamp(1_100); + client.fund_escrow(&escrow_id, &buyer); + + env.ledger().set_timestamp(1_200); + client.mark_shipped( + &seller, + &escrow_id, + &SorobanString::from_str(&env, "TRACK-HISTORY-002"), + ); + + env.ledger().set_timestamp(1_300); + client.record_delivery(&admin, &escrow_id); + + let history = client.get_state_history(&escrow_id); + assert_eq!(history.len(), 3); + assert_eq!(history.get(0).unwrap(), (EscrowState::Pending, 1_000)); + assert_eq!(history.get(1).unwrap(), (EscrowState::Funded, 1_100)); + assert_eq!(history.get(2).unwrap(), (EscrowState::Shipped, 1_200)); +} + +#[test] +fn state_history_rejects_unknown_escrow() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _seller, _resolver, _token, _admin) = setup(&env); + + assert_eq!( + client.try_get_state_history(&99_u64), + Err(Ok(ContractError::EscrowNotFound)) + ); +} diff --git a/contracts/escrow/src/types.rs b/contracts/escrow/src/types.rs index 4478cee4..e6687834 100644 --- a/contracts/escrow/src/types.rs +++ b/contracts/escrow/src/types.rs @@ -5,6 +5,7 @@ use soroban_sdk::{contracttype, Address, BytesN, Env, String, Symbol, Vec}; pub enum DataKey { Admin, Escrow(u64), + EscrowStateHistory(u64), EscrowCounter, FeeCollector, Dispute(u64), From 95f06429387d678c2004859cd40d34d63a67a59f Mon Sep 17 00:00:00 2001 From: Justice Date: Wed, 1 Jul 2026 09:37:58 +0100 Subject: [PATCH 3/3] chore: remove stale conflict markers --- contracts/escrow/src/test.rs | 53 ------------------------------------ 1 file changed, 53 deletions(-) diff --git a/contracts/escrow/src/test.rs b/contracts/escrow/src/test.rs index 60941720..a271544c 100644 --- a/contracts/escrow/src/test.rs +++ b/contracts/escrow/src/test.rs @@ -292,59 +292,6 @@ fn test_auto_release_before_window_fails() { )); } -/* -#[test] -fn test_raise_dispute_invalid_evidence_hash_rejected() { - let (env, admin, seller, buyer, resolver, token, fee_collector) = setup_env(); - let contract_id = env.register(Escrow, ()); - let client = EscrowClient::new(&env, &contract_id); - client.initialize(&admin, &fee_collector, &0_u32); - - mint_tokens(&env, &token, &buyer, 1000); - - let payees = single_payee(&env, &seller); - let id = client.create_escrow(&payees, &None::
, &resolver, &token, &100_i128, &200_u32, &0_u32, &3600_u64); - client.fund_escrow(&id, &buyer); - client.mark_shipped( - &seller, - &id, - &SorobanString::from_str(&env, "TRACK-BAD-HASH"), - ); - - let short_hash = BytesN::from_array(&env, &[0u8; 32]); - let res = client.try_raise_dispute( - &buyer, - &id, - &Symbol::new(&env, "reason"), - &SorobanString::from_str(&env, "desc"), - &short_hash, -======= ->>>>>>> 6329d33 (fixed ci failure) - ); - client.fund_escrow(&id, &buyer); - client.mark_shipped( - &seller, - &id, - &SorobanString::from_str(&env, "TRACK-BAD-HASH"), - ); - - let short_hash = soroban_sdk::Bytes::from_slice(&env, &[0u8; 16]); - let res = env.try_invoke_contract::<(), ContractError>( - &contract_id, - &Symbol::new(&env, "raise_dispute"), - soroban_sdk::vec![ - &env, - buyer.into_val(&env), - id.into_val(&env), - Symbol::new(&env, "reason").into_val(&env), - SorobanString::from_str(&env, "desc").into_val(&env), - short_hash.into_val(&env), - ], - ); - assert!(res.is_err()); -} -*/ - #[test] fn test_raise_dispute_only_once() { let (env, admin, seller, buyer, resolver, token, fee_collector) = setup_env();