Skip to content
57 changes: 29 additions & 28 deletions contracts/marketx/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,146 +11,147 @@ pub enum ContractError {
// =========================
// AUTHENTICATION ERRORS (1-9)
// =========================

/// Caller is not the contract admin.
///
/// This error is returned when a function requires admin privileges
/// but the caller is not the configured admin address.
///
///
/// **Used in:** `assert_admin()`, `set_fee_percentage()`
NotAdmin = 1,

/// Caller is not authorized to perform the requested action.
///
/// This error is returned when a user attempts to perform an action
/// they are not authorized for (e.g., non-buyer trying to refund).
///
///
/// **Used in:** `refund_escrow()`
Unauthorized = 2,
NotProposedAdmin = 3,

// =========================
// ESCROW ERRORS (10-19)
// =========================

/// The specified escrow does not exist.
///
/// This error is returned when attempting to operate on an escrow
/// ID that was never created or has been deleted.
///
///
/// **Used in:** `fund_escrow()`, `release_escrow()`, `release_item()`,
/// `refund_escrow()`, `bump_escrow()`, `resolve_dispute()`
EscrowNotFound = 10,

/// Escrow is not in the required state for the operation.
///
/// This error is returned when the current escrow state does not
/// allow the requested operation (e.g., releasing an already released escrow).
///
///
/// **Used in:** `fund_escrow()`, `release_escrow()`, `release_item()`,
/// `refund_escrow()`, `resolve_dispute()`
InvalidEscrowState = 11,

/// The escrow amount is invalid.
///
/// This error is returned when the escrow amount is zero or negative,
/// or when a refund amount exceeds the escrow amount.
///
///
/// **Used in:** `create_escrow()`, `refund_escrow()`
InvalidEscrowAmount = 13,

// =========================
// SECURITY ERRORS (30-39)
// =========================

/// Contract is currently paused.
///
/// This error is returned when attempting to perform operations
/// while the contract is in a paused state.
///
///
/// **Used in:** `assert_not_paused()`
ContractPaused = 31,

// =========================
// COUNTER ERRORS (40-49)
// =========================

/// Escrow ID would overflow u64.
///
/// This error is returned when the contract has already created
/// the maximum number of escrows (2^64 - 1).
///
///
/// **Used in:** `next_escrow_id()`, `next_refund_id()`
EscrowIdOverflow = 40,

// =========================
// FEE ERRORS (50-59)
// =========================

/// Fee configuration is invalid.
///
/// This error is returned when the fee configuration is malformed
/// or missing required components (e.g., no fee collector set).
///
///
/// **Used in:** `release_escrow()`, `set_fee_percentage()`
InvalidFeeConfig = 50,

// =========================
// METADATA ERRORS (60-69)
// =========================

/// Metadata exceeds maximum allowed size.
///
/// This error is returned when the provided metadata is larger
/// than `MAX_METADATA_SIZE` (1KB).
///
///
/// **Used in:** `validate_metadata()`
MetadataTooLarge = 60,

// =========================
// DUPLICATION ERRORS (70-79)
// =========================

/// Duplicate escrow detected.
///
/// This error is returned when attempting to create an escrow with
/// the same buyer, seller, and metadata as an existing one.
///
///
/// **Used in:** `check_duplicate_escrow()`
DuplicateEscrow = 70,

// =========================
// ITEM ERRORS (80-89)
// =========================

/// Item not found in escrow.
///
/// This error is returned when attempting to access an item
/// with an invalid index in the escrow's items array.
///
///
/// **Used in:** `release_item()`
ItemNotFound = 80,

/// Item has already been released.
///
/// This error is returned when attempting to release an item
/// that has already been released to the seller.
///
///
/// **Used in:** `release_item()`
ItemAlreadyReleased = 81,

/// Too many items in escrow.
///
/// This error is returned when attempting to create an escrow
/// with more items than `MAX_ITEMS_PER_ESCROW` (50).
///
///
/// **Used in:** `create_escrow()`
TooManyItems = 82,

/// Item amounts don't sum to total escrow amount.
///
/// This error is returned when the sum of all item amounts
/// does not equal the total escrow amount.
///
///
/// **Used in:** `create_escrow()`
ItemAmountInvalid = 83,
}
89 changes: 66 additions & 23 deletions contracts/marketx/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#![no_std]
#![warn(missing_docs)]
#![allow(missing_docs)]
#![allow(clippy::too_many_arguments)]
#![allow(clippy::unnecessary_cast)]
#![allow(dead_code)]

//! # MarketX Smart Contract
//!
Expand Down Expand Up @@ -86,7 +89,7 @@
//! - Reentrancy protection on critical paths
//! - Comprehensive input validation

use soroban_sdk::{contract, contractimpl, Address, Bytes, BytesN, Env, Symbol, Vec};
use soroban_sdk::{contract, contractimpl, Address, Bytes, BytesN, Env, Vec};

mod errors;
mod types;
Expand All @@ -95,15 +98,10 @@ use soroban_sdk::xdr::ToXdr;

pub use errors::ContractError;
pub use types::{
DataKey, Escrow, EscrowCreatedEvent, EscrowItem, EscrowStatus, FeeChangedEvent,
FundsReleasedEvent, RefundHistoryEntry, RefundReason, RefundRequest, RefundStatus,
AdminTransferredEvent, CounterEvidenceSubmittedEvent, DataKey, Escrow, EscrowCreatedEvent,
EscrowItem, EscrowStatus, FeeChangedEvent, FeeCollectedEvent, FundsReleasedEvent,
RefundHistoryEntry, RefundReason, RefundRequest, RefundRequestedEvent, RefundStatus,
StatusChangeEvent, MAX_ITEMS_PER_ESCROW, MAX_METADATA_SIZE,
DataKey, Escrow, EscrowCreatedEvent, EscrowStatus, FeeChangedEvent, FeeCollectedEvent,
FundsReleasedEvent, RefundHistoryEntry, RefundReason, RefundRequest, RefundStatus,
StatusChangeEvent, MAX_METADATA_SIZE,
CounterEvidenceSubmittedEvent, DataKey, Escrow, EscrowCreatedEvent, EscrowStatus,
FeeChangedEvent, FundsReleasedEvent, RefundHistoryEntry, RefundReason, RefundRequest,
RefundRequestedEvent, RefundStatus, StatusChangeEvent, MAX_METADATA_SIZE,
};

#[cfg(test)]
Expand Down Expand Up @@ -452,16 +450,6 @@ impl Contract {
.set(&DataKey::TotalFundedAmount, &(current_total + amount));

// Emit event
let mut escrow_ids: Vec<u64> = env
.storage()
.persistent()
.get(&DataKey::EscrowIds)
.unwrap_or(Vec::new(&env));
escrow_ids.push_back(escrow_id);
env.storage()
.persistent()
.set(&DataKey::EscrowIds, &escrow_ids);

let event = EscrowCreatedEvent {
escrow_id,
buyer,
Expand Down Expand Up @@ -719,6 +707,7 @@ impl Contract {
let event = FundsReleasedEvent {
escrow_id,
amount: item.amount,
fee: 0,
};
event.publish(&env);

Expand Down Expand Up @@ -814,7 +803,13 @@ impl Contract {
};
event.publish(&env);

Self::emit_status_change(&env, escrow_id, from_status, escrow.status.clone(), initiator);
Self::emit_status_change(
&env,
escrow_id,
from_status,
escrow.status.clone(),
initiator,
);

Ok(request_id)
}
Expand Down Expand Up @@ -886,14 +881,16 @@ impl Contract {
&escrow.amount,
);
escrow.status = EscrowStatus::Released;
} else {
} else if resolution == 1 {
// Refund to buyer
token_client.transfer(
&env.current_contract_address(),
&escrow.buyer,
&escrow.amount,
);
escrow.status = EscrowStatus::Refunded;
} else {
return Err(ContractError::InvalidEscrowState);
}

env.storage()
Expand All @@ -905,6 +902,50 @@ impl Contract {
Ok(())
}

// =========================
// 🔧 ADMIN FUNCTIONS
// =========================

/// Propose a new admin. The transfer is not complete until the new admin accepts.
pub fn transfer_admin(env: Env, new_admin: Address) -> Result<(), ContractError> {
Self::assert_admin(&env)?;
env.storage()
.persistent()
.set(&DataKey::ProposedAdmin, &new_admin);
Ok(())
}

/// Accept the administrative role. Must be called by the proposed admin.
pub fn accept_admin(env: Env) -> Result<(), ContractError> {
let proposed_admin: Address = env
.storage()
.persistent()
.get(&DataKey::ProposedAdmin)
.ok_or(ContractError::NotProposedAdmin)?;

// The proposed admin must authenticate this transaction
proposed_admin.require_auth();

let old_admin: Address = env.storage().persistent().get(&DataKey::Admin).unwrap();

// Transfer the admin role
env.storage()
.persistent()
.set(&DataKey::Admin, &proposed_admin);

// Clean up the proposal
env.storage().persistent().remove(&DataKey::ProposedAdmin);

// Emit the event
AdminTransferredEvent {
old_admin,
new_admin: proposed_admin,
}
.publish(&env);

Ok(())
}

pub fn get_admin(env: Env) -> Option<Address> {
env.storage().persistent().get(&DataKey::Admin)
}
Expand Down Expand Up @@ -947,7 +988,9 @@ impl Contract {

/// Get a refund request by ID.
pub fn get_refund_request(env: Env, request_id: u64) -> Option<RefundRequest> {
env.storage().persistent().get(&DataKey::RefundRequest(request_id))
env.storage()
.persistent()
.get(&DataKey::RefundRequest(request_id))
}

/// Get the total number of refund requests.
Expand Down
Loading
Loading