• ABOUT

  • WHITEPAPER

  • DOCS

  • DISCORD

  • WALLET

  • EXPLORE

The V461 Upgrade

Basket Trading · September 2026

Introduction

Spec 461 lets a root validator actively trade its beta basket, and makes trading the onlyway a fund's composition changes. Until now a fund could also be steered through the dividend stream: a set_root_weightsvector decided where new yield was deployed. That design is removed — dividends now always accumulate in place on the subnet they were earned on, direct deposits mirror the fund's current holdings, and the new swap_basket call sells part of one holding for TAO and buys another with it. Netuid 0 on either side is the fund's TAO cash slot, so a validator can also move into or out of cash.

Fund shares (β) and every staker's entitlement are untouched by a trade. Only what the fund holds changes, and with it the fund's NAV over time. Stakers keep choosing a validator; validators now also choose how to manage the basket between dividends.

Trading launches gated off network-wide. After the upgrade swap_basket fails with BasketTradingDisabled until governance flips BasketTradingEnabled on. Everything else in this release (proxy type, budgets, caps, reads, btcli) is live from the upgrade block. The operator walkthrough is the Basket trading guide.

The operating model

The intended setup is a validator coldkey that grants a BasketTrading proxy (new ProxyType, index 18) to a trader account, usually a multisig. That proxy type admits exactly one call: swap_basket. It cannot stake, unstake, transfer, claim, or change keys.

The grant is opt-in in both directions. No existing proxy gains trading rights at the upgrade: NonTransfer, NonCritical, Staking, and NonFungible delegates are all refused swap_basket. Only Any and BasketTrading admit it. Trades are submitted through the MEV shield by default (the SDK intent sets mev_shield_default = True), so a pending trade does not advertise its legs to the block's other traders.

# validator coldkey: delegate trading to the desk multisig
btcli proxy add --delegate <desk multisig> --proxy-type BasketTrading -w validator_cold

# the desk: sell 250 α of netuid 8 and buy netuid 64 in the validator's fund,
# insisting on at least 99% of the quoted fill (--max-slippage defaults to 1)
btcli root swap --from 8 --to 64 --amount 250 --hotkey <validator hotkey> \
  --max-slippage 1 -w desk --proxy-for <validator coldkey>

# move part of the fund's netuid 3 position into cash (netuid 0)
btcli root swap --from 3 --to 0 --amount 1200 --hotkey <validator hotkey> \
  -w desk --proxy-for <validator coldkey>

Guardrails

A trading key is a new way for a fund to lose value, so every trade must pass six checks. Two are price rules, one is a budget, two are shape rules, and the last is a set of switches. All numbers below are the launch defaults; the caps and budget are hyperparameters governance can move. On top of them the caller sets its own floor on the fill.

  • Per-leg price band: 2%. Each AMM leg must fill completelywithin 2% of the strictest of three references: the subnet's slow moving price (the monthly emission EMA), its new fast moving price (SubnetFastMovingPrice, a two-hour-half-life EMA of spot written each block from the previous block's close), and its spot price. A buy may not fill above 1.02 × min(slow, fast, spot); a sell may not fill below 0.98 × max(slow, fast, spot). The fast anchor is a price nobody can move inside a block, so a same-block pump or dump cannot make the fund fill at the manipulated price; the slow anchor caps how far a held pump can carry it; the spot anchor caps the trade's own price impact. Any miss is SlippageTooHigh, and the whole trade rolls back.
  • Caller floor: min_amount_out. The last argument of swap_basketis the least the buy leg must credit to the destination holding, after fees, in the destination subnet's alpha (TAO when the destination is netuid 0). Below it the trade fails with BasketMinOutNotMet and rolls back. 0sets no floor. This is the caller's protection against a fill worse than the quote it signed on; the band is the protocol's protection of the fund, is a price rule per leg, and applies regardless. btcli root swap derives the floor from a quote and --max-slippage (percent, default 1).
  • Turnover budget: a token bucket of 10% of NAV. Every fund has a bucket whose capacity is BasketDailyTurnoverCap of its current NAV (default u16::MAX / 10, 10%). The TAO that passes through the middle of each trade is taken out of the bucket; the bucket refills continuously at capacity / 7200 per block, so an empty bucket is full again after one day (7200 blocks at 12 s). A fund that has never traded starts full. The level is clamped to one capacity, so at most one budget can be spent at any instant and about one per day sustained. This replaces the fixed daily window of the first draft, which let two full budgets through in two adjacent blocks at the window edge. Refusal is BasketTurnoverBudgetExceeded.
  • Liquidity cap: 10% of the destination pool's alpha reserve. After the buy leg, the fund may not hold more than BasketLiquidityCap (default u16::MAX / 10, 10%) of the destination subnet's alpha reserve (SubnetAlphaIn). Root has no pool and is exempt; selling is never capped. This is the rule that bounds a thin-pool drain (below). Refusal is BasketLiquidityCapExceeded.
  • Concentration cap: 1/16 of NAV.The destination holding's realizable value may not end above BasketConcentrationCap of fund NAV — the 1/16 rule (and young-chain softening) that used to bound set_root_weights vectors, carried over as a pure trade guardrail. Selling out of an over-cap position is always allowed. Refusal is BasketConcentrationCapExceeded.
  • Switches. BasketTradingEnabled is the network-wide gate (default off). BasketTradingFrozen[hotkey]lets governance freeze one fund's trading, for example after a suspected key compromise; a frozen fund still accepts deposits and pays claims. Both follow the fund through a hotkey swap, as does the bucket. A trade is also refused while the beta-basket seed migration runs or a coldkey swap is announced for the signer.
  • Ownership. The signer must be the coldkey that owns the hotkey (or its BasketTradingproxy), the hotkey must be registered on root, both subnets must exist, and the destination's subtoken must be enabled.

What a rogue key can cost the fund

The question these rules answer is: if the trading key is stolen and the thief colludes with a counterparty, how much of the fund can they extract before governance freezes the hotkey? The original proposal stated the bound as cap × (2 × 2% + fees) ≈ 0.43% of NAV per day. That figure counted only the price band and was too low. The calibration pass on the first draft reproduced a loss of 8.3% of NAV in one window, with every guardrail passing on every trade, by buying a thin subnet in 2%-band slices while a counterparty sold alpha back to the EMA between slices. The concentration cap could not see it, because it marks holdings at realizablevalue, which is bounded by the pool's TAO reserve and so never grows no matter how much of the pool's supply the fund accumulates.

The liquidity cap closes that path. With cap L on a pool with TAO reserve R, the most a drain can extract from that pool is about R × L² / (1 + L): at the 10% default, about 0.9% of the pool's TAO reserve, or about 9% of the TAO the fund spent buying into it. The realizable haircut on any holding is capped at about L / (1 + L) ≈ 9%. On a 1,000 τ pool that is roughly 10 τ; before the cap it was the whole turnover budget.

Put together, the adversarial bound per unit of turnover is about 9% (liquidity drain) + 4% (two legs at the edge of the band) + fees, roughly 13% of the TAO traded. Because the bucket holds one budget at a time, the most that can be moved through the fund at any instant is 10% of NAV, so the worst case is about 1.3% of NAV in a burst and about 1.3% of NAV per day sustained until the fund is frozen, spread across as many pools as the budget and the 1/16 cap allow. Honest trading pays only AMM fees and the price impact of its own legs. These are upper bounds for a stolen key and a willing counterparty, not expected costs.

That 1.3% figure assumed the band's moving-price anchor sits near spot. The security review showed it did not have to: the only smoothed reference was the monthly emission EMA, which sits stale after any real move, and the band re-anchored to spot on every leg. A key holder could lift spot toward a stale-high EMA and have the fund chain-buy near it, or dump toward a stale-low EMA and have it chain-sell — 1.1% / 4.1% / 6.9% of NAV per day on the buy leg and 1.1% / 3.1% / 4.7% on the sell leg at EMA-to-spot ratios of 1.35, 2 and 4, all inside one block. The fast moving price closes this: replaying those sequences against the fixed band, the attacker's profit is at most rounding on either leg and the fund fills one in-band leg at the pre-move price. Regaining the old extraction now means holding a 30–100%-of-reserve pump against arbitrage for about ten hours, at which point the 1.3%-per-day bound above applies — at that capital-time cost, not for free.

The same review found the concentration cap's and the turnover budget's NAV denominator pumpable: a same-block pump of a thin pool the fund holds marked that holding at roughly the pump size, so a 1,500 τ buy the cap had refused became admitted, and a 9,500 τ buy the budget had refused too. Both are now measured against a guarded NAV that marks every holding at the lower of its realizable value and its alpha times the slow moving price, which cannot be moved inside a block; the same buys stay refused under the pump.

For today's largest funds (about 12,500 τ) the liquidity cap barely binds: 10% of a median mainnet pool is about 700 τ, which is about the fund's 1/16 slice. It binds correctly as funds grow. Raise BasketLiquidityCap if funds legitimately need larger positions in mid-depth pools; lower it to tighten the per-pool value at risk.

Edges to know about

  • The 2% band is per leg, not per block or per day, but every leg is bound to the fast moving price as well, so several legs in one block cannot walk a subnet's price more than about 2% from where it opened the block. The slow EMA moves slowly (a half-life of about a month on mainnet); the fast one has a two-hour half-life.
  • Because the band is anchored to the moving prices, a fund cannot sell a holding whose spot has fallen more than 2% below either average, or buy one that has risen more than 2% above either, until the averages catch up. After a real 5% move the fast anchor is back in band in about four hours; the slow anchor can hold the direction shut for weeks. Stop-losses are not possible by design.
  • A subnet with no moving price yet (before start_call, or newly started) cannot be traded until its EMA has warmed up. The EMA is min(price, 1), so a subnet trading above 1 τ per α cannot be bought within the band either.
  • The cash slot is a capped destination too: moving more than 1/16 of NAV into netuid 0 in one trade is refused by the concentration cap.

What changed on chain

  • Call SubtensorModule::swap_basket(hotkey, origin_netuid, destination_netuid, amount, min_amount_out) (call index 150). Declared weight is a cap sized for 256 holdings plus any pending-deposit flush work; the actual weight is computed from the real holding count and refunded post-dispatch.
  • Event BasketSwapped { hotkey, origin_netuid, destination_netuid, alpha_sold, tao_mid, alpha_bought }, appended at the tail of the event enum (index 149) so existing event indices are unchanged.
  • Errors BasketTradingDisabled, BasketTradingFrozen, BasketTurnoverBudgetExceeded, BasketSameSubnet, BasketLiquidityCapExceeded, BasketMinOutNotMet, and BasketConcentrationCapExceeded. SlippageTooHigh is reused for the band.
  • Storage BasketTradingEnabled (bool, default off), BasketTradingFrozen (per hotkey), BasketDailyTurnoverCap (u16, default 6553 = 10%), BasketLiquidityCap (u16, default 6553 = 10%), BasketConcentrationCap (u16, default 4096 = 1/16; the value governance had set in RootWeightsCap is carried over), and BasketTradeBucket (per hotkey: (tao_available, last_refill_block); a missing row is a full bucket).
  • Removed. The set_root_weights extrinsic (call 146), its RootWeightSettingEnabled gate and sudo_set_root_weight_setting_enabled setter, the RootWeightsCap map, the validator_root_weights read / get_validator_weights runtime API, and the weights field of BasketSummary. A migration clears every stored root vector; no fund's holdings change. Weights only ever decided how an earned dividend was deployed, never what a validator earned, so no validator's income moves.
  • Admin setters (root-only, in AdminUtils): sudo_set_basket_trading_enabled(enabled) (106), sudo_set_basket_trading_frozen(hotkey, frozen) (107), sudo_set_basket_daily_turnover_cap(cap) (108), sudo_set_basket_liquidity_cap(cap) (109), and sudo_set_basket_concentration_cap(cap) (105, replacing sudo_set_root_weights_cap), with events BasketTradingToggled, BasketTradingFrozenSet, BasketDailyTurnoverCapSet, BasketLiquidityCapSet, and BasketConcentrationCapSet. A zero cap is rejected with ValueNotInBounds.
  • Proxy ProxyType::BasketTrading (index 18), whose filter admits only swap_basket. The broad proxy types do not gain the call.
  • Runtime API BetaBasketRuntimeApi v4 adds get_basket_trading_status(hotkey) returning { enabled, frozen, refill_blocks, tao_available, budget_tao }: the gates, the bucket's refill period, what a trade at this block could push through, and the bucket's capacity at current NAV.
  • Weights. swap_basket and the five setters have their own benchmarks and WeightInfo entries, measured on the reference benchmarking hardware.

SDK and btcli

The SDK release that ships with the runtime adds the SwapBasket intent (amount="all" sells the whole origin holding), the basket_trading_status read (enabled, frozen, budget_tao, remaining_tao, used_tao, refill_per_block_tao, refill_blocks), the BasketTrading proxy type, and descriptions for every new error. The intent takes min_amount_out (destination alpha, or TAO for netuid 0; default 0, no floor). btcli root swap --from --to --amount [--max-slippage] [--hotkey] [--proxy-for] is the CLI surface; it quotes both legs, sets the floor to the quote less --max-slippage percent (default 1), and its review card shows the origin and destination holdings, the expected and minimum output, and how much of the bucket is left before you sign. If the quote is unavailable the floor is 0 and the card says so.

import bittensor as bt
from bittensor.wallet import Wallet

desk = Wallet(name="desk")

async with bt.Subtensor("finney") as client:
    status = await client.read("basket_trading_status", hotkey_ss58=HOTKEY)
    # {'enabled': True, 'frozen': False, 'budget_tao': τ1,250, 'remaining_tao': τ980, ...}

    tao_mid = (await client.read("quote_unstake", netuid=8, amount_alpha=250)).tao
    expected = (await client.read("quote_stake", netuid=64, amount_tao=tao_mid.tao)).alpha
    floor = bt.Balance.from_rao(expected.rao * 99 // 100, 64)   # 1% under the quote

    intent = bt.SwapBasket(
        hotkey_ss58=HOTKEY, origin_netuid=8, dest_netuid=64, amount=250, min_amount_out=floor
    )
    result = await client.execute(intent, desk, proxy_for=VALIDATOR_COLDKEY)
    if not result.success:
        print(result.error.code, result.error.remediation)

Upgrade with pip install -U bittensor. Reference pages: swap-basket, basket-trading-status, hyperparameters. Guides: Basket trading (the operator walkthrough for this release), Basket trading for governance (enable, freeze, tune, and the rogue-key playbook), Basket trading for stakers (nothing to do, and how to watch), Root Reborn, Proxies.