Describe the bug
The swap branch of root_claim_on_subnet (pallets/subtensor/src/staking/claim_root.rs:127-243) gates only the root-stake credit on transfer_tao_from_subnet.is_ok(), but leaves the RootClaimed watermark bumps unconditional. A single failed transfer silently marks the entire round's dividends as "paid" - across every subnet the hotkey earns on - without crediting any TAO or root stake. The extrinsic returns Ok, leaving no on-chain trail of the loss.
To Reproduce
- Set up coldkey
C with root stake on hotkey H, and H earning dividends on subnets X and Y so RootClaimable[H] contains both.
- Drain subnet X's chain account below
SubnetTAO[X]. Reachable via:
- A fresh subnet registration with
actual_tao_lock_amount < pool_initial_tao: SubnetTAO[netuid] is structurally above the chain balance (pallets/subtensor/src/subnets/subnet.rs:223-239).
- Freezes/holds on the subnet account reducing
reducible_balance(Expendable, Polite).
- Existing drift from any sibling silent-failure path (e.g. the
inject_and_maybe_swap swap-failure path).
C signs the unprivileged extrinsic: claim_root({X}) - a single-subnet claim.
- Inspect
RootClaimed::<T>::get((X, H, C)), RootClaimed::<T>::get((Y, H, C)), and root stake.
Observed: both RootClaimed entries bumped despite no TAO transferred and no root stake credited; get_root_owed_for_hotkey_coldkey(X) collapses to 0; get_root_owed_for_hotkey_coldkey(Y) also reduced (cross-subnet contamination). Extrinsic returns Ok with no event signalling the failure.
Expected behavior
On transfer_tao_from_subnet failure, RootClaimed must not be bumped on any subnet, and the prior SubnetRootSellTao::mutate + record_protocol_outflow writes must be rolled back. The extrinsic must propagate the error so the caller (and the auto-claim hook) learn that the dividend round is not consumed.
Screenshots
No response
Environment
opentensor/subtensor testnet @ e6a5f56cefeb96b9c63ea3ce6553a8e1066aaeb9
Additional context
Affected code (pallets/subtensor/src/staking/claim_root.rs:127-243):
// Lines 184-187 - fire UNCONDITIONALLY before the transfer attempt
SubnetRootSellTao::<T>::mutate(netuid, |t| *t = t.saturating_add(owed_tao.amount_paid_out));
Self::record_protocol_outflow(netuid, owed_tao.amount_paid_out);
// Lines 191-220 - root-stake credit + ROOT-side bookkeeping GATED on transfer success
if let Some(root_subnet_account_id) = Self::get_subnet_account_id(NetUid::ROOT)
&& Self::transfer_tao_from_subnet(netuid, &root_subnet_account_id,
owed_tao.amount_paid_out.into()).is_ok()
{
// increase_stake_for_hotkey_and_coldkey_on_subnet(.., ROOT, ..)
// SubnetTAO[ROOT] += ; SubnetAlphaOut[ROOT] += ; TotalStake += ;
}
// Lines 222-226 - fires UNCONDITIONALLY; walks every subnet in RootClaimable[hotkey]
Self::add_stake_adjust_root_claimed_for_hotkey_and_coldkey(
hotkey, coldkey, owed_tao.amount_paid_out.into(),
);
// Lines 240-242 - fires UNCONDITIONALLY
RootClaimed::<T>::mutate((netuid, hotkey, coldkey), |c| *c = c.saturating_add(owed_u64));
Cross-subnet contamination: add_stake_adjust_root_claimed_for_hotkey_and_coldkey walks RootClaimable[hotkey] and bumps RootClaimed[(sub, hotkey, coldkey)] += rate * owed_tao for every sub. A single failed claim on X silently consumes the user's outstanding dividends across every subnet the hotkey is registered on.
Impact
RootClaimed is monotonic (except via remove_stake_adjust_… from actual stake removals); get_root_owed_for_hotkey_coldkey clamps negative to 0. One failed claim wipes the round's dividends across every (hotkey, coldkey, subnet) tuple the hotkey earns on.
- The auto-claim hook
run_auto_claim_root_divs (in on_initialize) makes the loss realize passively, without any user action, for the rotating subset of coldkeys it visits each block.
SubnetRootSellTao[netuid] and SubnetProtocolFlow[netuid] carry phantom outflows that bias the get_shares_flow EMA used in cross-subnet emission distribution.
- Trigger is an unprivileged signed
claim_root extrinsic returning Ok - no event signals the loss, no error propagates.
Describe the bug
The swap branch of
root_claim_on_subnet(pallets/subtensor/src/staking/claim_root.rs:127-243) gates only the root-stake credit ontransfer_tao_from_subnet.is_ok(), but leaves theRootClaimedwatermark bumps unconditional. A single failed transfer silently marks the entire round's dividends as "paid" - across every subnet the hotkey earns on - without crediting any TAO or root stake. The extrinsic returnsOk, leaving no on-chain trail of the loss.To Reproduce
Cwith root stake on hotkeyH, andHearning dividends on subnets X and Y soRootClaimable[H]contains both.SubnetTAO[X]. Reachable via:actual_tao_lock_amount < pool_initial_tao:SubnetTAO[netuid]is structurally above the chain balance (pallets/subtensor/src/subnets/subnet.rs:223-239).reducible_balance(Expendable, Polite).inject_and_maybe_swapswap-failure path).Csigns the unprivileged extrinsic:claim_root({X})- a single-subnet claim.RootClaimed::<T>::get((X, H, C)),RootClaimed::<T>::get((Y, H, C)), and root stake.Observed: both
RootClaimedentries bumped despite no TAO transferred and no root stake credited;get_root_owed_for_hotkey_coldkey(X)collapses to 0;get_root_owed_for_hotkey_coldkey(Y)also reduced (cross-subnet contamination). Extrinsic returnsOkwith no event signalling the failure.Expected behavior
On
transfer_tao_from_subnetfailure,RootClaimedmust not be bumped on any subnet, and the priorSubnetRootSellTao::mutate+record_protocol_outflowwrites must be rolled back. The extrinsic must propagate the error so the caller (and the auto-claim hook) learn that the dividend round is not consumed.Screenshots
No response
Environment
opentensor/subtensor
testnet@e6a5f56cefeb96b9c63ea3ce6553a8e1066aaeb9Additional context
Affected code (
pallets/subtensor/src/staking/claim_root.rs:127-243):Cross-subnet contamination:
add_stake_adjust_root_claimed_for_hotkey_and_coldkeywalksRootClaimable[hotkey]and bumpsRootClaimed[(sub, hotkey, coldkey)] += rate * owed_taofor everysub. A single failed claim on X silently consumes the user's outstanding dividends across every subnet the hotkey is registered on.Impact
RootClaimedis monotonic (except viaremove_stake_adjust_…from actual stake removals);get_root_owed_for_hotkey_coldkeyclamps negative to 0. One failed claim wipes the round's dividends across every (hotkey, coldkey, subnet) tuple the hotkey earns on.run_auto_claim_root_divs(inon_initialize) makes the loss realize passively, without any user action, for the rotating subset of coldkeys it visits each block.SubnetRootSellTao[netuid]andSubnetProtocolFlow[netuid]carry phantom outflows that bias theget_shares_flowEMA used in cross-subnet emission distribution.claim_rootextrinsic returningOk- no event signals the loss, no error propagates.