Skip to content

test: swap_basket coverage - #3152

Closed
unarbos wants to merge 6 commits into
feat/basket-trading-swap-basketfrom
test/swap-basket-coverage
Closed

unarbos wants to merge 6 commits into
feat/basket-trading-swap-basketfrom
test/swap-basket-coverage

Conversation

@unarbos

@unarbos unarbos commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Description

Stacked on #3150 (feat/basket-trading-swap-basket). Adds the pallet, AdminUtils, and proxy-filter tests for swap_basket that the feature PR shipped without (its tests were temporary and removed), plus behaviour-documenting tests for the weaknesses found in the calibration pass.

Commits, in order:

  1. fix(swap_basket) — two non-test changes the tests exposed (kept separate on purpose):
    • pallets/subtensor/src/macros/events.rs: BasketSwapped was inserted mid-enum (before VotingPowerTrackingEnabled), renumbering every later event and failing tests::epoch::regression_liquid_alpha_event_indices_are_append_only (this is the cargo test failure on feat: swap_basket — validator-directed beta basket rebalancing #3150's CI). Moved to the tail (index 149).
    • pallets/subtensor/src/staking/basket_views.rs::basket_trade_window_at: the (0, 0) storage default was treated as an open window starting at block 0, so on a young chain every fund shared a first window ending at block 7200 regardless of when it first traded. An uncharged window now opens at the current block. (Never bites on mainnet where now ≫ 7200; matters for localnet/tests and the basket_trading_status view.)
  2. test: swap_basket coveragepallets/subtensor/src/tests/swap_basket.rs (31 tests), pallets/admin-utils/src/tests/mod.rs (+3), runtime/src/proxy_filters/mod.rs (+1).
  3. test: pin the swap_basket weaknesses — 5 pallet tests + 1 proxy test that document the findings in docs/pr-3150-swap-basket-exploit-calibration.md; each carries a comment naming the finding so it flips when the corresponding fix lands. No guardrail logic was changed.
  4. fix(proxy): swap_basket requires BasketTrading proxyBasketTradingCalls removed from the NonTransfer and NonCritical allow lists (runtime/src/proxy_filters/mod.rs); BasketTrading is the only proxy type that admits swap_basket (Any aside), Staking stays without it, and NonTransfer is no longer a superset of BasketTrading. The pinned broad-proxy test now asserts the denial (§5.6 of the calibration report).

Covered

  • Happy paths: alpha→alpha, alpha→TAO (dest 0), TAO→alpha (origin 0), whole-holding amounts; composition-only (shares, BasketRate, watermark, owed, root stake unchanged; NAV within fees); root reserve lockstep (SubnetTAO[ROOT], SubnetAlphaOut[ROOT] credited/debited by exactly tao_mid); TotalStake drops by exactly the sell-leg author fee; block author paid on both legs; protocol flow (sell outflow = tao_mid + author fee, buy inflow fee-excluded, user flow untouched); BasketSwapped payload; claim after rebalance pays the same value; post-dispatch weight = swap_basket_weight(rows) < declared cap.
  • Gates/errors: BasketTradingDisabled, BasketTradingFrozen (and unfreeze), BasketSameSubnet, NonAssociatedColdKey (stranger and non-existent hotkey), HotKeyNotRegisteredInSubNet, BetaBasketSeedInProgress, ColdkeySwapAnnounced via the CheckColdkeySwap dispatch extension, SubnetNotExists (both sides), SubtokenDisabled, AmountTooLow (zero and sub-DefaultMinStake), NotEnoughStakeToWithdraw (over-held and empty origin).
  • Slippage band: EMA-anchored refusal on buy and sell, spot-anchored (own-impact) refusal on buy and sell, missing EMA on either side, a fill just inside the band on both legs.
  • Turnover budget: accumulates tao_mid across trades, refuses at the cap, refuses at start + 7199, rolls at start + 7200 with a fresh window, root origin charged at face, basket_trading_status agrees. (These tests and basket_trade_window_at will be reworked in fix(swap_basket): liquidity-relative destination cap, token-bucket turnover, flush weight #3153, which replaces the fixed window with a token bucket and stacks on this branch.)
  • Concentration cap: refuses over cap (including topping up and the cash slot), allows selling out of an over-cap position, young-chain softening.
  • Hotkey swap: freeze copied to the new hotkey, window moved and charged against by the next trade.
  • Rollback: a failing second leg leaves holdings, pool reserves, TotalStake, author balance, turnover window, protocol flow, and events untouched.
  • AdminUtils: sudo_set_basket_trading_enabled, sudo_set_basket_trading_frozen, sudo_set_basket_daily_turnover_capBadOrigin for signed, ValueNotInBounds for a zero cap, storage effect, events.
  • Proxy filters: BasketTrading admits exactly swap_basket and denies adjacent calls; no other narrow proxy admits swap_basket; NonTransfer, NonCritical, and NonFungible deny it; superset relation is {Any} only.
  • Event index: BasketSwapped pinned at 149 after BasketAlphaWrittenOff (148); the existing append-only regression test passes again.

Weaknesses pinned (pass today, must flip with a fix)

Test Finding
finding_2_1_thin_pool_drain_passes_every_guardrail §2.1 sliced buys + counterparty sell-back: >500 trades all pass, >5% of NAV lost in one window (~90% of TAO traded); cap passes on realizable value while spot value is >5× realizable
finding_2_2_window_boundary_lets_two_budgets_through_in_adjacent_blocks §2.2 >1.9× the daily budget moved in two adjacent blocks (start + 7199, start + 7200)
finding_2_3_chained_legs_in_one_block_walk_price_to_ema_ceiling §2.3 ≥10 legs in one block, price +20% up to 1.02×EMA
finding_crash_lock_blocks_selling_a_falling_holding §4 sell refused 5% below EMA (into subnet or cash), buy refused 5% above
finding_cash_slot_is_capped_at_root_weights_cap §4 with the cap binding, 10% into cash refused, 6% allowed
broad_proxies_currently_admit_swap_basket_without_opt_in §3/§5.6 — fixed in commit 4; now broad_proxies_do_not_admit_swap_basket_without_explicit_grant asserts the denial

Bugs the tests exposed

  1. BasketSwapped event index shift (breaks feat: swap_basket — validator-directed beta basket rebalancing #3150 CI) — fixed in commit 1.
  2. First turnover window anchored at block 0 on young chains — fixed in commit 1.
  3. NonTransfer / NonCritical delegates gained swap_basket without opting in — fixed in commit 4.
  4. (Pre-existing, not fixed, asserted as-is) TotalStake keeps the buy-leg author fee counted as stake — inherited from stake_into_subnet; the happy-path test asserts the exact current relation (TotalStake drops only by the sell-leg fee).

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature
  • Breaking change
  • Documentation update
  • Other: test coverage

Checklist

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • ./scripts/fix_rust.sh (creates a commit; ran cargo fmt --check --all instead)
  • Documentation changes: none needed
  • My changes generate no new warnings (cargo clippy -p pallet-subtensor --all-targets -D warnings clean)
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes (cargo test -p pallet-subtensor swap_basket: 36 passed; cargo test -p pallet-admin-utils basket: 3 passed; cargo test -p node-subtensor-runtime proxy_filters: 17 passed; regression_liquid_alpha_event_indices_are_append_only: passes)
  • Dependent changes: none

Additional Notes

Per AGENTS.md: no spec_version change, no labels, no benchmarks. cargo fmt --check --all and git diff --check clean. Runtime tests were run with SKIP_WASM_BUILD=1 and CXX=g++ CC=gcc LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/13 (this VM's default cc is clang without libstdc++ on the link path).

Open in Web Open in Cursor 

cursoragent and others added 3 commits September 11, 2026 13:46
…rst turnover window at the first trade

Two behaviours the new swap_basket tests exposed:

- BasketSwapped was inserted mid-enum (before VotingPowerTrackingEnabled),
  renumbering every later event and failing the append-only regression
  test (tests::epoch::regression_liquid_alpha_event_indices_are_append_only).
  Move it after BasketAlphaWrittenOff so historical indices stay stable.
- basket_trade_window_at treated the (0, 0) storage default as an open
  window starting at block 0, so on a young chain every fund shared a
  first window ending at block 7200 regardless of when it first traded.
  Treat an uncharged window as absent and open it at the current block.

Co-authored-by: Arbos <unarbos@users.noreply.github.com>
Pallet tests (tests/swap_basket.rs): happy paths for alpha->alpha,
alpha->root and root->alpha (composition-only, reserve lockstep, author
fee, protocol flow, event payload, post-dispatch weight); every gate and
error; slippage band on both legs (EMA- and spot-anchored refusals, a
just-inside fill); turnover budget (accumulates, refuses, rolls at 7200
blocks, root origin at face); concentration cap (refuse over cap, sell out
of an over-cap position, young-chain softening); guardrails follow hotkey
swap; failed second leg rolls back fully; BasketSwapped index pinned.

AdminUtils: auth, storage, and events for the three sudo setters.
Runtime: BasketTrading proxy admits exactly swap_basket and no other
narrow proxy admits it.

Co-authored-by: Arbos <unarbos@users.noreply.github.com>
Behaviour-documenting tests that pass today and must flip when a fix
lands (each names its finding in docs/pr-3150-swap-basket-exploit-calibration.md):

- §2.1 thin-pool drain: sliced root->C buys with counterparty sell-back
  pass every guardrail until the turnover budget is spent and lose >5% of
  NAV (~90% of the TAO traded); the concentration cap passes on realizable
  value while the same holding at spot is many times the cap share.
- §2.2 window-boundary burst: 2x the daily budget moved in two adjacent
  blocks across the window edge.
- §2.3 chained legs in one block walk the price >20% up to 1.02 x EMA.
- crash lock: selling refused with spot 5% below EMA, buying refused 5%
  above; cash slot capped at RootWeightsCap (1/16) once the cap binds.
- proxies: NonTransfer / NonCritical currently admit swap_basket without
  an explicit BasketTrading grant.

Co-authored-by: Arbos <unarbos@users.noreply.github.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 2:45pm UTC

Request Review

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

🛡️ AI Review — Skeptic (security review)

VERDICT: SAFE

LOW contributor risk / baseline scrutiny: write-role maintainer with substantive merged history; disclosed Cursor coauthorship; no listed Gittensor association. Target: feat/basket-trading-swap-basket.

Reviewed all six changed files and the turnover-accounting call path. No review-trust, CI, or dependency changes. The added weakness-demonstration tests cover behavior inherited from #3150; this verdict applies only to #3152’s delta.

Checks: git diff --check passed; git status --short was clean. Formatting, builds, and tests were not run under the Skeptic static-only rule.

Findings

No findings.

Conclusion

No malicious behavior or newly introduced security vulnerability found. The event relocation restores historical ordering, and the window change preserves accounting for already-charged turnover.


🔍 AI Review — Auditor (domain review)

VERDICT: 👍

Established contributor with write permission; Gittensor UNKNOWN. Supplied overlap metadata shows no competing swap-basket coverage PR.

The event-index and initial-window fixes are sound, and the added tests provide substantial coverage. One calibration test needs its timing corrected to demonstrate the scenario claimed in the PR body.

Checks: git diff --check passed; git status --short was clean. cargo fmt --check --all could not run because rustup required a write to the read-only toolchain directory. Builds and tests were not run; no runtime confirmation was needed. No files were modified.

Findings

Sev File Finding
MEDIUM pallets/subtensor/src/tests/swap_basket.rs:1205 Spend the first budget at the window boundary inline

Conclusion

Approve with the test correction below. No blocking regression was identified in the changes against the feature branch.

@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.

Comment on lines +1203 to +1205
let start = System::block_number();
assert_ok!(swap(&fund, fund.netuid_a, fund.netuid_b, half));
assert_ok!(swap(&fund, fund.netuid_a, fund.netuid_b, half));

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.

[MEDIUM] Spend the first budget at the window boundary

At pallets/subtensor/src/tests/swap_basket.rs:1203–1205, both first-window trades execute at start; the next successful trades execute at start + 7200. Consequently, moved_in_two_blocks counts trades separated by an entire window, rather than adjacent blocks as the test and PR description claim.

Establish an earlier window with a small successful trade, advance to its penultimate block, spend the remaining budget there, then spend the refreshed budget in the next block. Assert the combined tao_mid from those two adjacent blocks, excluding the opening trade. This makes the test demonstrate the boundary burst it is intended to document.

@github-actions

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: SAFE Auditor: 👍

Open the window with a minimal trade, spend the remaining budget at
start + 7199, then a full budget at start + 7200, and assert on the sum of
those two adjacent blocks only (review finding on the earlier shape, which
spent the two budgets a whole window apart).

Co-authored-by: Arbos <unarbos@users.noreply.github.com>
…t.rs

Matches the other test modules (consensus.rs, migration.rs); CI runs
clippy with -D warnings over --all-targets.

Co-authored-by: Arbos <unarbos@users.noreply.github.com>
Drop BasketTradingCalls from the NonTransfer and NonCritical allow lists so
an existing broad delegate does not gain basket-trading power at upgrade;
the explicit BasketTrading grant is the only proxy type that admits
swap_basket (Any aside). NonTransfer is no longer a superset of
BasketTrading. Golden subtractive tests updated; the pinned broad-proxy
test now asserts the denial.

Co-authored-by: Arbos <unarbos@users.noreply.github.com>
@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 (60982234e) is already an ancestor of cursor/swap-basket-consolidated-b2ac; nothing here is lost. Closing; branch kept.

@unarbos unarbos closed this Sep 11, 2026
unarbos added a commit that referenced this pull request Sep 15, 2026
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>
unarbos added a commit that referenced this pull request Sep 15, 2026
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>
unarbos added a commit that referenced this pull request Sep 15, 2026
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>
unarbos added a commit that referenced this pull request Sep 16, 2026
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>
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.

2 participants