From 3425bd68da824de1c8b7660aea13f975c6cac3bc Mon Sep 17 00:00:00 2001 From: Mexes Date: Fri, 23 Jan 2026 05:30:53 +0000 Subject: [PATCH] feat: Implement a secure, efficient Marketplace Settlement smart contract using Soroban SDK --- .../marketplace_settlement/Cargo.toml | 1 + .../src/auction_engine.rs | 147 +++++ .../src/dispute_resolution.rs | 158 +++++ .../marketplace_settlement/src/error.rs | 18 + .../src/escrow_manager.rs | 22 + .../marketplace_settlement/src/fee_manager.rs | 83 +++ .../marketplace_settlement/src/lib.rs | 579 ++++++++++++++++++ .../marketplace_settlement/src/main.rs | 3 - .../src/royalty_distributor.rs | 105 ++++ .../src/security/frontrun_protection.rs | 8 + .../src/security/mod.rs | 2 + .../src/security/reentrancy_guard.rs | 19 + .../src/settlement_core.rs | 156 +++++ .../src/storage/auction_stores.rs | 16 + .../src/storage/dispute_store.rs | 16 + .../marketplace_settlement/src/storage/mod.rs | 3 + .../src/storage/transaction_store.rs | 16 + .../src/utils/math_utils.rs | 18 + .../marketplace_settlement/src/utils/mod.rs | 2 + .../src/utils/time_utils.rs | 5 + 20 files changed, 1374 insertions(+), 3 deletions(-) create mode 100644 nftopia-stellar/contracts/marketplace_settlement/src/auction_engine.rs create mode 100644 nftopia-stellar/contracts/marketplace_settlement/src/dispute_resolution.rs create mode 100644 nftopia-stellar/contracts/marketplace_settlement/src/error.rs create mode 100644 nftopia-stellar/contracts/marketplace_settlement/src/escrow_manager.rs create mode 100644 nftopia-stellar/contracts/marketplace_settlement/src/fee_manager.rs delete mode 100644 nftopia-stellar/contracts/marketplace_settlement/src/main.rs create mode 100644 nftopia-stellar/contracts/marketplace_settlement/src/royalty_distributor.rs create mode 100644 nftopia-stellar/contracts/marketplace_settlement/src/security/frontrun_protection.rs create mode 100644 nftopia-stellar/contracts/marketplace_settlement/src/security/mod.rs create mode 100644 nftopia-stellar/contracts/marketplace_settlement/src/security/reentrancy_guard.rs create mode 100644 nftopia-stellar/contracts/marketplace_settlement/src/settlement_core.rs create mode 100644 nftopia-stellar/contracts/marketplace_settlement/src/storage/auction_stores.rs create mode 100644 nftopia-stellar/contracts/marketplace_settlement/src/storage/dispute_store.rs create mode 100644 nftopia-stellar/contracts/marketplace_settlement/src/storage/mod.rs create mode 100644 nftopia-stellar/contracts/marketplace_settlement/src/storage/transaction_store.rs create mode 100644 nftopia-stellar/contracts/marketplace_settlement/src/utils/math_utils.rs create mode 100644 nftopia-stellar/contracts/marketplace_settlement/src/utils/mod.rs create mode 100644 nftopia-stellar/contracts/marketplace_settlement/src/utils/time_utils.rs diff --git a/nftopia-stellar/contracts/marketplace_settlement/Cargo.toml b/nftopia-stellar/contracts/marketplace_settlement/Cargo.toml index f5af8a87..97dfeb2a 100644 --- a/nftopia-stellar/contracts/marketplace_settlement/Cargo.toml +++ b/nftopia-stellar/contracts/marketplace_settlement/Cargo.toml @@ -4,3 +4,4 @@ version = "0.1.0" edition = "2024" [dependencies] +soroban-sdk = { workspace = true } diff --git a/nftopia-stellar/contracts/marketplace_settlement/src/auction_engine.rs b/nftopia-stellar/contracts/marketplace_settlement/src/auction_engine.rs new file mode 100644 index 00000000..297c2f14 --- /dev/null +++ b/nftopia-stellar/contracts/marketplace_settlement/src/auction_engine.rs @@ -0,0 +1,147 @@ +use soroban_sdk::{Address, BytesN, Env, Vec}; + +use crate::error::SettlementError; +use crate::escrow_manager; +use crate::security::frontrun_protection; +use crate::storage::auction_store; +use crate::utils::time_utils::now; +use crate::{AuctionTransaction, Bid, TransactionState}; + +pub fn place_bid( + env: &Env, + auction_id: u64, + bidder: Address, + bid_amount: i128, + commitment_hash: Option>, +) -> Result<(), SettlementError> { + let mut auction = auction_store::get_auction(env, auction_id)?; + if auction.state != TransactionState::Pending { + return Err(SettlementError::InvalidState); + } + let current_time = now(env); + if current_time < auction.start_time || current_time > auction.end_time { + return Err(SettlementError::InvalidTime); + } + let is_committed = commitment_hash.is_some(); + if bid_amount <= 0 && !is_committed { + return Err(SettlementError::InvalidAmount); + } + if !is_committed { + ensure_valid_bid(&auction, bid_amount)?; + escrow_manager::transfer_in(env, &auction.currency, &bidder, bid_amount)?; + if let Some(prev_bidder) = auction.highest_bidder.clone() { + if auction.highest_bid > 0 { + escrow_manager::transfer_out(env, &auction.currency, &prev_bidder, auction.highest_bid)?; + } + } + auction.highest_bid = bid_amount; + auction.highest_bidder = Some(bidder.clone()); + maybe_extend_auction(&mut auction, current_time); + } + + let bid = Bid { + bidder, + amount: if is_committed { 0 } else { bid_amount }, + placed_at: current_time, + is_committed, + commitment_hash, + }; + auction.bids.push_back(bid); + auction_store::set_auction(env, &auction); + Ok(()) +} + +pub fn reveal_bid( + env: &Env, + auction_id: u64, + bidder: Address, + bid_amount: i128, + salt: BytesN<32>, +) -> Result<(), SettlementError> { + let mut auction = auction_store::get_auction(env, auction_id)?; + if auction.state != TransactionState::Pending { + return Err(SettlementError::InvalidState); + } + let current_time = now(env); + if current_time < auction.start_time || current_time > auction.end_time { + return Err(SettlementError::InvalidTime); + } + ensure_valid_bid(&auction, bid_amount)?; + + let mut commitment: Option> = None; + for bid in auction.bids.iter() { + if bid.is_committed && bid.bidder == bidder { + commitment = bid.commitment_hash; + } + } + let stored_commitment = commitment.ok_or(SettlementError::NotFound)?; + let computed = frontrun_protection::compute_commitment(env, &bidder, bid_amount, &salt); + if stored_commitment != computed { + return Err(SettlementError::CommitmentMismatch); + } + + escrow_manager::transfer_in(env, &auction.currency, &bidder, bid_amount)?; + if let Some(prev_bidder) = auction.highest_bidder.clone() { + if auction.highest_bid > 0 { + escrow_manager::transfer_out(env, &auction.currency, &prev_bidder, auction.highest_bid)?; + } + } + auction.highest_bid = bid_amount; + auction.highest_bidder = Some(bidder); + maybe_extend_auction(&mut auction, current_time); + + auction_store::set_auction(env, &auction); + Ok(()) +} + +pub fn finalize_auction(env: &Env, auction_id: u64) -> Result { + let mut auction = auction_store::get_auction(env, auction_id)?; + if auction.state != TransactionState::Pending { + return Err(SettlementError::InvalidState); + } + let current_time = now(env); + if current_time < auction.end_time { + return Err(SettlementError::AuctionNotEnded); + } + auction.state = TransactionState::Executed; + auction_store::set_auction(env, &auction); + Ok(auction) +} + +pub fn cancel_auction(env: &Env, auction_id: u64) -> Result { + let mut auction = auction_store::get_auction(env, auction_id)?; + if auction.state != TransactionState::Pending { + return Err(SettlementError::InvalidState); + } + auction.state = TransactionState::Cancelled; + auction_store::set_auction(env, &auction); + Ok(auction) +} + +fn ensure_valid_bid(auction: &AuctionTransaction, bid_amount: i128) -> Result<(), SettlementError> { + if bid_amount < auction.starting_price { + return Err(SettlementError::BidTooLow); + } + if auction.highest_bid > 0 { + let min_bid = auction + .highest_bid + .checked_add(auction.bid_increment) + .ok_or(SettlementError::Overflow)?; + if bid_amount < min_bid { + return Err(SettlementError::BidTooLow); + } + } + if auction.reserve_price > 0 && bid_amount < auction.reserve_price { + return Err(SettlementError::BidTooLow); + } + Ok(()) +} + +fn maybe_extend_auction(auction: &mut AuctionTransaction, now: u64) { + if auction.extension_window == 0 { + return; + } + if auction.end_time > now && auction.end_time - now <= auction.extension_window { + auction.end_time += auction.extension_window; + } +} diff --git a/nftopia-stellar/contracts/marketplace_settlement/src/dispute_resolution.rs b/nftopia-stellar/contracts/marketplace_settlement/src/dispute_resolution.rs new file mode 100644 index 00000000..05a928b9 --- /dev/null +++ b/nftopia-stellar/contracts/marketplace_settlement/src/dispute_resolution.rs @@ -0,0 +1,158 @@ +use soroban_sdk::{Address, Env, Map, String, Vec}; + +use crate::error::SettlementError; +use crate::storage::{auction_store, dispute_store, transaction_store}; +use crate::utils::time_utils::now; +use crate::{ + escrow_manager, Dispute, DisputeResolution, TransactionState, +}; +use crate::NftClient; + +pub fn initiate_dispute( + env: &Env, + transaction_id: u64, + initiator: Address, + reason: String, + evidence_uri: Option, + arbitrators: Vec
, + required_votes: u32, +) -> Result { + if arbitrators.is_empty() || required_votes == 0 || required_votes as usize > arbitrators.len() as usize { + return Err(SettlementError::InvalidAmount); + } + if let Ok(mut sale) = transaction_store::get_sale(env, transaction_id) { + if sale.state != TransactionState::Pending { + return Err(SettlementError::InvalidState); + } + if initiator != sale.seller { + if let Some(buyer) = sale.buyer.clone() { + if initiator != buyer { + return Err(SettlementError::Unauthorized); + } + } else { + return Err(SettlementError::Unauthorized); + } + } + sale.state = TransactionState::Disputed; + transaction_store::set_sale(env, &sale); + } else if let Ok(mut auction) = auction_store::get_auction(env, transaction_id) { + if auction.state != TransactionState::Pending { + return Err(SettlementError::InvalidState); + } + if initiator != auction.seller { + if let Some(highest) = auction.highest_bidder.clone() { + if initiator != highest { + return Err(SettlementError::Unauthorized); + } + } else { + return Err(SettlementError::Unauthorized); + } + } + auction.state = TransactionState::Disputed; + auction_store::set_auction(env, &auction); + } else { + return Err(SettlementError::NotFound); + } + + let dispute_id = crate::next_dispute_id(env)?; + let dispute = Dispute { + dispute_id, + transaction_id, + initiator, + reason, + evidence_uri, + arbitrators, + votes: Map::new(env), + required_votes, + created_at: now(env), + resolved_at: None, + resolution: None, + }; + dispute_store::set_dispute(env, &dispute); + Ok(dispute) +} + +pub fn vote_on_dispute( + env: &Env, + dispute_id: u64, + voter: Address, + vote: bool, +) -> Result { + let mut dispute = dispute_store::get_dispute(env, dispute_id)?; + if dispute.resolution.is_some() { + return Err(SettlementError::InvalidState); + } + let mut is_arbitrator = false; + for arbitrator in dispute.arbitrators.iter() { + if arbitrator == voter { + is_arbitrator = true; + } + } + if !is_arbitrator { + return Err(SettlementError::Unauthorized); + } + dispute.votes.set(&voter, vote); + + let mut yes_votes = 0u32; + let mut no_votes = 0u32; + for entry in dispute.votes.iter() { + if entry.1 { + yes_votes += 1; + } else { + no_votes += 1; + } + } + + if yes_votes >= dispute.required_votes || no_votes >= dispute.required_votes { + let resolution = if yes_votes >= dispute.required_votes { + DisputeResolution::InitiatorWins + } else { + DisputeResolution::InitiatorLoses + }; + dispute.resolution = Some(resolution.clone()); + dispute.resolved_at = Some(now(env)); + dispute_store::set_dispute(env, &dispute); + resolve_transaction(env, dispute.transaction_id, resolution)?; + return Ok(dispute); + } + + dispute_store::set_dispute(env, &dispute); + Ok(dispute) +} + +fn resolve_transaction( + env: &Env, + transaction_id: u64, + resolution: DisputeResolution, +) -> Result<(), SettlementError> { + if let Ok(mut sale) = transaction_store::get_sale(env, transaction_id) { + if sale.state != TransactionState::Disputed { + return Err(SettlementError::InvalidState); + } + let nft_client = NftClient::new(env, &sale.nft_address); + nft_client.transfer(&env.current_contract_address(), &sale.seller, &sale.token_id); + sale.state = TransactionState::Resolved; + transaction_store::set_sale(env, &sale); + let _ = resolution; + return Ok(()); + } + + if let Ok(mut auction) = auction_store::get_auction(env, transaction_id) { + if auction.state != TransactionState::Disputed { + return Err(SettlementError::InvalidState); + } + if let Some(highest_bidder) = auction.highest_bidder.clone() { + if auction.highest_bid > 0 { + escrow_manager::transfer_out(env, &auction.currency, &highest_bidder, auction.highest_bid)?; + } + } + let nft_client = NftClient::new(env, &auction.nft_address); + nft_client.transfer(&env.current_contract_address(), &auction.seller, &auction.token_id); + auction.state = TransactionState::Resolved; + auction_store::set_auction(env, &auction); + let _ = resolution; + return Ok(()); + } + + Err(SettlementError::NotFound) +} diff --git a/nftopia-stellar/contracts/marketplace_settlement/src/error.rs b/nftopia-stellar/contracts/marketplace_settlement/src/error.rs new file mode 100644 index 00000000..9c3a934e --- /dev/null +++ b/nftopia-stellar/contracts/marketplace_settlement/src/error.rs @@ -0,0 +1,18 @@ +use soroban_sdk::contracttype; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum SettlementError { + Unauthorized, + NotInitialized, + NotFound, + InvalidState, + InvalidAmount, + InvalidTime, + Expired, + AuctionNotEnded, + BidTooLow, + CommitmentMismatch, + AlreadyExists, + Overflow, +} diff --git a/nftopia-stellar/contracts/marketplace_settlement/src/escrow_manager.rs b/nftopia-stellar/contracts/marketplace_settlement/src/escrow_manager.rs new file mode 100644 index 00000000..d7c8b4df --- /dev/null +++ b/nftopia-stellar/contracts/marketplace_settlement/src/escrow_manager.rs @@ -0,0 +1,22 @@ +use soroban_sdk::{Address, Env}; +use soroban_sdk::token::TokenClient; + +use crate::error::SettlementError; + +pub fn transfer_in(env: &Env, asset: &Address, from: &Address, amount: i128) -> Result<(), SettlementError> { + if amount <= 0 { + return Err(SettlementError::InvalidAmount); + } + let client = TokenClient::new(env, asset); + client.transfer(from, &env.current_contract_address(), &amount); + Ok(()) +} + +pub fn transfer_out(env: &Env, asset: &Address, to: &Address, amount: i128) -> Result<(), SettlementError> { + if amount <= 0 { + return Err(SettlementError::InvalidAmount); + } + let client = TokenClient::new(env, asset); + client.transfer(&env.current_contract_address(), to, &amount); + Ok(()) +} diff --git a/nftopia-stellar/contracts/marketplace_settlement/src/fee_manager.rs b/nftopia-stellar/contracts/marketplace_settlement/src/fee_manager.rs new file mode 100644 index 00000000..4158eeab --- /dev/null +++ b/nftopia-stellar/contracts/marketplace_settlement/src/fee_manager.rs @@ -0,0 +1,83 @@ +use soroban_sdk::{Address, Env, Map, Vec}; + +use crate::{error::SettlementError, storage::transaction_store, DataKey, FeeConfig, SaleTransaction}; +use crate::utils::math_utils::{checked_add, mul_bps}; + +pub fn get_fee_config(env: &Env) -> Result { + env.storage() + .persistent() + .get(&DataKey::FeeConfig) + .ok_or(SettlementError::NotInitialized) +} + +pub fn set_fee_config(env: &Env, config: &FeeConfig) { + env.storage() + .persistent() + .set(&DataKey::FeeConfig, config); +} + +pub fn add_platform_fee(env: &Env, asset: &Address, amount: i128) -> Result<(), SettlementError> { + if amount <= 0 { + return Ok(()); + } + let mut fees: Map = env + .storage() + .persistent() + .get(&DataKey::PlatformFees) + .unwrap_or(Map::new(env)); + let current = fees.get(asset).unwrap_or(0); + let updated = checked_add(current, amount)?; + fees.set(asset, updated); + env.storage() + .persistent() + .set(&DataKey::PlatformFees, &fees); + Ok(()) +} + +pub fn take_platform_fee(env: &Env, sale: &SaleTransaction) -> Result { + let config = get_fee_config(env)?; + let base_fee = mul_bps(sale.price, config.platform_fee_bps)?; + let fee = apply_fee_bounds(base_fee, config.minimum_fee, config.maximum_fee); + if fee > sale.price { + return Ok(sale.price); + } + Ok(fee) +} + +pub fn apply_dynamic_discount( + env: &Env, + sale: &SaleTransaction, + base_fee: i128, + tiers: &Vec, +) -> Result { + let mut best_discount_bps = 0u32; + let sale_volume = sale.price; + for tier in tiers.iter() { + if sale_volume >= tier.min_volume && tier.fee_discount_bps > best_discount_bps { + best_discount_bps = tier.fee_discount_bps; + } + } + if best_discount_bps == 0 { + return Ok(base_fee); + } + let discount = mul_bps(base_fee, best_discount_bps)?; + Ok(base_fee - discount) +} + +pub fn apply_fee_bounds(base_fee: i128, min_fee: i128, max_fee: i128) -> i128 { + let mut fee = base_fee; + if fee < min_fee { + fee = min_fee; + } + if max_fee > 0 && fee > max_fee { + fee = max_fee; + } + fee +} + +pub fn update_sale_platform_fee(env: &Env, sale_id: u64, new_fee: i128) -> Result<(), SettlementError> { + let mut sale = transaction_store::get_sale(env, sale_id)?; + sale.platform_fee = new_fee; + transaction_store::set_sale(env, &sale); + Ok(()) +} diff --git a/nftopia-stellar/contracts/marketplace_settlement/src/lib.rs b/nftopia-stellar/contracts/marketplace_settlement/src/lib.rs index e69de29b..1fe82cc9 100644 --- a/nftopia-stellar/contracts/marketplace_settlement/src/lib.rs +++ b/nftopia-stellar/contracts/marketplace_settlement/src/lib.rs @@ -0,0 +1,579 @@ +#![no_std] + +use soroban_sdk::{contract, contractclient, contractimpl, contracttype, Address, BytesN, Env, Map, String, Vec}; + +mod auction_engine; +mod dispute_resolution; +mod escrow_manager; +mod fee_manager; +mod royalty_distributor; +mod settlement_core; +mod storage; +mod utils; +mod security; +mod error; + +pub use error::SettlementError; + +pub type Asset = Address; + +#[contractclient(name = "NftClient")] +pub trait NftInterface { + fn transfer(env: Env, from: Address, to: Address, token_id: u64); +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum TransactionState { + Pending, + Funded, + Executed, + Cancelled, + Disputed, + Resolved, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SaleTransaction { + pub transaction_id: u64, + pub seller: Address, + pub buyer: Option
, + pub nft_address: Address, + pub token_id: u64, + pub price: i128, + pub currency: Asset, + pub state: TransactionState, + pub created_at: u64, + pub expires_at: u64, + pub escrow_address: Address, + pub royalty_info: RoyaltyDistribution, + pub platform_fee: i128, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AuctionTransaction { + pub auction_id: u64, + pub seller: Address, + pub nft_address: Address, + pub token_id: u64, + pub starting_price: i128, + pub reserve_price: i128, + pub highest_bid: i128, + pub highest_bidder: Option
, + pub bid_increment: i128, + pub start_time: u64, + pub end_time: u64, + pub state: TransactionState, + pub bids: Vec, + pub extension_window: u64, + pub currency: Asset, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Bid { + pub bidder: Address, + pub amount: i128, + pub placed_at: u64, + pub is_committed: bool, + pub commitment_hash: Option>, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RoyaltyDistribution { + pub creator_address: Address, + pub creator_percentage: u32, + pub seller_percentage: u32, + pub platform_percentage: u32, + pub total_amount: i128, + pub amounts: Map, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Dispute { + pub dispute_id: u64, + pub transaction_id: u64, + pub initiator: Address, + pub reason: String, + pub evidence_uri: Option, + pub arbitrators: Vec
, + pub votes: Map, + pub required_votes: u32, + pub created_at: u64, + pub resolved_at: Option, + pub resolution: Option, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct FeeConfig { + pub platform_fee_bps: u32, + pub minimum_fee: i128, + pub maximum_fee: i128, + pub fee_recipient: Address, + pub dynamic_fee_enabled: bool, + pub volume_discounts: Vec, + pub vip_exemptions: Vec
, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VolumeTier { + pub min_volume: i128, + pub fee_discount_bps: u32, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum DisputeResolution { + InitiatorWins, + InitiatorLoses, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum EmergencyWithdrawalReason { + StuckFunds, + ContractUpgrade, + ArbitratorDecision, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ExecutionResult { + pub transaction_id: u64, + pub nft_transferred: bool, + pub funds_distributed: bool, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DistributionResult { + pub total_amount: i128, + pub platform_fee: i128, + pub creator_amount: i128, + pub seller_amount: i128, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Counters { + pub sale: u64, + pub auction: u64, + pub dispute: u64, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum DataKey { + Sale(u64), + Auction(u64), + Dispute(u64), + Counters, + FeeConfig, + Admin, + PlatformFees, + DefaultCurrency, +} + +#[contract] +pub struct MarketplaceSettlement; + +#[contractimpl] +impl MarketplaceSettlement { + pub fn initialize(env: Env, admin: Address, fee_config: FeeConfig, default_currency: Asset) -> Result<(), SettlementError> { + if env.storage().persistent().has(&DataKey::Admin) { + return Err(SettlementError::AlreadyExists); + } + admin.require_auth(); + validate_fee_config(&fee_config)?; + env.storage().persistent().set(&DataKey::Admin, &admin); + env.storage().persistent().set(&DataKey::FeeConfig, &fee_config); + env.storage().persistent().set(&DataKey::DefaultCurrency, &default_currency); + env.storage().persistent().set(&DataKey::Counters, &Counters { sale: 0, auction: 0, dispute: 0 }); + Ok(()) + } + + pub fn create_sale( + env: Env, + seller: Address, + nft_address: Address, + token_id: u64, + price: i128, + currency: Asset, + duration_seconds: u64, + ) -> Result { + settlement_core::create_sale(&env, seller, nft_address, token_id, price, currency, duration_seconds) + } + + pub fn create_auction( + env: Env, + seller: Address, + nft_address: Address, + token_id: u64, + starting_price: i128, + reserve_price: i128, + duration_seconds: u64, + bid_increment: i128, + ) -> Result { + seller.require_auth(); + if starting_price <= 0 || bid_increment <= 0 || duration_seconds == 0 { + return Err(SettlementError::InvalidAmount); + } + if reserve_price > 0 && reserve_price < starting_price { + return Err(SettlementError::InvalidAmount); + } + let default_currency: Asset = env + .storage() + .persistent() + .get(&DataKey::DefaultCurrency) + .ok_or(SettlementError::NotInitialized)?; + let start_time = utils::time_utils::now(&env); + let end_time = start_time.saturating_add(duration_seconds); + let auction_id = next_auction_id(&env)?; + let nft_client = NftClient::new(&env, &nft_address); + nft_client.transfer(&seller, &env.current_contract_address(), &token_id); + + let auction = AuctionTransaction { + auction_id, + seller: seller.clone(), + nft_address, + token_id, + starting_price, + reserve_price, + highest_bid: 0, + highest_bidder: None, + bid_increment, + start_time, + end_time, + state: TransactionState::Pending, + bids: Vec::new(&env), + extension_window: 120, + currency: default_currency, + }; + storage::auction_store::set_auction(&env, &auction); + Ok(auction_id) + } + + pub fn execute_sale( + env: Env, + transaction_id: u64, + buyer: Address, + payment_amount: i128, + ) -> Result { + settlement_core::execute_sale(&env, transaction_id, buyer, payment_amount) + } + + pub fn execute_bid(env: Env, auction_id: u64) -> Result { + security::reentrancy_guard::enter(&env)?; + let result = execute_bid_inner(&env, auction_id); + security::reentrancy_guard::exit(&env); + result + } + + pub fn place_bid( + env: Env, + auction_id: u64, + bid_amount: i128, + commitment_hash: Option>, + ) -> Result<(), SettlementError> { + let bidder = env.invoker(); + bidder.require_auth(); + auction_engine::place_bid(&env, auction_id, bidder, bid_amount, commitment_hash) + } + + pub fn reveal_bid( + env: Env, + auction_id: u64, + bid_amount: i128, + salt: BytesN<32>, + ) -> Result<(), SettlementError> { + let bidder = env.invoker(); + bidder.require_auth(); + auction_engine::reveal_bid(&env, auction_id, bidder, bid_amount, salt) + } + + pub fn distribute_transaction(env: Env, transaction_id: u64) -> Result { + security::reentrancy_guard::enter(&env)?; + let result = distribute_transaction_inner(&env, transaction_id); + security::reentrancy_guard::exit(&env); + result + } + + pub fn calculate_royalties( + env: Env, + nft_address: Address, + token_id: u64, + sale_price: i128, + ) -> Result { + let _ = (nft_address, token_id); + let fee_config = fee_manager::get_fee_config(&env)?; + let creator = env.invoker(); + let sale = SaleTransaction { + transaction_id: 0, + seller: creator.clone(), + buyer: None, + nft_address: creator.clone(), + token_id, + price: sale_price, + currency: env + .storage() + .persistent() + .get(&DataKey::DefaultCurrency) + .ok_or(SettlementError::NotInitialized)?, + state: TransactionState::Pending, + created_at: 0, + expires_at: 0, + escrow_address: creator.clone(), + royalty_info: RoyaltyDistribution { + creator_address: creator.clone(), + creator_percentage: 0, + seller_percentage: 0, + platform_percentage: fee_config.platform_fee_bps, + total_amount: sale_price, + amounts: Map::new(&env), + }, + platform_fee: 0, + }; + royalty_distributor::calculate_royalties(&env, &sale, &creator, 0, 0, fee_config.platform_fee_bps) + } + + pub fn initiate_dispute( + env: Env, + transaction_id: u64, + reason: String, + evidence_uri: Option, + ) -> Result { + let initiator = env.invoker(); + initiator.require_auth(); + let fee_config = fee_manager::get_fee_config(&env)?; + let mut arbitrators = Vec::new(&env); + arbitrators.push_back(fee_config.fee_recipient); + let dispute = dispute_resolution::initiate_dispute( + &env, + transaction_id, + initiator, + reason, + evidence_uri, + arbitrators, + 1, + )?; + Ok(dispute.dispute_id) + } + + pub fn vote_on_dispute( + env: Env, + dispute_id: u64, + vote: bool, + ) -> Result<(), SettlementError> { + let voter = env.invoker(); + voter.require_auth(); + let dispute = dispute_resolution::vote_on_dispute(&env, dispute_id, voter, vote)?; + let _ = dispute; + Ok(()) + } + + pub fn update_fee_config(env: Env, new_config: FeeConfig) -> Result<(), SettlementError> { + let admin: Address = env + .storage() + .persistent() + .get(&DataKey::Admin) + .ok_or(SettlementError::NotInitialized)?; + admin.require_auth(); + validate_fee_config(&new_config)?; + fee_manager::set_fee_config(&env, &new_config); + Ok(()) + } + + pub fn withdraw_platform_fees(env: Env, asset: Asset, amount: i128) -> Result<(), SettlementError> { + let fee_config = fee_manager::get_fee_config(&env)?; + fee_config.fee_recipient.require_auth(); + if amount <= 0 { + return Err(SettlementError::InvalidAmount); + } + let mut fees: Map = env + .storage() + .persistent() + .get(&DataKey::PlatformFees) + .unwrap_or(Map::new(&env)); + let available = fees.get(&asset).unwrap_or(0); + if amount > available { + return Err(SettlementError::InvalidAmount); + } + let remaining = utils::math_utils::checked_sub(available, amount)?; + fees.set(&asset, remaining); + env.storage() + .persistent() + .set(&DataKey::PlatformFees, &fees); + escrow_manager::transfer_out(&env, &asset, &fee_config.fee_recipient, amount)?; + Ok(()) + } + + pub fn cancel_transaction(env: Env, transaction_id: u64) -> Result<(), SettlementError> { + let caller = env.invoker(); + caller.require_auth(); + if storage::transaction_store::get_sale(&env, transaction_id).is_ok() { + let _ = settlement_core::cancel_sale(&env, transaction_id, caller)?; + return Ok(()); + } + if storage::auction_store::get_auction(&env, transaction_id).is_ok() { + let _ = settlement_core::cancel_auction(&env, transaction_id, caller)?; + return Ok(()); + } + Err(SettlementError::NotFound) + } + + pub fn emergency_withdraw( + env: Env, + transaction_id: u64, + _reason: EmergencyWithdrawalReason, + ) -> Result<(), SettlementError> { + let admin: Address = env + .storage() + .persistent() + .get(&DataKey::Admin) + .ok_or(SettlementError::NotInitialized)?; + admin.require_auth(); + if let Ok(sale) = storage::transaction_store::get_sale(&env, transaction_id) { + let nft_client = NftClient::new(&env, &sale.nft_address); + nft_client.transfer(&env.current_contract_address(), &sale.seller, &sale.token_id); + return Ok(()); + } + if let Ok(auction) = storage::auction_store::get_auction(&env, transaction_id) { + if let Some(highest_bidder) = auction.highest_bidder.clone() { + if auction.highest_bid > 0 { + escrow_manager::transfer_out(&env, &auction.currency, &highest_bidder, auction.highest_bid)?; + } + } + let nft_client = NftClient::new(&env, &auction.nft_address); + nft_client.transfer(&env.current_contract_address(), &auction.seller, &auction.token_id); + return Ok(()); + } + Err(SettlementError::NotFound) + } +} + +fn execute_bid_inner(env: &Env, auction_id: u64) -> Result { + let auction = auction_engine::finalize_auction(env, auction_id)?; + let winner = auction.highest_bidder.ok_or(SettlementError::InvalidState)?; + if auction.reserve_price > 0 && auction.highest_bid < auction.reserve_price { + if auction.highest_bid > 0 { + escrow_manager::transfer_out(env, &auction.currency, &winner, auction.highest_bid)?; + } + let nft_client = NftClient::new(env, &auction.nft_address); + nft_client.transfer(&env.current_contract_address(), &auction.seller, &auction.token_id); + let mut cancelled = auction.clone(); + cancelled.state = TransactionState::Cancelled; + storage::auction_store::set_auction(env, &cancelled); + return Ok(ExecutionResult { + transaction_id: auction.auction_id, + nft_transferred: true, + funds_distributed: false, + }); + } + let platform_fee = { + let sale = SaleTransaction { + transaction_id: auction.auction_id, + seller: auction.seller.clone(), + buyer: Some(winner.clone()), + nft_address: auction.nft_address.clone(), + token_id: auction.token_id, + price: auction.highest_bid, + currency: auction.currency.clone(), + state: TransactionState::Pending, + created_at: auction.start_time, + expires_at: auction.end_time, + escrow_address: env.current_contract_address(), + royalty_info: RoyaltyDistribution { + creator_address: auction.seller.clone(), + creator_percentage: 0, + seller_percentage: 0, + platform_percentage: fee_manager::get_fee_config(env)?.platform_fee_bps, + total_amount: auction.highest_bid, + amounts: Map::new(env), + }, + platform_fee: 0, + }; + fee_manager::take_platform_fee(env, &sale)? + }; + + let seller_amount = auction.highest_bid - platform_fee; + if seller_amount > 0 { + escrow_manager::transfer_out(env, &auction.currency, &auction.seller, seller_amount)?; + } + fee_manager::add_platform_fee(env, &auction.currency, platform_fee)?; + + let nft_client = NftClient::new(env, &auction.nft_address); + nft_client.transfer(&env.current_contract_address(), &winner, &auction.token_id); + + Ok(ExecutionResult { + transaction_id: auction.auction_id, + nft_transferred: true, + funds_distributed: true, + }) +} + +pub(crate) fn distribute_transaction_inner(env: &Env, transaction_id: u64) -> Result { + let mut sale = storage::transaction_store::get_sale(env, transaction_id)?; + if sale.state != TransactionState::Funded { + return Err(SettlementError::InvalidState); + } + let distribution = royalty_distributor::distribute_funds(env, &sale)?; + sale.platform_fee = distribution.platform_fee; + sale.state = TransactionState::Executed; + storage::transaction_store::set_sale(env, &sale); + Ok(distribution) +} + +pub fn next_sale_id(env: &Env) -> Result { + let mut counters: Counters = env + .storage() + .persistent() + .get(&DataKey::Counters) + .ok_or(SettlementError::NotInitialized)?; + counters.sale += 1; + env.storage() + .persistent() + .set(&DataKey::Counters, &counters); + Ok(counters.sale) +} + +pub fn next_auction_id(env: &Env) -> Result { + let mut counters: Counters = env + .storage() + .persistent() + .get(&DataKey::Counters) + .ok_or(SettlementError::NotInitialized)?; + counters.auction += 1; + env.storage() + .persistent() + .set(&DataKey::Counters, &counters); + Ok(counters.auction) +} + +pub fn next_dispute_id(env: &Env) -> Result { + let mut counters: Counters = env + .storage() + .persistent() + .get(&DataKey::Counters) + .ok_or(SettlementError::NotInitialized)?; + counters.dispute += 1; + env.storage() + .persistent() + .set(&DataKey::Counters, &counters); + Ok(counters.dispute) +} + +fn validate_fee_config(config: &FeeConfig) -> Result<(), SettlementError> { + if config.platform_fee_bps > 10_000 { + return Err(SettlementError::InvalidAmount); + } + if config.minimum_fee < 0 || config.maximum_fee < 0 { + return Err(SettlementError::InvalidAmount); + } + Ok(()) +} diff --git a/nftopia-stellar/contracts/marketplace_settlement/src/main.rs b/nftopia-stellar/contracts/marketplace_settlement/src/main.rs deleted file mode 100644 index e7a11a96..00000000 --- a/nftopia-stellar/contracts/marketplace_settlement/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("Hello, world!"); -} diff --git a/nftopia-stellar/contracts/marketplace_settlement/src/royalty_distributor.rs b/nftopia-stellar/contracts/marketplace_settlement/src/royalty_distributor.rs new file mode 100644 index 00000000..b358f30d --- /dev/null +++ b/nftopia-stellar/contracts/marketplace_settlement/src/royalty_distributor.rs @@ -0,0 +1,105 @@ +use soroban_sdk::{Address, Env, Map}; + +use crate::error::SettlementError; +use crate::escrow_manager; +use crate::fee_manager; +use crate::utils::math_utils::{checked_add, checked_sub, mul_bps}; +use crate::{DistributionResult, RoyaltyDistribution, SaleTransaction}; + +pub fn calculate_royalties( + env: &Env, + sale: &SaleTransaction, + creator: &Address, + creator_bps: u32, + seller_bps: u32, + platform_bps: u32, +) -> Result { + let mut amounts = Map::new(env); + let creator_amount = mul_bps(sale.price, creator_bps)?; + let platform_amount = mul_bps(sale.price, platform_bps)?; + let mut remaining = checked_sub(sale.price, creator_amount)?; + remaining = checked_sub(remaining, platform_amount)?; + let seller_amount = if seller_bps > 0 { + mul_bps(sale.price, seller_bps)? + } else { + remaining + }; + amounts.set(creator, creator_amount); + amounts.set(&sale.seller, seller_amount); + Ok(RoyaltyDistribution { + creator_address: creator.clone(), + creator_percentage: creator_bps, + seller_percentage: seller_bps, + platform_percentage: platform_bps, + total_amount: sale.price, + amounts, + }) +} + +pub fn distribute_funds(env: &Env, sale: &SaleTransaction) -> Result { + let config = fee_manager::get_fee_config(env)?; + let mut platform_fee = fee_manager::take_platform_fee(env, sale)?; + + if config.dynamic_fee_enabled { + platform_fee = fee_manager::apply_dynamic_discount(env, sale, platform_fee, &config.volume_discounts)?; + } + let mut vip_exempt = is_vip(&config.vip_exemptions, &sale.seller); + if !vip_exempt { + if let Some(buyer) = sale.buyer.clone() { + vip_exempt = is_vip(&config.vip_exemptions, &buyer); + } + } + if vip_exempt { + platform_fee = 0; + } + + let creator_address = sale.royalty_info.creator_address.clone(); + let creator_bps = sale.royalty_info.creator_percentage; + let seller_bps = sale.royalty_info.seller_percentage; + + let distribution = calculate_royalties( + env, + sale, + &creator_address, + creator_bps, + seller_bps, + config.platform_fee_bps, + )?; + + let creator_amount = distribution + .amounts + .get(&creator_address) + .unwrap_or(0); + let seller_amount = distribution + .amounts + .get(&sale.seller) + .unwrap_or(0); + + let total_out = checked_add(creator_amount, seller_amount)?; + let expected_out = checked_sub(sale.price, platform_fee)?; + if total_out != expected_out { + return Err(SettlementError::InvalidAmount); + } + + if creator_amount > 0 { + escrow_manager::transfer_out(env, &sale.currency, &creator_address, creator_amount)?; + } + escrow_manager::transfer_out(env, &sale.currency, &sale.seller, seller_amount)?; + fee_manager::add_platform_fee(env, &sale.currency, platform_fee)?; + + Ok(DistributionResult { + total_amount: sale.price, + platform_fee, + creator_amount, + seller_amount, + }) +} + +fn is_vip(vips: &soroban_sdk::Vec
, address: &Address) -> bool { + for vip in vips.iter() { + if vip == *address { + return true; + } + } + false +} diff --git a/nftopia-stellar/contracts/marketplace_settlement/src/security/frontrun_protection.rs b/nftopia-stellar/contracts/marketplace_settlement/src/security/frontrun_protection.rs new file mode 100644 index 00000000..f9cb88f2 --- /dev/null +++ b/nftopia-stellar/contracts/marketplace_settlement/src/security/frontrun_protection.rs @@ -0,0 +1,8 @@ +use soroban_sdk::{BytesN, Env}; + +pub fn compute_commitment(env: &Env, _bidder: &soroban_sdk::Address, bid_amount: i128, salt: &BytesN<32>) -> BytesN<32> { + let mut data = soroban_sdk::Bytes::new(env); + data.append(&bid_amount.to_be_bytes()); + data.append(&salt.clone().into()); + env.crypto().sha256(&data) +} diff --git a/nftopia-stellar/contracts/marketplace_settlement/src/security/mod.rs b/nftopia-stellar/contracts/marketplace_settlement/src/security/mod.rs new file mode 100644 index 00000000..66c23f5d --- /dev/null +++ b/nftopia-stellar/contracts/marketplace_settlement/src/security/mod.rs @@ -0,0 +1,2 @@ +pub mod frontrun_protection; +pub mod reentrancy_guard; diff --git a/nftopia-stellar/contracts/marketplace_settlement/src/security/reentrancy_guard.rs b/nftopia-stellar/contracts/marketplace_settlement/src/security/reentrancy_guard.rs new file mode 100644 index 00000000..90b252f0 --- /dev/null +++ b/nftopia-stellar/contracts/marketplace_settlement/src/security/reentrancy_guard.rs @@ -0,0 +1,19 @@ +use soroban_sdk::{Env, Symbol}; + +use crate::error::SettlementError; + +const REENTRANCY_KEY: Symbol = Symbol::short("reent"); + +pub fn enter(env: &Env) -> Result<(), SettlementError> { + let storage = env.storage().instance(); + let locked: bool = storage.get(&REENTRANCY_KEY).unwrap_or(false); + if locked { + return Err(SettlementError::InvalidState); + } + storage.set(&REENTRANCY_KEY, &true); + Ok(()) +} + +pub fn exit(env: &Env) { + env.storage().instance().set(&REENTRANCY_KEY, &false); +} diff --git a/nftopia-stellar/contracts/marketplace_settlement/src/settlement_core.rs b/nftopia-stellar/contracts/marketplace_settlement/src/settlement_core.rs new file mode 100644 index 00000000..7b4474ce --- /dev/null +++ b/nftopia-stellar/contracts/marketplace_settlement/src/settlement_core.rs @@ -0,0 +1,156 @@ +use soroban_sdk::{Address, Env}; + +use crate::auction_engine; +use crate::error::SettlementError; +use crate::escrow_manager; +use crate::fee_manager; +use crate::royalty_distributor; +use crate::security::reentrancy_guard; +use crate::storage::{transaction_store, auction_store}; +use crate::utils::time_utils::now; +use crate::{ + ExecutionResult, RoyaltyDistribution, SaleTransaction, TransactionState, +}; + +use crate::NftClient; + +pub fn create_sale( + env: &Env, + seller: Address, + nft_address: Address, + token_id: u64, + price: i128, + currency: Address, + duration_seconds: u64, +) -> Result { + seller.require_auth(); + if price <= 0 || duration_seconds == 0 { + return Err(SettlementError::InvalidAmount); + } + let created_at = now(env); + let expires_at = created_at.saturating_add(duration_seconds); + let sale_id = crate::next_sale_id(env)?; + + let nft_client = NftClient::new(env, &nft_address); + nft_client.transfer(&seller, &env.current_contract_address(), &token_id); + + let fee_config = fee_manager::get_fee_config(env)?; + let royalty_info = RoyaltyDistribution { + creator_address: seller.clone(), + creator_percentage: 0, + seller_percentage: 0, + platform_percentage: fee_config.platform_fee_bps, + total_amount: price, + amounts: soroban_sdk::Map::new(env), + }; + + let mut sale = SaleTransaction { + transaction_id: sale_id, + seller: seller.clone(), + buyer: None, + nft_address, + token_id, + price, + currency: currency.clone(), + state: TransactionState::Pending, + created_at, + expires_at, + escrow_address: env.current_contract_address(), + royalty_info, + platform_fee: 0, + }; + + transaction_store::set_sale(env, &sale); + Ok(sale_id) +} + +pub fn execute_sale( + env: &Env, + transaction_id: u64, + buyer: Address, + payment_amount: i128, +) -> Result { + buyer.require_auth(); + reentrancy_guard::enter(env)?; + let result = execute_sale_inner(env, transaction_id, buyer, payment_amount); + reentrancy_guard::exit(env); + result +} + +fn execute_sale_inner( + env: &Env, + transaction_id: u64, + buyer: Address, + payment_amount: i128, +) -> Result { + let mut sale = transaction_store::get_sale(env, transaction_id)?; + if sale.state != TransactionState::Pending { + return Err(SettlementError::InvalidState); + } + let current_time = now(env); + if current_time > sale.expires_at { + return Err(SettlementError::Expired); + } + if payment_amount != sale.price { + return Err(SettlementError::InvalidAmount); + } + + escrow_manager::transfer_in(env, &sale.currency, &buyer, payment_amount)?; + sale.state = TransactionState::Funded; + transaction_store::set_sale(env, &sale); + + let distribution = crate::distribute_transaction_inner(env, sale.transaction_id)?; + + let nft_client = NftClient::new(env, &sale.nft_address); + nft_client.transfer(&env.current_contract_address(), &buyer, &sale.token_id); + + sale.buyer = Some(buyer); + let mut updated_sale = transaction_store::get_sale(env, sale.transaction_id)?; + updated_sale.buyer = Some(buyer); + updated_sale.platform_fee = distribution.platform_fee; + updated_sale.state = TransactionState::Executed; + transaction_store::set_sale(env, &updated_sale); + + Ok(ExecutionResult { + transaction_id: sale.transaction_id, + nft_transferred: true, + funds_distributed: distribution.total_amount == sale.price, + }) +} + +pub fn cancel_sale( + env: &Env, + transaction_id: u64, + caller: Address, +) -> Result { + caller.require_auth(); + let mut sale = transaction_store::get_sale(env, transaction_id)?; + if sale.state != TransactionState::Pending { + return Err(SettlementError::InvalidState); + } + if caller != sale.seller { + return Err(SettlementError::Unauthorized); + } + + let nft_client = NftClient::new(env, &sale.nft_address); + nft_client.transfer(&env.current_contract_address(), &sale.seller, &sale.token_id); + sale.state = TransactionState::Cancelled; + transaction_store::set_sale(env, &sale); + Ok(sale) +} + +pub fn cancel_auction( + env: &Env, + auction_id: u64, + caller: Address, +) -> Result { + caller.require_auth(); + let auction = auction_store::get_auction(env, auction_id)?; + if caller != auction.seller { + return Err(SettlementError::Unauthorized); + } + let auction = auction_engine::cancel_auction(env, auction_id)?; + let nft_client = NftClient::new(env, &auction.nft_address); + nft_client.transfer(&env.current_contract_address(), &auction.seller, &auction.token_id); + Ok(auction) +} diff --git a/nftopia-stellar/contracts/marketplace_settlement/src/storage/auction_stores.rs b/nftopia-stellar/contracts/marketplace_settlement/src/storage/auction_stores.rs new file mode 100644 index 00000000..a7c865de --- /dev/null +++ b/nftopia-stellar/contracts/marketplace_settlement/src/storage/auction_stores.rs @@ -0,0 +1,16 @@ +use soroban_sdk::Env; + +use crate::{error::SettlementError, AuctionTransaction, DataKey}; + +pub fn get_auction(env: &Env, auction_id: u64) -> Result { + env.storage() + .persistent() + .get(&DataKey::Auction(auction_id)) + .ok_or(SettlementError::NotFound) +} + +pub fn set_auction(env: &Env, auction: &AuctionTransaction) { + env.storage() + .persistent() + .set(&DataKey::Auction(auction.auction_id), auction); +} diff --git a/nftopia-stellar/contracts/marketplace_settlement/src/storage/dispute_store.rs b/nftopia-stellar/contracts/marketplace_settlement/src/storage/dispute_store.rs new file mode 100644 index 00000000..a9603c1c --- /dev/null +++ b/nftopia-stellar/contracts/marketplace_settlement/src/storage/dispute_store.rs @@ -0,0 +1,16 @@ +use soroban_sdk::Env; + +use crate::{error::SettlementError, DataKey, Dispute}; + +pub fn get_dispute(env: &Env, dispute_id: u64) -> Result { + env.storage() + .persistent() + .get(&DataKey::Dispute(dispute_id)) + .ok_or(SettlementError::NotFound) +} + +pub fn set_dispute(env: &Env, dispute: &Dispute) { + env.storage() + .persistent() + .set(&DataKey::Dispute(dispute.dispute_id), dispute); +} diff --git a/nftopia-stellar/contracts/marketplace_settlement/src/storage/mod.rs b/nftopia-stellar/contracts/marketplace_settlement/src/storage/mod.rs new file mode 100644 index 00000000..bd6a59b5 --- /dev/null +++ b/nftopia-stellar/contracts/marketplace_settlement/src/storage/mod.rs @@ -0,0 +1,3 @@ +pub mod transaction_store; +pub mod auction_store; +pub mod dispute_store; diff --git a/nftopia-stellar/contracts/marketplace_settlement/src/storage/transaction_store.rs b/nftopia-stellar/contracts/marketplace_settlement/src/storage/transaction_store.rs new file mode 100644 index 00000000..487df45c --- /dev/null +++ b/nftopia-stellar/contracts/marketplace_settlement/src/storage/transaction_store.rs @@ -0,0 +1,16 @@ +use soroban_sdk::Env; + +use crate::{error::SettlementError, DataKey, SaleTransaction}; + +pub fn get_sale(env: &Env, sale_id: u64) -> Result { + env.storage() + .persistent() + .get(&DataKey::Sale(sale_id)) + .ok_or(SettlementError::NotFound) +} + +pub fn set_sale(env: &Env, sale: &SaleTransaction) { + env.storage() + .persistent() + .set(&DataKey::Sale(sale.transaction_id), sale); +} diff --git a/nftopia-stellar/contracts/marketplace_settlement/src/utils/math_utils.rs b/nftopia-stellar/contracts/marketplace_settlement/src/utils/math_utils.rs new file mode 100644 index 00000000..a5e304dc --- /dev/null +++ b/nftopia-stellar/contracts/marketplace_settlement/src/utils/math_utils.rs @@ -0,0 +1,18 @@ +use crate::error::SettlementError; + +pub fn checked_add(a: i128, b: i128) -> Result { + a.checked_add(b).ok_or(SettlementError::Overflow) +} + +pub fn checked_sub(a: i128, b: i128) -> Result { + a.checked_sub(b).ok_or(SettlementError::Overflow) +} + +pub fn checked_mul(a: i128, b: i128) -> Result { + a.checked_mul(b).ok_or(SettlementError::Overflow) +} + +pub fn mul_bps(amount: i128, bps: u32) -> Result { + let numerator = checked_mul(amount, bps as i128)?; + Ok(numerator / 10_000) +} diff --git a/nftopia-stellar/contracts/marketplace_settlement/src/utils/mod.rs b/nftopia-stellar/contracts/marketplace_settlement/src/utils/mod.rs new file mode 100644 index 00000000..9d750f58 --- /dev/null +++ b/nftopia-stellar/contracts/marketplace_settlement/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod math_utils; +pub mod time_utils; diff --git a/nftopia-stellar/contracts/marketplace_settlement/src/utils/time_utils.rs b/nftopia-stellar/contracts/marketplace_settlement/src/utils/time_utils.rs new file mode 100644 index 00000000..2877b94c --- /dev/null +++ b/nftopia-stellar/contracts/marketplace_settlement/src/utils/time_utils.rs @@ -0,0 +1,5 @@ +use soroban_sdk::Env; + +pub fn now(env: &Env) -> u64 { + env.ledger().timestamp() +}