Skip to content

root_claim_on_subnet swap branch silently marks claim as paid when transfer_tao_from_subnet fails #2662

@Maksandre

Description

@Maksandre

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

  1. Set up coldkey C with root stake on hotkey H, and H earning dividends on subnets X and Y so RootClaimable[H] contains both.
  2. 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).
  3. C signs the unprivileged extrinsic: claim_root({X}) - a single-subnet claim.
  4. 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.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions