Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 72 additions & 1 deletion contracts/atomic_swap/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ pub enum ContractError {
SchemaNotGreater = 29,
MissingFunc = 30,
FuncChanged = 31,
InsufficientReputation = 40,
}

// ── TTL ───────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -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 ─────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"),),
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 ─────────────────────────────────────────────────────────────────────
Expand Down
271 changes: 271 additions & 0 deletions contracts/atomic_swap/src/reputation_tests.rs
Original file line number Diff line number Diff line change
@@ -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, &registry_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, &registry_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, &registry_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, &registry_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, &registry_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, &registry_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, &registry_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, &registry_id);
let client = AtomicSwapClient::new(&env, &contract_id);

// Complete 10 swaps: 50 + 10*5 = 100
let registry = IpRegistryClient::new(&env, &registry_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, &registry_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, &registry_id);
let client = AtomicSwapClient::new(&env, &contract_id);

// Boost buyer reputation to 55 via a prior successful swap
let registry = IpRegistryClient::new(&env, &registry_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);
}
}