Skip to content
Closed
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
20 changes: 20 additions & 0 deletions pallets/limit-orders/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,11 @@ pub mod pallet {
/// delivering any output (conservation), and the order stays retryable in a
/// differently-composed batch.
ZeroShareInBatch,
/// A sell order's individual slippage floor (derived from `limit_price` and
/// `max_slippage`) was breached by the current execution price. The whole
/// batch is rejected so the order stays retryable, matching the single-order
/// path which rejects the same condition and keeps the order open.
SlippageTooHigh,
}

// ── Hooks ─────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -1171,6 +1176,21 @@ pub mod pallet {
let mut sell_fees: Vec<(T::AccountId, u64)> = Vec::new();

for e in sells.iter() {
// Per-order slippage recheck: in a buy-dominant batch, sellers are
// paid at `current_price` rather than through the pool swap. Verify
// that the execution price meets each seller's individual floor
// (`effective_swap_limit`), mirroring the single-order path which
// rejects the same condition and keeps the order retryable.
if net_side == &OrderSide::Buy && e.effective_swap_limit > 0 {
let scaled_price = current_price
.saturating_mul(U64F64::from_num(1_000_000_000u64))
.saturating_to_num::<u64>();
ensure!(
scaled_price >= e.effective_swap_limit,
Error::<T>::SlippageTooHigh
);
}

let sell_tao_equiv = Self::alpha_to_tao(e.net as u128, current_price);
let gross_share: u64 = if total_sell_tao_equiv > 0 {
total_tao
Expand Down
69 changes: 69 additions & 0 deletions pallets/limit-orders/src/tests/extrinsics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2636,6 +2636,75 @@ fn execute_batched_orders_limitbuy_and_stoploss_offset_coexist_buy_dominant() {
});
}

/// In a buy-dominant batch the counter-side sells are settled at `current_price`
/// rather than through the pool swap, so the pool's price-limit check never
/// inspects them. A StopLoss with a tight `max_slippage` derives a floor above
/// that spot price; without the per-order slippage recheck the seller would be
/// filled below their floor. The batch is rejected with `SlippageTooHigh` so the
/// order stays retryable, mirroring the single-order path.
#[test]
fn execute_batched_orders_buy_dominant_rejects_sell_below_slippage_floor() {
new_test_ext().execute_with(|| {
// Price = 1.0, scaled = 1_000_000_000.
MockTime::set(1_000_000);
MockSwap::set_price(1.0);
MockSwap::set_buy_alpha_return(900);

// Alice + Bob LimitBuy dominate the batch (net side = Buy).
MockSwap::set_tao_balance(alice(), 600);
MockSwap::set_tao_balance(bob(), 400);
MockSwap::set_alpha_balance(dave(), alice(), netuid(), 100);

let alice_order = make_signed_order_with_slippage(
AccountKeyring::Alice,
bob(),
netuid(),
OrderType::LimitBuy,
600,
1_000_000_000, // 1.0 in x10^9 scale; scaled=1_000_000_000 <= 1_000_000_000 holds
FAR_FUTURE,
Perbill::zero(),
fee_recipient(),
Some(Perbill::from_percent(2)),
);
let bob_order = make_signed_order_with_slippage(
AccountKeyring::Bob,
bob(),
netuid(),
OrderType::LimitBuy,
400,
1_000_000_000, // 1.0 in x10^9 scale; scaled=1_000_000_000 <= 1_000_000_000 holds
FAR_FUTURE,
Perbill::zero(),
fee_recipient(),
Some(Perbill::from_percent(1)),
);
// Dave StopLoss: limit = 2.0 (scaled 1_000_000_000 <= 2_000_000_000 holds),
// 25% slippage -> floor = 1_500_000_000, above the 1_000_000_000 spot price.
let dave_stoploss = make_signed_order_with_slippage(
AccountKeyring::Dave,
alice(),
netuid(),
OrderType::StopLoss,
100,
2_000_000_000, // 2.0 in x10^9 scale; scaled=1_000_000_000 <= 2_000_000_000 holds
FAR_FUTURE,
Perbill::zero(),
fee_recipient(),
Some(Perbill::from_percent(25)), // floor = 1_500_000_000 > spot 1_000_000_000
);

assert_noop!(
LimitOrders::execute_batched_orders(
RuntimeOrigin::signed(charlie()),
netuid(),
bounded(vec![alice_order, bob_order, dave_stoploss]),
),
Error::<Test>::SlippageTooHigh
);
});
}

/// StopLoss with a narrow slippage sets an effective floor above the current market price,
/// making the pool swap impossible and failing the entire batch.
///
Expand Down