Skip to content

Feat/auction refund losers#131

Open
beebozy wants to merge 3 commits intoAlien-Protocol:mainfrom
beebozy:feat/auction-refund-losers
Open

Feat/auction refund losers#131
beebozy wants to merge 3 commits intoAlien-Protocol:mainfrom
beebozy:feat/auction-refund-losers

Conversation

@beebozy
Copy link
Copy Markdown

@beebozy beebozy commented Mar 24, 2026

Hi, kindly review the PR

closes #100

Summary by CodeRabbit

  • New Features

    • Auction support keyed by username-hash: create, place bids, close, and public queries for auction/bidder state
    • Trustless refund flow for losing bidders with emitted refund events; refunds are idempotent and can be invoked without caller auth
  • Tests

    • Expanded coverage: close-time settlement, refund preconditions and idempotency, winner retention, no-bids behavior, unauthenticated refund access, and rejection of post-close bids

@drips-wave
Copy link
Copy Markdown

drips-wave bot commented Mar 24, 2026

@beebozy Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 2026

📝 Walkthrough

Walkthrough

Replaces id-scoped auction implementation with a username-hash–keyed auction contract: adds AuctionState/Bid types, new DataKey storage layout and helpers, new public entrypoints (create_auction, place_bid, close_auction, refund_losers), refund events, and updated tests.

Changes

Cohort / File(s) Summary
Contract Core
gateway-contract/contracts/auction_contract/src/lib.rs
Rewrote public contract API to username-hash–keyed auctions. Added create_auction, place_bid, close_auction, refund_losers, view helpers (get_auction, has_auction, get_bid, get_all_bidders), BidRefundedEvent and BID_RFND constant; removed old id-based API and claim flow; expanded tests.
Persistent Storage
gateway-contract/contracts/auction_contract/src/storage.rs
Introduced DataKey enum and content-addressed storage keys (Auction, Bid, AllBidders). Added get/set/has helpers, TTL bumping behavior, add_bidder/get_all_bidders, and remove_bid. Removed previous id/AuctionKey-based accessors.
Contract Types
gateway-contract/contracts/auction_contract/src/types.rs
Added #[contracttype] definitions AuctionState and Bid (fields for creator, times, reserve, highest bid/bidder, settled flag; and bidder, amount, timestamp).

Sequence Diagram(s)

sequenceDiagram
    actor Creator
    actor Bidder1
    actor Bidder2
    participant Contract
    participant Storage
    participant Ledger as "XLM Ledger"

    Creator->>Contract: create_auction(username_hash, start, end, reserve)
    Contract->>Storage: set_auction(AuctionState)
    Storage-->>Contract: ack

    Bidder1->>Contract: place_bid(username_hash, amount=100)
    Contract->>Storage: get_auction(username_hash)
    Storage-->>Contract: AuctionState
    Contract->>Storage: set_bid(username_hash, Bid{100})
    Contract->>Storage: add_bidder(username_hash, Bidder1)
    Storage-->>Contract: ack

    Bidder2->>Contract: place_bid(username_hash, amount=150)
    Contract->>Storage: get_auction(username_hash)
    Storage-->>Contract: AuctionState
    Contract->>Storage: set_bid(username_hash, Bid{150})
    Contract->>Storage: add_bidder(username_hash, Bidder2)
    Storage-->>Contract: ack

    Creator->>Contract: close_auction(username_hash)
    Contract->>Storage: get_auction(username_hash)
    Storage-->>Contract: AuctionState
    Contract->>Storage: set_auction(is_settled=true)
    Storage-->>Contract: ack

    Caller->>Contract: refund_losers(username_hash)
    Contract->>Storage: get_all_bidders(username_hash)
    Storage-->>Contract: [Bidder1,Bidder2]
    Contract->>Storage: get_bid(username_hash, Bidder1)
    Storage-->>Contract: Bid{100}
    Contract->>Ledger: transfer 100 -> Bidder1
    Contract->>Storage: remove_bid(username_hash, Bidder1)
    Contract->>Contract: emit BID_RFND(Bidder1,100)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Suggested labels

Approved

Suggested reviewers

  • ryzen-xp

Poem

🐰 I hopped in code with a carrot pen,

Auctions bloom for hash-locked men,
Bids leap high, then losers sigh—refunds hop home,
Storage burrows keep each bid known,
🥕 Hop, the ledger hums, contracts roamed.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Feat/auction refund losers' is concise and directly related to the main change: implementing the refund_losers function for auction losers.
Linked Issues check ✅ Passed The implementation meets all requirements from issue #100: refund_losers function is implemented, rejects unsettled auctions, refunds all non-winners, emits BID_REFUNDED events, and requires no authentication.
Out of Scope Changes check ✅ Passed Changes include refund_losers implementation along with supporting infrastructure changes (new types AuctionState and Bid, storage refactoring with DataKey enum, new contract methods and helpers). While broader than minimal scope, all changes directly support the refund_losers functionality and improve the overall contract architecture.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
gateway-contract/contracts/auction_contract/src/types.rs (1)

1-1: Remove unused imports.

Env and Vec are imported but not used in this file. Only contracttype, Address, and BytesN are needed.

Suggested fix
-use soroban_sdk::{contracttype, Address, BytesN, Env, Vec};
+use soroban_sdk::{contracttype, Address, BytesN};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gateway-contract/contracts/auction_contract/src/types.rs` at line 1, The
imports list in types.rs includes unused symbols Env and Vec; remove those from
the use statement so only contracttype, Address, and BytesN remain (update the
use line that currently references soroban_sdk::{contracttype, Address, BytesN,
Env, Vec} to drop Env and Vec) to eliminate unused-import warnings.
gateway-contract/contracts/auction_contract/src/lib.rs (1)

65-89: Consider adding input validation for auction parameters.

The function doesn't validate that:

  • start_time < end_time (could create an auction that's already ended)
  • reserve_price >= 0 (negative reserve would be nonsensical)

Also, fix formatting per pipeline errors (run cargo fmt).

Suggested validation
 pub fn create_auction(
     env: Env,
     creator: Address,
     username_hash: BytesN<32>,
     start_time: u64,
     end_time: u64,
     reserve_price: i128,
 ) {
     creator.require_auth();
+    assert!(start_time < end_time, "start_time must be before end_time");
+    assert!(reserve_price >= 0, "reserve_price cannot be negative");
     assert!(
         !has_auction(&env, &username_hash),
         "auction already exists"
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gateway-contract/contracts/auction_contract/src/lib.rs` around lines 65 - 89,
create_auction currently allows invalid auctions; add explicit input validation
in create_auction to assert start_time < end_time and reserve_price >= 0 before
constructing AuctionState (return early or panic with clear messages like
"start_time must be before end_time" and "reserve_price must be non-negative");
keep existing creator.require_auth() and has_auction check order, then call
set_auction as before, and finally run cargo fmt to fix formatting pipeline
errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@gateway-contract/contracts/auction_contract/src/lib.rs`:
- Around line 1-578: The file fails rustfmt checks; run rustfmt (cargo fmt) to
reformat the source so the style errors are resolved across the contract and
tests (affecting items like the module headers, function signatures such as
create_auction, close_auction, refund_losers, place_bid, and the test module).
Re-run cargo fmt at the repo root and commit the resulting changes so cargo fmt
--check passes in CI; no behavioral code changes are required—only apply the
automatic formatting edits.
- Around line 542-567: The test currently creates a second Env (env2) which has
no contract state so it doesn't actually verify auth behavior; instead remove
env2 and use the same Env/contract/client (created via Env::default,
env.register_contract and AuctionContractClient::new) for the whole test,
perform setup (create_auction and close_auction) with env.mock_all_auths() only
for the setup phase, then clear the mock (or avoid mock_all_auths from the start
and only mock around the authenticated setup calls) and call
refund_losers(&hash) on the same client to assert it succeeds without
require_auth; reference: refund_losers, create_auction, close_auction,
env.mock_all_auths, AuctionContractClient::new.
- Around line 209-214: The auction currently refunds losers from contract
balance but never receives bidder funds; in the place_bid function add a call to
native_client.transfer to move the bid amount from the bidder into the contract
(use env.current_contract_address() as recipient and the bidder as sender with
the bid amount variable used in place_bid), so that refund_losers can later
transfer XLM from the contract to losing bidders; ensure the transfer uses the
same amount variable used for bids and handles any transfer result/error
consistently with other native_client.transfer calls.
- Around line 177-180: The native token client is being constructed with
env.current_contract_address(), which is the auction contract's address; update
the code to load the SAC address stored at contract initialization (or stored in
contract storage) and pass that SAC address into StellarAssetClient::new instead
of env.current_contract_address(); locate the initialization logic (or add it)
to persist the native SAC address (e.g., in contract storage) and update any
calls that create the token client (StellarAssetClient::new) to read that stored
SAC address so transfer calls use the correct native asset contract.

In `@gateway-contract/contracts/auction_contract/src/storage.rs`:
- Around line 242-262: The doc comment claiming a nonexistent MAX_BATCH_SIZE is
enforced is misleading; either remove or correct that comment in the place that
documents place_bid/lib.rs, or implement the promised enforcement: define a
constant MAX_BATCH_SIZE and add a check in place_bid (returning an error or
rejecting bids when batch size > MAX_BATCH_SIZE) before proceeding. Locate
references to place_bid and the documentation in lib.rs (and any doc comments
near add_bidder if present) and either update the text to accurately describe
current behavior or add the MAX_BATCH_SIZE constant and batch-size validation in
place_bid so the comment becomes truthful.

---

Nitpick comments:
In `@gateway-contract/contracts/auction_contract/src/lib.rs`:
- Around line 65-89: create_auction currently allows invalid auctions; add
explicit input validation in create_auction to assert start_time < end_time and
reserve_price >= 0 before constructing AuctionState (return early or panic with
clear messages like "start_time must be before end_time" and "reserve_price must
be non-negative"); keep existing creator.require_auth() and has_auction check
order, then call set_auction as before, and finally run cargo fmt to fix
formatting pipeline errors.

In `@gateway-contract/contracts/auction_contract/src/types.rs`:
- Line 1: The imports list in types.rs includes unused symbols Env and Vec;
remove those from the use statement so only contracttype, Address, and BytesN
remain (update the use line that currently references
soroban_sdk::{contracttype, Address, BytesN, Env, Vec} to drop Env and Vec) to
eliminate unused-import warnings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a2bb4f7c-4741-4926-9f11-3f08da069b2a

📥 Commits

Reviewing files that changed from the base of the PR and between a30ab14 and 85cb7f7.

📒 Files selected for processing (3)
  • gateway-contract/contracts/auction_contract/src/lib.rs
  • gateway-contract/contracts/auction_contract/src/storage.rs
  • gateway-contract/contracts/auction_contract/src/types.rs

Comment on lines +1 to +578
#![no_std]
use soroban_sdk::{contract, contractimpl};

mod storage;
mod types;

pub use storage::{
add_bidder, get_all_bidders, get_auction, get_bid, has_auction, remove_bid, set_auction,
set_bid, DataKey,
};
pub use types::{AuctionState, Bid};

use soroban_sdk::{
contract, contractimpl, contracttype, symbol_short, token, Address, BytesN, Env, Symbol, Vec,
};

// ---------------------------------------------------------------------------
// Event symbols
// ---------------------------------------------------------------------------

/// Topic symbol for the per-loser refund event.
///
/// Topics: `[BID_REFUNDED, username_hash]`
/// Data : [`BidRefundedEvent`]
const BID_REFUNDED: Symbol = symbol_short!("BID_RFND");

// ---------------------------------------------------------------------------
// Event payload types
// ---------------------------------------------------------------------------

/// Data payload emitted with every `BID_RFND` event.
///
/// Published once per outbid bidder when `refund_losers` is called so that
/// off-chain indexers can reconcile balances without replaying every ledger.
#[contracttype]
#[derive(Clone, Debug)]
pub struct BidRefundedEvent {
/// Address that received the refund.
pub bidder: Address,
/// Exact XLM amount (stroops) returned.
pub amount: i128,
/// Auction the bid belonged to.
pub auction_hash: BytesN<32>,
}

// ---------------------------------------------------------------------------
// Contract
// ---------------------------------------------------------------------------

#[contract]
pub struct AuctionContract;

#[contractimpl]
impl AuctionContract {}
impl AuctionContract {
// -----------------------------------------------------------------------
// Auction lifecycle
// -----------------------------------------------------------------------

/// Create a new auction identified by `username_hash`.
///
/// The hash must not already be in use. The `creator` must authorise
/// the call.
///
/// # Panics
/// * `"auction already exists"` — `username_hash` is already registered.
pub fn create_auction(
env: Env,
creator: Address,
username_hash: BytesN<32>,
start_time: u64,
end_time: u64,
reserve_price: i128,
) {
creator.require_auth();
assert!(
!has_auction(&env, &username_hash),
"auction already exists"
);

let state = AuctionState {
creator,
start_time,
end_time,
reserve_price,
highest_bid: 0,
highest_bidder: None,
is_settled: false,
};
set_auction(&env, &username_hash, &state);
}

/// Mark an auction as settled so that `refund_losers` becomes callable.
///
/// Only the auction creator may close it, and only after `end_time` has
/// been reached. Once closed the auction is permanently settled —
/// `is_settled` cannot be reset to `false`.
///
/// # Panics
/// * `"auction not found"` — unknown `username_hash`.
/// * `"not authorised"` — caller is not the auction `creator`.
/// * `"auction not yet ended"` — ledger timestamp < `end_time`.
/// * `"auction already settled"` — already closed.
pub fn close_auction(env: Env, caller: Address, username_hash: BytesN<32>) {
caller.require_auth();

let mut state = get_auction(&env, &username_hash).expect("auction not found");

assert!(caller == state.creator, "not authorised");
assert!(
env.ledger().timestamp() >= state.end_time,
"auction not yet ended"
);
assert!(!state.is_settled, "auction already settled");

state.is_settled = true;
set_auction(&env, &username_hash, &state);
}

// -----------------------------------------------------------------------
// Refunds
// -----------------------------------------------------------------------

/// Refund every non-winning bidder their full XLM after the auction closes.
///
/// **Trustless** — no authentication required; anyone may trigger refunds.
/// This allows a keeper, a cron job, or any third party to ensure losers
/// are made whole without relying on the auction creator.
///
/// ## Eligibility
/// An auction is eligible for `refund_losers` when `is_settled == true`
/// (set by `close_auction`). Calling before the auction is closed panics
/// with `"auction not closed"`.
///
/// ## Per-bidder logic
/// For every address in `AllBidders`:
/// 1. Skip if the address is `highest_bidder` (the winner).
/// 2. Skip if no `Bid` record exists — they were already refunded in a
/// prior call (idempotency guard).
/// 3. **Effect**: delete the `Bid` record from storage.
/// 4. **Interaction**: transfer `bid.amount` XLM from the contract to the
/// bidder via the native token client.
/// 5. **Event**: emit `BID_RFND` with a [`BidRefundedEvent`] payload.
///
/// Steps 3–5 follow the Checks–Effects–Interactions pattern to prevent
/// reentrancy from causing a double-refund.
///
/// ## Repeated calls
/// Safe to call multiple times. Bidders whose records were already
/// removed are silently skipped, so the second and subsequent calls
/// transfer nothing and emit no events.
///
/// # Arguments
/// * `env` — the contract environment.
/// * `username_hash` — 32-byte commitment hash identifying the auction.
///
/// # Panics
/// * `"auction not found"` — no auction exists for `username_hash`.
/// * `"auction not closed"` — auction is still active (`is_settled == false`).
pub fn refund_losers(env: Env, username_hash: BytesN<32>) {
// ── CHECKS ──────────────────────────────────────────────────────────

let state = get_auction(&env, &username_hash).expect("auction not found");

// Reject if auction is still open — losers cannot be determined yet.
assert!(state.is_settled, "auction not closed");

// Capture winner before iterating; None when no bids were placed.
let winner: Option<Address> = state.highest_bidder.clone();

// Snapshot the bidder list. Iterating over this Vec while we remove
// individual Bid keys underneath is safe — AllBidders is not mutated.
let bidders: Vec<Address> = get_all_bidders(&env, &username_hash);

// Native XLM client — the Stellar native asset contract is addressed
// by the network's built-in token address. In the gateway contract
// this address is stored at init time; here we derive it the standard
// Soroban way.
let native_client = token::StellarAssetClient::new(
&env,
&env.current_contract_address(),
);

// ── EFFECTS + INTERACTIONS (CEI applied per-bidder) ─────────────────

for bidder in bidders.iter() {
// Skip the winner — their bid stays locked until the claim step.
if let Some(ref w) = winner {
if bidder == *w {
continue;
}
}

// Read the bid. A None here means this bidder was already
// refunded in a previous call to refund_losers — skip to avoid
// a double-refund.
let bid = match get_bid(&env, &username_hash, &bidder) {
Some(b) => b,
None => continue,
};

let refund_amount = bid.amount;

// EFFECT: remove the bid record before the transfer. If the
// native_client.transfer panicked and the transaction was
// reverted, the remove would also be reverted, leaving the bid
// intact for a retry. On success, the record is gone and a
// second call finds None above.
remove_bid(&env, &username_hash, &bidder);

// INTERACTION: return XLM to the bidder.
native_client.transfer(
&env.current_contract_address(),
&bidder,
&refund_amount,
);

// EVENT: one emission per refunded loser.
env.events().publish(
(BID_REFUNDED, username_hash.clone()),
BidRefundedEvent {
bidder: bidder.clone(),
amount: refund_amount,
auction_hash: username_hash.clone(),
},
);
}
}

// -----------------------------------------------------------------------
// Bidding
// -----------------------------------------------------------------------

/// Place or update a bid on auction `username_hash`.
///
/// * The auction must exist and must not be settled.
/// * `amount` must strictly exceed the current `highest_bid`.
/// * `bidder` must authorise the call.
///
/// # Panics
/// * `"auction not found"` — unknown hash.
/// * `"auction already settled"` — bidding is closed.
/// * `"bid must exceed highest bid"` — `amount` is not strictly greater.
pub fn place_bid(env: Env, username_hash: BytesN<32>, bidder: Address, amount: i128) {
bidder.require_auth();

let mut state = get_auction(&env, &username_hash).expect("auction not found");

assert!(!state.is_settled, "auction already settled");
assert!(amount > state.highest_bid, "bid must exceed highest bid");

let bid = Bid {
bidder: bidder.clone(),
amount,
timestamp: env.ledger().timestamp(),
};

// Maintain bidder list before writing the bid record so the
// AllBidders key is always at least as fresh as any Bid key.
add_bidder(&env, &username_hash, bidder.clone());
set_bid(&env, &username_hash, &bidder, &bid);

state.highest_bid = amount;
state.highest_bidder = Some(bidder);
set_auction(&env, &username_hash, &state);
}

// -----------------------------------------------------------------------
// View helpers
// -----------------------------------------------------------------------

/// Returns the full [`AuctionState`] for `username_hash`.
///
/// # Panics
/// `"auction not found"` when the hash is unknown.
pub fn get_auction(env: Env, username_hash: BytesN<32>) -> AuctionState {
get_auction(&env, &username_hash).expect("auction not found")
}

/// Returns `true` if an auction record exists for `username_hash`.
pub fn has_auction(env: Env, username_hash: BytesN<32>) -> bool {
has_auction(&env, &username_hash)
}

/// Returns the [`Bid`] placed by `bidder` on `username_hash`, or `None`.
pub fn get_bid(env: Env, username_hash: BytesN<32>, bidder: Address) -> Option<Bid> {
get_bid(&env, &username_hash, &bidder)
}

/// Returns every address that has bid on `username_hash`.
pub fn get_all_bidders(env: Env, username_hash: BytesN<32>) -> Vec<Address> {
get_all_bidders(&env, &username_hash)
}
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod test {
use super::*;
use soroban_sdk::{
testutils::{Address as _, Events, Ledger, LedgerInfo},
Address, BytesN, Env, IntoVal,
};

// ── helpers ──────────────────────────────────────────────────────────────

fn test_hash(env: &Env) -> BytesN<32> {
BytesN::from_array(env, &[1u8; 32])
}

/// Standard fixture: env + client + four addresses + one open auction.
struct Setup {
env: Env,
client: AuctionContractClient<'static>,
creator: Address,
alice: Address,
bob: Address,
charlie: Address,
hash: BytesN<32>,
}

impl Setup {
fn new() -> Self {
let env = Env::default();
env.mock_all_auths();

let creator = Address::generate(&env);
let alice = Address::generate(&env);
let bob = Address::generate(&env);
let charlie = Address::generate(&env);

let contract_id = env.register_contract(None, AuctionContract);
let client = AuctionContractClient::new(&env, &contract_id);
let hash = test_hash(&env);

client.create_auction(
&creator,
&hash,
&0u64, // start_time
&1000u64, // end_time
&100i128, // reserve_price
);

Setup { env, client, creator, alice, bob, charlie, hash }
}

/// Advance ledger past end_time and mark the auction settled.
fn close(&self) {
self.env.ledger().set(LedgerInfo {
timestamp: 1001,
..self.env.ledger().get()
});
self.client.close_auction(&self.creator, &self.hash);
}
}

// ── close_auction ─────────────────────────────────────────────────────

#[test]
fn close_sets_is_settled_true() {
let s = Setup::new();
s.close();
assert!(s.client.get_auction(&s.hash).is_settled);
}

#[test]
#[should_panic(expected = "auction not yet ended")]
fn close_before_end_time_panics() {
let s = Setup::new();
// ledger timestamp is 0 — below end_time 1000.
s.client.close_auction(&s.creator, &s.hash);
}

#[test]
#[should_panic(expected = "auction already settled")]
fn close_twice_panics() {
let s = Setup::new();
s.close();
s.client.close_auction(&s.creator, &s.hash); // second call
}

// ── refund_losers — guard checks ──────────────────────────────────────

#[test]
#[should_panic(expected = "auction not found")]
fn refund_losers_unknown_hash_panics() {
let s = Setup::new();
let bad = BytesN::from_array(&s.env, &[9u8; 32]);
s.client.refund_losers(&bad);
}

#[test]
#[should_panic(expected = "auction not closed")]
fn refund_losers_before_close_panics() {
let s = Setup::new();
// is_settled is false — must be rejected.
s.client.refund_losers(&s.hash);
}

// ── refund_losers — no-bid edge case ─────────────────────────────────

#[test]
fn refund_losers_with_no_bids_is_noop() {
let s = Setup::new();
s.close();
s.client.refund_losers(&s.hash); // must not panic

// No BID_RFND events expected.
let rfnd_count = s
.env
.events()
.all()
.iter()
.filter(|e| {
e.1.get(0)
.map(|t| {
let sym: Symbol = t.into_val(&s.env);
sym == BID_REFUNDED
})
.unwrap_or(false)
})
.count();
assert_eq!(rfnd_count, 0);
}

// ── refund_losers — winner is not refunded ────────────────────────────

#[test]
fn winner_bid_record_is_not_removed() {
let s = Setup::new();
s.client.place_bid(&s.hash, &s.alice, &500i128);
s.close();
s.client.refund_losers(&s.hash);

// Alice's bid record must still exist.
assert!(
s.client.get_bid(&s.hash, &s.alice).is_some(),
"winner bid record was incorrectly removed"
);
}

// ── refund_losers — multi-bidder: all losers refunded ─────────────────

#[test]
fn all_losers_refunded_winner_kept() {
let s = Setup::new();

s.client.place_bid(&s.hash, &s.alice, &200i128); // loser
s.client.place_bid(&s.hash, &s.bob, &300i128); // loser
s.client.place_bid(&s.hash, &s.charlie, &400i128); // winner

s.close();
s.client.refund_losers(&s.hash);

// Losers' bid records must be deleted.
assert!(
s.client.get_bid(&s.hash, &s.alice).is_none(),
"alice bid should be gone after refund"
);
assert!(
s.client.get_bid(&s.hash, &s.bob).is_none(),
"bob bid should be gone after refund"
);

// Winner's record must remain.
assert!(
s.client.get_bid(&s.hash, &s.charlie).is_some(),
"winner bid must not be removed"
);

// Auction state must still name Charlie as highest_bidder.
let state = s.client.get_auction(&s.hash);
assert_eq!(state.highest_bidder, Some(s.charlie.clone()));
}

// ── refund_losers — correct event count ───────────────────────────────

#[test]
fn emits_one_event_per_loser() {
let s = Setup::new();

s.client.place_bid(&s.hash, &s.alice, &200i128); // loser → event
s.client.place_bid(&s.hash, &s.bob, &300i128); // loser → event
s.client.place_bid(&s.hash, &s.charlie, &400i128); // winner → no event

s.close();
s.client.refund_losers(&s.hash);

let rfnd_count = s
.env
.events()
.all()
.iter()
.filter(|e| {
e.1.get(0)
.map(|t| {
let sym: Symbol = t.into_val(&s.env);
sym == BID_REFUNDED
})
.unwrap_or(false)
})
.count();

assert_eq!(rfnd_count, 2, "expected exactly 2 BID_RFND events");
}

// ── refund_losers — idempotency ───────────────────────────────────────

#[test]
fn second_call_is_noop_no_extra_events() {
let s = Setup::new();

s.client.place_bid(&s.hash, &s.alice, &200i128); // loser
s.client.place_bid(&s.hash, &s.charlie, &400i128); // winner

s.close();
s.client.refund_losers(&s.hash); // first call — refunds Alice
s.client.refund_losers(&s.hash); // second call — must be a no-op

// Still only one BID_RFND event total.
let rfnd_count = s
.env
.events()
.all()
.iter()
.filter(|e| {
e.1.get(0)
.map(|t| {
let sym: Symbol = t.into_val(&s.env);
sym == BID_REFUNDED
})
.unwrap_or(false)
})
.count();

assert_eq!(rfnd_count, 1, "second call must not produce extra events");
}

// ── refund_losers — trustless: no auth required ───────────────────────

#[test]
fn refund_losers_requires_no_auth() {
// Build a closed auction with mocked auth.
let env = Env::default();
env.mock_all_auths();

let creator = Address::generate(&env);
let contract_id = env.register_contract(None, AuctionContract);
let client = AuctionContractClient::new(&env, &contract_id);
let hash = test_hash(&env);

client.create_auction(&creator, &hash, &0u64, &1000u64, &100i128);
env.ledger().set(LedgerInfo {
timestamp: 1001,
..env.ledger().get()
});
client.close_auction(&creator, &hash);

// Clear the auth mock so only explicit require_auth calls would fail.
// refund_losers has no require_auth — it must still succeed.
let env2 = Env::default();
let client2 = AuctionContractClient::new(&env2, &contract_id);
// Calling on the same contract address without mock_all_auths.
// Any require_auth inside refund_losers would panic here.
client2.refund_losers(&hash);
}

// ── place_bid — cannot bid on settled auction ─────────────────────────

#[test]
#[should_panic(expected = "auction already settled")]
fn place_bid_after_close_panics() {
let s = Setup::new();
s.close();
s.client.place_bid(&s.hash, &s.alice, &200i128);
}
} No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Run cargo fmt to fix CI failures.

The pipeline reports multiple cargo fmt --check failures throughout this file (lines 71, 174, 207, 335, 344, 446, 480, 511, 576). Run cargo fmt to auto-fix all formatting issues before merging.

🧰 Tools
🪛 GitHub Actions: Checks

[error] 71-71: cargo fmt --check failed due to Rust formatting differences. Example change: assert! macro formatting was expected to be split across multiple lines.


[error] 174-174: cargo fmt --check failed due to Rust formatting differences. Example change: token::StellarAssetClient::new call should be formatted with line breaks.


[error] 207-207: cargo fmt --check failed due to Rust formatting differences. Example change: native_client.transfer call should be formatted with line breaks.


[error] 335-335: cargo fmt --check failed due to Rust formatting differences. Example change: create_auction argument formatting (line breaks/spacing) did not match rustfmt output.


[error] 344-344: cargo fmt --check failed due to Rust formatting differences. Example change: Setup struct literal formatting was expected in multi-line form.


[error] 446-446: cargo fmt --check failed due to Rust formatting differences. Example change: comment spacing/line formatting in place_bid calls.


[error] 480-480: cargo fmt --check failed due to Rust formatting differences. Example change: comment spacing/line formatting in place_bid calls.


[error] 511-511: cargo fmt --check failed due to Rust formatting differences. Example change: comment spacing/line formatting in place_bid call.


[error] 576-576: cargo fmt --check failed due to Rust formatting differences in test code around place_bid call.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gateway-contract/contracts/auction_contract/src/lib.rs` around lines 1 - 578,
The file fails rustfmt checks; run rustfmt (cargo fmt) to reformat the source so
the style errors are resolved across the contract and tests (affecting items
like the module headers, function signatures such as create_auction,
close_auction, refund_losers, place_bid, and the test module). Re-run cargo fmt
at the repo root and commit the resulting changes so cargo fmt --check passes in
CI; no behavioral code changes are required—only apply the automatic formatting
edits.

Comment on lines +177 to +180
let native_client = token::StellarAssetClient::new(
&env,
&env.current_contract_address(),
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

How to get native XLM token address in Soroban SDK for transfers?

💡 Result:

To transfer native XLM via the Soroban token interface, you don’t use a special “XLM address” baked into soroban-sdk. Instead you use the Stellar Asset Contract (SAC) contract ID for the native asset (it’s deterministic per network) and pass that Address into token::TokenClient. The SAC is the standard way contracts interact with XLM and issued assets. [1]

Get the native XLM SAC contract address (off-chain)

Use the Stellar CLI to derive it:

stellar contract id asset --network <testnet|mainnet|...> --asset native

This returns the contract address (C...) you should use as the “XLM token address”. [2]

(Older Soroban CLI flows also expose this via soroban lab token id --asset native --network ....) [3]

Use it for transfers (on-chain contract code)

Once you have that address, treat it like any other SEP-41 token contract:

use soroban_sdk::{Env, Address};
use soroban_sdk::token::TokenClient;

pub fn pay_xlm(e: &Env, xlm_sac: &Address, from: &Address, to: &Address, amount: i128) {
    let xlm = TokenClient::new(e, xlm_sac);
    xlm.transfer(from, to, &amount);
}

TokenClient works with any token contract address, including an SAC address. [4]

Sources
[1] Stellar docs: Stellar Asset Contract (SAC) overview / “only way for contracts to interact with…native XLM” https://developers.stellar.org/docs/tokens/stellar-asset-contract
[2] Stellar docs (CLI cookbook): stellar contract id asset --asset native https://developers.stellar.org/docs/tools/cli/cookbook/deploy-stellar-asset-contract
[3] Token Playground (CLI): soroban lab token id --asset native https://dev.to/esteblock/token-playground-chapter-9-read-the-native-token-xlm-using-soroban-cli-cb3
[4] Stellar docs: instantiate token::TokenClient with a token identifier/address and call transfer https://developers.stellar.org/docs/build/guides/tokens/stellar-asset-contract


🏁 Script executed:

# First, let's read the file around lines 177-180 to see the exact code
sed -n '170,190p' gateway-contract/contracts/auction_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 955


🏁 Script executed:

# Check the imports to see what types are being used
head -50 gateway-contract/contracts/auction_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 1649


🏁 Script executed:

# Search for other usages of native token or token client in this file
rg -n "token::" gateway-contract/contracts/auction_contract/src/lib.rs | head -20

Repository: Alien-Protocol/Alien-Gateway

Length of output: 135


🏁 Script executed:

# Check if there's initialization code that stores native token address
rg -n "init\|new\|native" gateway-contract/contracts/auction_contract/src/lib.rs | head -30

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

# Look for the storage or data structures that might contain token addresses
rg -n "struct\|const\|static" gateway-contract/contracts/auction_contract/src/lib.rs | head -20

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

# Search for init function and how it might store native token address
rg -n "fn init\|fn new\|#\[contractimpl\]" gateway-contract/contracts/auction_contract/src/lib.rs -A 10

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

# Search for storage patterns - check the storage module
cat gateway-contract/contracts/auction_contract/src/storage.rs | head -100

Repository: Alien-Protocol/Alien-Gateway

Length of output: 4073


🏁 Script executed:

# Search for how native token address might be accessed
rg -n "native\|token_address\|xlm\|XLM" gateway-contract/contracts/auction_contract/src/

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

# Look at the entire lib.rs to understand the contract structure better
wc -l gateway-contract/contracts/auction_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 130


🏁 Script executed:

# Check if there's any documentation or examples of how this contract uses native token
rg -n "refund\|transfer" gateway-contract/contracts/auction_contract/src/lib.rs | head -20

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

# Find the refund_losers function completely
rg -n "fn refund_losers" gateway-contract/contracts/auction_contract/src/lib.rs -A 70

Repository: Alien-Protocol/Alien-Gateway

Length of output: 7858


🏁 Script executed:

# Search for any public contract functions (entry points)
rg -n "#\[contractimpl\]|pub fn " gateway-contract/contracts/auction_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 704


🏁 Script executed:

# Let's check how native_client is actually used after creation
sed -n '177,226p' gateway-contract/contracts/auction_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 1897


🏁 Script executed:

# Search for any comments or documentation about StellarAssetClient usage
rg -n "StellarAssetClient\|native_client" gateway-contract/contracts/auction_contract/src/lib.rs -B 3 -A 3

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

# Check if there's any setup or initialization happening elsewhere
find gateway-contract/contracts/auction_contract -name "*.rs" -type f

Repository: Alien-Protocol/Alien-Gateway

Length of output: 358


🏁 Script executed:

# Check the Soroban SDK usage - look for any documentation or other token client patterns
rg -n "TokenClient\|token::" gateway-contract/contracts/auction_contract/src/ -B 1 -A 1

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

# Check if StellarAssetClient is imported from soroban_sdk
grep -n "use soroban_sdk" gateway-contract/contracts/auction_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 120


🏁 Script executed:

# Look at the create_auction function to see if there's any initialization pattern
sed -n '52,102p' gateway-contract/contracts/auction_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 1827


🏁 Script executed:

# Check other Soroban contracts in the repo to see how they handle native XLM
find gateway-contract -name "*.rs" -type f -exec grep -l "StellarAssetClient\|native.*token\|XLM" {} \;

Repository: Alien-Protocol/Alien-Gateway

Length of output: 126


🏁 Script executed:

# Search for any Soroban SDK documentation or examples in comments
rg -n "developers.stellar\|soroban.*native\|SAC\|asset.*contract" gateway-contract/

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

# Let's check the Soroban SDK version or look for documentation in the repo
find gateway-contract -name "Cargo.toml" -type f | head -3

Repository: Alien-Protocol/Alien-Gateway

Length of output: 206


🏁 Script executed:

# Check the Cargo.toml to see soroban-sdk version
cat gateway-contract/contracts/auction_contract/Cargo.toml

Repository: Alien-Protocol/Alien-Gateway

Length of output: 344


🏁 Script executed:

# Look for any README or documentation
find gateway-contract -name "README*" -o -name "*.md" | head -10

Repository: Alien-Protocol/Alien-Gateway

Length of output: 310


🏁 Script executed:

# Let me check the test setup to see how the native token is supposed to work
sed -n '300,380p' gateway-contract/contracts/auction_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 2532


🏁 Script executed:

# Check the workspace Cargo.toml for soroban-sdk version
cat gateway-contract/Cargo.toml

Repository: Alien-Protocol/Alien-Gateway

Length of output: 632


🏁 Script executed:

# Let's look at the auction.md documentation
cat gateway-contract/contracts/auction_contract/auction.md

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

# Search for how other contracts might handle token transfers
find gateway-contract -name "*.rs" -type f -exec grep -l "transfer\|token" {} \;

Repository: Alien-Protocol/Alien-Gateway

Length of output: 405


🏁 Script executed:

# Check the escrow contract's token handling
grep -n "token\|transfer\|StellarAsset" gateway-contract/contracts/escrow_contract/src/lib.rs | head -30

Repository: Alien-Protocol/Alien-Gateway

Length of output: 184


🏁 Script executed:

# Let's look at the escrow contract transfer implementation
rg -n "transfer|token::" gateway-contract/contracts/escrow_contract/src/lib.rs -B 2 -A 5

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

# Search for how the escrow contract gets the native token address
rg -n "native\|current_contract\|token.*address" gateway-contract/contracts/escrow_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

# Let's check the soroban-sdk v23 documentation by looking at usage patterns
web_search
Soroban SDK v23 StellarAssetClient vs TokenClient native XLM

Repository: Alien-Protocol/Alien-Gateway

Length of output: 168


🌐 Web query:

Soroban SDK v23 StellarAssetClient native XLM token address

💡 Result:

In Soroban SDK v23 there isn’t a single “native XLM token address” you can hardcode. XLM is represented in contracts via the Stellar Asset Contract (SAC), whose contract ID (address) is deterministic per network and must be derived (or looked up) for the asset. (developers.stellar.org)

To get the XLM SAC contract address (ID), use the Stellar CLI with the native-asset identifier "native": (developers.stellar.org)

stellar contract id asset --asset native --network testnet
# or --network mainnet

Use the resulting C… contract ID as the address you pass to TokenClient / StellarAssetClient. (developers.stellar.org)

Citations:


🌐 Web query:

Soroban SDK TokenClient StellarAssetClient difference

💡 Result:

  • token::TokenClient: a generic Rust client for calling any Soroban token contract that implements the SEP-41 “TokenInterface” (e.g., transfer, balance, approve/allowance, etc.). Use this when you want interoperability with any SEP-41 token (including Stellar Asset Contracts). [1][2]

  • token::StellarAssetClient: a Rust client for calling the Stellar Asset Contract–specific interface (“StellarAssetInterface” / CAP-46-6 standardized asset), which includes extra SAC-only functions such as mint, clawback, set_admin, set_authorized, and related admin/authorization queries. Use this when you specifically need SAC behavior beyond SEP-41. [1][3]

Rule of thumb: default to TokenClient; switch to StellarAssetClient only when you need SAC-only admin/issuer controls (mint/clawback/authorization/admin changes). [1]

Sources:
[1] https://developers.stellar.org/docs/build/guides/tokens/stellar-asset-contract
[2] https://docs.rs/soroban-sdk/latest/soroban_sdk/token/struct.TokenClient.html
[3] https://docs.rs/soroban-sdk/latest/soroban_sdk/token/struct.StellarAssetClient.html


🏁 Script executed:

# Let's look at the entire refund_losers function and the native_client.transfer call more carefully
sed -n '173,215p' gateway-contract/contracts/auction_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 1782


Critical: Native token client initialized with wrong address.

StellarAssetClient::new is being passed env.current_contract_address(), which returns the auction contract's address, not the Stellar Asset Contract (SAC) address for native XLM. This will cause transfer calls to fail.

The SAC address for native XLM is deterministic per network and must be obtained via the Stellar CLI (stellar contract id asset --asset native --network <testnet|mainnet>), then either stored during contract initialization or accessed from storage. The current code does neither—the misleading comment claims the address is "stored at init time," but there is no such initialization logic.

To fix: store the native token SAC address and pass it to the token client, rather than using env.current_contract_address().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gateway-contract/contracts/auction_contract/src/lib.rs` around lines 177 -
180, The native token client is being constructed with
env.current_contract_address(), which is the auction contract's address; update
the code to load the SAC address stored at contract initialization (or stored in
contract storage) and pass that SAC address into StellarAssetClient::new instead
of env.current_contract_address(); locate the initialization logic (or add it)
to persist the native SAC address (e.g., in contract storage) and update any
calls that create the token client (StellarAssetClient::new) to read that stored
SAC address so transfer calls use the correct native asset contract.

Comment on lines +209 to +214
// INTERACTION: return XLM to the bidder.
native_client.transfer(
&env.current_contract_address(),
&bidder,
&refund_amount,
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's a deposit mechanism elsewhere
rg -n "transfer.*bidder" gateway-contract/contracts/auction_contract/
rg -n "native.*transfer" gateway-contract/contracts/auction_contract/

Repository: Alien-Protocol/Alien-Gateway

Length of output: 296


🏁 Script executed:

cat -n gateway-contract/contracts/auction_contract/src/lib.rs | sed -n '242,280p'

Repository: Alien-Protocol/Alien-Gateway

Length of output: 1889


🏁 Script executed:

rg -A 30 "fn place_bid" gateway-contract/contracts/auction_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 1378


🏁 Script executed:

rg -B 5 -A 15 "transfer.*amount\|transfer.*bid" gateway-contract/contracts/auction_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

rg -B 5 -A 40 "fn settle_auction" gateway-contract/contracts/auction_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

rg -n "native_client\|transfer\|deposit" gateway-contract/contracts/auction_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

cat -n gateway-contract/contracts/auction_contract/src/lib.rs | sed -n '190,230p'

Repository: Alien-Protocol/Alien-Gateway

Length of output: 1901


🏁 Script executed:

rg -n "pub fn|fn " gateway-contract/contracts/auction_contract/src/lib.rs | head -20

Repository: Alien-Protocol/Alien-Gateway

Length of output: 1198


🏁 Script executed:

cat -n gateway-contract/contracts/auction_contract/src/lib.rs | head -70

Repository: Alien-Protocol/Alien-Gateway

Length of output: 2781


🏁 Script executed:

rg -B 5 -A 5 "native_client" gateway-contract/contracts/auction_contract/src/lib.rs | head -40

Repository: Alien-Protocol/Alien-Gateway

Length of output: 1216


🏁 Script executed:

cat -n gateway-contract/contracts/auction_contract/src/lib.rs | sed -n '320,450p'

Repository: Alien-Protocol/Alien-Gateway

Length of output: 4931


🏁 Script executed:

rg -B 10 -A 20 "fn new\(\)" gateway-contract/contracts/auction_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 995


place_bid must transfer XLM from bidder to contract for refund_losers to work.

The refund_losers function attempts to transfer XLM from the contract to losing bidders (line 210), but place_bid never transfers XLM from the bidder into the contract. Add the corresponding transfer in place_bid to deposit the bid amount, otherwise the contract will have no funds to refund and refund_losers will fail at runtime.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gateway-contract/contracts/auction_contract/src/lib.rs` around lines 209 -
214, The auction currently refunds losers from contract balance but never
receives bidder funds; in the place_bid function add a call to
native_client.transfer to move the bid amount from the bidder into the contract
(use env.current_contract_address() as recipient and the bidder as sender with
the bid amount variable used in place_bid), so that refund_losers can later
transfer XLM from the contract to losing bidders; ensure the transfer uses the
same amount variable used for bids and handles any transfer result/error
consistently with other native_client.transfer calls.

Comment on lines +242 to +264
pub fn place_bid(env: Env, username_hash: BytesN<32>, bidder: Address, amount: i128) {
bidder.require_auth();

let mut state = get_auction(&env, &username_hash).expect("auction not found");

assert!(!state.is_settled, "auction already settled");
assert!(amount > state.highest_bid, "bid must exceed highest bid");

let bid = Bid {
bidder: bidder.clone(),
amount,
timestamp: env.ledger().timestamp(),
};

// Maintain bidder list before writing the bid record so the
// AllBidders key is always at least as fresh as any Bid key.
add_bidder(&env, &username_hash, bidder.clone());
set_bid(&env, &username_hash, &bidder, &bid);

state.highest_bid = amount;
state.highest_bidder = Some(bidder);
set_auction(&env, &username_hash, &state);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing reserve price validation and previous bid handling.

  1. Reserve price not enforced: Bids are only checked against highest_bid, not reserve_price. A first bid of 1 stroop would be accepted even if reserve_price is 100.

  2. Previous bid handling unclear: If a bidder places multiple bids (each overwriting the previous), the doc in types.rs says "placing a new bid overwrites the previous record", but the previous bid amount isn't refunded or accounted for.

Suggested fix for reserve price
 assert!(!state.is_settled, "auction already settled");
+assert!(amount >= state.reserve_price, "bid below reserve price");
 assert!(amount > state.highest_bid, "bid must exceed highest bid");

Comment on lines +542 to +567
#[test]
fn refund_losers_requires_no_auth() {
// Build a closed auction with mocked auth.
let env = Env::default();
env.mock_all_auths();

let creator = Address::generate(&env);
let contract_id = env.register_contract(None, AuctionContract);
let client = AuctionContractClient::new(&env, &contract_id);
let hash = test_hash(&env);

client.create_auction(&creator, &hash, &0u64, &1000u64, &100i128);
env.ledger().set(LedgerInfo {
timestamp: 1001,
..env.ledger().get()
});
client.close_auction(&creator, &hash);

// Clear the auth mock so only explicit require_auth calls would fail.
// refund_losers has no require_auth — it must still succeed.
let env2 = Env::default();
let client2 = AuctionContractClient::new(&env2, &contract_id);
// Calling on the same contract address without mock_all_auths.
// Any require_auth inside refund_losers would panic here.
client2.refund_losers(&hash);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Test logic is flawed — env2 doesn't have the contract state.

This test attempts to verify trustless operation by creating a fresh Env without mock_all_auths(), but:

  1. env2 is a completely separate environment that doesn't share state with env
  2. The contract registered on env isn't accessible from env2
  3. This test likely passes only because it fails silently or the assertion logic is never reached

A correct approach would be to not call mock_all_auths() in a dedicated setup, then verify refund_losers succeeds while an authenticated method like close_auction would fail without auth.

Suggested test approach
#[test]
fn refund_losers_requires_no_auth() {
    // Don't mock auth - real auth checks will apply
    let env = Env::default();
    
    let creator = Address::generate(&env);
    let contract_id = env.register_contract(None, AuctionContract);
    let client = AuctionContractClient::new(&env, &contract_id);
    let hash = test_hash(&env);

    // These need auth - mock just for setup
    env.mock_all_auths();
    client.create_auction(&creator, &hash, &0u64, &1000u64, &100i128);
    env.ledger().set(LedgerInfo {
        timestamp: 1001,
        ..env.ledger().get()
    });
    client.close_auction(&creator, &hash);
    
    // Clear mock - from here, require_auth() calls would panic
    // But refund_losers has no require_auth, so it should succeed
    // Note: Soroban test env may not support clearing mocks mid-test;
    // alternative is to verify via auth inspection
    client.refund_losers(&hash); // should succeed without auth
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gateway-contract/contracts/auction_contract/src/lib.rs` around lines 542 -
567, The test currently creates a second Env (env2) which has no contract state
so it doesn't actually verify auth behavior; instead remove env2 and use the
same Env/contract/client (created via Env::default, env.register_contract and
AuctionContractClient::new) for the whole test, perform setup (create_auction
and close_auction) with env.mock_all_auths() only for the setup phase, then
clear the mock (or avoid mock_all_auths from the start and only mock around the
authenticated setup calls) and call refund_losers(&hash) on the same client to
assert it succeeds without require_auth; reference: refund_losers,
create_auction, close_auction, env.mock_all_auths, AuctionContractClient::new.

Comment on lines +242 to +262
pub fn add_bidder(env: &Env, hash: &BytesN<32>, bidder: Address) {
let key = DataKey::AllBidders(hash.clone());
let mut bidders: Vec<Address> = env
.storage()
.persistent()
.get(&key)
.unwrap_or_else(|| Vec::new(env));

// Deduplication: only append if the address is not already in the list.
for existing in bidders.iter() {
if existing == bidder {
return;
}
}

bidders.push_back(bidder);
env.storage().persistent().set(&key, &bidders);
env.storage()
.persistent()
.extend_ttl(&key, LEDGER_THRESHOLD, LEDGER_BUMP);
} No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify MAX_BATCH_SIZE constant exists and is enforced
rg -n "MAX_BATCH_SIZE" gateway-contract/

Repository: Alien-Protocol/Alien-Gateway

Length of output: 194


🏁 Script executed:

# Check if MAX_BATCH_SIZE is defined in lib.rs
rg -n "MAX_BATCH_SIZE\s*=" gateway-contract/contracts/auction_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

# Get the actual code at line 260 and surrounding context to see the formatting issue
sed -n '255,265p' gateway-contract/contracts/auction_contract/src/storage.rs | cat -n

Repository: Alien-Protocol/Alien-Gateway

Length of output: 317


🏁 Script executed:

# Check if lib.rs exists and search for any batch size or bidder limits
ls -la gateway-contract/contracts/auction_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 176


🏁 Script executed:

# Search across entire gateway-contract for MAX_BATCH_SIZE definition (not just references)
rg "MAX_BATCH_SIZE\s*[:=]" gateway-contract/

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

# Look for any batch size related constants or validation
rg -i "batch.*size|max.*bidder" gateway-contract/contracts/auction_contract/src/

Repository: Alien-Protocol/Alien-Gateway

Length of output: 190


🏁 Script executed:

# Search lib.rs for calls to add_bidder and any batch-related logic
rg -n "add_bidder|bidder" gateway-contract/contracts/auction_contract/src/lib.rs | head -30

Repository: Alien-Protocol/Alien-Gateway

Length of output: 1924


🏁 Script executed:

# Get a broader view of lib.rs to understand the context
wc -l gateway-contract/contracts/auction_contract/src/lib.rs && head -100 gateway-contract/contracts/auction_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 3328


Remove misleading doc comment about MAX_BATCH_SIZE enforcement.

The doc comment claims MAX_BATCH_SIZE is enforced at the call site in lib.rs, but this constant does not exist anywhere in the codebase, nor is any batch size validation performed in place_bid. Either remove the misleading claim or implement the enforced limit it promises.

🧰 Tools
🪛 GitHub Actions: Checks

[error] 260-260: cargo fmt --check failed due to Rust formatting differences (blank line/formatting near persistent().extend_ttl(...)).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gateway-contract/contracts/auction_contract/src/storage.rs` around lines 242
- 262, The doc comment claiming a nonexistent MAX_BATCH_SIZE is enforced is
misleading; either remove or correct that comment in the place that documents
place_bid/lib.rs, or implement the promised enforcement: define a constant
MAX_BATCH_SIZE and add a check in place_bid (returning an error or rejecting
bids when batch size > MAX_BATCH_SIZE) before proceeding. Locate references to
place_bid and the documentation in lib.rs (and any doc comments near add_bidder
if present) and either update the text to accurately describe current behavior
or add the MAX_BATCH_SIZE constant and batch-size validation in place_bid so the
comment becomes truthful.

@ryzen-xp ryzen-xp added the CI Failed Please check why you CI is faileing fix your code label Mar 24, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@gateway-contract/contracts/auction_contract/src/lib.rs`:
- Around line 69-77: Add a validation that end_time is strictly greater than
start_time in the auction-creation path: after the creator.require_auth() call
and before checking/creating the auction (around where has_auction(&env,
&username_hash) is used), insert an assertion or conditional that enforces
end_time > start_time (e.g., assert!(end_time > start_time, "end_time must be
greater than start_time")). This ensures the auction window is valid before
proceeding to create the auction.
- Around line 245-248: The bid path allows bids before the auction opens; update
the bid validation in the function using get_auction (where you check
state.is_settled and amount > state.highest_bid) to also verify the current
chain time by calling env.ledger().timestamp() and comparing it to
state.start_time (e.g. assert env.ledger().timestamp() >= state.start_time or
return an appropriate error/message if the auction hasn't started) before
accepting the bid.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e25e445e-f879-4b93-be0e-7298c13c98f0

📥 Commits

Reviewing files that changed from the base of the PR and between 85cb7f7 and 185ce35.

📒 Files selected for processing (2)
  • gateway-contract/contracts/auction_contract/src/lib.rs
  • gateway-contract/contracts/auction_contract/src/storage.rs

Comment on lines +69 to +77
start_time: u64,
end_time: u64,
reserve_price: i128,
) {
creator.require_auth();
assert!(
!has_auction(&env, &username_hash),
"auction already exists"
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

No validation that end_time > start_time in auction creation.

The function accepts any start_time and end_time values without verifying the auction window is valid. An auction with end_time <= start_time would be immediately closed or have undefined behavior.

Suggested fix
 creator.require_auth();
 assert!(
     !has_auction(&env, &username_hash),
     "auction already exists"
 );
+assert!(end_time > start_time, "end_time must be after start_time");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
start_time: u64,
end_time: u64,
reserve_price: i128,
) {
creator.require_auth();
assert!(
!has_auction(&env, &username_hash),
"auction already exists"
);
start_time: u64,
end_time: u64,
reserve_price: i128,
) {
creator.require_auth();
assert!(
!has_auction(&env, &username_hash),
"auction already exists"
);
assert!(end_time > start_time, "end_time must be after start_time");
🧰 Tools
🪛 GitHub Actions: Checks

[error] 71-71: cargo fmt --check failed. rustfmt reported formatting changes needed (e.g., assert! formatting collapsed to single line).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gateway-contract/contracts/auction_contract/src/lib.rs` around lines 69 - 77,
Add a validation that end_time is strictly greater than start_time in the
auction-creation path: after the creator.require_auth() call and before
checking/creating the auction (around where has_auction(&env, &username_hash) is
used), insert an assertion or conditional that enforces end_time > start_time
(e.g., assert!(end_time > start_time, "end_time must be greater than
start_time")). This ensures the auction window is valid before proceeding to
create the auction.

Comment on lines +245 to +248
let mut state = get_auction(&env, &username_hash).expect("auction not found");

// Get winner and winning bid
let winner = storage::get_highest_bidder(&env);
let winning_bid = storage::get_highest_bid(&env);
assert!(!state.is_settled, "auction already settled");
assert!(amount > state.highest_bid, "bid must exceed highest bid");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Bids can be placed before the auction starts.

The function validates the auction exists and isn't settled, but doesn't check if env.ledger().timestamp() >= state.start_time. Users could place bids before the auction officially opens.

Suggested fix
 let mut state = get_auction(&env, &username_hash).expect("auction not found");

 assert!(!state.is_settled, "auction already settled");
+assert!(
+    env.ledger().timestamp() >= state.start_time,
+    "auction not yet started"
+);
+assert!(amount >= state.reserve_price, "bid below reserve price");
 assert!(amount > state.highest_bid, "bid must exceed highest bid");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gateway-contract/contracts/auction_contract/src/lib.rs` around lines 245 -
248, The bid path allows bids before the auction opens; update the bid
validation in the function using get_auction (where you check state.is_settled
and amount > state.highest_bid) to also verify the current chain time by calling
env.ledger().timestamp() and comparing it to state.start_time (e.g. assert
env.ledger().timestamp() >= state.start_time or return an appropriate
error/message if the auction hasn't started) before accepting the bid.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI Failed Please check why you CI is faileing fix your code

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

[Auction] Implement refund_losers(username_hash) in lib.rs — full XLM refund for all outbid bidders

2 participants