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
72 changes: 71 additions & 1 deletion veritixpay/contract/token/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,21 @@ use crate::balance::{
decrease_supply, increase_supply, read_balance, read_total_supply, receive_balance,
spend_balance,
};
use crate::dispute::{get_dispute as dispute_get, open_dispute, resolve_dispute, DisputeRecord};
use crate::escrow::{
create_escrow as escrow_create, get_escrow as escrow_get, refund_escrow as escrow_refund,
release_escrow as escrow_release, EscrowRecord,
};
use crate::freeze::{freeze_account, is_frozen as read_frozen_status, unfreeze_account};
use crate::metadata::{
read_decimal, read_name, read_symbol, validate_metadata, write_metadata, TokenMetadata,
};
use crate::splitter::{
create_split as split_create, distribute as split_distribute, get_split as split_get,
SplitRecord, SplitRecipient,
};
use crate::validation::{require_not_frozen_account, require_positive_amount};
use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, String};
use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, String, Vec};

#[contract]
pub struct VeritixToken;
Expand Down Expand Up @@ -171,4 +180,65 @@ impl VeritixToken {
pub fn symbol(e: Env) -> String {
read_symbol(&e)
}

// --- Escrow ---

pub fn create_escrow(e: Env, depositor: Address, beneficiary: Address, amount: i128) -> u32 {
escrow_create(&e, depositor, beneficiary, amount)
}

pub fn release_escrow(e: Env, caller: Address, escrow_id: u32) {
escrow_release(&e, caller, escrow_id)
}

pub fn refund_escrow(e: Env, caller: Address, escrow_id: u32) {
escrow_refund(&e, caller, escrow_id)
}

pub fn get_escrow(e: Env, escrow_id: u32) -> EscrowRecord {
escrow_get(&e, escrow_id)
}

// --- Dispute ---

pub fn open_dispute(
e: Env,
claimant: Address,
escrow_id: u32,
resolver: Address,
) -> u32 {
open_dispute(&e, claimant, escrow_id, resolver)
}

pub fn resolve_dispute(
e: Env,
resolver: Address,
dispute_id: u32,
release_to_beneficiary: bool,
) {
resolve_dispute(&e, resolver, dispute_id, release_to_beneficiary)
}

pub fn get_dispute(e: Env, dispute_id: u32) -> DisputeRecord {
dispute_get(&e, dispute_id)
}

// --- Splitter ---

pub fn create_split(
e: Env,
sender: Address,
recipients: Vec<SplitRecipient>,
total_amount: i128,
) -> u32 {
split_create(&e, sender, recipients, total_amount)
}

pub fn distribute(e: Env, caller: Address, split_id: u32) {
split_distribute(&e, caller, split_id)
}

pub fn get_split(e: Env, split_id: u32) -> SplitRecord {
split_get(&e, split_id)
}
}
77 changes: 46 additions & 31 deletions veritixpay/contract/token/src/dispute.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::escrow::{get_escrow, release_escrow, refund_escrow};
use crate::storage_types::{increment_counter, DataKey};
use crate::balance::{receive_balance, spend_balance};
use crate::escrow::get_escrow;
use crate::storage_types::{increment_counter, write_persistent_record, DataKey};
use soroban_sdk::{contracttype, Address, Env, Symbol};

#[contracttype]
Expand Down Expand Up @@ -27,95 +28,109 @@ pub fn open_dispute(
escrow_id: u32,
resolver: Address,
) -> u32 {
// 1. Authorization: Only the claimant can initiate this call
claimant.require_auth();

// 2. Fetch escrow and validate current state
let escrow = get_escrow(e, escrow_id);

// Check if the escrow is already finalized

if escrow.released || escrow.refunded {
panic!("InvalidState: Cannot open dispute on a settled escrow");
}

// 3. Authorization check: Claimant must be a party involved in the escrow
if claimant != escrow.depositor && claimant != escrow.beneficiary {
panic!("Unauthorized: Only depositor or beneficiary can open a dispute");
}

// 4. Generate a new Dispute ID using the counter in storage
let count = increment_counter(e, &DataKey::DisputeCount);

// 5. Create and store the dispute record
let record = DisputeRecord {
id: count,
escrow_id,
claimant: claimant.clone(),
resolver,
status: DisputeStatus::Open,
};

// Store in persistent storage as disputes may last longer than instance TTL

e.storage().persistent().set(&DataKey::Dispute(count), &record);

// 6. Emit Observability Event
e.events().publish(
(Symbol::new(e, "dispute"), Symbol::new(e, "opened"), escrow_id),
claimant
claimant,
);

count
}

/// Resolves an open dispute.
/// Private helper: settle an escrow by outcome without requiring depositor/beneficiary auth.
/// The resolver has already been authenticated by `resolve_dispute`.
fn settle_escrow_by_outcome(e: &Env, escrow_id: u32, release_to_beneficiary: bool) {
let mut escrow = get_escrow(e, escrow_id);

if escrow.released || escrow.refunded {
panic!("AlreadySettled: escrow is already settled");
}

if release_to_beneficiary {
escrow.released = true;
write_persistent_record(e, &DataKey::Escrow(escrow_id), &escrow);
spend_balance(e, e.current_contract_address(), escrow.amount);
receive_balance(e, escrow.beneficiary.clone(), escrow.amount);
e.events().publish(
(Symbol::new(e, "escrow"), Symbol::new(e, "released"), escrow_id),
escrow.beneficiary,
);
} else {
escrow.refunded = true;
write_persistent_record(e, &DataKey::Escrow(escrow_id), &escrow);
spend_balance(e, e.current_contract_address(), escrow.amount);
receive_balance(e, escrow.depositor.clone(), escrow.amount);
e.events().publish(
(Symbol::new(e, "escrow"), Symbol::new(e, "refunded"), escrow_id),
escrow.depositor,
);
}
}

/// Resolves an open dispute. Only the designated resolver can call this.
/// Settlement does not require beneficiary/depositor auth.
pub fn resolve_dispute(
e: &Env,
resolver: Address,
dispute_id: u32,
release_to_beneficiary: bool,
) {
// 1. Authorization: Only the designated resolver can resolve the dispute
resolver.require_auth();

// 2. Fetch the dispute record
let mut dispute: DisputeRecord = e
.storage()
.persistent()
.get(&DataKey::Dispute(dispute_id))
.expect("Dispute not found");

// 3. Validation: Check if already resolved (Double-resolution panic)
if dispute.status != DisputeStatus::Open {
panic!("AlreadyResolved: This dispute has already been resolved");
}

// 4. Validation: Verify the resolver matches the record
if dispute.resolver != resolver {
panic!("UnauthorizedResolver: Only the designated resolver can resolve this");
}

// 5. Execute resolution by calling the core escrow logic
if release_to_beneficiary {
// Triggers the standard release logic from escrow.rs
release_escrow(e, dispute.escrow_id);
dispute.status = DisputeStatus::ResolvedForBeneficiary;
settle_escrow_by_outcome(e, dispute.escrow_id, release_to_beneficiary);

dispute.status = if release_to_beneficiary {
DisputeStatus::ResolvedForBeneficiary
} else {
// Triggers the standard refund logic from escrow.rs
refund_escrow(e, dispute.escrow_id);
dispute.status = DisputeStatus::ResolvedForDepositor;
}
DisputeStatus::ResolvedForDepositor
};

// 6. Persist the updated dispute status
e.storage().persistent().set(&DataKey::Dispute(dispute_id), &dispute);

// 7. Emit Observability Event
e.events().publish(
(Symbol::new(e, "dispute"), Symbol::new(e, "resolved"), dispute_id),
release_to_beneficiary
release_to_beneficiary,
);
}

/// Helper to read a dispute record
/// Helper to read a dispute record.
pub fn get_dispute(e: &Env, dispute_id: u32) -> DisputeRecord {
e.storage()
.persistent()
Expand Down
129 changes: 129 additions & 0 deletions veritixpay/contract/token/src/dispute_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
use soroban_sdk::{testutils::Address as _, Address, Env};

use crate::balance::read_balance;
use crate::contract::VeritixToken;
use crate::dispute::{get_dispute, open_dispute, resolve_dispute, DisputeStatus};
use crate::escrow::{create_escrow, get_escrow};

fn setup_env() -> Env {
let e = Env::default();
e.mock_all_auths();
e
}

fn setup_escrow(e: &Env, contract_id: &Address) -> (Address, Address, u32) {
let depositor = Address::generate(e);
let beneficiary = Address::generate(e);
let amount = 1_000i128;
let mut escrow_id = 0u32;
e.as_contract(contract_id, || {
crate::balance::receive_balance(e, depositor.clone(), amount);
escrow_id = create_escrow(e, depositor.clone(), beneficiary.clone(), amount);
});
(depositor, beneficiary, escrow_id)
}

#[test]
fn test_open_dispute_stores_record() {
let e = setup_env();
let contract_id = e.register_contract(None, VeritixToken);
let resolver = Address::generate(&e);
let (depositor, _beneficiary, escrow_id) = setup_escrow(&e, &contract_id);

e.as_contract(&contract_id, || {
let dispute_id = open_dispute(&e, depositor.clone(), escrow_id, resolver.clone());
let record = get_dispute(&e, dispute_id);
assert_eq!(record.escrow_id, escrow_id);
assert_eq!(record.claimant, depositor);
assert_eq!(record.resolver, resolver);
assert_eq!(record.status, DisputeStatus::Open);
});
}

#[test]
fn test_resolve_dispute_for_beneficiary() {
let e = setup_env();
let contract_id = e.register_contract(None, VeritixToken);
let resolver = Address::generate(&e);
let (_depositor, beneficiary, escrow_id) = setup_escrow(&e, &contract_id);

e.as_contract(&contract_id, || {
let dispute_id =
open_dispute(&e, beneficiary.clone(), escrow_id, resolver.clone());
resolve_dispute(&e, resolver.clone(), dispute_id, true);

let record = get_dispute(&e, dispute_id);
assert_eq!(record.status, DisputeStatus::ResolvedForBeneficiary);

let escrow = get_escrow(&e, escrow_id);
assert!(escrow.released);

assert_eq!(read_balance(&e, beneficiary.clone()), 1_000);
});
}

#[test]
fn test_resolve_dispute_for_depositor() {
let e = setup_env();
let contract_id = e.register_contract(None, VeritixToken);
let resolver = Address::generate(&e);
let (depositor, _beneficiary, escrow_id) = setup_escrow(&e, &contract_id);

e.as_contract(&contract_id, || {
let dispute_id =
open_dispute(&e, depositor.clone(), escrow_id, resolver.clone());
resolve_dispute(&e, resolver.clone(), dispute_id, false);

let record = get_dispute(&e, dispute_id);
assert_eq!(record.status, DisputeStatus::ResolvedForDepositor);

let escrow = get_escrow(&e, escrow_id);
assert!(escrow.refunded);

assert_eq!(read_balance(&e, depositor.clone()), 1_000);
});
}

#[test]
#[should_panic(expected = "UnauthorizedResolver")]
fn test_resolve_dispute_wrong_resolver_panics() {
let e = setup_env();
let contract_id = e.register_contract(None, VeritixToken);
let resolver = Address::generate(&e);
let impostor = Address::generate(&e);
let (depositor, _beneficiary, escrow_id) = setup_escrow(&e, &contract_id);

e.as_contract(&contract_id, || {
let dispute_id = open_dispute(&e, depositor.clone(), escrow_id, resolver.clone());
resolve_dispute(&e, impostor.clone(), dispute_id, true);
});
}

#[test]
#[should_panic(expected = "AlreadyResolved")]
fn test_double_resolve_panics() {
let e = setup_env();
let contract_id = e.register_contract(None, VeritixToken);
let resolver = Address::generate(&e);
let (depositor, _beneficiary, escrow_id) = setup_escrow(&e, &contract_id);

e.as_contract(&contract_id, || {
let dispute_id = open_dispute(&e, depositor.clone(), escrow_id, resolver.clone());
resolve_dispute(&e, resolver.clone(), dispute_id, true);
resolve_dispute(&e, resolver.clone(), dispute_id, false);
});
}

#[test]
#[should_panic(expected = "InvalidState")]
fn test_open_dispute_on_settled_escrow_panics() {
let e = setup_env();
let contract_id = e.register_contract(None, VeritixToken);
let resolver = Address::generate(&e);
let (_depositor, beneficiary, escrow_id) = setup_escrow(&e, &contract_id);

e.as_contract(&contract_id, || {
crate::escrow::release_escrow(&e, beneficiary.clone(), escrow_id);
open_dispute(&e, beneficiary.clone(), escrow_id, resolver.clone());
});
}
8 changes: 8 additions & 0 deletions veritixpay/contract/token/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
pub mod admin;
pub mod allowance;
pub mod balance;
pub mod dispute;
pub mod escrow;
pub mod freeze;
pub mod metadata;
pub mod splitter;
pub mod storage_types;
pub mod validation;

Expand All @@ -24,4 +26,10 @@ mod escrow_test;
#[cfg(test)]
mod admin_test;

#[cfg(test)]
mod splitter_test;

#[cfg(test)]
mod dispute_test;

pub use crate::contract::VeritixToken;
Loading
Loading