Skip to content

release-root-reborn - #2968

Merged
IntiTechnologies merged 122 commits into
mainfrom
release-v438
Aug 3, 2026
Merged

release-root-reborn#2968
IntiTechnologies merged 122 commits into
mainfrom
release-v438

Conversation

@unarbos

@unarbos unarbos commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Runtime release 438 (spec_version 441). Brings the Root Reborn feature branch (#2759, root-reborn) up to date with main, and hardens it for mainnet: a gated launch, a multi-block seed migration, and an epoch pipeline that no longer scales block time with validator × holding counts.

Root Reborn (beta baskets)

  • Root dividends are reinvested into per-validator beta baskets: one fund per validator, held under a global escrow coldkey, allocated per the validator's root weight vector (set_root_weights, stored in Weights[ROOT]).
  • Deposits mint fund shares at pre-deposit realizable NAV (valued at executable quotes, not spot marks), so late stakers can't dilute or skim. Stakers accrue claimable shares via a per-validator rate accumulator; claim_root redeems pro-rata across every holding into root TAO.
  • Only hotkeys registered on the root network earn root dividends; root alpha that finds no eligible earner is recycled instead of accumulating to unregistered keys.
  • Root unstakes sit behind a hold interval to stop epoch-boundary JIT dividend snipes. Root registration is burn-based.
  • Retires set_root_claim_type (call 122) and sudo_set_num_root_claims (123) plus the auto-claim scheduler. Legacy RootClaimable/RootClaimed are drained by the seed migration.
  • New BetaBasketRuntimeApi (basket NAV, holdings, owed TAO, validator weights views).

Gated launch

  • set_root_weights ships disabled network-wide (RootWeightSettingEnabled = false; new root call sudo_set_root_weight_setting_enabled). Until governance flips it, every fund runs the null strategy: dividends accumulate in place on the origin subnet.

Seed migration (migrate_seed_beta_basket_v2)

  • Multi-block, metered, resumable migration that converts legacy claim state into basket shares, with phase measurement, an on-chain audit trail (Σ owed == BasketShares), and recovery paths.
  • While the seed runs, operations that could desync snapshotted conversion from live stake are paused (BetaBasketSeedInProgress): basket deposits/claims, coldkey and root-touching hotkey swaps, root stake add/remove/transfer/swap, and subnet dissolution. Dividends earned mid-seed are deferred and released after.
  • Validated against mainnet clones on validator hardware, measured to completion.

Epoch performance (fixes the 24s epoch blocks)

  • Deposit queue: epochs no longer deposit into baskets inline (each deposit prices a share mint against full fund NAV — one AMM quote per holding). They enqueue (hotkey, origin netuid) → alpha credits into PendingBasketDeposits; a round-robin drain flushes one hotkey per block, and flush-on-touch hooks (claims, basket stakes, root stake changes, hotkey swaps) keep results exact — stake added after an epoch can never capture dividends earned before it arrived.
  • Dust control: credits below RootClaimableThreshold stay queued and keep merging until worth a deposit; dust consolidation now also runs on uncurated funds, capping holding-count growth.
  • Swap fast path: sim_swap on initialized pools skips the transactional storage overlay, and the 256-bit bignum pow (the dominant cost of a swap) is computed once and reused for full fills.

Dissolution

  • Basket holdings on a dissolving subnet are converted to each fund's root (TAO) slot in a metered cleanup phase, before stake/AMM teardown.
  • A terminal cleanup phase purges the dissolved netuid's PendingBasketDeposits credits, so a subnet later reusing the netuid can never inherit the old subnet's queued alpha.

Rank-pinned emission gate bar (#3014)

  • EmissionBarRank (default 64) pins theta to the Nth-largest demand share instead of the q-mass quantile; sudo_set_emission_bar_rank to tune, N=0 falls back to q-mass.
  • One-shot migrate_reset_emission_gate_bar clears the stale quantile bar so rank-64 applies at upgrade.

SDK / CLI / docs

  • Wire layer (errors, calls, storage) regenerated against the upgraded chain; error catalog and docs cover the new gates (BetaBasketSeedInProgress, RootWeightSettingDisabled, …).
  • btcli: unified register command, root claim/basket surfaces, MEV-shield submissions preflight the carrier fee (free TAO) and warn instead of failing mid-flight, unstaking root principal warns when basket yield would go unclaimed, conviction-lock transfer UX, natural-sorted wallet/hotkey listings.

Key merge decisions (root-reborn was based on spec-418 main)

  • set_root_weights moved from call index 139 (taken on main) to 146.
  • Coinbase: main's miner-collateral settlement is kept; the claimable remainder flows to the basket deposit queue instead of the removed claim-root ledger.
  • Hotkey-swap root-cleanliness gate checks BasketRate/BasketShares and residual legacy claim state.
  • Proxy filters: root-reborn's removal of set_root_claim_type ported into main's refactored proxy_filters/ call groups; set_root_weights and the new admin gate added to the appropriate groups.

Also included

  • srtool builder image: retry the whole build up to 5 times with linear backoff on transient CI egress failures.

Test plan

  • Full pallet unit suite (1373 tests) including rewritten 02.0x claim-root/basket suites
  • Mainnet-clone upgrade runs: seed migration to completion, issuance trackers, prune gating
  • CI: wasm runtime build, try-runtime, rust-e2e shards, zombienet staking/evm/shield audits

unconst and others added 11 commits June 15, 2026 15:30
Replace the per-block auto-sell of root dividends with a compounding,
redeemable beta basket. Root validators set a distribution vector over
subnets via `set_root_weights`; each validator's root dividends are sold
to TAO and re-bought as alpha across those subnets, staked under a global
escrow coldkey (so the basket counts toward the validator's stake and
compounds), and redeemed to TAO on demand through the existing claim path
using an E/P growth multiplier. Auto-claim/auto-sell removed. Adds a
dedicated `RootBasketWeights` map, `BasketPrincipal` accounting, hotkey-swap
and subnet-dissolve handling, a legacy-state seed migration, and RPC views
(staker pending TAO, validator NAV + basket, network-wide NAV).

Co-authored-by: Cursor <cursoragent@cursor.com>
Revive the existing root-weights plumbing: store the basket vector under
Weights[ROOT][uid] (uid-keyed, so it follows the validator through hotkey
swaps automatically and reuses existing weight terms/limits) rather than a
separate RootBasketWeights map. Keep the dedicated `set_root_weights`
extrinsic since the generic set_weights rejects netuid 0 and root needs
different checks. Retain the dust-recycle fix (Σ owed == BasketPrincipal).

Co-authored-by: Cursor <cursoragent@cursor.com>
On subnet dissolve, liquidate_basket_to_root_stakers previously credited the
swapped basket value to the validator's current root nominators in proportion
to their *current root stake* (increase_stake_for_hotkey_on_subnet), ignoring
the per-coldkey owed entitlement. That windfalls recent/large-current-stake
nominators and short-changes stakers who actually accrued the basket, then
wipes the ledger — a provable intra-staker fairness bug.

Now: swap the whole basket once, then distribute the realized TAO pro-rata by
each staker's owed (rate*root_stake - claimed == owed*E/P), crediting each
coldkey individually and rebasing its claimed watermark (mirrors a normal
claim). Degenerate zero-owed case falls back to stake-proportional so value is
never orphaned. Adds a regression test proving a zero-owed fresh staker
receives nothing while the accruing staker receives the basket.

Co-authored-by: Cursor <cursoragent@cursor.com>
Mint basket principal *shares* at the live escrow NAV (E/P) instead of at
par. A deposit into an already-compounded basket now mints fewer shares
than the alpha bought, leaving E/P unchanged: existing holders are not
diluted and a late staker cannot skim past compounding. Makes the
staker-facing guarantee strict — a new staker only ever earns their fair
share of distributions from the point they join forward.

Adds tests proving claims 1-4 (principal never lost; accrued beta
unchanged by others staking; beta compounds; no dilution/skim on late
stake, incl. E/P invariance across a deposit).

Also includes the dissolve liquidation distributing pro-rata by owed
entitlement rather than current root share.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add observability for off-chain indexing / future tokenization cost-basis:
- Events: BasketDeposited (alpha bought + shares minted at NAV, per
  validator/subnet), BasketClaimed (TAO realized by a staker), and
  BasketLiquidated (TAO returned to root stakers on subnet dissolve).
- RPC/runtime-API: betaBasket_getValidatorWeights returns a validator's
  basket weight vector (its curation strategy) so dashboards can display it.

Co-authored-by: Cursor <cursoragent@cursor.com>
… claim type

- Record protocol outflow on the origin sell and inflow on each
  redistribution buy in distribute_root_alpha_to_basket, so a deposit->claim
  round-trip nets ~0 on the dest pools (symmetric with the claim/liquidation
  outflow that was already recorded). Records sit inside with_transaction so
  they roll back with the swaps.
- Exclude the beta-escrow coldkey from clear_small_nomination_if_required:
  basket positions are not nominations, and sweeping one stranded TAO in the
  keyless escrow account while leaving BasketPrincipal untouched (breaking
  Sum(owed) == BasketPrincipal and zeroing every staker's owed * E/P payout).
- Deprecate the claim-type surface: set_root_claim_type now rejects the no-op
  Keep/KeepSubnets variants (new RootClaimTypeNotSupported error); fixed the
  false "(Keep was removed)" comment and documented the variants as no-ops.
- Remove dead auto-claim machinery (run_auto_claim_root_divs,
  block_hash_to_indices, block_hash_to_indices_weight) and its false-coverage
  test; update affected tests and benchmarks.

Co-authored-by: Cursor <cursoragent@cursor.com>
A validator can now weight root (uid 0) in its basket vector to opt out of
subnet exposure: that slice is held as root stake (TAO at 1:1) under the
escrow instead of being swapped into subnet alpha, and it compounds and is
claimable through the same E/P machinery as the alpha slots.

Root has no AMM pool, so the swap is elided (valuation is already 1:1) and
the reserve bookkeeping is mirrored directly; the escrow custody account is
excluded from the claimant base and from dissolution payouts since it is not
a claimant. Adds 4 tests covering deposit, claim (reassign, no swap),
compounding, and the escrow-denominator exclusion.

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

Align the set_root_weights producer with the basket consumer so a validator
can actually populate a root (uid 0) slot on-chain (previously the extrinsic
rejected root, leaving the new path unreachable in production).

Code-quality cleanups: extract credit_root_reserves (the SubnetTAO/
SubnetAlphaOut/TotalStake triple, previously hand-mirrored in 3 places) and
hoist the shared post-claim watermark advance so root and subnet claims share
one settlement tail instead of duplicating it.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ble-ready)

Restructure the basket's unit of account so entitlements are shares of a
single fund per validator, never claims on a specific subnet's alpha. This
decouples what stakers are owed (fund shares) from what the fund holds
(escrow positions), which is the prerequisite for validator-directed
rebalancing and share tokenization later: holdings can change without
touching any staker's claim.

How it works now:

* Storage: BasketShares(hot) = outstanding fund shares P (TAO-denominated);
  BasketRate(hot) = single shares-per-root-stake accumulator; and
  BasketClaimed(hot, cold) = signed i128 claimed watermark. The watermark is
  signed on purpose: stake-change rebasing (claimed +/- rate * delta) must be
  exact in both directions or unstake-before-claim forfeits accrued
  entitlement (the old unsigned per-subnet RootClaimed had this bug).
* Deposit: each root dividend is sold for TAO and deployed across subnets
  per the validator's Weights[ROOT] vector into the keyless escrow. Fund NAV
  N is snapshotted after the origin sell (the fund may hold origin-subnet
  alpha) and shares = tao_deployed * P / N mint at the pre-deposit NAV, so
  existing holders are never diluted and late deposits cannot skim past
  compounding. Mint/payout math is u128 (U96F32 saturates at chain scale).
  Dust deposits (rate increment below I96F32 resolution) roll back and
  recycle so sum(owed) == P is never broken.
* Claim (claim_root, now arg-less): fund-level pro-rata redemption. Owed
  shares define fraction f = owed / P; exactly f of every holding is
  redeemed (subnet alpha sold to TAO, the root cash slot reassigned without
  a swap) and staked on root. Composition is preserved by every claim. A
  claim that realizes zero TAO (all alpha takes floor to zero) rolls back
  rather than burning shares. Only the ROOT RootClaimableThreshold entry
  gates dust claims; the sudo setter rejects other netuids.
* Dissolution: a dying subnet's holdings convert into each fund's root
  (TAO) slot. NAV is continuous, entitlements untouched; the old
  liquidate-to-stakers machinery is deleted.
* Key swaps: root hotkey swaps move the whole fund (shares, rate,
  watermarks, holdings) by value; coldkey swaps carry the watermark even at
  zero live root stake (negative watermark = owed with no stake).
* Migration is migrate_seed_beta_basket_v2 under a fresh key: the v1 name
  was already consumed on chains that ran the abandoned per-slot seed, which
  would have silently skipped conversion and stranded every basket. v2
  converts legacy per-subnet state at fixed moving prices (spot fallback),
  preserving each staker's owed TAO value exactly (sum(owed) == P), tolerates
  pre-existing v1 escrow/root-slot state without double-staking or minting
  unbacked shares, and clears orphaned BasketPrincipal entries.
* Retired dead surface: set_root_claim_type (call 122), sudo_set_num_root_
  claims (call 123), RootClaimType/RootClaimTypeEnum/NumRootClaim storage,
  and their events/errors/benchmarks/ts-tests. Redemption is always a full
  swap to root TAO; there is no auto-claim scheduler.

Covered by 50+ basket/migration tests including conservation under
interleaved deposits/claims, chain-scale magnitudes, self-referential
origin deposits, v1-already-ran migration state, coldkey-swap
entitlement carry, and zero-realized-claim no-ops.

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

# Conflicts:
#	pallets/subtensor/src/benchmarks/benchmarks.rs
#	pallets/subtensor/src/coinbase/root.rs
#	pallets/subtensor/src/coinbase/run_coinbase.rs
#	pallets/subtensor/src/lib.rs
#	pallets/subtensor/src/macros/dispatches.rs
#	pallets/subtensor/src/macros/hooks.rs
#	pallets/subtensor/src/staking/claim_root.rs
#	pallets/subtensor/src/subnets/weights.rs
#	pallets/subtensor/src/swap/swap_hotkey.rs
#	pallets/subtensor/src/tests/claim_root.rs
#	pallets/subtensor/src/tests/migration.rs
#	runtime/src/lib.rs
#	ts-tests/suites/zombienet_staking/02.04-claim-root-hotkey-swap.test.ts
…ures.

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

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
subtensor Ready Ready Preview Aug 3, 2026 5:46pm

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.

Comment thread pallets/subtensor/src/migrations/migrate_seed_beta_basket.rs Outdated
Comment thread pallets/subtensor/src/macros/dispatches.rs Outdated
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

🛡️ AI Review — Skeptic (security review)

VERDICT: VULNERABLE

MEDIUM scrutiny: established high-volume contributor with repository write access, substantial merged history, and no known Gittensor association; release-v438 -> main.

All four previously reported weight-accounting vulnerabilities remain on the current head. No AI-review trust-boundary files were changed.

Findings

Sev File Finding
HIGH pallets/subtensor/src/coinbase/block_step.rs:24 Per-block hook performs variable basket work without metering inline
HIGH pallets/subtensor/src/macros/dispatches.rs:1924 Root claims can exceed their pre-dispatch reservation inline
HIGH pallets/subtensor/src/macros/dispatches.rs:2006 Basket deposits can exceed their pre-dispatch weight cap inline
HIGH pallets/subtensor/src/swap/swap_hotkey.rs:50 Dispatch-weight calculation performs an unbounded storage scan inline

Prior-comment reconciliation

  • 0b31a961: not addressed — The mandatory block hook still invokes an unmetered full-hotkey queue flush.
  • e48b88fb: not addressed — Claims still reserve MAX_ROOT_CLAIM_WORK while explicitly permitting larger actual work.
  • a716cecb: not addressed — The fixed slot/holding reservation remains unenforced, and eager queue flushing adds unreserved work.
  • c7ce28ed: not addressed — Pre-dispatch weight calculation still scans the entire unbounded BasketClaimed prefix.

Conclusion

Claims, basket deposits, root-touching hotkey swaps, and the mandatory per-block queue drain can still execute work beyond their reserved block weight. These remain blocking denial-of-service risks.


📜 Previous run (superseded)
Sev File Finding Status
HIGH pallets/subtensor/src/coinbase/block_step.rs:24 Per-block hook performs variable basket work without metering ➡️ Carried forward to current findings
The mandatory block hook still invokes an unmetered full-hotkey queue flush.
HIGH pallets/subtensor/src/macros/dispatches.rs:1924 Root claims can exceed their pre-dispatch reservation ➡️ Carried forward to current findings
Claims still reserve MAX_ROOT_CLAIM_WORK while explicitly permitting larger actual work.
HIGH pallets/subtensor/src/macros/dispatches.rs:2006 Basket deposits can exceed their pre-dispatch weight cap ➡️ Carried forward to current findings
The fixed slot/holding reservation remains unenforced, and eager queue flushing adds unreserved work.
HIGH pallets/subtensor/src/swap/swap_hotkey.rs:50 Dispatch-weight calculation performs an unbounded storage scan ➡️ Carried forward to current findings
Pre-dispatch weight calculation still scans the entire unbounded BasketClaimed prefix.

🔍 AI Review — Auditor (domain review)

VERDICT: 👍

Established repository contributor with write access and substantial prior contributions; no trusted Gittensor association found.

The final commit’s relaxed basket assertions match runtime timing: dividends may increase the old validator’s rate and shares between the pre-swap read and finalization, while the zero-value checks on the old hotkey still prove that the fund moved completely.

The migration bounds, cooldown behavior, and spec_version 441 remain intact. Overlapping PRs #3009 and #2989 are unrelated despite sharing release/generated files. Static analysis was sufficient; no runtime tests or auto-fixes were needed.

Findings

No findings.

Conclusion

The latest test adjustment reflects valid runtime behavior without weakening the ownership invariant. No substantive domain issues remain.

@github-actions

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

A tiny buy on a thin pool can push spot arbitrarily high, inflating a
validator's marked basket NAV (up to u64 saturation) so that every
subsequent root dividend deposit rounds to zero shares and is recycled,
and network-wide NAV metrics go blind.

- Replace alpha_to_tao_value (spot price * amount) with
  realizable_tao_for_alpha: a sim_swap quote bounded by the pool's TAO
  reserve, 1:1 for root and stable-mechanism holdings. Deposit share
  pricing, redemption sizing, and all NAV views now share this one
  valuation, so they cannot diverge under a manipulated spot price.
- When shares are outstanding but NAV marks to zero, only mint at par if
  the stale shares are rounding dust from a full drain (capture <= ~1%);
  otherwise recycle the dividend instead of mispricing the mint.
- Accumulate the network-wide NAV aggregate in u128 before saturating.

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

@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 thread pallets/subtensor/src/migrations/migrate_seed_beta_basket.rs Outdated
Comment thread pallets/subtensor/src/macros/dispatches.rs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

Adds get_basket_payout(hotkey, coldkey) to BetaBasketRuntimeApi (+ RPC) so
clients can itemize owed dividends per validator. Regenerates the SDK chain
bindings and golden/shape fixtures against a v438 localnet. SDK: new
SetRootWeights and SetRootClaimThreshold intents, parameterless ClaimRoot,
reads/root.py wrapping the beta basket runtime APIs and claim threshold;
retires SetRootClaimType and the root_claim_type read. CLI: btcli weights
set-root/get-root and stake claim/basket/owed replace set-claim and
process-claim. Docs: new root-dividends guide, staking guide rewrite,
migration mappings, regenerated tx/query/error references, and the
v438-upgrade release page.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions github-actions Bot mentioned this pull request Jul 23, 2026
unarbos and others added 2 commits July 23, 2026 15:27
…igration.

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

Dividend deposits now mint shares against the realizable NAV actually added
(net of slippage and fees) instead of the raw TAO deployed, and the slice of
each dividend attributable to the fund's own root-slot position accrues to
existing share holders via NAV growth rather than minting new shares. Adds a
stake_into_basket extrinsic that deploys a staker's TAO across the
validator's weight vector and credits shares through the signed claimed
watermark, plus tests covering round-trip symmetry, dilution protection, and
the root-slot yield attribution fix.

Co-authored-by: Cursor <cursoragent@cursor.com>
@unarbos unarbos changed the title Release v438: Root Reborn (beta baskets) on current main release-root-reborn Jul 23, 2026

@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 thread pallets/subtensor/src/migrations/migrate_seed_beta_basket.rs Outdated
Comment thread pallets/subtensor/src/macros/dispatches.rs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

Security pass: the stake_into_basket extrinsic now declares a capacity-sized
weight cap and refunds the actual cost post-dispatch, scaled by both the
weight-vector length and the holdings the NAV valuations sweep; a user deposit
slice whose swap rounds to zero alpha now rolls the whole deposit back instead
of silently donating the TAO, and zero-alpha slices no longer book a protocol
inflow with no matching outflow; the clear-root-basket-weights migration drops
its redundant counting pre-pass and documents the 64-uid bound that makes a
single-block clear safe.

Quality pass: both deposit paths now share one deployment engine
(deploy_tao_into_basket with an explicit funding-source enum) instead of two
copy-pasted buy loops; the read-only valuation views moved to a new
basket_views.rs, putting claim_root.rs back under 1k lines; the negative-
watermark share grant got a named mutator (grant_basket_shares) and honest
storage docs; the direct-deposit tests moved to their own module with named
tolerance constants replacing magic epsilons.

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

@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 thread pallets/subtensor/src/migrations/migrate_seed_beta_basket.rs Outdated
Comment thread pallets/subtensor/src/macros/dispatches.rs Outdated
Co-authored-by: Cursor <cursoragent@cursor.com>

@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 thread pallets/subtensor/src/coinbase/block_step.rs
Comment thread pallets/subtensor/src/macros/dispatches.rs
Comment thread pallets/subtensor/src/macros/dispatches.rs
Comment thread pallets/subtensor/src/swap/swap_hotkey.rs
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

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

@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 thread pallets/subtensor/src/coinbase/block_step.rs
Comment thread pallets/subtensor/src/macros/dispatches.rs
Comment thread pallets/subtensor/src/macros/dispatches.rs
Comment thread pallets/subtensor/src/swap/swap_hotkey.rs
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

Flush flushable deposits while the hotkey is still on root, then purge
leftover dust after membership drops so earned dividends are not recycled.

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

@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 thread pallets/subtensor/src/coinbase/block_step.rs
Comment thread pallets/subtensor/src/macros/dispatches.rs
Comment thread pallets/subtensor/src/macros/dispatches.rs
Comment thread pallets/subtensor/src/swap/swap_hotkey.rs
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

One bad origin no longer destroys healthy credits: multi-credit failures split into per-origin retries, and still-failing credits stay pending for a later flush.

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

@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 thread pallets/subtensor/src/coinbase/block_step.rs
Comment thread pallets/subtensor/src/macros/dispatches.rs
Comment thread pallets/subtensor/src/macros/dispatches.rs
}
let claimed_rows = BasketClaimed::<T>::iter_prefix(old_hotkey).count() as u64;

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] Dispatch-weight calculation performs an unbounded storage scan

This function runs while evaluating the dispatch weight, before admission, and iterates the entire claimant prefix. A popular validator can have an unbounded number of BasketClaimed rows, allowing transaction validation itself to consume unmetered work. Maintain a bounded row count in storage or redesign the migration as a bounded, cursor-based operation.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

unarbos and others added 2 commits August 3, 2026 13:33
Co-authored-by: Cursor <cursoragent@cursor.com>
11.0.1 and 0.1.1 are already on PyPI, so the release train refuses new rcs until the bases move.

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

@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 thread pallets/subtensor/src/coinbase/block_step.rs
Comment thread pallets/subtensor/src/macros/dispatches.rs
// sim-swap valuations); the actual weight is computed in `do_stake_into_basket`
// from the real slot and holding counts and refunded post-dispatch, mirroring
// `claim_root`.
#[pallet::weight((Pallet::<T>::stake_into_basket_weight(128, 256), DispatchClass::Normal, Pays::Yes))]

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] Basket deposits can exceed their pre-dispatch weight cap

The fixed reservation assumes at most 128 slots and 256 holdings, but the call does not reject larger existing baskets. It also eagerly calls flush_basket_deposits_for_hotkey before computing its returned weight and discards that flush work. Consequently both NAV/deployment work and queued-credit flushing can exceed the inclusion reservation. Enforce hard bounds and include the flush in pre-dispatch weight, or make the operation resumable and metered.

Comment thread pallets/subtensor/src/swap/swap_hotkey.rs
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

Enforce the root hold on stake transitions and unstake_from_subnet,
keep LastColdkeyHotkeyStakeBlock root-only, migrate age across hotkey
and coldkey swaps, and stamp age when claim_root mints user root stake.

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

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

// --- 4b. Flush queued root-dividend basket deposits: one hotkey per block,
// round-robin. Runs right after coinbase (and before the price EMA update) so a
// flush lands where the epoch-inline deposits used to happen.
Self::flush_pending_basket_deposits_block();

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] Per-block hook performs variable basket work without metering

This mandatory block hook flushes every pending origin for one hotkey and may additionally value every basket holding and execute swaps. Those collections are not bounded here, and hook work is not charged against an extrinsic, so a sufficiently large basket can make every drain block exceed its execution budget. Meter the drain with a strict per-block weight budget and resume partially processed hotkeys across blocks.

Comment on lines +1921 to +1924
// Declared weight is a soft envelope sized for [`MAX_ROOT_CLAIM_WORK`]; actual work
// is measured and refunded post-dispatch (fat coldkeys may exceed the reservation).
#[pallet::weight(
<T as crate::pallet::Config>::WeightInfo::claim_root()
.saturating_mul(MAX_ROOT_CLAIM_HOTKEYS as u64)
.saturating_mul(MAX_SUBNET_CLAIMS as u64)
<T as crate::pallet::Config>::WeightInfo::claim_root(MAX_ROOT_CLAIM_WORK)

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] Root claims can exceed their pre-dispatch reservation

The call reserves only claim_root(MAX_ROOT_CLAIM_WORK), while the removed hotkey bound and the accompanying comment explicitly permit actual work beyond that envelope. Post-dispatch weight can refund unused weight but cannot retroactively reserve capacity, so an included claim may exceed the block's normal-extrinsic budget. Reject claims above the benchmarked bound or process them through a bounded, resumable cursor.

Comment on lines +2000 to +2006
#[pallet::call_index(147)]
// Declared weight is a cap sized for a 128-slot weight vector over 256 holdings
// (each slot costs a balance transfer + swap + escrow write; each holding two NAV
// sim-swap valuations); the actual weight is computed in `do_stake_into_basket`
// from the real slot and holding counts and refunded post-dispatch, mirroring
// `claim_root`.
#[pallet::weight((Pallet::<T>::stake_into_basket_weight(128, 256), DispatchClass::Normal, Pays::Yes))]

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] Basket deposits can exceed their pre-dispatch weight cap

The fixed reservation assumes at most 128 slots and 256 holdings, but neither limit is enforced before execution. The call also eagerly flushes all queued origins for the hotkey before computing its returned weight, and that flush work is absent from the reservation and returned calculation. Enforce the declared bounds and include bounded flush work, or move the operation to a metered resumable path.

}
let claimed_rows = BasketClaimed::<T>::iter_prefix(old_hotkey).count() as u64;

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] Dispatch-weight calculation performs an unbounded storage scan

This function is evaluated to determine pre-dispatch weight, yet it scans the entire BasketClaimed prefix. Weight calculation itself is not metered by the returned weight, so an attacker can grow claimant rows for a popular validator and force block authors to perform an unbounded scan merely to validate a root-touching swap. Maintain a bounded row count in storage or redesign the move as bounded resumable work.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

Sync generated errors.py and docs catalogs with the root unlock hold
message so clone-upgrade drift and the docs build gate pass.

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

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

// --- 4b. Flush queued root-dividend basket deposits: one hotkey per block,
// round-robin. Runs right after coinbase (and before the price EMA update) so a
// flush lands where the epoch-inline deposits used to happen.
Self::flush_pending_basket_deposits_block();

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] Per-block hook performs variable basket work without metering

This mandatory block hook flushes an entire hotkey. That operation scans all its pending origins and basket holdings and may perform multiple quotes and swaps, but block_step does not meter or cap that work. A sufficiently large fund can therefore make block initialization exceed its budget. Process the queue through a WeightMeter with resumable cursors and stop before exhausting the per-block allowance.

Comment on lines +1921 to +1924
// Declared weight is a soft envelope sized for [`MAX_ROOT_CLAIM_WORK`]; actual work
// is measured and refunded post-dispatch (fat coldkeys may exceed the reservation).
#[pallet::weight(
<T as crate::pallet::Config>::WeightInfo::claim_root()
.saturating_mul(MAX_ROOT_CLAIM_HOTKEYS as u64)
.saturating_mul(MAX_SUBNET_CLAIMS as u64)
<T as crate::pallet::Config>::WeightInfo::claim_root(MAX_ROOT_CLAIM_WORK)

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] Root claims can exceed their pre-dispatch reservation

The call reserves only MAX_ROOT_CLAIM_WORK, while the adjacent comment explicitly permits the actual claim to exceed that envelope. Post-dispatch weight can refund unused capacity but cannot retroactively reserve additional block capacity, so a fat coldkey can execute more work than the scheduler admitted. Enforce the work limit before mutation or split claims into bounded, resumable calls.

Comment on lines +2001 to +2006
// Declared weight is a cap sized for a 128-slot weight vector over 256 holdings
// (each slot costs a balance transfer + swap + escrow write; each holding two NAV
// sim-swap valuations); the actual weight is computed in `do_stake_into_basket`
// from the real slot and holding counts and refunded post-dispatch, mirroring
// `claim_root`.
#[pallet::weight((Pallet::<T>::stake_into_basket_weight(128, 256), DispatchClass::Normal, Pays::Yes))]

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] Basket deposits can exceed their pre-dispatch weight cap

The fixed reservation assumes at most 128 weight slots and 256 holdings, but this call does not enforce those bounds. It can also eagerly flush queued deposits before performing the deposit, adding prefix scans, valuations, and swaps outside this cap. Derive a provable worst-case bound from enforced storage limits, or make flushing/deposit processing bounded and resumable.

}
let claimed_rows = BasketClaimed::<T>::iter_prefix(old_hotkey).count() as u64;

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] Dispatch-weight calculation performs an unbounded storage scan

A #[pallet::weight] calculation runs before block admission, so iterating the entire BasketClaimed prefix here performs unmetered work merely to calculate the declared weight. The number of rows is not bounded at this point, allowing transaction validation/block construction to be forced through an arbitrarily large scan. Maintain a bounded row count in storage or enforce a protocol-level bound and use that constant-time value here.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

unarbos and others added 2 commits August 3, 2026 14:41
Unblock Request Docs Preview npm audit after GHSA-rgw5-rvv9-x895.

Co-authored-by: Cursor <cursoragent@cursor.com>
Previous --package-lock-only refresh dropped optional @emnapi entries
and broke Request Docs Preview / trust-boundary npm ci.

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

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

// --- 4b. Flush queued root-dividend basket deposits: one hotkey per block,
// round-robin. Runs right after coinbase (and before the price EMA update) so a
// flush lands where the epoch-inline deposits used to happen.
Self::flush_pending_basket_deposits_block();

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] Per-block hook performs variable basket work without metering

This mandatory block hook flushes one hotkey, but that flush scans every pending origin and may value every basket holding. Neither dimension is bounded or charged through the block hook's weight, so an attacker can make normal block initialization exceed its budget. Meter this work with a strict per-block limit and a resumable cursor.

Comment on lines +1921 to +1924
// Declared weight is a soft envelope sized for [`MAX_ROOT_CLAIM_WORK`]; actual work
// is measured and refunded post-dispatch (fat coldkeys may exceed the reservation).
#[pallet::weight(
<T as crate::pallet::Config>::WeightInfo::claim_root()
.saturating_mul(MAX_ROOT_CLAIM_HOTKEYS as u64)
.saturating_mul(MAX_SUBNET_CLAIMS as u64)
<T as crate::pallet::Config>::WeightInfo::claim_root(MAX_ROOT_CLAIM_WORK)

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] Root claims can exceed their pre-dispatch reservation

The call reserves work for MAX_ROOT_CLAIM_WORK, while the implementation explicitly permits larger actual work and only reports it after dispatch. Post-dispatch weight cannot increase the scheduler's original inclusion reservation, allowing a fat coldkey or basket to execute beyond the block budget. Enforce the bound before execution or split claims into bounded resumable batches.

Comment on lines +2001 to +2006
// Declared weight is a cap sized for a 128-slot weight vector over 256 holdings
// (each slot costs a balance transfer + swap + escrow write; each holding two NAV
// sim-swap valuations); the actual weight is computed in `do_stake_into_basket`
// from the real slot and holding counts and refunded post-dispatch, mirroring
// `claim_root`.
#[pallet::weight((Pallet::<T>::stake_into_basket_weight(128, 256), DispatchClass::Normal, Pays::Yes))]

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] Basket deposits can exceed their pre-dispatch weight cap

The fixed reservation assumes 128 slots and 256 holdings, but neither cap is enforced before do_stake_into_basket; that function also eagerly flushes an unbounded pending-deposit prefix whose work is absent from this reservation. Refund-only post-dispatch accounting cannot cover excess execution. Bound all dimensions before mutation or implement metered resumable processing.

}
let claimed_rows = BasketClaimed::<T>::iter_prefix(old_hotkey).count() as u64;

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] Dispatch-weight calculation performs an unbounded storage scan

This pre-dispatch weight function scans the entire BasketClaimed prefix merely to calculate the call's weight. Weight calculation itself runs before inclusion accounting and is not protected by the weight it returns, so an attacker-controlled number of rows can impose unbounded validation or block-construction work. Maintain a bounded row count or make the migration resumable under a fixed limit.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🔄 AI review updated — Skeptic: VULNERABLE

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.

5 participants