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
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ version = "0.1.0"
edition = "2024"

[dependencies]
soroban-sdk = { workspace = true }
147 changes: 147 additions & 0 deletions nftopia-stellar/contracts/marketplace_settlement/src/auction_engine.rs
Original file line number Diff line number Diff line change
@@ -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<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);
}
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<BytesN<32>> = 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<AuctionTransaction, 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.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<AuctionTransaction, SettlementError> {
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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String>,
arbitrators: Vec<Address>,
required_votes: u32,
) -> Result<Dispute, SettlementError> {
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<Dispute, SettlementError> {
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)
}
18 changes: 18 additions & 0 deletions nftopia-stellar/contracts/marketplace_settlement/src/error.rs
Original file line number Diff line number Diff line change
@@ -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,
}
Original file line number Diff line number Diff line change
@@ -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(())
}
Loading
Loading