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
- Use a subnet pricing alpha at 1,000 TAO/alpha.
- As a relayer, build a batch with a large buyer (net 1,000,000 TAO) and a victim small buy (net 1 TAO).
- 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.
- Observe the victim is debited the full 1 TAO, receives zero alpha, and the order is marked
Fulfilled.
- 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
Describe the bug
execute_batched_ordersdebits 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_assetspulls each buyer's gross TAO into the pallet intermediary account with a rawtransfer_taoand no minimum-amount guard (lib.rs:989). Output alpha is then distributed pro-rata with integer floor division indistribute_alpha_pro_rata:When
sharefloors to0, the transfer is skipped butOrders::insertruns anyway. For a non-partial order,compute_order_statusreturnsFulfilledunconditionally (lib.rs:682), so the slot is now terminal. The signer's input is gone, no alpha is received, and any retry is rejected withOrderAlreadyProcessed. The collected input strands as dust in the keylessbt/limitaccount. 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
execute_batched_orders:total_buy_net = 1,000,001,total_alpha = floor(1,000,001 / 1,000) = 1,000, so the victim's share isfloor(1,000 * 1 / 1,000,001) = 0.Fulfilled.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
testnetAdditional context
No response