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.
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
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. |
| 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. |
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.
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. |
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.
| 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. |
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.
Origin: any signed account (typically a relayer).
Aggregates all valid orders targeting netuid into a single net pool
interaction:
-
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. -
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. -
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.
-
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.
-
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.
-
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_recipientand forwarded in a single transfer per recipient. -
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.
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.
| 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. |
| 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. |
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.