Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🛡️ AI Review — Skeptic (security review)VERDICT: SAFE LOW contributor risk / baseline scrutiny: write-access maintainer with substantive merged history; disclosed Cursor co-authorship; no listed Gittensor association. Targets feat/basket-trading-swap-basket. Reviewed all 14 changed files and traced cap arithmetic, transaction rollback, root authorization, and proxy classification. No review trust-boundary or dependency changes. Static analysis only; formatting, builds, and tests were not run under Skeptic restrictions. FindingsNo findings. ConclusionNo malicious behavior or newly introduced security vulnerability found. The cap uses overflow-safe arithmetic on post-buy holdings and reserves, and rejection rolls back the trade. 🔍 AI Review — Auditor (domain review)VERDICT: 👍 Established contributor with repository write access; Gittensor association UNKNOWN. No duplicate implementation identified in supplied PR metadata. The cap implements the documented inequality using overflow-safe u128 arithmetic, checks post-buy reserves inside the rollback boundary, and exempts root. Tests cover thin-pool accumulation, rejection rollback, selling to root, governance tuning, and setter authorization.
Findings
ConclusionThe liquidity-cap implementation is sound on static review. Add the setter’s benchmark and dedicated weight wiring to complete the repository’s dispatchable requirements. |
| /// reserve `R` is about `R × L² / (1 + L)`. Root-only. One storage write; reuses the | ||
| /// `sudo_set_root_weights_cap` weight. | ||
| #[pallet::call_index(109)] | ||
| #[pallet::weight(<T as pallet::Config>::WeightInfo::sudo_set_root_weights_cap())] |
There was a problem hiding this comment.
[MEDIUM] Add benchmark coverage and dedicated weight wiring
pallets/admin-utils/src/lib.rs:2599 borrows sudo_set_root_weights_cap's weight, but the new dispatchable has no matching benchmark or WeightInfo entry. The existing benchmark only measures the concentration setter, so future reference runs cannot validate this call independently. Add a sudo_set_basket_liquidity_cap benchmark and dedicated weight wiring; leave reference measurements to CI as required by AGENTS.md. The borrowed single-write weight does not establish a concrete undercharge here, so this is non-blocking.
|
🔄 AI review updated — Skeptic: SAFE Auditor: 👍 |
After the buy leg, refuse a swap_basket trade that would leave the fund holding more than BasketLiquidityCap (default 10%) of the destination subnet's alpha reserve. The concentration cap marks holdings at realizable value, which is bounded by the pool's TAO reserve, so on a thin pool a stolen key could keep buying in 2%-band slices while counterparties sold back into its price support: every slice passed every guardrail and the fund's realizable NAV fell by ~90% of the turnover spent. The new cap bounds exposure per pool to about R x L^2 / (1 + L) of that pool's TAO reserve. - BasketLiquidityCap storage (u16-normalized, default u16::MAX / 10) - BasketLiquidityCapExceeded error (appended) - AdminUtils::sudo_set_basket_liquidity_cap (call index 109) and BasketLiquidityCapSet event (appended), in RootConfigCalls - SDK error map/description and codegen allowlist entries - Regression tests: thin-pool drain now stops with the new error, band slices up to the boundary, cap follows storage, sudo setter Co-authored-by: Arbos <unarbos@users.noreply.github.com>
Appreciation via direct pool state: a holding over RootWeightsCap or BasketLiquidityCap only refuses further buys into that subnet. Partial sells to cash or another subnet, trades between other holdings, curated dividend redeploy, dust consolidation, pro-rata claims, and hotkey swap all leave the winner alone. Profit-taking after a run-up is not blocked by the EMA band (sell floor anchors to spot when spot > EMA). Co-authored-by: Arbos <unarbos@users.noreply.github.com>
do_swap_basket flushes the hotkey's queued dividend credits before the trade but swap_basket_weight did not price that work. Declare the weight from the pending-deposit count (one unit per queued credit plus the deposit's own sweeps, bounded by the same 256-row cap the trade assumes) and refund to the flush work actually reported, priced per quote unit like claim_root prices its flush. An empty queue costs nothing extra, so the existing post-dispatch weight pin is unchanged. Co-authored-by: Arbos <unarbos@users.noreply.github.com>
Replace the fixed 7200-block turnover window with a per-hotkey token bucket: BasketTradeBucket stores (tao_available, last_refill_block), a missing row is a full bucket, each trade refills blocks_elapsed x budget / BASKET_TRADE_REFILL_BLOCKS, clamps to one budget at current NAV, then takes tao_mid out. An empty bucket refuses with BasketTurnoverBudgetExceeded. This removes the window-boundary burst (two budgets in adjacent blocks): at any instant at most one budget can be spent, and about one per day sustained. - BasketTradeWindow removed (never on a live chain), BasketTradeBucket added; BASKET_TRADE_WINDOW_BLOCKS renamed BASKET_TRADE_REFILL_BLOCKS - hotkey swap carries the bucket conservatively (min level, max block) - BasketTradingStatus keeps its encoding (no API bump): the u64 slot is now refill_blocks, tao_used is now tao_available; freeze_struct hash updated; SDK read exposes remaining, capacity and refill per block - window tests rewritten for bucket semantics; the 2.2 burst pin now asserts adjacent blocks cannot exceed one budget Co-authored-by: Arbos <unarbos@users.noreply.github.com>
32b2891 to
e276f4f
Compare
Describe the swap_basket stack (#3150, #3152, #3153): what validators can do, the BasketTrading proxy model, every guardrail with its launch default (2% per-leg band, token-bucket turnover budget of 10% of NAV refilling over 7200 blocks, 10% per-pool liquidity cap, 1/16 concentration cap, enable and freeze switches), the corrected worst-case loss bound, the new calls, events, errors, storage, admin setters, runtime API, and SDK/btcli surface. Tag the shipped v450 entry with its version so only the new release is 'next'. Co-authored-by: Arbos <unarbos@users.noreply.github.com>
Describe the swap_basket stack (#3150, #3152, #3153): what validators can do, the BasketTrading proxy model, every guardrail with its launch default (2% per-leg band, token-bucket turnover budget of 10% of NAV refilling over 7200 blocks, 10% per-pool liquidity cap, 1/16 concentration cap, enable and freeze switches), the corrected worst-case loss bound, the new calls, events, errors, storage, admin setters, runtime API, and SDK/btcli surface. Tag the shipped v450 entry with its version so only the new release is 'next'. Co-authored-by: Arbos <unarbos@users.noreply.github.com>
Describe the swap_basket stack (#3150, #3152, #3153): what validators can do, the BasketTrading proxy model, every guardrail with its launch default (2% per-leg band, token-bucket turnover budget of 10% of NAV refilling over 7200 blocks, 10% per-pool liquidity cap, 1/16 concentration cap, enable and freeze switches), the corrected worst-case loss bound, the new calls, events, errors, storage, admin setters, runtime API, and SDK/btcli surface. Tag the shipped v450 entry with its version so only the new release is 'next'. Co-authored-by: Arbos <unarbos@users.noreply.github.com>
Describe the swap_basket stack (#3150, #3152, #3153): what validators can do, the BasketTrading proxy model, every guardrail with its launch default (2% per-leg band, token-bucket turnover budget of 10% of NAV refilling over 7200 blocks, 10% per-pool liquidity cap, 1/16 concentration cap, enable and freeze switches), the corrected worst-case loss bound, the new calls, events, errors, storage, admin setters, runtime API, and SDK/btcli surface. Tag the shipped v450 entry with its version so only the new release is 'next'. Co-authored-by: Arbos <unarbos@users.noreply.github.com>
Description
Stacked on #3150 ← #3152 (base is
test/swap-basket-coverageat60982234e). Four commits, each self-contained:1.
fix(swap_basket): liquidity-relative destination capThe exploit. A stolen trader key buys a thin subnet with fund cash in 2%-band slices while a counterparty (a colluding subnet owner, or plain arbitrageurs) sells alpha back to the EMA between slices; every slice passes the slippage, turnover, and concentration rules. Reproduced against #3150: the fund lost 8.3% of NAV in one turnover window (≈90% of the TAO it spent), because the concentration cap marks holdings at realizable value, which is bounded by the pool's TAO reserve and never grows no matter how much of the pool's supply the fund accumulates (it held 9× the pool's alpha reserve and passed the 1/16 cap at 0.98%).
The fix. After the buy leg,
swap_basketrefuses a trade that would leave the fund holding more thanBasketLiquidityCapof the destination subnet's alpha reserve (SubnetAlphaIn). Root (the cash slot) has no pool and is exempt. Selling is never capped.The bound. With cap
Land a destination pool with TAO reserveR, a drain can extract aboutR × L² / (1 + L)from that pool — ≈1% ofRat the 10% default (≈10 τ on a 1,000 τ pool, vs. the whole turnover budget before). Today's largest funds (~12.5k τ) are barely constrained: 10% of a median mainnet pool (~2.4M α ≈ 7k τ) is ≈700 τ, about the fund's 1/16 slice.Default and tuning.
BasketLiquidityCapis u16-normalized, defaultu16::MAX / 10(10%).AdminUtils::sudo_set_basket_liquidity_cap(cap)(call index 109, root-only, zero rejected); e.g.16383= 25%. Global, not per subnet.Also flips #3152's §2.1 pin (
finding_2_1_thin_pool_drain_is_stopped_by_liquidity_cap) and isolates its §2.3 pin from the new cap.2.
test(swap_basket): winners over either cap are never clippedAppreciation via direct pool state. A holding over
RootWeightsCaporBasketLiquidityCaponly refuses further buys into that subnet. Partial sells to cash or another subnet, trades between other holdings, curated dividend redeploy, dust consolidation, pro-rata claims and hotkey swap all leave the winner alone. Profit-taking after a run-up is not blocked by the EMA band (sell floor anchors to spot when spot > EMA; chained slices may walk price down to 0.98 × EMA).3.
fix(swap_basket): charge pending-deposit flush workdo_swap_basketflushes queued dividend credits before trading but did not price that work. Declared weight now comes from the pending-deposit count (swap_basket_declared_weight(hotkey): the 256-row cap plus one unit per queued credit plus4 × 256for the deposit's own sweeps), refunded post-dispatch to the flush work actually reported, priced per quote unit likeclaim_root. An empty queue costs nothing extra, so the existing weight pin is unchanged.4.
feat(swap_basket): token-bucket turnover budgetReplaces the fixed 7200-block window. Per hotkey
BasketTradeBucket = (tao_available, last_refill_block); a missing row is a full bucket. On each trade: refillblocks_elapsed × budget / BASKET_TRADE_REFILL_BLOCKS, clamp to one budget (NAV at that moment), requiretao_available ≥ tao_mid, subtract. Empty →BasketTurnoverBudgetExceeded. Hotkey swap carries the bucket conservatively (min level, max refill block). Bound: at most 1× the budget at any instant and ≈1× per day sustained; the §2.2 two-blocks-two-budgets burst is gone (pin flipped:finding_2_2_bucket_denies_a_second_budget_in_the_adjacent_block).BasketTradeWindowis removed (never on a live chain; no migration).BasketTradingStatuskeeps its SCALE encoding, so no runtime API bump: theu64slot is nowrefill_blocks(7200) andtao_usedis nowtao_available; refill rate =budget_tao / refill_blocks.freeze_structhash updated. SDKbasket_trading_statusreturnsremaining_tao,budget_tao,used_tao,refill_blocks,refill_per_block_tao; CLI text updated.Related Issue(s)
swap_basket) and test: swap_basket coverage #3152 (coverage); see the exploitation/calibration review §2.1, §2.2, §3.Type of Change
Breaking Change
None on chain (new storage with defaults; error/event appended; the removed window storage never shipped). Clients decoding
BasketTradingStatusby field name must switchwindow_start_block/tao_used→refill_blocks/tao_available(encoding unchanged; the SDK read is updated here).Checklist
./scripts/fix_rust.sh(rancargo fmt --check --allandcargo clippy -p pallet-subtensor -p pallet-admin-utils --tests -- -D warningsinstead; both clean)Additional Notes
Tests run (final stack)
cargo test -p pallet-subtensor -- swap_basket claim_root stake_into_basket basket_flush swap_hotkey staking::— 347 passed (includes test: swap_basket coverage #3152'sswap_basket.rswith the rewritten bucket tests and both flipped pins, and the 9 tests inswap_basket_liquidity_cap.rs).cargo test -p pallet-admin-utils -- basket root_weights_cap— 5 passed.proxy_filters— 15 passed (before the restack; unchanged files).cargo fmt --check --all,git diff --check,cargo clippy --tests -D warningson both pallets: clean.Not in this PR (per AGENTS.md)
sudo_set_basket_liquidity_capreuses the measuredsudo_set_root_weights_capweight; a benchmark/WeightInfoentry is still owed for it and for the four feat: swap_basket — validator-directed beta basket rebalancing #3150 dispatchables.swap_basketkeeps the hand formula + refund pattern.spec_versionchange or labels.docs/errors/chain/*,docs/hyperparameters/index.mdx,docs/query/basket-trading-status.mdx) not regenerated here (nouvenvironment); the SDK registries and docstrings they derive from are updated.sdk/python/bittensor/_generated/still needs regeneration against a node running this runtime.