Concepts

Staking and pools

The per-subnet balancer pools, what a stake operation actually does, price impact, swap fees, and MEV-shielded submission.

View as Markdown

Staking on a subnet is not a deposit — it is a swap. Every subnet has a liquidity pool holding TAO and the subnet's alpha; staking sells TAO into that pool for alpha, unstaking sells alpha back for TAO. Everything that follows — price impact, swap fees, limit orders, MEV — falls out of that one fact. This page explains the machinery; the staking guide covers the commands, and how money moves covers units and valuation.

The pool

Each subnet's pool is a weighted balancer pool (a generalization of the constant-product AMM): two reserves, TAO (SubnetTAO) and alpha (SubnetAlphaIn), plus a pair of weights w1+w2=1w_1 + w_2 = 1. The spot price is

p=w1TAO reservew2alpha reservep = \frac{w_1 \cdot \text{TAO reserve}}{w_2 \cdot \text{alpha reserve}}

With the default equal weights w1=w2=0.5w_1 = w_2 = 0.5 this reduces to the familiar reserve ratio TAO / alpha. Unlike Uniswap-style pools, the weights let the protocol add liquidity disproportionally to price without moving the price — the weights absorb the imbalance instead (they are re-solved on every injection and bounded so they stay near 0.5 in practice). Only the protocol adds liquidity this way; user staking always trades against the pool.

A swap moves along the weighted invariant L=xw1yw2L = x^{w_1} \cdot y^{w_2}. Selling TAO into the pool (staking), the alpha you receive for Δy TAO is

Δx=x(1(yy+Δy)w2/w1)\Delta x = x \cdot \left(1 - \left(\tfrac{y}{y+\Delta y}\right)^{w_2/w_1}\right)

which is why the execution price curves away from spot as the trade grows — each slice of your order fills at a slightly worse price than the last.

Staking pool swap
balancer pool

Each subnet's stake operations swap through a weighted pool of TAO and alpha (pallets/swap/src/pallet/balancer.rs): buying alpha pays ∆x = x·(1 − (y/(y+∆y))^(w2/w1)), so the effective price drifts away from spot p = (w1·y)/(w2·x) as trade size grows. A ≈0.05% fee (FeeRate 33/65535) is taken from the input side before the swap and paid to the block author. Alpha reserve is derived so spot is 0.05 τ/α.

Spot price

0.050000 τ/α

p = (w1·y)/(w2·x), w2 = 0.50

Alpha received

38,442.92 α

after 1.01 τ fee (≈0.05% of input)

Effective price

0.052025 τ/α

TAO paid per alpha received

Price impact

4.050%

paying above spot

direction
50,000 τ
2,000 τ
w2 = 0.50, w1 = 0.50

The math lives in pallets/swap/src/pallet/balancer.rs; quotes over RPC use exactly the same path (quote-stake, quote-unstake), so a quote is a faithful simulation, not an estimate.

What a stake operation does

add-stake is a coldkey-signed extrinsic that moves TAO out of your free balance, swaps it through the subnet's pool, and credits the resulting alpha to the (hotkey, coldkey, netuid) stake position. Step through it:

What add_stake / remove_stake actually does
add_stake.rs · swap_step.rs

Step through the on-chain path from add_stake.rs → stake_into_subnet (stake_utils.rs) → the balancer swap in swap_step.rs. Illustrative pool: 10,000 τ / 200,000 α at 0.05 τ/α; the fee is 0.05% of the input side and 100% of it goes to the block author.

operation

coldkey free balance

balance
900.00 τ
signs
add_stake(100 τ)

swap fee → block author

fee (0.05%)
into pool

balancer pool

τ reserve
10,000.00 τ
α reserve
200,000 α
price
0.0500 τ/α

(hotkey, coldkey, netuid) stake

alpha
0 α
value

step 1 / 5 Coldkey signs add_stake(hotkey, netuid, 100 τ) — 100 τ leaves the coldkey's free balance into the subnet account.

alpha received

1,979.2 α

1,999 α at spot — slippage eats the rest

effective price

0.05050 τ/α

spot was 0.0500 τ/α

fee to block author

0.05 τ

input × FeeRate/65535, default ≈ 0.05%

Root staking (netuid 0) skips all of this: there is no pool, so TAO is credited 1:1 as root stake — no swap fee, no slippage, no price movement.

Points worth pinning down:

  • The fee comes off the input side first. Amount × FeeRate/65535 (default 33 ≈ 0.05%, per-subnet) is deducted before the swap and paid to the block author — TAO when staking, alpha when unstaking.
  • The swap updates both reserves. Your TAO enters SubnetTAO, alpha leaves the pool's side (SubnetAlphaIn) and joins the staked total (SubnetAlphaOut). The price after your trade is the new reserve ratio — your own trade moved it.
  • The position is alpha, not TAO. From the moment the swap settles, the position's TAO value floats with the pool price, and it earns the validator's staking emissions (minus its take) every epoch.
  • Unstaking is the mirror image (remove-stake): alpha leaves the position, the fee is taken in alpha, the swap pays out TAO. There is no unbonding period — the TAO is spendable immediately.

Moving a position between hotkeys on the same subnet (move-stake) is not a swap — no fee, no price impact. Cross-subnet moves run two swaps (alpha → TAO → alpha) and pay the fee once.

Price impact, slippage, and limit orders

Two different things eat your execution price, and they have different defenses:

  • Price impact — self-inflicted: your own order consumes reserves as it fills. Deterministic, included in every quote.
  • Slippage — external: other transactions land before yours and move the price between quote and execution. Not knowable in advance.

The limit variants (add-stake-limit, remove-stake-limit) defend against both by bounding the execution price itself: the chain computes exactly how much can fill before the pool price reaches your limit, fills up to that point, and either stops there (allow_partial=true) or rejects the whole order (allow_partial=false).

Limit-price slippage protection
add_stake_limit · balancer pool

add_stake_limit stops filling once the pool price reaches your limit (pallets/swap/src/pallet/balancer.rs): the maximum TAO that fits is ∆y = y·((p′/p)^w1 − 1).

The thin curve is the marginal pool price, which meets the dashed limit line exactly at the fill boundary; the shaded execution-price curve is what you actually pay on average. remove_stake_limit is symmetric with ∆x = x·((p/p′)^w2 − 1).

995 τ of 1,200 τ executes

Limit price

0.051000 τ/α

spot × (1 + 2.0%)

Max fill at limit

995 τ

∆y = y·((p′/p)^w1 − 1)

Requested trade

1,200 τ

crosses the limit price

Outcome

fills partially

995 τ of 1,200 τ executes, the rest is returned

allow_partial
2.0%
1,200 τ
100,000 τ

To turn a tolerance into a limit_price: staking, ceiling = spot × (1 + tolerance); unstaking, floor = spot × (1 − tolerance). Read spot with alpha-price.

MEV and shielded submission

A large stake order sitting in the public mempool is legible to everyone — including a front-runner who can buy alpha before your order executes, let your price impact reprice the pool, and sell back after. The sandwich's profit is carved out of your execution price.

MEV-shielded submission closes the window: the SDK signs your call as a complete inner extrinsic, encrypts it to the chain's rotating ML-KEM-768 key, and submits only the ciphertext. The mempool sees an opaque blob; the block author decrypts it at block-build time and includes the inner extrinsic in the block it builds. There is no interval where the order is both public and pending.

Plain vs MEV-shielded submission
pallets/shield

The same 500 τ stake into a 50,000 τ pool, submitted plain and shielded (docs/concepts/advanced.mdx, pallets/shield). Plain, the order is readable in the mempool and gets sandwiched; shielded, the mempool holds only ciphertext that the block author decrypts at build time.

mempool

block N

block N+1

lane A — plain submission

add_stake(500 τ) — plaintext
attacker buy 2,000 τ (higher tip)
attacker buy fills — price 0.0500 → 0.0541
victim fills @ 0.0546 τ/α — 9,158 α
attacker sells 38,462 α — +38 τ profit

lane B — shielded submission

MevShield.submit_encrypted(0x8f3a…) — ciphertext, 8-block era
author decrypts, includes inner extrinsic
victim fills @ 0.0505 τ/α — 9,901 α
nothing — no sandwich

step 1 / 5 The victim submits a 500 τ stake. Plain: the call sits readable in the public mempool. Shielded: the inner call is signed, encrypted to the chain's rotating ML-KEM-768 key with XChaCha20-Poly1305, and wrapped in MevShield.submit_encrypted — the mempool sees only ciphertext.

plain — victim exec price

0.0546 τ/α

9,158 α received

shielded — victim exec price

0.0505 τ/α

9,901 α received

sandwich cost

−743 α

price 8.1% worse; attacker keeps ≈38 τ

Shielding matters for large pool swaps with loose price limits — the cases where your own price impact is worth stealing. Root staking (netuid 0) has no pool, and small swaps move the price too little to sandwich; neither is worth shielding. SDK: client.submit_shielded(intent, wallet).

await client.submit_shielded(
    bt.AddStakeLimit(hotkey_ss58="5F...", netuid=1, amount_tao=5_000,
                      limit_price_rao=int(spot["price_rao"] * 1.01),
                      allow_partial=False),
    wallet,
)

Shield and bound the price — they protect against different things. Shielding hides the order from front-runners; the limit caps damage from whatever moves the price anyway. What is worth shielding follows the same economics as price impact: a large swap with a loose limit is the prime target, while small swaps (well under ~1 TAO) and root-network staking (no pool, nothing to front-run) gain nothing from it.

Root staking: the exception

Staking on the root network (netuid 0) is not a pool swap. TAO stays TAO-denominated and converts 1:1 — no swap fee, no price impact, no MEV exposure. The limit_price machinery degenerates accordingly: any limit ≥ 1 TAO/TAO fills fully, anything lower fills nothing. See root stake and dividends for how root positions earn.

Where the liquidity comes from

Nobody LPs these pools. Every block, the coinbase injects each subnet's share of TAO emission into its pool's TAO reserve, alongside newly issued alpha into the alpha reserve (alpha_in ≈ tao_in / price, capped by the subnet's root proportion) — see emissions. Because the protocol may inject the two sides disproportionally to price, the balancer weights absorb the imbalance; this is the reason the pools are weighted at all. Reserves only grow through injection and trading — stake swaps in TAO, unstake swaps it back out — and the pool refuses trades that would drain a reserve below a minimum, and rejects single swaps larger than 1,000× the TAO reserve (InsufficientLiquidity).

Reading the pool

btcli query alpha-price --netuid 1 --json      # spot price now
btcli query quote-stake --netuid 1 --amount-tao 100 --json    # simulate entry
btcli query quote-unstake --netuid 1 --amount-alpha 50 --json # simulate exit

Spot valuation of a position (alpha × price, stake-value-for-coldkey) marks to the current reserve ratio and ignores what your own exit would do to it. For what you would actually receive, quote the exit — the quote runs the real swap math against the real reserves.