feat: swap_basket — validator-directed beta basket rebalancing - #3150
feat: swap_basket — validator-directed beta basket rebalancing#3150unarbos wants to merge 1 commit into
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
|
||
| // 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); |
There was a problem hiding this comment.
[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.
🛡️ 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: Findings
ConclusionThe 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. |
|
🔄 AI review updated — Skeptic: VULNERABLE |
|
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 What the stack does about it.
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 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. |
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
BasketTradingproxy (newProxyType, index 18, allows onlyswap_basket) to a trader multisig; trades are submitted through the MEV shield (mev_shield_default = Trueon the intent).Guardrails
Two rate rules and one shape rule bound what a rogue or stolen key can do:
SlippageTooHigh.BasketDailyTurnoverCapshare of NAV (default 10%). OtherwiseBasketTurnoverBudgetExceeded. Worst-case daily value moved against the fund iscap × (2 × 2% + fees).RootWeightsCapof NAV (same rule and young-chain softening asset_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 byCheckColdkeySwap. Both the freeze and the turnover window follow the fund on hotkey swap.Runtime surface
SubtensorModule::swap_basket— call index 150,BasketSwappedevent, errorsBasketTradingDisabled/BasketTradingFrozen/BasketTurnoverBudgetExceeded/BasketSameSubnet.AdminUtils:sudo_set_basket_trading_enabled(106),sudo_set_basket_trading_frozen(107),sudo_set_basket_daily_turnover_cap(108).BetaBasketRuntimeApiv4:get_basket_trading_status(hotkey) -> BasketTradingStatus.settle_alpha_fee_to_author/settle_tao_fee_to_author(now also used bystake_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
SwapBasketintent (all= whole origin holding),basket_trading_statusread,btcli root trade --from --to --amount [--proxy-for], error map/descriptions,BasketTradingproxy type, harness samples, regenerated docs and catalogs.Verification
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 --checkclean.Cargo.lockunchanged.Not in this PR (needs upgraded node / reference hardware)
sdk/python/bittensor/_generated/regeneration against a node running this runtime (codegen.check --namesreports the four new errors until then).spec_versionbump (currently 455) or theno-spec-version-bumplabel.WeightInfofor the four new dispatchables.swap_basketuses the hand formula + post-dispatch refund pattern ofstake_into_basket; the sudo setters reuse the measured one-writesudo_set_root_weights_capweight, following thesudo_set_root_weight_setting_enabledprecedent.Known edge
SubnetMovingPriceis an EMA ofmin(price, 1), so a subnet trading above 1 TAO/alpha cannot be bought within 2% of the EMA.