Skip to content

Batched order with a zero pro-rata share is charged in full, paid nothing, and permanently closed #2792

Description

@Maksandre

Describe the bug

execute_batched_orders debits every order's input up front but skips the output transfer when an order's pro-rata share floors to zero, while still marking the order terminal.

collect_assets pulls each buyer's gross TAO into the pallet intermediary account with a raw transfer_tao and no minimum-amount guard (lib.rs:989). Output alpha is then distributed pro-rata with integer floor division in distribute_alpha_pro_rata:

let share: u64 = if total_buy_net > 0 {
    total_alpha.saturating_mul(e.net as u128).checked_div(total_buy_net).unwrap_or(0) as u64
} else { 0 };
@>  if share > 0 {                                   // transfer skipped when share == 0
        T::SwapInterface::transfer_staked_alpha(/* ... */)?;
    }
let status = Self::compute_order_status(e.order_id, e.partial_fill, e.order_amount);
@>  Orders::<T>::insert(e.order_id, status);          // order closed regardless of share

When share floors to 0, the transfer is skipped but Orders::insert runs anyway. For a non-partial order, compute_order_status returns Fulfilled unconditionally (lib.rs:682), so the slot is now terminal. The signer's input is gone, no alpha is received, and any retry is rejected with OrderAlreadyProcessed. The collected input strands as dust in the keyless bt/limit account. The sell side (distribute_tao_pro_rata) has the identical shape. This is distinct from the acknowledged rounding-dust remainder: here an entire order receives nothing while its full input is consumed.

To Reproduce

  1. Use a subnet pricing alpha at 1,000 TAO/alpha.
  2. As a relayer, build a batch with a large buyer (net 1,000,000 TAO) and a victim small buy (net 1 TAO).
  3. Submit it through execute_batched_orders: total_buy_net = 1,000,001, total_alpha = floor(1,000,001 / 1,000) = 1,000, so the victim's share is floor(1,000 * 1 / 1,000,001) = 0.
  4. Observe the victim is debited the full 1 TAO, receives zero alpha, and the order is marked Fulfilled.
  5. Re-submit the victim's order: it is rejected with OrderAlreadyProcessed. The loss is permanent.

Expected behavior

An order that receives zero output must never be marked Fulfilled. Before collecting an order's input, reject or short-circuit any order whose computed share is zero (and exclude it from the aggregates); alternatively, refund the collected input and leave the order open when the share is zero.

Screenshots

No response

Environment

testnet

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions