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
48 changes: 47 additions & 1 deletion gateway-contract/contracts/auction_contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,25 +129,71 @@ impl AuctionContract {

pub fn place_bid(env: Env, id: u32, bidder: Address, amount: i128) {
bidder.require_auth();
let status = storage::auction_get_status(&env, id);
if status != types::AuctionStatus::Open {
soroban_sdk::panic_with_error!(&env, errors::AuctionError::AuctionNotOpen);
}

let end_time = storage::auction_get_end_time(&env, id);
if env.ledger().timestamp() >= end_time {
soroban_sdk::panic_with_error!(&env, errors::AuctionError::AuctionNotOpen);
}

let min_bid = storage::auction_get_min_bid(&env, id);
let highest_bid = storage::auction_get_highest_bid(&env, id);
if amount < min_bid || amount <= highest_bid {
soroban_sdk::panic_with_error!(&env, errors::AuctionError::BidTooLow);
}

let asset = storage::auction_get_asset(&env, id);
let token = soroban_sdk::token::Client::new(&env, &asset);

// Accept bid funds into contract.
token.transfer(&bidder, env.current_contract_address(), &amount);

if let Some(prev_bidder) = storage::auction_get_highest_bidder(&env, id) {
token.transfer(&env.current_contract_address(), &prev_bidder, &highest_bid);
// Record outbid amount for later refund by the bidder.
let prev_amount = highest_bid;
let existing_outbid = storage::auction_get_outbid_amount(&env, id, &prev_bidder);
storage::auction_set_outbid_amount(&env, id, &prev_bidder, existing_outbid + prev_amount);
}

storage::auction_set_highest_bidder(&env, id, &bidder);
storage::auction_set_highest_bid(&env, id, amount);
}

pub fn refund_bid(env: Env, id: u32, bidder: Address) {
bidder.require_auth();

let status = storage::auction_get_status(&env, id);
if status != types::AuctionStatus::Closed {
soroban_sdk::panic_with_error!(&env, errors::AuctionError::NotClosed);
}

let highest_bidder = storage::auction_get_highest_bidder(&env, id);
if highest_bidder.as_ref().map(|h| h == &bidder).unwrap_or(false) {
soroban_sdk::panic_with_error!(&env, errors::AuctionError::NotWinner);
}

if storage::auction_is_bid_refunded(&env, id, &bidder) {
soroban_sdk::panic_with_error!(&env, errors::AuctionError::AlreadyClaimed);
}

let refund_amount = storage::auction_get_outbid_amount(&env, id, &bidder);
if refund_amount <= 0 {
soroban_sdk::panic_with_error!(&env, errors::AuctionError::InvalidState);
}

let asset = storage::auction_get_asset(&env, id);
let token = soroban_sdk::token::Client::new(&env, &asset);

storage::auction_set_bid_refunded(&env, id, &bidder);
storage::auction_set_outbid_amount(&env, id, &bidder, 0);

token.transfer(&env.current_contract_address(), &bidder, &refund_amount);
events::emit_bid_refunded(&env, &BytesN::from_array(&env, &[0u8; 32]), &bidder, refund_amount);
}

pub fn close_auction_by_id(env: Env, id: u32) {
let end_time = storage::auction_get_end_time(&env, id);
if env.ledger().timestamp() < end_time {
Expand Down
35 changes: 35 additions & 0 deletions gateway-contract/contracts/auction_contract/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,38 @@ pub fn auction_set_claimed(env: &Env, id: u32) {
PERSISTENT_BUMP_AMOUNT,
);
}

pub fn auction_get_outbid_amount(env: &Env, id: u32, bidder: &Address) -> i128 {
env.storage()
.persistent()
.get(&AuctionKey::OutbidAmount(id, bidder.clone()))
.unwrap_or(0)
}

pub fn auction_set_outbid_amount(env: &Env, id: u32, bidder: &Address, amount: i128) {
let key = AuctionKey::OutbidAmount(id, bidder.clone());
env.storage().persistent().set(&key, &amount);
env.storage().persistent().extend_ttl(
&key,
PERSISTENT_LIFETIME_THRESHOLD,
PERSISTENT_BUMP_AMOUNT,
);
}

pub fn auction_is_bid_refunded(env: &Env, id: u32, bidder: &Address) -> bool {
env.storage()
.persistent()
.get(&AuctionKey::BidRefunded(id, bidder.clone()))
.unwrap_or(false)
}

pub fn auction_set_bid_refunded(env: &Env, id: u32, bidder: &Address) {
let key = AuctionKey::BidRefunded(id, bidder.clone());
env.storage().persistent().set(&key, &true);
env.storage().persistent().extend_ttl(
&key,
PERSISTENT_LIFETIME_THRESHOLD,
PERSISTENT_BUMP_AMOUNT,
);
}

82 changes: 79 additions & 3 deletions gateway-contract/contracts/auction_contract/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,17 +234,93 @@ fn test_auction_full_lifecycle() {
client.place_bid(&1, &bidder1, &150);
client.place_bid(&1, &bidder2, &200);

// bidder1 refunded, bidder2 holds 800
assert_eq!(token.balance(&bidder1), 1000);
// bidder1 is outbid and funds are held for refund; bidder2 is highest bidder.
assert_eq!(token.balance(&bidder1), 850);
assert_eq!(token.balance(&bidder2), 800);

env.ledger().set_timestamp(1001);
client.close_auction_by_id(&1);
client.claim(&1, &bidder2);

client.refund_bid(&1, &bidder1);
assert_eq!(token.balance(&bidder1), 1000);

client.claim(&1, &bidder2);
assert_eq!(token.balance(&seller), 200);
}

#[test]
fn test_refund_bid_success() {
let env = Env::default();
env.mock_all_auths();
let (client, seller, asset) = setup(&env);
let token_admin = soroban_sdk::token::StellarAssetClient::new(&env, &asset);
let token = soroban_sdk::token::Client::new(&env, &asset);
let bidder1 = Address::generate(&env);
let bidder2 = Address::generate(&env);

token_admin.mint(&bidder1, &1000);
token_admin.mint(&bidder2, &1000);

client.create_auction(&1, &seller, &asset, &100, &1000u64);
client.place_bid(&1, &bidder1, &150);
client.place_bid(&1, &bidder2, &200);

env.ledger().set_timestamp(1001);
client.close_auction_by_id(&1);

client.refund_bid(&1, &bidder1);

assert_eq!(token.balance(&bidder1), 1000);
assert_eq!(token.balance(&bidder2), 800);
}

#[test]
#[should_panic(expected = "Error(Contract, #1)")]
fn test_refund_bid_winner_rejected() {
let env = Env::default();
env.mock_all_auths();
let (client, seller, asset) = setup(&env);
let token_admin = soroban_sdk::token::StellarAssetClient::new(&env, &asset);
let bidder1 = Address::generate(&env);
let bidder2 = Address::generate(&env);

token_admin.mint(&bidder1, &1000);
token_admin.mint(&bidder2, &1000);

client.create_auction(&1, &seller, &asset, &100, &1000u64);
client.place_bid(&1, &bidder1, &150);
client.place_bid(&1, &bidder2, &200);

env.ledger().set_timestamp(1001);
client.close_auction_by_id(&1);

client.refund_bid(&1, &bidder2);
}

#[test]
#[should_panic(expected = "Error(Contract, #2)")]
fn test_refund_bid_double_refund_panics() {
let env = Env::default();
env.mock_all_auths();
let (client, seller, asset) = setup(&env);
let token_admin = soroban_sdk::token::StellarAssetClient::new(&env, &asset);
let bidder1 = Address::generate(&env);
let bidder2 = Address::generate(&env);

token_admin.mint(&bidder1, &1000);
token_admin.mint(&bidder2, &1000);

client.create_auction(&1, &seller, &asset, &100, &1000u64);
client.place_bid(&1, &bidder1, &150);
client.place_bid(&1, &bidder2, &200);

env.ledger().set_timestamp(1001);
client.close_auction_by_id(&1);

client.refund_bid(&1, &bidder1);
client.refund_bid(&1, &bidder1);
}

#[test]
fn test_auction_no_bids_close() {
let env = Env::default();
Expand Down
64 changes: 64 additions & 0 deletions gateway-contract/contracts/core_contract/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,70 @@ fn test_resolve_stellar_linked_address_differs_from_owner() {
assert_eq!(resolved, payment_address);
}

#[test]
fn test_resolve_stellar_owner_is_linked_address() {
let env = Env::default();
env.mock_all_auths();
let (_, client) = setup(&env);

let owner = Address::generate(&env);
let hash = commitment(&env, 41);

client.register(&owner, &hash);
client.add_stellar_address(&owner, &hash, &owner);

let resolved = client.resolve_stellar(&hash);
assert_eq!(resolved, owner);
}

#[test]
fn test_resolve_stellar_after_ownership_transfer() {
let env = Env::default();
env.mock_all_auths();
let (_, client, root) = setup_with_root(&env);

let owner = Address::generate(&env);
let new_owner = Address::generate(&env);
let hash = commitment(&env, 42);

client.register(&owner, &hash);
client.add_stellar_address(&owner, &hash, &owner);

let signals = PublicSignals {
old_root: root,
new_root: BytesN::from_array(&env, &[43u8; 32]),
};

client.transfer(&owner, &hash, &new_owner, &dummy_proof(&env), &signals);

let new_address = Address::generate(&env);
client.add_stellar_address(&new_owner, &hash, &new_address);

let resolved = client.resolve_stellar(&hash);
assert_eq!(resolved, new_address);
}

#[test]
fn test_add_stellar_address_overwrites_previous() {
let env = Env::default();
env.mock_all_auths();
let (_, client) = setup(&env);

let owner = Address::generate(&env);
let hash = commitment(&env, 43);

client.register(&owner, &hash);

let original_address = Address::generate(&env);
let updated_address = Address::generate(&env);

client.add_stellar_address(&owner, &hash, &original_address);
client.add_stellar_address(&owner, &hash, &updated_address);

let resolved = client.resolve_stellar(&hash);
assert_eq!(resolved, updated_address);
}

#[test]
#[should_panic(expected = "Error(Contract, #1)")]
fn test_resolve_stellar_not_found_for_unregistered_hash() {
Expand Down
Loading