diff --git a/contracts/invoice-escrow/src/lib.rs b/contracts/invoice-escrow/src/lib.rs
index c654064..f96f7bf 100644
--- a/contracts/invoice-escrow/src/lib.rs
+++ b/contracts/invoice-escrow/src/lib.rs
@@ -75,6 +75,7 @@ impl InvoiceEscrow {
paused: false,
whitelist_enabled: false,
min_investment: 0,
+ dispute_timeout_secs: 604_800,
};
storage::set_config(&env, &config);
Ok(())
@@ -876,6 +877,140 @@ impl InvoiceEscrow {
Ok(config.paused)
}
+ /// Raise a dispute on a Funded escrow.
+ pub fn raise_dispute(
+ env: Env,
+ caller: Address,
+ invoice_id: Symbol,
+ reason: soroban_sdk::Bytes,
+ ) -> Result<(), Error> {
+ caller.require_auth();
+ let config = storage::get_config(&env).ok_or(Error::NotInit)?;
+ ensure_not_paused(&config)?;
+
+ let mut data = storage::get_escrow(&env, invoice_id.clone()).ok_or(Error::EscrowNotFound)?;
+ if data.status != EscrowStatus::Funded {
+ return Err(Error::EscrowNotFunded);
+ }
+
+ let dispute = types::DisputeData {
+ raiser: caller.clone(),
+ reason: reason.clone(),
+ raised_at: env.ledger().timestamp(),
+ resolved: false,
+ };
+ storage::set_dispute(&env, &invoice_id, &dispute);
+
+ data.status = EscrowStatus::Disputed;
+ storage::set_escrow(&env, invoice_id.clone(), &data);
+
+ events::dispute_raised(&env, invoice_id.clone(), &caller, &reason);
+ events::escrow_status_changed(&env, invoice_id, EscrowStatus::Disputed, env.ledger().timestamp());
+ Ok(())
+ }
+
+ /// Resolve a dispute by admin or via timeout.
+ pub fn resolve_dispute(
+ env: Env,
+ admin: Address,
+ invoice_id: Symbol,
+ favour: Symbol,
+ ) -> Result<(), Error> {
+ admin.require_auth();
+ let config = storage::get_config(&env).ok_or(Error::NotInit)?;
+ if config.admin != admin {
+ return Err(Error::Unauthorized);
+ }
+
+ let mut data = storage::get_escrow(&env, invoice_id.clone()).ok_or(Error::EscrowNotFound)?;
+ if data.status != EscrowStatus::Disputed {
+ return Err(Error::NotDisputed);
+ }
+
+ let mut dispute = storage::get_dispute(&env, &invoice_id).ok_or(Error::NotDisputed)?;
+ if dispute.resolved {
+ return Err(Error::AlreadyResolved);
+ }
+
+ let current_ts = env.ledger().timestamp();
+ let timeout = dispute.raised_at.saturating_add(config.dispute_timeout_secs);
+
+ let actual_favour = if current_ts > timeout {
+ Symbol::new(&env, "buyer")
+ } else {
+ favour.clone()
+ };
+
+ if actual_favour != Symbol::new(&env, "buyer") && actual_favour != Symbol::new(&env, "seller") {
+ return Err(Error::Unauthorized);
+ }
+
+ dispute.resolved = true;
+ storage::set_dispute(&env, &invoice_id, &dispute);
+
+ let token = token::Client::new(&env, &data.token);
+ let contract = env.current_contract_address();
+
+ if actual_favour == Symbol::new(&env, "buyer") {
+ let amount_to_refund = data.funded_amt;
+ let funder_opt = data.funder.clone();
+
+ if let Some(distributor) = config.payment_distributor.as_ref() {
+ token.transfer(&contract, distributor, &amount_to_refund);
+ env.invoke_contract::<()>(
+ distributor,
+ &Symbol::new(&env, DISTRIBUTE_REFUND_FN),
+ soroban_sdk::vec![
+ &env,
+ contract.to_val(),
+ invoice_id.clone().into_val(&env),
+ soroban_sdk::vec![
+ &env,
+
>::into_val(&data.token, &env),
+