Skip to content

feat: swap_basket — validator-directed beta basket rebalancing - #3150

Closed
unarbos wants to merge 1 commit into
mainfrom
feat/basket-trading-swap-basket
Closed

feat: swap_basket — validator-directed beta basket rebalancing#3150
unarbos wants to merge 1 commit into
mainfrom
feat/basket-trading-swap-basket

Conversation

@unarbos

@unarbos unarbos commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

What

Lets a root validator actively trade its beta basket: swap_basket(hotkey, origin_netuid, destination_netuid, amount) sells one escrow holding for TAO and buys another with it. Netuid 0 on either side is the fund's TAO cash slot. Fund shares and staker entitlements are untouched; only the fund's composition changes.

Intended operating model: the validator coldkey grants a BasketTrading proxy (new ProxyType, index 18, allows only swap_basket) to a trader multisig; trades are submitted through the MEV shield (mev_shield_default = True on the intent).

Guardrails

Two rate rules and one shape rule bound what a rogue or stolen key can do:

  1. Per-leg slippage (2%). Each AMM leg must fill fully within 2% of the stricter of the subnet's moving (EMA) price and its spot price. The EMA anchor defeats a pre-trade pump; the spot anchor caps the trade's own price impact. Otherwise SlippageTooHigh.
  2. Daily turnover budget. The TAO through the middle of each swap is charged against a per-hotkey 7200-block window: BasketDailyTurnoverCap share of NAV (default 10%). Otherwise BasketTurnoverBudgetExceeded. Worst-case daily value moved against the fund is cap × (2 × 2% + fees).
  3. Concentration. The destination holding may not end above RootWeightsCap of NAV (same rule and young-chain softening as set_root_weights). Selling out of an over-cap position is always allowed.

Gates: BasketTradingEnabled (global, default off), BasketTradingFrozen[hotkey] (governance freeze), seed-idle, owner and root-registration checks. Coldkey swap in progress is already blocked by CheckColdkeySwap. Both the freeze and the turnover window follow the fund on hotkey swap.

Runtime surface

  • SubtensorModule::swap_basket — call index 150, BasketSwapped event, errors BasketTradingDisabled / BasketTradingFrozen / BasketTurnoverBudgetExceeded / BasketSameSubnet.
  • AdminUtils: sudo_set_basket_trading_enabled (106), sudo_set_basket_trading_frozen (107), sudo_set_basket_daily_turnover_cap (108).
  • BetaBasketRuntimeApi v4: get_basket_trading_status(hotkey) -> BasketTradingStatus.
  • Shared helpers extracted: settle_alpha_fee_to_author / settle_tao_fee_to_author (now also used by stake_into_subnet / unstake_from_subnet), binding_root_weights_cap / share_within_root_cap, try_valued_basket_holdings, basket_nav_sweep_weight, debit_root_reserves.

SDK / CLI

SwapBasket intent (all = whole origin holding), basket_trading_status read, btcli root trade --from --to --amount [--proxy-for], error map/descriptions, BasketTrading proxy type, harness samples, regenerated docs and catalogs.

Verification

  • Temporary pallet tests (removed): round trip through both subnets and root with exact reserve unwind; gates and window roll; EMA- and spot-anchored refusals on both legs; concentration cap; guardrails follow hotkey swap.
  • Existing suites claim_root, stake_into_basket, staking, basket_flush, swap_hotkey: 308 passed. Proxy filter tests 15/15. SDK table tests 1432 passed.
  • cargo fmt, clippy (touched crates), ruff, docs --check, git diff --check clean. Cargo.lock unchanged.

Not in this PR (needs upgraded node / reference hardware)

  • sdk/python/bittensor/_generated/ regeneration against a node running this runtime (codegen.check --names reports the four new errors until then).
  • spec_version bump (currently 455) or the no-spec-version-bump label.
  • Benchmarks and WeightInfo for the four new dispatchables. swap_basket uses the hand formula + post-dispatch refund pattern of stake_into_basket; the sudo setters reuse the measured one-write sudo_set_root_weights_cap weight, following the sudo_set_root_weight_setting_enabled precedent.

Known edge

SubnetMovingPrice is an EMA of min(price, 1), so a subnet trading above 1 TAO/alpha cannot be bought within 2% of the EMA.

Adds `SubtensorModule::swap_basket(hotkey, origin_netuid, destination_netuid, amount)`
(call index 150) so a root validator can sell one escrow holding and buy another
(netuid 0 = the fund's TAO cash slot). Fund shares and staker entitlements are
untouched; only composition changes.

Guardrails:
- each AMM leg must fill fully within 2% of the stricter of the subnet's moving
  (EMA) price and spot price (EMA defeats pre-trade pumps, spot caps impact);
- per-hotkey daily turnover budget on the TAO through the middle
  (`BasketDailyTurnoverCap`, default 10% of NAV per 7200 blocks);
- post-trade destination share <= `RootWeightsCap` (shared rule with
  `set_root_weights`);
- global gate `BasketTradingEnabled` (default off) and per-hotkey governance
  freeze `BasketTradingFrozen`; both follow the fund on hotkey swap.

Also:
- AdminUtils: sudo_set_basket_trading_enabled (106), sudo_set_basket_trading_frozen
  (107), sudo_set_basket_daily_turnover_cap (108).
- ProxyType::BasketTrading (18) allowing only swap_basket.
- BetaBasketRuntimeApi v4: get_basket_trading_status -> BasketTradingStatus.
- Shared helpers: settle_{alpha,tao}_fee_to_author (now used by stake_into_subnet /
  unstake_from_subnet too), binding_root_weights_cap / share_within_root_cap,
  try_valued_basket_holdings, basket_nav_sweep_weight, debit_root_reserves.
- SDK: SwapBasket intent, basket_trading_status read, `btcli root trade`,
  error map/descriptions, proxy type list, harness samples; docs/catalog regenerated.

Not included (needs upgraded node / reference hardware): `_generated/` bindings
regeneration, spec_version bump, benchmarks + WeightInfo for the new dispatchables.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented Sep 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
subtensor Ready Ready Preview Sep 11, 2026 1:01pm UTC

Request Review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review — see the sticky summary comment for the verdict and the inline comments below for specific findings.


// Settle queued dividend credits first so the budget and the cap are measured
// against the fund's full, current NAV.
Self::flush_basket_deposits_for_hotkey(&hotkey);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] Bound and charge pending-deposit flush work

pallets/subtensor/src/staking/basket_trade.rs:97 discards the flush's work counter, while swap_basket_weight charges only two NAV sweeps plus the trade legs. A curated deposit flush performs additional NAV sweeps, origin sells, and destination buys; on batch failure, deposit_root_alpha_batch retries each origin separately, repeating that work. Neither pending-credit count nor retry work is bounded by the dispatcher's fixed 256-holding allowance.

Once trading is enabled, an authorized trader can invoke this expensive path even when the subsequent trade fails, without consuming turnover budget. Requeued failures allow repeated execution, creating a block-resource exhaustion path. Enforce a work envelope before executing the flush, include its worst-case work in declared weight, and account for executed flush work in post-dispatch charging, including failures. A larger post-dispatch weight alone cannot repair an insufficient admission weight.

@github-actions

Copy link
Copy Markdown
Contributor

🛡️ AI Review — Skeptic (security review)

VERDICT: VULNERABLE

LOW contributor risk / baseline scrutiny: author has write permission and substantive merged contributions; no listed Gittensor association. Branch targets main.

Static review found one resource-exhaustion issue. No dependency or AI-review trust-boundary changes were found.

Checks: git diff --check passed; git status --short was clean. Builds, tests, and executable formatting checks were not run under the Skeptic’s static-only rules.

Findings

Sev File Finding
HIGH pallets/subtensor/src/staking/basket_trade.rs:97 Bound and charge pending-deposit flush work inline

Conclusion

The PR appears legitimate, but basket trading introduces an undercharged execution path that must be bounded and accounted for before enabling it.


# 🔍 AI Review — Auditor (domain review) has not yet run on this PR.

@github-actions

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

@cursor

cursor Bot commented Sep 11, 2026

Copy link
Copy Markdown

Thanks for the PR, Arbos — the guardrail design and the transactional rollback held up well under the calibration pass. One correction for the description, so it matches what the stack now ships:

The worst-case figure. The body says the daily value a rogue key can move against the fund is cap × (2 × 2% + fees) ≈ 0.43% of NAV per day. That counts only the price band. The calibration pass reproduced a loss of 8.3% of NAV in one window with every guardrail passing on every trade: buy a thin subnet in 2%-band slices while a counterparty sells alpha back to the EMA between slices. The concentration cap cannot see it because it marks holdings at realizable value, which is bounded by the pool's TAO reserve and so never grows no matter how much of the pool's supply the fund holds (the fund ended up holding 9× the pool's alpha reserve and still passed the 1/16 cap at 0.98%). Combined with the fixed-window boundary, ~16.6% of NAV in two adjacent blocks.

What the stack does about it.

  • fix(swap_basket): liquidity-relative destination cap, token-bucket turnover, flush weight #3153 adds a liquidity-relative destination cap (BasketLiquidityCap, default 10% of the destination pool's alpha reserve, BasketLiquidityCapExceeded). With cap L on a pool with TAO reserve R, the most a drain can extract from that pool is ≈ R × L² / (1 + L) ≈ 0.9% of R, i.e. ≈ 9% of the TAO spent buying into it. It also replaces the fixed 7200-block window with a token bucket (capacity 10% of NAV, refill capacity / 7200 per block, clamped to one capacity), so at most one budget can be spent at any instant and the 2× boundary burst is gone. BasketTradingStatus now reports refill_blocks / tao_available.
  • test: swap_basket coverage #3152 fixes the BasketSwapped event index (it was inserted mid-enum and renumbered later events — that is the regression_liquid_alpha_event_indices_are_append_only failure on this PR's CI; it is now appended at 149), anchors the first turnover window at the first trade instead of block 0, and removes BasketTradingCalls from NonTransfer / NonCritical so only BasketTrading (and Any) admit swap_basket.

Corrected bound. Per unit of turnover: ≈ 9% (liquidity drain, bounded by the cap) + 4% (two legs at the edge of the band) + fees ≈ 13% of the TAO traded. With one bucket of 10% of NAV available at any instant, that is ≈ 1.3% of NAV in a burst and ≈ 1.3% of NAV per day sustained until governance freezes the hotkey — an upper bound for a stolen key with a willing counterparty, not an expected cost. Two smaller notes for the text: the per-block price bound is "up to 1.02 × EMA" (several legs in one block can walk spot up to it), not 2%; and the mainnet EMA half-life is about 8 hours (SubnetMovingAlpha is 0.0003 on chain, not the code default), so a subnet 5% below its EMA is unsellable for ~10 h.

Would you mind updating the description with these numbers and the token-bucket wording? The release notes and the operator guide in #3154 (spec 456, benchmarks, SDK/docs regen, stacked on #3153) already use them, so it would be good for the PR body to agree.

@cursor

cursor Bot commented Sep 11, 2026

Copy link
Copy Markdown

Superseded by #3155, which consolidates the whole swap_basket stack (#3150#3152#3153#3154) onto main. This branch's tip (bb1eab6bd) is already an ancestor of cursor/swap-basket-consolidated-b2ac; nothing here is lost. Closing; branch kept.

@unarbos unarbos closed this Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant