Skip to content

Latest commit

 

History

History
232 lines (170 loc) · 12 KB

File metadata and controls

232 lines (170 loc) · 12 KB

pallet-limit-orders

A FRAME pallet for off-chain signed limit orders on Bittensor subnets.

Users sign orders off-chain and submit them to a relayer. The relayer batches orders targeting the same subnet and submits them via execute_batched_orders, which nets the buy and sell sides, executes a single AMM pool swap for the residual, and distributes outputs pro-rata to all participants. This minimises price impact compared to executing each order independently against the pool.

MEV protection is available for free: any caller can wrap execute_orders or execute_batched_orders inside pallet_shield::submit_encrypted to hide the batch contents from the mempool until the block is proposed.


Order lifecycle

User signs Order off-chain
        │
        ▼
Relayer submits via execute_orders        Relayer submits via execute_batched_orders
        (one-by-one, best-effort)                  (aggregated, atomic)
        │                                           │
        ├─ Invalid / expired /                      ├─ Any order invalid / expired /
        │  price-not-met →                          │  price-not-met / root netuid →
        │  silently skipped (no state change)       │  entire batch fails (DispatchError)
        │                                           │
        └─ Valid → executed                         └─ All orders valid → net pool swap
                        │                                   → distribute pro-rata
                        └─ order_id written to Orders as Fulfilled
                           (prevents replay)

User can cancel at any time via cancel_order
        └─ order_id written to Orders as Cancelled

Data structures

Order<AccountId>

The payload that a user signs off-chain. Never stored in full on-chain — only its blake2_256 hash (OrderId) is persisted.

Field Type Description
signer AccountId Coldkey that authorises the order. For buy types: pays TAO. For sell types: owns the staked alpha.
hotkey AccountId Hotkey to stake to (buy types) or unstake from (sell types).
netuid NetUid Target subnet.
order_type OrderType One of LimitBuy, TakeProfit, or StopLoss (see table below).
amount u64 Input amount in raw units. TAO for buy types; alpha for sell types.
limit_price u64 Price threshold in TAO/alpha raw units. Trigger direction depends on OrderType (see table below).
expiry u64 Unix timestamp in milliseconds. Order must not execute after this time.
fee_rate Perbill Per-order fee as a fraction of the input amount. Perbill::zero() = no fee.
fee_recipient AccountId Account that receives the fee collected for this order.

OrderType

Variant Action Triggers when Use case
LimitBuy Buy alpha price ≤ limit_price Enter a position at or below a target price.
TakeProfit Sell alpha price ≥ limit_price Exit a position once price rises to a profit target.
StopLoss Sell alpha price ≤ limit_price Exit a position to limit downside if price falls to a floor.

SignedOrder<AccountId, Signature>

Envelope submitted by the relayer: the Order payload plus the user's sr25519/ed25519 signature over its SCALE encoding. Signature verification uses order.signer as the expected public key.

OrderStatus

Terminal state of a processed order, stored under its OrderId.

Variant Meaning
Fulfilled Order was successfully executed.
Cancelled User registered a cancellation intent before execution.

Storage

Orders: StorageMap<H256, OrderStatus>

Maps an OrderId (blake2_256 of the SCALE-encoded Order) to its terminal OrderStatus. Absence means the order has never been seen and is still executable (provided it is valid). Presence means it is permanently closed — neither Fulfilled nor Cancelled orders can be re-executed.


Config

Item Type Description
Signature Verify + ... Signature type for off-chain order authorisation. Set to sp_runtime::MultiSignature in the subtensor runtime.
SwapInterface OrderSwapInterface<Self::AccountId> Full swap + balance execution interface. Implemented by pallet_subtensor::Pallet<T>. Provides buy_alpha, sell_alpha, transfer_tao, transfer_staked_alpha, and current_alpha_price.
TimeProvider UnixTime Current wall-clock time for expiry checks.
MaxOrdersPerBatch Get<u32> (constant) Maximum number of orders accepted in a single execute_orders or execute_batched_orders call. Should equal floor(max_block_weight / per_order_weight).
PalletId Get<PalletId> (constant) Used to derive the pallet intermediary account (PalletId::into_account_truncating). This account temporarily holds pooled TAO and staked alpha during execute_batched_orders.
PalletHotkey Get<Self::AccountId> (constant) Hotkey the pallet intermediary account stakes to/from during batch execution. Must be a dedicated hotkey registered on every subnet the pallet may operate on. Operators should register it as a non-validator neuron.

Extrinsics

execute_orders(orders) — call index 0

Origin: any signed account (typically a relayer).

Executes a list of signed limit orders one by one, each interacting with the AMM pool independently. Orders that fail validation or whose price condition is not met are silently skipped — a single bad order does not revert the batch.

Fee handling: each order's fee_rate is deducted from the input amount and forwarded to that order's fee_recipient after execution.

When to use: suitable for small batches or when orders target different subnets. Use execute_batched_orders for same-subnet batches to reduce price impact.


execute_batched_orders(netuid, orders) — call index 4

Origin: any signed account (typically a relayer).

Aggregates all valid orders targeting netuid into a single net pool interaction:

  1. Validate & classify — if any order has the wrong netuid, an invalid signature, an already-processed id, a past expiry, a price condition not met, or targets the root netuid (0), the entire call fails with the corresponding error. All orders must be valid for execution to proceed. Valid orders are split into buy-side (LimitBuy) and sell-side (TakeProfit, StopLoss) groups. For buy orders the net TAO (after fee) is pre-computed here.

  2. Collect assets — gross TAO is pulled from each buyer's free balance into the pallet intermediary account. Gross alpha stake is moved from each seller's (coldkey, hotkey) position to the pallet intermediary's (pallet_account, pallet_hotkey) position.

  3. Net pool swap — buy TAO and sell alpha are converted to a common TAO basis at the current spot price and offset against each other. Only the residual amount touches the pool in a single swap:

    • Buy-dominant: residual TAO is sent to the pool; pool returns alpha.
    • Sell-dominant: residual alpha is sent to the pool; pool returns TAO.
    • Perfectly offset: no pool interaction.
  4. Distribute alpha pro-rata — every buyer receives their share of the total available alpha (pool output + seller passthrough alpha). Share is proportional to each buyer's net TAO contribution. Integer division floors each share; any remainder stays in the pallet intermediary account as dust.

  5. Distribute TAO pro-rata — every seller receives their share of the total available TAO (pool output + buyer passthrough TAO), minus their order's fee. Share is proportional to each seller's alpha valued at the current spot price. Integer division floors each share; any remainder stays in the pallet intermediary account as dust.

  6. Collect fees — buy-side fees (withheld from each order's TAO input) and sell-side fees (withheld from each order's TAO output) are accumulated per unique fee_recipient and forwarded in a single transfer per recipient.

  7. Emit GroupExecutionSummary.

Note: rounding dust (alpha and TAO) accumulates in the pallet intermediary account between batches. If an emission epoch fires while dust is present, the pallet earns emissions it never distributes.


cancel_order(order) — call index 1

Origin: the order's signer (coldkey).

Registers a cancellation intent by writing the OrderId into Orders as Cancelled. Once cancelled an order can never be executed. The full Order payload is required so the pallet can derive the OrderId.


Events

Event Fields Emitted when
OrderExecuted order_id, signer, netuid, side An individual order was successfully executed (by either extrinsic).
OrderSkipped order_id An order was skipped by execute_orders (bad signature, expired, wrong netuid, already processed, price condition not met, or root netuid). Not emitted by execute_batched_orders — invalid orders there cause the whole call to fail.
OrderCancelled order_id, signer The signer registered a cancellation via cancel_order.
GroupExecutionSummary netuid, net_side, net_amount, actual_out, executed_count Emitted once per execute_batched_orders call summarising the net pool trade. net_side is Buy if TAO was sent to the pool, Sell if alpha was sent. net_amount and actual_out are zero when the two sides perfectly offset.

Errors

Error Cause
InvalidSignature Signature does not match the order payload and signer. Also used as a catch-all for failed validation in execute_orders.
OrderAlreadyProcessed The OrderId is already present in Orders (either Fulfilled or Cancelled).
OrderExpired now > order.expiry. Only returned as a hard error by execute_batched_orders; silently skipped in execute_orders.
PriceConditionNotMet Current spot price is beyond the order's limit_price. Only returned as a hard error by execute_batched_orders; silently skipped in execute_orders.
OrderNetUidMismatch An order inside a execute_batched_orders call targets a different netuid than the batch parameter.
RootNetUidNotAllowed The order or batch targets netuid 0 (root). Root uses a fixed 1:1 stable mechanism with no AMM — limit orders are not meaningful there.
Unauthorized Caller of cancel_order is not the order's signer.
SwapReturnedZero The pool swap returned zero output for a non-zero residual input.

Fee model

Fees are specified per-order via fee_rate: Perbill and fee_recipient: AccountId fields on the Order struct. There is no global protocol fee or admin key.

All fees are collected in TAO regardless of order side.

Order type Fee deducted from Timing
LimitBuy TAO input Pre-computed in validate_and_classify, before pool swap.
TakeProfit, StopLoss TAO output Deducted in distribute_tao_pro_rata, after pool swap.

Fee formula: fee = fee_rate * amount (using Perbill multiplication, which upcasts to u128 internally to avoid overflow).

At the end of each batch, fees are accumulated per unique fee_recipient and forwarded in a single transfer per recipient. If multiple orders share the same fee_recipient, they result in exactly one transfer rather than one per order.