diff --git a/crates/contracts/core/src/lib.rs b/crates/contracts/core/src/lib.rs index 223ad49..68fd637 100644 --- a/crates/contracts/core/src/lib.rs +++ b/crates/contracts/core/src/lib.rs @@ -42,6 +42,7 @@ pub enum SessionStatus { Disputed = 2, Cancelled = 3, Locked = 4, + Resolved = 5, } /// Pending upgrade information for 2-phase commit upgrade pattern @@ -120,6 +121,10 @@ pub struct Session { pub payer_approved: bool, pub payee_approved: bool, pub approved_at: u64, + // Resolution fields for dispute resolution + pub resolved_at: u64, + pub resolver: Option
, + pub resolution_note: Option, } const VERSION: u32 = 1; @@ -572,6 +577,9 @@ impl SkillSyncContract { payer_approved: false, payee_approved: false, approved_at: 0, + resolved_at: 0, + resolver: None, + resolution_note: None, }; // Store session (this also checks for duplicate session_id) @@ -964,6 +972,112 @@ impl SkillSyncContract { Ok(total_refund) } + + /// Resolves a disputed escrow by splitting funds between payer and payee. + /// + /// This function can only be called by the admin/arbiter. It allows for partial + /// refunds to the payer and partial payouts to the beneficiary, with fee deduction. + /// + /// # Arguments + /// + /// * `env` - The contract environment + /// * `session_id` - The unique session identifier + /// * `to_payer` - Amount to refund to the payer + /// * `to_payee` - Amount to pay to the payee (beneficiary) + /// * `note` - Optional resolution note/reason + /// + /// # Returns + /// + /// - `Ok(())` if dispute was successfully resolved + /// - `Err(Error::SessionNotFound)` if session doesn't exist + /// - `Err(Error::SessionNotDisputed)` if session status is not Disputed + /// - `Err(Error::Unauthorized)` if caller is not admin/arbiter + /// - `Err(Error::InvalidResolutionAmount)` if amounts don't sum correctly + /// - `Err(Error::TransferError)` if token transfer fails + /// + /// # Events + /// + /// Emits `DisputeResolved { session_id, to_payer, to_payee, fee_total }` upon success + pub fn resolve_dispute( + env: Env, + session_id: Vec, + to_payer: i128, + to_payee: i128, + note: Option, + ) -> Result<(), Error> { + // Require admin/arbiter authorization + let admin = read_admin(&env)?; + admin.require_auth(); + + // Retrieve session + let mut session = Self::get_session(env.clone(), session_id.clone()) + .ok_or(Error::SessionNotFound)?; + + // Validate session status is Disputed + if session.status != SessionStatus::Disputed { + return Err(Error::SessionNotDisputed); + } + + // Calculate the platform fee that was already collected when funds were locked + let fee = session.amount + .checked_mul(session.fee_bps as i128) + .ok_or(Error::ResolutionFeeError)? + .checked_div(10000) + .ok_or(Error::ResolutionFeeError)?; + + // The available amount is the original amount (fee is already taken by platform) + let available_amount = session.amount; + + // Validate that the split amounts sum to the available amount + let total_split = to_payer + .checked_add(to_payee) + .ok_or(Error::InvalidResolutionAmount)?; + + if total_split != available_amount { + return Err(Error::InvalidResolutionAmount); + } + + // Validate non-negative amounts + if to_payer < 0 || to_payee < 0 { + return Err(Error::InvalidResolutionAmount); + } + + // Create token client + let token_client = token::Client::new(&env, &session.asset); + let contract_id = env.current_contract_address(); + + // Transfer to payer (if any) + if to_payer > 0 { + token_client.transfer(&contract_id, &session.payer, &to_payer); + } + + // Transfer to payee (if any) + if to_payee > 0 { + token_client.transfer(&contract_id, &session.payee, &to_payee); + } + + // Update session status and resolution fields + let now = env.ledger().timestamp(); + session.status = SessionStatus::Resolved; + session.updated_at = now; + session.resolved_at = now; + session.resolver = Some(admin); + session.resolution_note = note; + + let key = DataKey::Session(session_id.clone()); + env.storage().persistent().set(&key, &session); + + // Remove from expiry index since session is resolved + Self::remove_from_expiry_index(env.clone(), session_id.clone(), session.expires_at)?; + + // Emit DisputeResolved event + env.events().publish( + (Symbol::new(&env, "DisputeResolved"),), + (session_id, to_payer, to_payee, fee), + ); + + Ok(()) + } } fn read_admin(env: &Env) -> Result { @@ -1222,6 +1336,9 @@ mod tests { payer_approved: false, payee_approved: false, approved_at: 0, + resolved_at: 0, + resolver: None, + resolution_note: None, }; client.put_session(&s); @@ -1272,6 +1389,9 @@ mod tests { payer_approved: false, payee_approved: false, approved_at: 0, + resolved_at: 0, + resolver: None, + resolution_note: None, }; let s2 = Session { @@ -1314,6 +1434,9 @@ mod tests { payer_approved: false, payee_approved: false, approved_at: 0, + resolved_at: 0, + resolver: None, + resolution_note: None, }; client.put_session(&old); @@ -1379,6 +1502,9 @@ mod tests { payer_approved: false, payee_approved: false, approved_at: 0, + resolved_at: 0, + resolver: None, + resolution_note: None, }; // store and ensure we can read back (decode) older versions @@ -1419,6 +1545,9 @@ mod tests { payer_approved: false, payee_approved: false, approved_at: 0, + resolved_at: 0, + resolver: None, + resolution_note: None, }; // First insertion should succeed @@ -1455,6 +1584,9 @@ mod tests { payer_approved: false, payee_approved: false, approved_at: 0, + resolved_at: 0, + resolver: None, + resolution_note: None, }; let mut session2 = session1.clone(); @@ -1495,6 +1627,9 @@ mod tests { payer_approved: false, payee_approved: false, approved_at: 0, + resolved_at: 0, + resolver: None, + resolution_note: None, }; let session2 = Session { @@ -1542,6 +1677,9 @@ mod tests { payer_approved: false, payee_approved: false, approved_at: 0, + resolved_at: 0, + resolver: None, + resolution_note: None, }; // First insertion succeeds @@ -1783,6 +1921,9 @@ mod tests { payer_approved: false, payee_approved: false, approved_at: 0, + resolved_at: 0, + resolver: None, + resolution_note: None, }; client.put_session(&session_min); @@ -2458,6 +2599,9 @@ mod tests { payer_approved: false, payee_approved: false, approved_at: 0, + resolved_at: 0, + resolver: None, + resolution_note: None, }; client.put_session(&session).unwrap(); @@ -2495,6 +2639,9 @@ mod tests { payer_approved: false, payee_approved: false, approved_at: 0, + resolved_at: 0, + resolver: None, + resolution_note: None, }; client.put_session(&session).unwrap(); @@ -3065,6 +3212,9 @@ mod tests { payer_approved: false, payee_approved: false, approved_at: 0, + resolved_at: 0, + resolver: None, + resolution_note: None, }; client.put_session(&session).unwrap(); @@ -3631,6 +3781,11 @@ mod tests { } // ============================================================================ + // resolve_dispute tests + // ============================================================================ + + #[test] + fn test_resolve_dispute_happy_path() { // Upgradeability Tests // ============================================================================ @@ -3647,6 +3802,73 @@ mod tests { let treasury = Address::generate(&env); client.init(&admin, &250, &treasury, &DEFAULT_DISPUTE_WINDOW_SECONDS); + // Setup addresses and token + let payer = Address::generate(&env); + let payee = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract(payer.clone()); + let token_id = Address::from_contract_id(&env, &token_contract); + let token_client = token::Client::new(&env, &token_id); + + // Mint tokens for payer + let amount = 1_000_000_i128; + let fee_bps = 250u32; + let fee = (amount * fee_bps as i128) / 10000; + let total = amount + fee; + token_client.mint(&payer, &total); + + // Create a disputed session directly + let session_id = Bytes::from_array(&env, &[200u8, 201u8]); + let now = env.ledger().timestamp(); + let session = Session { + version: 1, + session_id: session_id.clone(), + payer: payer.clone(), + payee: payee.clone(), + asset: token_id.clone(), + amount, + fee_bps, + status: SessionStatus::Disputed, + created_at: now, + updated_at: now, + dispute_deadline: now + DEFAULT_DISPUTE_WINDOW_SECONDS, + expires_at: now + ESCROW_DURATION_SECONDS, + payer_approved: false, + payee_approved: false, + approved_at: 0, + resolved_at: 0, + resolver: None, + resolution_note: None, + }; + + // Store session and fund the contract + client.put_session(&session); + token_client.mint(&contract_id, &amount); + + // Record balances before resolution + let payer_balance_before = token_client.balance(&payer); + let payee_balance_before = token_client.balance(&payee); + + // Resolve dispute: 30% to payer, 70% to payee + let to_payer = 300_000_i128; + let to_payee = 700_000_i128; + let note = Some(Bytes::from_array(&env, &[1u8, 2u8, 3u8])); + + client.resolve_dispute(&session_id, &to_payer, &to_payee, ¬e); + + // Verify balances after resolution + assert_eq!(token_client.balance(&payer), payer_balance_before + to_payer); + assert_eq!(token_client.balance(&payee), payee_balance_before + to_payee); + + // Verify session status + let resolved_session = client.get_session(&session_id).unwrap(); + assert_eq!(resolved_session.status, SessionStatus::Resolved); + assert_eq!(resolved_session.resolved_at, now); + assert_eq!(resolved_session.resolver, Some(admin)); + assert_eq!(resolved_session.resolution_note, note); + } + + #[test] + fn test_resolve_dispute_requires_admin_auth() { // Propose upgrade with 1 hour timelock let wasm_hash = Bytes::from_array(&env, &[1u8; 32]); let timelock: u64 = 3600; // 1 hour @@ -3687,6 +3909,47 @@ mod tests { let treasury = Address::generate(&env); client.init(&admin, &250, &treasury, &DEFAULT_DISPUTE_WINDOW_SECONDS); + // Setup addresses + let payer = Address::generate(&env); + let payee = Address::generate(&env); + let token_id = Address::generate(&env); + + // Create a disputed session + let session_id = Bytes::from_array(&env, &[202u8, 203u8]); + let now = env.ledger().timestamp(); + let session = Session { + version: 1, + session_id: session_id.clone(), + payer: payer.clone(), + payee: payee.clone(), + asset: token_id.clone(), + amount: 1_000_000, + fee_bps: 250, + status: SessionStatus::Disputed, + created_at: now, + updated_at: now, + dispute_deadline: now + DEFAULT_DISPUTE_WINDOW_SECONDS, + expires_at: now + ESCROW_DURATION_SECONDS, + payer_approved: false, + payee_approved: false, + approved_at: 0, + resolved_at: 0, + resolver: None, + resolution_note: None, + }; + client.put_session(&session); + + // Resolve dispute + client.resolve_dispute(&session_id, &500_000, &500_000, &None); + + // Verify admin auth was required + let auths = env.auths(); + assert_eq!(auths.len(), 1); + assert_eq!(auths[0].0, admin); + } + + #[test] + fn test_resolve_dispute_requires_disputed_status() { // Propose upgrade with 0 timelock (should use default) let wasm_hash = Bytes::from_array(&env, &[1u8; 32]); client.propose_upgrade(&wasm_hash, &0); @@ -3713,6 +3976,43 @@ mod tests { let treasury = Address::generate(&env); client.init(&admin, &250, &treasury, &DEFAULT_DISPUTE_WINDOW_SECONDS); + // Setup addresses + let payer = Address::generate(&env); + let payee = Address::generate(&env); + let token_id = Address::generate(&env); + + // Create a Locked session (not Disputed) + let session_id = Bytes::from_array(&env, &[204u8, 205u8]); + let now = env.ledger().timestamp(); + let session = Session { + version: 1, + session_id: session_id.clone(), + payer: payer.clone(), + payee: payee.clone(), + asset: token_id.clone(), + amount: 1_000_000, + fee_bps: 250, + status: SessionStatus::Locked, + created_at: now, + updated_at: now, + dispute_deadline: now + DEFAULT_DISPUTE_WINDOW_SECONDS, + expires_at: now + ESCROW_DURATION_SECONDS, + payer_approved: false, + payee_approved: false, + approved_at: 0, + resolved_at: 0, + resolver: None, + resolution_note: None, + }; + client.put_session(&session); + + // Try to resolve - should fail because not Disputed + let result = client.try_resolve_dispute(&session_id, &500_000, &500_000, &None); + assert_eq!(result, Err(Ok(Error::SessionNotDisputed))); + } + + #[test] + fn test_resolve_dispute_requires_exact_amount_sum() { // Try to propose upgrade as non-admin (should panic) let non_admin = Address::generate(&env); env.set_auths(&[soroban_sdk::testutils::AuthorizedInvocation { @@ -3741,6 +4041,48 @@ mod tests { let treasury = Address::generate(&env); client.init(&admin, &250, &treasury, &DEFAULT_DISPUTE_WINDOW_SECONDS); + // Setup addresses + let payer = Address::generate(&env); + let payee = Address::generate(&env); + let token_id = Address::generate(&env); + + // Create a disputed session with amount = 1_000_000 + let session_id = Bytes::from_array(&env, &[206u8, 207u8]); + let now = env.ledger().timestamp(); + let session = Session { + version: 1, + session_id: session_id.clone(), + payer: payer.clone(), + payee: payee.clone(), + asset: token_id.clone(), + amount: 1_000_000, + fee_bps: 250, + status: SessionStatus::Disputed, + created_at: now, + updated_at: now, + dispute_deadline: now + DEFAULT_DISPUTE_WINDOW_SECONDS, + expires_at: now + ESCROW_DURATION_SECONDS, + payer_approved: false, + payee_approved: false, + approved_at: 0, + resolved_at: 0, + resolver: None, + resolution_note: None, + }; + client.put_session(&session); + + // Try to resolve with amounts that don't sum to available amount + // Sum is 900_000, but available is 1_000_000 + let result = client.try_resolve_dispute(&session_id, &400_000, &500_000, &None); + assert_eq!(result, Err(Ok(Error::InvalidResolutionAmount))); + + // Try with amounts that sum to more than available + let result = client.try_resolve_dispute(&session_id, &600_000, &500_000, &None); + assert_eq!(result, Err(Ok(Error::InvalidResolutionAmount))); + } + + #[test] + fn test_resolve_dispute_zero_split_to_payer() { // Get initial version let initial_version = client.get_version(); assert_eq!(initial_version, VERSION); @@ -3794,6 +4136,50 @@ mod tests { let treasury = Address::generate(&env); client.init(&admin, &250, &treasury, &DEFAULT_DISPUTE_WINDOW_SECONDS); + // Setup addresses and token + let payer = Address::generate(&env); + let payee = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract(payer.clone()); + let token_id = Address::from_contract_id(&env, &token_contract); + let token_client = token::Client::new(&env, &token_id); + + // Create a disputed session + let amount = 1_000_000_i128; + let session_id = Bytes::from_array(&env, &[208u8, 209u8]); + let now = env.ledger().timestamp(); + let session = Session { + version: 1, + session_id: session_id.clone(), + payer: payer.clone(), + payee: payee.clone(), + asset: token_id.clone(), + amount, + fee_bps: 250, + status: SessionStatus::Disputed, + created_at: now, + updated_at: now, + dispute_deadline: now + DEFAULT_DISPUTE_WINDOW_SECONDS, + expires_at: now + ESCROW_DURATION_SECONDS, + payer_approved: false, + payee_approved: false, + approved_at: 0, + resolved_at: 0, + resolver: None, + resolution_note: None, + }; + client.put_session(&session); + token_client.mint(&contract_id, &amount); + + // Resolve with 0 to payer, all to payee + client.resolve_dispute(&session_id, &0, &amount, &None); + + // Verify session status + let resolved_session = client.get_session(&session_id).unwrap(); + assert_eq!(resolved_session.status, SessionStatus::Resolved); + } + + #[test] + fn test_resolve_dispute_zero_split_to_payee() { // Propose upgrade with 1 hour timelock let wasm_hash = Bytes::from_array(&env, &[1u8; 32]); let timelock: u64 = 3600; @@ -3817,6 +4203,50 @@ mod tests { let treasury = Address::generate(&env); client.init(&admin, &250, &treasury, &DEFAULT_DISPUTE_WINDOW_SECONDS); + // Setup addresses and token + let payer = Address::generate(&env); + let payee = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract(payer.clone()); + let token_id = Address::from_contract_id(&env, &token_contract); + let token_client = token::Client::new(&env, &token_id); + + // Create a disputed session + let amount = 1_000_000_i128; + let session_id = Bytes::from_array(&env, &[210u8, 211u8]); + let now = env.ledger().timestamp(); + let session = Session { + version: 1, + session_id: session_id.clone(), + payer: payer.clone(), + payee: payee.clone(), + asset: token_id.clone(), + amount, + fee_bps: 250, + status: SessionStatus::Disputed, + created_at: now, + updated_at: now, + dispute_deadline: now + DEFAULT_DISPUTE_WINDOW_SECONDS, + expires_at: now + ESCROW_DURATION_SECONDS, + payer_approved: false, + payee_approved: false, + approved_at: 0, + resolved_at: 0, + resolver: None, + resolution_note: None, + }; + client.put_session(&session); + token_client.mint(&contract_id, &amount); + + // Resolve with all to payer, 0 to payee (full refund) + client.resolve_dispute(&session_id, &amount, &0, &None); + + // Verify session status + let resolved_session = client.get_session(&session_id).unwrap(); + assert_eq!(resolved_session.status, SessionStatus::Resolved); + } + + #[test] + fn test_resolve_dispute_rejects_negative_amounts() { // Try to apply upgrade without proposing (should panic) client.apply_upgrade(); } @@ -3835,6 +4265,47 @@ mod tests { let treasury = Address::generate(&env); client.init(&admin, &250, &treasury, &DEFAULT_DISPUTE_WINDOW_SECONDS); + // Setup addresses + let payer = Address::generate(&env); + let payee = Address::generate(&env); + let token_id = Address::generate(&env); + + // Create a disputed session + let session_id = Bytes::from_array(&env, &[212u8, 213u8]); + let now = env.ledger().timestamp(); + let session = Session { + version: 1, + session_id: session_id.clone(), + payer: payer.clone(), + payee: payee.clone(), + asset: token_id.clone(), + amount: 1_000_000, + fee_bps: 250, + status: SessionStatus::Disputed, + created_at: now, + updated_at: now, + dispute_deadline: now + DEFAULT_DISPUTE_WINDOW_SECONDS, + expires_at: now + ESCROW_DURATION_SECONDS, + payer_approved: false, + payee_approved: false, + approved_at: 0, + resolved_at: 0, + resolver: None, + resolution_note: None, + }; + client.put_session(&session); + + // Try with negative to_payer + let result = client.try_resolve_dispute(&session_id, &-100, &1_000_100, &None); + assert_eq!(result, Err(Ok(Error::InvalidResolutionAmount))); + + // Try with negative to_payee + let result = client.try_resolve_dispute(&session_id, &1_000_100, &-100, &None); + assert_eq!(result, Err(Ok(Error::InvalidResolutionAmount))); + } + + #[test] + fn test_resolve_dispute_emits_event() { // Propose upgrade as admin let wasm_hash = Bytes::from_array(&env, &[1u8; 32]); let timelock: u64 = 3600; @@ -3871,6 +4342,47 @@ mod tests { let treasury = Address::generate(&env); client.init(&admin, &250, &treasury, &DEFAULT_DISPUTE_WINDOW_SECONDS); + // Setup addresses and token + let payer = Address::generate(&env); + let payee = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract(payer.clone()); + let token_id = Address::from_contract_id(&env, &token_contract); + let token_client = token::Client::new(&env, &token_id); + + // Create a disputed session + let amount = 1_000_000_i128; + let fee_bps = 250u32; + let session_id = Bytes::from_array(&env, &[214u8, 215u8]); + let now = env.ledger().timestamp(); + let session = Session { + version: 1, + session_id: session_id.clone(), + payer: payer.clone(), + payee: payee.clone(), + asset: token_id.clone(), + amount, + fee_bps, + status: SessionStatus::Disputed, + created_at: now, + updated_at: now, + dispute_deadline: now + DEFAULT_DISPUTE_WINDOW_SECONDS, + expires_at: now + ESCROW_DURATION_SECONDS, + payer_approved: false, + payee_approved: false, + approved_at: 0, + resolved_at: 0, + resolver: None, + resolution_note: None, + }; + client.put_session(&session); + token_client.mint(&contract_id, &amount); + + // Resolve dispute + let to_payer = 400_000_i128; + let to_payee = 600_000_i128; + client.resolve_dispute(&session_id, &to_payer, &to_payee, &None); + + // Verify DisputeResolved event was emitted // Propose upgrade let wasm_hash = Bytes::from_array(&env, &[1u8; 32]); client.propose_upgrade(&wasm_hash, &3600); @@ -3890,6 +4402,7 @@ mod tests { for event in events { if let Some(topics) = event.2.get(0) { if let Ok(symbol) = Symbol::try_from(topics) { + if symbol.to_string(&env) == Some("DisputeResolved".to_string()) { if symbol.to_string(&env) == Some("UpgradeCancelled".to_string()) { found_event = true; break; @@ -3897,6 +4410,11 @@ mod tests { } } } + assert!(found_event, "DisputeResolved event not found"); + } + + #[test] + fn test_resolve_dispute_idempotency() { assert!(found_event, "UpgradeCancelled event not found"); } @@ -3977,6 +4495,50 @@ mod tests { let treasury = Address::generate(&env); client.init(&admin, &250, &treasury, &DEFAULT_DISPUTE_WINDOW_SECONDS); + // Setup addresses and token + let payer = Address::generate(&env); + let payee = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract(payer.clone()); + let token_id = Address::from_contract_id(&env, &token_contract); + let token_client = token::Client::new(&env, &token_id); + + // Create a disputed session + let amount = 1_000_000_i128; + let session_id = Bytes::from_array(&env, &[216u8, 217u8]); + let now = env.ledger().timestamp(); + let session = Session { + version: 1, + session_id: session_id.clone(), + payer: payer.clone(), + payee: payee.clone(), + asset: token_id.clone(), + amount, + fee_bps: 250, + status: SessionStatus::Disputed, + created_at: now, + updated_at: now, + dispute_deadline: now + DEFAULT_DISPUTE_WINDOW_SECONDS, + expires_at: now + ESCROW_DURATION_SECONDS, + payer_approved: false, + payee_approved: false, + approved_at: 0, + resolved_at: 0, + resolver: None, + resolution_note: None, + }; + client.put_session(&session); + token_client.mint(&contract_id, &amount); + + // First resolution + client.resolve_dispute(&session_id, &500_000, &500_000, &None); + + // Second resolution attempt should fail (session not Disputed anymore) + let result = client.try_resolve_dispute(&session_id, &500_000, &500_000, &None); + assert_eq!(result, Err(Ok(Error::SessionNotDisputed))); + } + + #[test] + fn test_resolve_dispute_session_not_found() { // Version should be set during init assert_eq!(client.get_version(), VERSION); } @@ -3995,6 +4557,14 @@ mod tests { let treasury = Address::generate(&env); client.init(&admin, &250, &treasury, &DEFAULT_DISPUTE_WINDOW_SECONDS); + // Try to resolve non-existent session + let non_existent_id = Bytes::from_array(&env, &[255u8, 255u8]); + let result = client.try_resolve_dispute(&non_existent_id, &500_000, &500_000, &None); + assert_eq!(result, Err(Ok(Error::SessionNotFound))); + } + + #[test] + fn test_resolve_dispute_50_50_split() { // Try to propose upgrade with timelock below minimum (should panic) let wasm_hash = Bytes::from_array(&env, &[1u8; 32]); client.propose_upgrade(&wasm_hash, &30); // Less than MIN_UPGRADE_TIMELOCK_SECONDS (60) @@ -4013,6 +4583,54 @@ mod tests { let treasury = Address::generate(&env); client.init(&admin, &250, &treasury, &DEFAULT_DISPUTE_WINDOW_SECONDS); + // Setup addresses and token + let payer = Address::generate(&env); + let payee = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract(payer.clone()); + let token_id = Address::from_contract_id(&env, &token_contract); + let token_client = token::Client::new(&env, &token_id); + + // Create a disputed session + let amount = 1_000_000_i128; + let session_id = Bytes::from_array(&env, &[218u8, 219u8]); + let now = env.ledger().timestamp(); + let session = Session { + version: 1, + session_id: session_id.clone(), + payer: payer.clone(), + payee: payee.clone(), + asset: token_id.clone(), + amount, + fee_bps: 250, + status: SessionStatus::Disputed, + created_at: now, + updated_at: now, + dispute_deadline: now + DEFAULT_DISPUTE_WINDOW_SECONDS, + expires_at: now + ESCROW_DURATION_SECONDS, + payer_approved: false, + payee_approved: false, + approved_at: 0, + resolved_at: 0, + resolver: None, + resolution_note: None, + }; + client.put_session(&session); + token_client.mint(&contract_id, &amount); + + // Record balances before + let payer_balance_before = token_client.balance(&payer); + let payee_balance_before = token_client.balance(&payee); + + // 50-50 split + let to_payer = 500_000_i128; + let to_payee = 500_000_i128; + client.resolve_dispute(&session_id, &to_payer, &to_payee, &None); + + // Verify exact amounts transferred + assert_eq!(token_client.balance(&payer), payer_balance_before + to_payer); + assert_eq!(token_client.balance(&payee), payee_balance_before + to_payee); + } +} // Propose first upgrade let wasm_hash1 = Bytes::from_array(&env, &[1u8; 32]); client.propose_upgrade(&wasm_hash1, &3600);