diff --git a/contracts/atomic_swap/src/lib.rs b/contracts/atomic_swap/src/lib.rs index 795e656..44e7649 100644 --- a/contracts/atomic_swap/src/lib.rs +++ b/contracts/atomic_swap/src/lib.rs @@ -54,6 +54,7 @@ pub enum ContractError { SchemaNotGreater = 29, MissingFunc = 30, FuncChanged = 31, + InsufficientReputation = 40, } // ── TTL ─────────────────────────────────────────────────────────────────────── @@ -126,6 +127,10 @@ pub enum DataKey { SwapMode(u64), /// Escrow: maps swap_id → deposited amount (set when buyer deposits). EscrowDeposit(u64), + /// Maps address → reputation score (0–100). + UserReputation(Address), + /// Maps ip_id → minimum buyer reputation required (set by seller per swap). + ReputationMultiplier(u64), } // ── Types ───────────────────────────────────────────────────────────────────── @@ -395,6 +400,24 @@ impl AtomicSwap { Self::evaluate_conditions(&env, &swap); } + // Check minimum reputation requirement set by seller + if let Some(min_rep) = env + .storage() + .persistent() + .get::<_, u32>(&DataKey::ReputationMultiplier(swap_id)) + { + let buyer_rep = env + .storage() + .persistent() + .get::<_, u32>(&DataKey::UserReputation(swap.buyer.clone())) + .unwrap_or(50u32); + if buyer_rep < min_rep { + env.panic_with_error(Error::from_contract_error( + ContractError::InsufficientReputation as u32, + )); + } + } + // #350: Deposit collateral if required if swap.collateral_amount > 0 { // Check if collateral already deposited @@ -602,7 +625,8 @@ impl AtomicSwap { } // #359: Update reputation on completion - // reputation::update_reputation_on_completion(&env, &swap.seller, &swap.buyer); + Self::update_reputation(&env, &swap.seller, 5); + Self::update_reputation(&env, &swap.buyer, 5); env.events().publish( (soroban_sdk::symbol_short!("key_rev"),), @@ -962,6 +986,9 @@ impl AtomicSwap { // #253: Log history entry Self::append_history(&env, swap_id, SwapStatus::Cancelled); + // Update reputation: canceller loses 10 points + Self::update_reputation(&env, &canceller, -10); + env.events().publish( (soroban_sdk::symbol_short!("swap_cncl"),), SwapCancelledEvent { swap_id, canceller }, @@ -1026,6 +1053,9 @@ impl AtomicSwap { // #253: Log history entry Self::append_history(&env, swap_id, SwapStatus::Cancelled); + // Seller defaulted (expired without revealing key): seller loses 10 points + Self::update_reputation(&env, &swap.seller, -10); + env.events().publish( (soroban_sdk::symbol_short!("s_cancel"),), SwapCancelledEvent { @@ -2825,6 +2855,47 @@ impl AtomicSwap { SwapCancelledEvent { swap_id, canceller: swap.buyer }, ); } + + // ── Reputation ──────────────────────────────────────────────────────────── + + /// Returns the reputation score (0–100) for an address. Defaults to 50. + pub fn get_reputation(env: Env, address: Address) -> u32 { + env.storage() + .persistent() + .get(&DataKey::UserReputation(address)) + .unwrap_or(50u32) + } + + /// Seller sets a minimum buyer reputation required for a specific swap. + /// Must be called by the swap's seller before the buyer accepts. + pub fn set_reputation_multiplier(env: Env, swap_id: u64, min_reputation: u32) { + let swap = require_swap_exists(&env, swap_id); + swap.seller.require_auth(); + require_swap_status(&env, &swap, SwapStatus::Pending, ContractError::NotPending); + + env.storage() + .persistent() + .set(&DataKey::ReputationMultiplier(swap_id), &min_reputation); + env.storage() + .persistent() + .extend_ttl(&DataKey::ReputationMultiplier(swap_id), LEDGER_BUMP, LEDGER_BUMP); + } + + /// Internal: adjust reputation score, clamped to [0, 100]. + fn update_reputation(env: &Env, address: &Address, delta: i32) { + let current: u32 = env + .storage() + .persistent() + .get(&DataKey::UserReputation(address.clone())) + .unwrap_or(50u32); + let updated = (current as i32).saturating_add(delta).clamp(0, 100) as u32; + env.storage() + .persistent() + .set(&DataKey::UserReputation(address.clone()), &updated); + env.storage() + .persistent() + .extend_ttl(&DataKey::UserReputation(address.clone()), LEDGER_BUMP, LEDGER_BUMP); + } } // ── Tests ───────────────────────────────────────────────────────────────────── diff --git a/contracts/atomic_swap/src/reputation_tests.rs b/contracts/atomic_swap/src/reputation_tests.rs new file mode 100644 index 0000000..98a2bc6 --- /dev/null +++ b/contracts/atomic_swap/src/reputation_tests.rs @@ -0,0 +1,271 @@ +#[cfg(test)] +mod reputation_tests { + use ip_registry::{IpRegistry, IpRegistryClient}; + use soroban_sdk::{ + testutils::{Address as _, Ledger}, + token::StellarAssetClient, + Address, Bytes, BytesN, Env, + }; + + use crate::{AtomicSwap, AtomicSwapClient, ContractError, SwapStatus}; + + // ── Helpers ─────────────────────────────────────────────────────────────── + + fn setup_registry(env: &Env, owner: &Address) -> (Address, u64, BytesN<32>, BytesN<32>) { + let registry_id = env.register(IpRegistry, ()); + let registry = IpRegistryClient::new(env, ®istry_id); + + let secret = BytesN::from_array(env, &[0xAAu8; 32]); + let blinding = BytesN::from_array(env, &[0xBBu8; 32]); + + let mut preimage = Bytes::new(env); + preimage.append(&Bytes::from(secret.clone())); + preimage.append(&Bytes::from(blinding.clone())); + let commitment_hash: BytesN<32> = env.crypto().sha256(&preimage).into(); + + let ip_id = registry.commit_ip(owner, &commitment_hash); + (registry_id, ip_id, secret, blinding) + } + + fn setup_token(env: &Env, admin: &Address, recipient: &Address, amount: i128) -> Address { + let token_id = env + .register_stellar_asset_contract_v2(admin.clone()) + .address(); + StellarAssetClient::new(env, &token_id).mint(recipient, &amount); + token_id + } + + fn setup_swap_contract(env: &Env, registry_id: &Address) -> Address { + let contract_id = env.register(AtomicSwap, ()); + AtomicSwapClient::new(env, &contract_id).initialize(registry_id); + contract_id + } + + // ── Tests ───────────────────────────────────────────────────────────────── + + #[test] + fn test_default_reputation_is_50() { + let env = Env::default(); + env.mock_all_auths(); + + let seller = Address::generate(&env); + let (registry_id, _, _, _) = setup_registry(&env, &seller); + let contract_id = setup_swap_contract(&env, ®istry_id); + let client = AtomicSwapClient::new(&env, &contract_id); + + let unknown = Address::generate(&env); + assert_eq!(client.get_reputation(&unknown), 50); + } + + #[test] + fn test_reputation_increases_on_successful_swap() { + let env = Env::default(); + env.mock_all_auths(); + + let seller = Address::generate(&env); + let buyer = Address::generate(&env); + let (registry_id, ip_id, secret, blinding) = setup_registry(&env, &seller); + let token_id = setup_token(&env, &seller, &buyer, 1_000_000); + let contract_id = setup_swap_contract(&env, ®istry_id); + let client = AtomicSwapClient::new(&env, &contract_id); + + let swap_id = client.initiate_swap( + &token_id, &ip_id, &seller, &1000i128, &buyer, + &0u32, &None, &0i128, &false, + ); + client.accept_swap(&swap_id); + client.reveal_key(&swap_id, &seller, &secret, &blinding); + + assert_eq!(client.get_reputation(&seller), 55); + assert_eq!(client.get_reputation(&buyer), 55); + } + + #[test] + fn test_reputation_decreases_on_cancel_swap() { + let env = Env::default(); + env.mock_all_auths(); + + let seller = Address::generate(&env); + let buyer = Address::generate(&env); + let (registry_id, ip_id, _, _) = setup_registry(&env, &seller); + let token_id = setup_token(&env, &seller, &buyer, 1_000_000); + let contract_id = setup_swap_contract(&env, ®istry_id); + let client = AtomicSwapClient::new(&env, &contract_id); + + let swap_id = client.initiate_swap( + &token_id, &ip_id, &seller, &1000i128, &buyer, + &0u32, &None, &0i128, &false, + ); + client.cancel_swap(&swap_id, &seller); + + assert_eq!(client.get_reputation(&seller), 40); + } + + #[test] + fn test_reputation_decreases_on_cancel_expired_swap() { + let env = Env::default(); + env.mock_all_auths(); + + let seller = Address::generate(&env); + let buyer = Address::generate(&env); + let (registry_id, ip_id, _, _) = setup_registry(&env, &seller); + let token_id = setup_token(&env, &seller, &buyer, 1_000_000); + let contract_id = setup_swap_contract(&env, ®istry_id); + let client = AtomicSwapClient::new(&env, &contract_id); + + let swap_id = client.initiate_swap( + &token_id, &ip_id, &seller, &1000i128, &buyer, + &0u32, &None, &0i128, &false, + ); + client.accept_swap(&swap_id); + + // Advance time past expiry (7 days + 1) + env.ledger().with_mut(|l| l.timestamp += 604801); + + client.cancel_expired_swap(&swap_id, &buyer); + + // Seller defaulted: seller loses 10 points + assert_eq!(client.get_reputation(&seller), 40); + } + + #[test] + fn test_reputation_clamped_at_zero() { + let env = Env::default(); + env.mock_all_auths(); + + let seller = Address::generate(&env); + let buyer = Address::generate(&env); + let (registry_id, ip_id, _, _) = setup_registry(&env, &seller); + let token_id = setup_token(&env, &seller, &buyer, 1_000_000); + let contract_id = setup_swap_contract(&env, ®istry_id); + let client = AtomicSwapClient::new(&env, &contract_id); + + // Cancel 6 times to drive reputation from 50 down to 0 (50 - 6*10 = -10 → clamped 0) + for i in 0..6u64 { + // Need a fresh IP for each swap since active swap lock is released on cancel + let registry = IpRegistryClient::new(&env, ®istry_id); + let secret = BytesN::from_array(&env, &[(i as u8) + 1; 32]); + let blinding = BytesN::from_array(&env, &[(i as u8) + 0x80; 32]); + let mut preimage = Bytes::new(&env); + preimage.append(&Bytes::from(secret.clone())); + preimage.append(&Bytes::from(blinding.clone())); + let hash: BytesN<32> = env.crypto().sha256(&preimage).into(); + let ip_id_i = registry.commit_ip(&seller, &hash); + + let swap_id = client.initiate_swap( + &token_id, &ip_id_i, &seller, &100i128, &buyer, + &0u32, &None, &0i128, &false, + ); + client.cancel_swap(&swap_id, &seller); + } + + assert_eq!(client.get_reputation(&seller), 0); + let _ = ip_id; // suppress unused warning + } + + #[test] + fn test_reputation_clamped_at_100() { + let env = Env::default(); + env.mock_all_auths(); + + let seller = Address::generate(&env); + let buyer = Address::generate(&env); + let (registry_id, ip_id, secret, blinding) = setup_registry(&env, &seller); + let token_id = setup_token(&env, &seller, &buyer, 10_000_000); + let contract_id = setup_swap_contract(&env, ®istry_id); + let client = AtomicSwapClient::new(&env, &contract_id); + + // Complete 10 swaps: 50 + 10*5 = 100 + let registry = IpRegistryClient::new(&env, ®istry_id); + for i in 0..10u64 { + let s = BytesN::from_array(&env, &[(i as u8) + 1; 32]); + let b = BytesN::from_array(&env, &[(i as u8) + 0x80; 32]); + let mut preimage = Bytes::new(&env); + preimage.append(&Bytes::from(s.clone())); + preimage.append(&Bytes::from(b.clone())); + let hash: BytesN<32> = env.crypto().sha256(&preimage).into(); + let ip_id_i = registry.commit_ip(&seller, &hash); + + let swap_id = client.initiate_swap( + &token_id, &ip_id_i, &seller, &100i128, &buyer, + &0u32, &None, &0i128, &false, + ); + client.accept_swap(&swap_id); + client.reveal_key(&swap_id, &seller, &s, &b); + } + + assert_eq!(client.get_reputation(&seller), 100); + let _ = (ip_id, secret, blinding); + } + + #[test] + fn test_set_reputation_multiplier_enforced() { + let env = Env::default(); + env.mock_all_auths(); + + let seller = Address::generate(&env); + let buyer = Address::generate(&env); + let (registry_id, ip_id, _, _) = setup_registry(&env, &seller); + let token_id = setup_token(&env, &seller, &buyer, 1_000_000); + let contract_id = setup_swap_contract(&env, ®istry_id); + let client = AtomicSwapClient::new(&env, &contract_id); + + let swap_id = client.initiate_swap( + &token_id, &ip_id, &seller, &1000i128, &buyer, + &0u32, &None, &0i128, &false, + ); + + // Seller requires buyer reputation ≥ 80; buyer has default 50 + client.set_reputation_multiplier(&swap_id, &80u32); + + let result = client.try_accept_swap(&swap_id); + assert!( + result.is_err(), + "accept_swap must fail when buyer reputation is below minimum" + ); + } + + #[test] + fn test_set_reputation_multiplier_passes_when_met() { + let env = Env::default(); + env.mock_all_auths(); + + let seller = Address::generate(&env); + let buyer = Address::generate(&env); + let (registry_id, ip_id, secret, blinding) = setup_registry(&env, &seller); + let token_id = setup_token(&env, &seller, &buyer, 1_000_000); + let contract_id = setup_swap_contract(&env, ®istry_id); + let client = AtomicSwapClient::new(&env, &contract_id); + + // Boost buyer reputation to 55 via a prior successful swap + let registry = IpRegistryClient::new(&env, ®istry_id); + let s2 = BytesN::from_array(&env, &[0x11u8; 32]); + let b2 = BytesN::from_array(&env, &[0x22u8; 32]); + let mut preimage = Bytes::new(&env); + preimage.append(&Bytes::from(s2.clone())); + preimage.append(&Bytes::from(b2.clone())); + let hash2: BytesN<32> = env.crypto().sha256(&preimage).into(); + let ip_id2 = registry.commit_ip(&seller, &hash2); + + let swap_id2 = client.initiate_swap( + &token_id, &ip_id2, &seller, &100i128, &buyer, + &0u32, &None, &0i128, &false, + ); + client.accept_swap(&swap_id2); + client.reveal_key(&swap_id2, &seller, &s2, &b2); + // buyer reputation is now 55 + + // Now initiate the real swap with min_reputation = 55 + let swap_id = client.initiate_swap( + &token_id, &ip_id, &seller, &1000i128, &buyer, + &0u32, &None, &0i128, &false, + ); + client.set_reputation_multiplier(&swap_id, &55u32); + + // Should succeed + client.accept_swap(&swap_id); + let swap = client.get_swap(&swap_id).unwrap(); + assert_eq!(swap.status, SwapStatus::Accepted); + let _ = (secret, blinding); + } +}