release-root-reborn - #2968
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🛡️ 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
Prior-comment reconciliation
ConclusionClaims, 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)
🔍 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 FindingsNo findings. ConclusionThe latest test adjustment reflects valid runtime behavior without weakening the ownership invariant. No substantive domain issues remain. |
|
🔄 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>
|
🔄 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>
…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>
|
🔄 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>
Co-authored-by: Cursor <cursoragent@cursor.com>
|
🔄 AI review updated — Skeptic: VULNERABLE |
Co-authored-by: Cursor <cursoragent@cursor.com>
|
🔄 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>
|
🔄 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>
| } | ||
| let claimed_rows = BasketClaimed::<T>::iter_prefix(old_hotkey).count() as u64; |
There was a problem hiding this comment.
[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.
|
🔄 AI review updated — Skeptic: VULNERABLE |
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>
| // 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))] |
There was a problem hiding this comment.
[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.
|
🔄 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>
| // --- 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(); |
There was a problem hiding this comment.
[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.
| // 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) |
There was a problem hiding this comment.
[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.
| #[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))] |
There was a problem hiding this comment.
[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; |
There was a problem hiding this comment.
[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.
|
🔄 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>
| // --- 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(); |
There was a problem hiding this comment.
[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.
| // 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) |
There was a problem hiding this comment.
[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.
| // 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))] |
There was a problem hiding this comment.
[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; |
There was a problem hiding this comment.
[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.
|
🔄 AI review updated — Skeptic: VULNERABLE |
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>
| // --- 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(); |
There was a problem hiding this comment.
[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.
| // 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) |
There was a problem hiding this comment.
[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.
| // 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))] |
There was a problem hiding this comment.
[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; |
There was a problem hiding this comment.
[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.
|
🔄 AI review updated — Skeptic: VULNERABLE |
Summary
Runtime release 438 (
spec_version441). Brings the Root Reborn feature branch (#2759,root-reborn) up to date withmain, 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)
set_root_weights, stored inWeights[ROOT]).claim_rootredeems pro-rata across every holding into root TAO.set_root_claim_type(call 122) andsudo_set_num_root_claims(123) plus the auto-claim scheduler. LegacyRootClaimable/RootClaimedare drained by the seed migration.BetaBasketRuntimeApi(basket NAV, holdings, owed TAO, validator weights views).Gated launch
set_root_weightsships disabled network-wide (RootWeightSettingEnabled = false; new root callsudo_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)Σ owed == BasketShares), and recovery paths.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.Epoch performance (fixes the 24s epoch blocks)
(hotkey, origin netuid) → alphacredits intoPendingBasketDeposits; 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.RootClaimableThresholdstay queued and keep merging until worth a deposit; dust consolidation now also runs on uncurated funds, capping holding-count growth.sim_swapon 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
PendingBasketDepositscredits, 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) pinsthetato the Nth-largest demand share instead of the q-mass quantile;sudo_set_emission_bar_rankto tune, N=0 falls back to q-mass.migrate_reset_emission_gate_barclears the stale quantile bar so rank-64 applies at upgrade.SDK / CLI / docs
BetaBasketSeedInProgress,RootWeightSettingDisabled, …).Key merge decisions (root-reborn was based on spec-418 main)
set_root_weightsmoved from call index 139 (taken on main) to 146.BasketRate/BasketSharesand residual legacy claim state.set_root_claim_typeported into main's refactoredproxy_filters/call groups;set_root_weightsand the new admin gate added to the appropriate groups.Also included
Test plan
02.0xclaim-root/basket suites