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
16 changes: 8 additions & 8 deletions contracts/atomic_swap/src/arbitration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ mod arbitration_tests {
let client = AtomicSwapClient::new(env, &contract_id);
client.initialize(&registry_id);

let swap_id = client.initiate_swap(&token_id, &ip_id, &seller, &500_i128, &buyer, &0_u32);
let swap_id = client.initiate_swap(&token_id, &ip_id, &seller, &500_i128, &buyer, &0_u32, &None, &0_i128, &false);
client.accept_swap(&swap_id);
client.raise_dispute(&swap_id);

Expand Down Expand Up @@ -95,7 +95,7 @@ mod arbitration_tests {
let client = AtomicSwapClient::new(&env, &contract_id);
client.initialize(&registry_id);

let swap_id = client.initiate_swap(&token_id, &ip_id, &seller, &500_i128, &buyer, &0_u32);
let swap_id = client.initiate_swap(&token_id, &ip_id, &seller, &500_i128, &buyer, &0_u32, &None, &0_i128, &false);
// Swap is Pending, not Disputed — should panic
let admin = Address::generate(&env);
let arbitrator = Address::generate(&env);
Expand Down Expand Up @@ -253,7 +253,7 @@ mod arbitration_tests {
client.initialize(&registry_id);

// Initiate with flat price 500
let swap_id = client.initiate_swap(&token_id, &ip_id, &seller, &500_i128, &buyer, &0_u32);
let swap_id = client.initiate_swap(&token_id, &ip_id, &seller, &500_i128, &buyer, &0_u32, &None, &0_i128, &false);

// Accept with quantity=1 (no tiers set, uses flat price)
client.accept_swap_with_quantity(&swap_id, &1_u32);
Expand All @@ -278,7 +278,7 @@ mod arbitration_tests {
let client = AtomicSwapClient::new(&env, &contract_id);
client.initialize(&registry_id);

let swap_id = client.initiate_swap(&token_id, &ip_id, &seller, &1000_i128, &buyer, &0_u32);
let swap_id = client.initiate_swap(&token_id, &ip_id, &seller, &1000_i128, &buyer, &0_u32, &None, &0_i128, &false);
client.accept_swap_with_quantity(&swap_id, &5_u32);

let swap = client.get_swap(&swap_id).unwrap();
Expand Down Expand Up @@ -306,7 +306,7 @@ mod arbitration_tests {

// Initiate with price=1000, default quantity=1 — set quantity via initiate_swap
// then manually bump quantity to 10 by accepting partial
let swap_id = client.initiate_swap(&token_id, &ip_id, &seller, &1000_i128, &buyer, &0_u32);
let swap_id = client.initiate_swap(&token_id, &ip_id, &seller, &1000_i128, &buyer, &0_u32, &None, &0_i128, &false);

// Patch quantity to 10 so partial acceptance makes sense
let mut swap = client.get_swap(&swap_id).unwrap();
Expand Down Expand Up @@ -342,7 +342,7 @@ mod arbitration_tests {
let client = AtomicSwapClient::new(&env, &contract_id);
client.initialize(&registry_id);

let swap_id = client.initiate_swap(&token_id, &ip_id, &seller, &500_i128, &buyer, &0_u32);
let swap_id = client.initiate_swap(&token_id, &ip_id, &seller, &500_i128, &buyer, &0_u32, &None, &0_i128, &false);

// quantity=1 (default), accepting 1/1 = full price
client.accept_swap_partial(&swap_id, &1_u32);
Expand All @@ -368,7 +368,7 @@ mod arbitration_tests {
let client = AtomicSwapClient::new(&env, &contract_id);
client.initialize(&registry_id);

let swap_id = client.initiate_swap(&token_id, &ip_id, &seller, &500_i128, &buyer, &0_u32);
let swap_id = client.initiate_swap(&token_id, &ip_id, &seller, &500_i128, &buyer, &0_u32, &None, &0_i128, &false);
client.accept_swap_partial(&swap_id, &0_u32);
}

Expand All @@ -388,7 +388,7 @@ mod arbitration_tests {
let client = AtomicSwapClient::new(&env, &contract_id);
client.initialize(&registry_id);

let swap_id = client.initiate_swap(&token_id, &ip_id, &seller, &500_i128, &buyer, &0_u32);
let swap_id = client.initiate_swap(&token_id, &ip_id, &seller, &500_i128, &buyer, &0_u32, &None, &0_i128, &false);
// quantity=1 by default, requesting 2 should panic
client.accept_swap_partial(&swap_id, &2_u32);
}
Expand Down
2 changes: 1 addition & 1 deletion contracts/atomic_swap/src/escrow_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ mod tests {
let client = AtomicSwapClient::new(&env, &contract_id);

// Regular atomic swap
let swap_id = client.initiate_swap(&token_id, &ip_id, &seller, &500_i128, &buyer, &0_u32, &None);
let swap_id = client.initiate_swap(&token_id, &ip_id, &seller, &500_i128, &buyer, &0_u32, &None, &0_i128, &false);

// escrow_deposit on an atomic swap must panic
client.escrow_deposit(&swap_id);
Expand Down
164 changes: 151 additions & 13 deletions contracts/atomic_swap/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ pub enum ContractError {
OraclePriceInvalid = 47,
OraclePriceBelowMin = 48,
OraclePriceAboveMax = 49,
/// #314: Arbitration errors
ArbitratorAlreadySet = 35,
NotArbitrator = 36,
NoArbitratorSet = 37,
/// #313: Dispute evidence errors
UnauthorizedEvidenceSubmitter = 38,
}

// ── TTL ───────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -159,6 +165,10 @@ pub enum DataKey {
CompletionTimestamp(u64),
/// #470: Price oracle configuration (oracle contract address + enabled flag).
OracleConfig,
/// #314: Maps swap_id → arbitrator Address for dispute resolution.
SwapArbitrator(u64),
/// #313: Maps swap_id → Vec<BytesN<32>> of dispute evidence hashes.
DisputeEvidence(u64),
}

// ── Types ─────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -227,6 +237,8 @@ pub struct SwapRecord {
pub paid_amount: i128,
/// #installments: Whether this swap uses an installment payment schedule.
pub is_installment: bool,
/// #314: Optional arbitrator address for dispute resolution.
pub arbitrator: Option<Address>,
}

// ── Events ────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -395,6 +407,7 @@ impl AtomicSwap {
conditions: Vec::new(&env),
paid_amount: 0,
is_installment: false,
arbitrator: None,
};

// Store insurance premium in dedicated key so accept_swap can collect it
Expand Down Expand Up @@ -1089,9 +1102,130 @@ impl AtomicSwap {

// ── #314: Third-Party Arbitration ─────────────────────────────────────────

// set_arbitrator removed - arbitrator field not in SwapRecord
/// Admin sets an arbitrator for a disputed swap. Can only be set once.
pub fn set_arbitrator(env: Env, swap_id: u64, admin: Address, arbitrator: Address) {
admin.require_auth();
require_admin(&env, &admin);

let mut swap = require_swap_exists(&env, swap_id);
require_swap_status(&env, &swap, SwapStatus::Disputed, ContractError::NotDisputed);

if env.storage().persistent().has(&DataKey::SwapArbitrator(swap_id)) {
env.panic_with_error(Error::from_contract_error(
ContractError::ArbitratorAlreadySet as u32,
));
}

env.storage()
.persistent()
.set(&DataKey::SwapArbitrator(swap_id), &arbitrator);
env.storage()
.persistent()
.extend_ttl(&DataKey::SwapArbitrator(swap_id), LEDGER_BUMP, LEDGER_BUMP);

swap.arbitrator = Some(arbitrator.clone());
swap::save_swap(&env, swap_id, &swap);

env.events().publish(
(soroban_sdk::symbol_short!("arb_set"),),
ArbitratorSetEvent { swap_id, arbitrator },
);
}

/// Arbitrator resolves a disputed swap. refund=true refunds buyer; false completes to seller.
pub fn arbitrate_dispute(env: Env, swap_id: u64, arbitrator: Address, refund: bool) {
arbitrator.require_auth();

let stored_arbitrator: Address = env
.storage()
.persistent()
.get(&DataKey::SwapArbitrator(swap_id))
.unwrap_or_else(|| {
env.panic_with_error(Error::from_contract_error(
ContractError::NoArbitratorSet as u32,
))
});

// arbitrate_dispute removed - arbitrator field not in SwapRecord
if arbitrator != stored_arbitrator {
env.panic_with_error(Error::from_contract_error(
ContractError::NotArbitrator as u32,
));
}

let mut swap = require_swap_exists(&env, swap_id);
require_swap_status(&env, &swap, SwapStatus::Disputed, ContractError::NotDisputed);

let token_client = token::Client::new(&env, &swap.token);

if refund {
token_client.transfer(&env.current_contract_address(), &swap.buyer, &swap.price);
swap.status = SwapStatus::Cancelled;
} else {
let config = Self::protocol_config(&env);
let fee_amount = if config.protocol_fee_bps > 0 {
(swap.price * config.protocol_fee_bps as i128) / 10000
} else {
0
};
let seller_amount = swap.price - fee_amount;
if fee_amount > 0 {
token_client.transfer(&env.current_contract_address(), &config.treasury, &fee_amount);
}
token_client.transfer(&env.current_contract_address(), &swap.seller, &seller_amount);
swap.status = SwapStatus::Completed;
}

swap::save_swap(&env, swap_id, &swap);
env.storage().persistent().remove(&DataKey::ActiveSwap(swap.ip_id));
env.storage().persistent().remove(&DataKey::SwapArbitrator(swap_id));

Self::append_history(&env, swap_id, swap.status.clone());

env.events().publish(
(soroban_sdk::symbol_short!("arb_dec"),),
ArbitratedEvent { swap_id, arbitrator, refunded: refund },
);
}

/// Buyer or seller submits dispute evidence (a hash of off-chain evidence).
pub fn submit_dispute_evidence(
env: Env,
swap_id: u64,
submitter: Address,
evidence_hash: BytesN<32>,
) {
submitter.require_auth();
let swap = require_swap_exists(&env, swap_id);

if submitter != swap.buyer && submitter != swap.seller {
env.panic_with_error(Error::from_contract_error(
ContractError::UnauthorizedEvidenceSubmitter as u32,
));
}

let key = DataKey::DisputeEvidence(swap_id);
let mut evidence: Vec<BytesN<32>> = env
.storage()
.persistent()
.get(&key)
.unwrap_or(Vec::new(&env));
evidence.push_back(evidence_hash.clone());
env.storage().persistent().set(&key, &evidence);
env.storage().persistent().extend_ttl(&key, LEDGER_BUMP, LEDGER_BUMP);

env.events().publish(
(soroban_sdk::symbol_short!("evid_sub"),),
DisputeEvidenceSubmittedEvent { swap_id, submitter, evidence_hash },
);
}

/// Returns all dispute evidence hashes submitted for a swap.
pub fn get_dispute_evidence(env: Env, swap_id: u64) -> Vec<BytesN<32>> {
env.storage()
.persistent()
.get(&DataKey::DisputeEvidence(swap_id))
.unwrap_or(Vec::new(&env))
}

// submit_dispute_evidence removed - DisputeEvidence DataKey variant not defined
// get_dispute_evidence removed - DisputeEvidence DataKey variant not defined
Expand Down Expand Up @@ -1862,6 +1996,7 @@ impl AtomicSwap {
conditions: Vec::new(&env),
paid_amount: 0,
is_installment: false,
arbitrator: None,
};

env.storage().persistent().set(&DataKey::Swap(id), &swap);
Expand Down Expand Up @@ -2143,6 +2278,7 @@ impl AtomicSwap {
conditions: Vec::new(&env),
paid_amount: 0,
is_installment: false,
arbitrator: None,
};

env.storage()
Expand Down Expand Up @@ -2611,8 +2747,6 @@ impl AtomicSwap {
arbitrator.require_auth();

// Verify arbitrator is set and matches
// SwapArbitrator DataKey variant not defined - commenting out
/*
let stored_arbitrator: Address = env
.storage()
.persistent()
Expand All @@ -2628,7 +2762,6 @@ impl AtomicSwap {
ContractError::NotArbitrator as u32,
));
}
*/

let token_client = token::Client::new(&env, &swap.token);

Expand Down Expand Up @@ -2719,8 +2852,7 @@ impl AtomicSwap {
env.storage()
.persistent()
.remove(&DataKey::ActiveSwap(swap.ip_id));
// SwapArbitrator DataKey variant not defined
// env.storage().persistent().remove(&DataKey::SwapArbitrator(swap_id));
env.storage().persistent().remove(&DataKey::SwapArbitrator(swap_id));

env.events().publish(
(soroban_sdk::symbol_short!("arb_dec"),),
Expand Down Expand Up @@ -3248,6 +3380,7 @@ impl AtomicSwap {
conditions: Vec::new(&env),
paid_amount: 0,
is_installment: false,
arbitrator: None,
};

if insurance_enabled {
Expand Down Expand Up @@ -3405,6 +3538,7 @@ impl AtomicSwap {
conditions: Vec::new(&env),
paid_amount: 0,
is_installment: false,
arbitrator: None,
};

env.storage().persistent().set(&DataKey::Swap(id), &swap);
Expand Down Expand Up @@ -3480,6 +3614,7 @@ impl AtomicSwap {
conditions: Vec::new(&env),
paid_amount: 0,
is_installment: false,
arbitrator: None,
};

env.storage().persistent().set(&DataKey::Swap(id), &swap);
Expand Down Expand Up @@ -3768,6 +3903,7 @@ impl AtomicSwap {
conditions: Vec::new(&env),
paid_amount: 0,
is_installment: false,
arbitrator: None,
};

env.storage().persistent().set(&DataKey::Swap(id), &swap);
Expand Down Expand Up @@ -4132,18 +4268,12 @@ impl AtomicSwap {
// #[cfg(test)]
// mod tests;

// #[cfg(test)]
// mod escrow_tests;

// #[cfg(test)]
// mod prop_tests;

// #[cfg(test)]
// mod regression_tests;

// #[cfg(test)]
// mod arbitration_tests;

// #[cfg(test)]
// mod benchmarks;

Expand All @@ -4156,6 +4286,12 @@ impl AtomicSwap {
// #[cfg(test)]
// mod upgrade_chaos_tests;

#[cfg(test)]
mod escrow_tests;

#[cfg(test)]
mod arbitration_tests;

include!("multi_signer_tests.rs");

#[cfg(test)]
Expand Down Expand Up @@ -4196,6 +4332,7 @@ mod installment_tests {
conditions: Vec::new(env),
paid_amount: paid,
is_installment,
arbitrator: None,
}
}

Expand Down Expand Up @@ -4366,6 +4503,7 @@ mod batch_enhancement_tests {
conditions: Vec::new(env),
paid_amount: 0,
is_installment: false,
arbitrator: None,
};
env.storage().persistent().set(&DataKey::Swap(id), &swap);
env.storage().persistent().extend_ttl(&DataKey::Swap(id), LEDGER_BUMP, LEDGER_BUMP);
Expand Down
Loading