Skip to content
Open
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
192 changes: 138 additions & 54 deletions app/contract/contracts/quickex/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ use errors::QuickexError;
use storage::*;
use types::{EscrowEntry, EscrowStatus, FeeConfig, PrivacyAwareEscrowView, StealthDepositParams};

/// Current version of the contract code.
/// Used during upgrades to detect and handle schema migrations.
const CONTRACT_VERSION: u32 = 1;

/// QuickEx Privacy Contract
///
/// Soroban smart contract providing escrow, privacy controls, and X-Ray-style amount
Expand All @@ -39,6 +43,9 @@ use types::{EscrowEntry, EscrowStatus, FeeConfig, PrivacyAwareEscrowView, Stealt
/// [*] --> Pending : deposit() / deposit_with_commitment()
/// Pending --> Spent : withdraw(proof) [now < expires_at, or no expiry]
/// Pending --> Refunded : refund(owner) [now >= expires_at]
/// Pending --> Disputed : dispute() [any participant can call]
/// Disputed --> Spent : resolve_dispute() [arbiter decides for recipient]
/// Disputed --> Refunded: resolve_dispute() [arbiter decides for owner]
/// ```
#[contract]
pub struct QuickexContract;
Expand Down Expand Up @@ -77,7 +84,7 @@ impl QuickexContract {
if admin::is_paused(&env) {
return Err(QuickexError::ContractPaused);
}
if storage::is_feature_paused(&env, storage::PauseFlag::Withdrawal as u64) {
if is_feature_paused(&env, PauseFlag::Withdrawal) {
return Err(QuickexError::OperationPaused);
}
escrow::withdraw(&env, amount, to, salt)
Expand Down Expand Up @@ -134,6 +141,9 @@ impl QuickexContract {
if admin::is_paused(&env) {
return Err(QuickexError::ContractPaused);
}
if is_feature_paused(&env, PauseFlag::SetPrivacy) {
return Err(QuickexError::OperationPaused);
}
privacy::set_privacy(&env, owner, enabled)
}

Expand All @@ -160,6 +170,7 @@ impl QuickexContract {
/// * `owner` - Owner of the funds (must authorize)
/// * `salt` - Random salt (0–1024 bytes) for uniqueness
/// * `timeout_secs` - Seconds from now until the escrow expires (0 = no expiry)
/// * `arbiter` - Optional arbiter address who can resolve disputes
///
/// # Errors
/// * `InvalidAmount` - Amount is zero or negative
Expand All @@ -178,7 +189,7 @@ impl QuickexContract {
if admin::is_paused(&env) {
return Err(QuickexError::ContractPaused);
}
if storage::is_feature_paused(&env, storage::PauseFlag::Deposit as u64) {
if is_feature_paused(&env, PauseFlag::Deposit) {
return Err(QuickexError::OperationPaused);
}
escrow::deposit(&env, token, amount, owner, salt, timeout_secs, arbiter)
Expand All @@ -204,6 +215,12 @@ impl QuickexContract {
amount: i128,
salt: Bytes,
) -> Result<BytesN<32>, QuickexError> {
if admin::is_paused(&env) {
return Err(QuickexError::ContractPaused);
}
if is_feature_paused(&env, PauseFlag::CreateAmountCommitment) {
return Err(QuickexError::OperationPaused);
}
commitment::create_amount_commitment(&env, owner, amount, salt)
}

Expand Down Expand Up @@ -261,6 +278,7 @@ impl QuickexContract {
/// * `amount` - Amount to deposit; must be positive
/// * `commitment` - 32-byte commitment hash (must be unique)
/// * `timeout_secs` - Seconds from now until the escrow expires (0 = no expiry)
/// * `arbiter` - Optional arbiter address who can resolve disputes
///
/// # Errors
/// * `InvalidAmount` - Amount is zero or negative
Expand All @@ -278,7 +296,7 @@ impl QuickexContract {
if admin::is_paused(&env) {
return Err(QuickexError::ContractPaused);
}
if storage::is_feature_paused(&env, storage::PauseFlag::DepositWithCommitment as u64) {
if is_feature_paused(&env, PauseFlag::DepositWithCommitment) {
return Err(QuickexError::OperationPaused);
}
escrow::deposit_with_commitment(
Expand Down Expand Up @@ -308,12 +326,64 @@ impl QuickexContract {
/// * `EscrowNotExpired` - Escrow has no expiry or has not yet expired
/// * `InvalidOwner` - Caller is not the original owner
pub fn refund(env: Env, commitment: BytesN<32>, caller: Address) -> Result<(), QuickexError> {
if storage::is_feature_paused(&env, storage::PauseFlag::Refund as u64) {
if admin::is_paused(&env) {
return Err(QuickexError::ContractPaused);
}
if is_feature_paused(&env, PauseFlag::Refund) {
return Err(QuickexError::OperationPaused);
}

escrow::refund(&env, commitment, caller)
}

/// Initiate a dispute for a pending escrow, locking the funds.
///
/// Any participant can call this function to start a dispute. The escrow must
/// have an assigned arbiter and be in `Pending` status. Changes status to `Disputed`.
///
/// # Arguments
/// * `env` - The contract environment
/// * `commitment` - 32-byte commitment hash identifying the escrow
///
/// # Errors
/// * `CommitmentNotFound` - No escrow exists for the commitment
/// * `NoArbiter` - No arbiter assigned to the escrow
/// * `InvalidDisputeState` - Escrow is not in `Pending` status
pub fn dispute(env: Env, commitment: BytesN<32>) -> Result<(), QuickexError> {
if admin::is_paused(&env) {
return Err(QuickexError::ContractPaused);
}
escrow::dispute(&env, commitment)
}

/// Resolve a disputed escrow by determining the recipient of funds.
///
/// Only callable by the assigned arbiter. The arbiter decides whether funds
/// go to the original owner (refund) or to a specified recipient (spend).
///
/// # Arguments
/// * `env` - The contract environment
/// * `commitment` - 32-byte commitment hash identifying the escrow
/// * `resolve_for_owner` - If true, funds go to owner; if false, funds go to recipient
/// * `recipient` - Address to receive funds when resolve_for_owner is false
///
/// # Errors
/// * `CommitmentNotFound` - No escrow exists for the commitment
/// * `NotArbiter` - Caller is not the assigned arbiter
/// * `NoArbiter` - No arbiter assigned to the escrow
/// * `InvalidDisputeState` - Escrow is not in `Disputed` status
pub fn resolve_dispute(
env: Env,
commitment: BytesN<32>,
resolve_for_owner: bool,
recipient: Address,
) -> Result<(), QuickexError> {
if admin::is_paused(&env) {
return Err(QuickexError::ContractPaused);
}
escrow::resolve_dispute(&env, commitment, resolve_for_owner, recipient)
}

/// Initialize the contract with an admin address (one-time only).
///
/// Sets the admin who can pause/unpause, transfer admin, and upgrade the contract.
Expand Down Expand Up @@ -343,6 +413,42 @@ impl QuickexContract {
admin::set_paused(&env, caller, new_state)
}

/// Check if the functiom is currently paused.
///
/// Returns `true` if paused, `false` otherwise.
pub fn is_feature_paused(env: &Env, flag: PauseFlag) -> bool {
storage::is_feature_paused(env, flag)
}

/// Pause a function in the contract (**Admin only**).
///
/// When paused, the particular operations isblocked. Caller must equal the stored admin.
///
/// # Arguments
/// * `env` - The contract environment
/// * `caller` - Caller address (must equal admin)
/// * `mask` - PauseFlag Enum
///
/// # Errors
/// * `Unauthorized` - Caller is not the admin, or admin not set
pub fn pause_features(env: Env, caller: Address, mask: u64) -> Result<(), QuickexError> {
admin::set_pause_flags(&env, &caller, mask, 0)
}

/// UnPause a function in the contract (**Admin only**).
///
///
/// # Arguments
/// * `env` - The contract environment
/// * `caller` - Caller address (must equal admin)
/// * `mask` - PauseFlag Enum
///
/// # Errors
/// * `Unauthorized` - Caller is not the admin, or admin not set
pub fn unpause_features(env: Env, caller: Address, mask: u64) -> Result<(), QuickexError> {
admin::set_pause_flags(&env, &caller, 0, mask)
}

/// Transfer admin rights to a new address (**Admin only**).
///
/// Caller must equal the current admin. The new admin can later transfer again.
Expand Down Expand Up @@ -372,6 +478,9 @@ impl QuickexContract {
admin::get_admin(&env)
}

/// Get the current contract version stored in state.
pub fn version(env: Env) -> u32 {
get_version(&env)
/// Get the current fee configuration (read-only).
pub fn get_fee_config(env: Env) -> FeeConfig {
storage::get_fee_config(&env)
Expand Down Expand Up @@ -460,9 +569,10 @@ impl QuickexContract {
///
/// ## Privacy behaviour
/// - If the escrow owner **has privacy enabled** and `caller` is **not** the owner,
/// the `amount` and `owner` fields are returned as `None`.
/// the `amount`, `owner`, and `arbiter` fields are returned as `None`.
/// - If privacy is **disabled**, or `caller` equals the escrow owner,
/// all fields are returned in full.
/// - If `caller` equals the arbiter, the arbiter field is always visible.
///
/// # Arguments
/// * `env` - The contract environment
Expand All @@ -479,70 +589,31 @@ impl QuickexContract {

let privacy_on = privacy::get_privacy(&env, entry.owner.clone());
let is_owner = caller == entry.owner;
let is_arbiter = entry.arbiter.as_ref().is_some_and(|a| *a == caller);
let is_arbiter = entry.arbiter.as_ref().is_some_and(|a| caller == *a);
let show_sensitive = !privacy_on || is_owner || is_arbiter;

if privacy_on && !is_owner && !is_arbiter {
if show_sensitive {
Some(PrivacyAwareEscrowView {
token: entry.token,
amount: None,
owner: None,
amount: Some(entry.amount),
owner: Some(entry.owner),
status: entry.status,
created_at: entry.created_at,
expires_at: entry.expires_at,
arbiter: None,
arbiter: entry.arbiter,
})
} else {
Some(PrivacyAwareEscrowView {
token: entry.token,
amount: Some(entry.amount),
owner: Some(entry.owner.clone()),
amount: None,
owner: None,
status: entry.status,
created_at: entry.created_at,
expires_at: entry.expires_at,
arbiter: entry.arbiter,
arbiter: None,
})
}
}
// -----------------------------------------------------------------------
// Dispute resolution (arbiter flow)
// -----------------------------------------------------------------------

/// Raise a dispute on a pending escrow.
///
/// The escrow must have an arbiter assigned and be in `Pending` status.
/// Locks funds until the arbiter calls `resolve_dispute`.
pub fn dispute(env: Env, commitment: BytesN<32>) -> Result<(), QuickexError> {
if admin::is_paused(&env) {
return Err(QuickexError::ContractPaused);
}
escrow::dispute(&env, commitment)
}

/// Resolve a disputed escrow.
///
/// Only callable by the assigned arbiter. Pass `resolve_for_owner: true`
/// to refund the owner, or `false` to pay out to `recipient`.
pub fn resolve_dispute(
env: Env,
commitment: BytesN<32>,
resolve_for_owner: bool,
recipient: Address,
) -> Result<(), QuickexError> {
if admin::is_paused(&env) {
return Err(QuickexError::ContractPaused);
}
escrow::resolve_dispute(&env, commitment, resolve_for_owner, recipient)
}

/// Pause specific operation flags (Admin only).
pub fn pause_features(env: Env, caller: Address, flags: u64) -> Result<(), QuickexError> {
admin::set_pause_flags(&env, &caller, flags, 0)
}

/// Unpause specific operation flags (Admin only).
pub fn unpause_features(env: Env, caller: Address, flags: u64) -> Result<(), QuickexError> {
admin::set_pause_flags(&env, &caller, 0, flags)
}

// -----------------------------------------------------------------------
// Stealth Address – Privacy v2 (Issue #157)
Expand Down Expand Up @@ -625,7 +696,7 @@ impl QuickexContract {
/// Upgrade the contract to a new WASM implementation (**Admin only**).
///
/// Caller must equal admin and authorize. The new WASM must be pre-uploaded to the network.
/// Emits an upgrade event for audit.
/// Emits an upgrade event for audit. Handles storage migrations if the version has changed.
///
/// # Arguments
/// * `env` - The contract environment
Expand All @@ -649,9 +720,22 @@ impl QuickexContract {

caller.require_auth();

// 1. Perform migrations if needed
let old_version = get_version(&env);
if old_version < CONTRACT_VERSION {
// Placeholder for actual migration logic
// Example:
// if old_version == 1 && CONTRACT_VERSION == 2 {
// migrate_v1_to_v2(&env);
// }
set_version(&env, CONTRACT_VERSION);
}

// 2. Update WASM
env.deployer()
.update_current_contract_wasm(new_wasm_hash.clone());

// 3. Emit event
events::publish_contract_upgraded(&env, new_wasm_hash, &admin);

Ok(())
Expand Down
Loading
Loading