Bug: swap_hotkey on a single subnet wipes all root dividend accumulations
Summary
perform_hotkey_swap_on_one_subnet (swap_hotkey.rs:510) unconditionally calls
transfer_root_claimable_for_new_hotkey, which transfers the entire
RootClaimable BTreeMap (accumulated dividend rates for all subnets) from the
old hotkey to the new hotkey — even when only swapping on a single non-root subnet.
Meanwhile, RootClaimed is only transferred for the one subnet being swapped
(line 515). The old hotkey retains its root stake and RootClaimed entries for all
other subnets, putting it into a permanently overclaimed state where
claimed >> claimable, yielding effectively zero root dividends.
Impact
Any validator who performs a single-subnet hotkey swap (not an all-subnet swap)
will have their root dividends frozen to near-zero for weeks or months until the
re-accumulating RootClaimable rates catch up to the orphaned RootClaimed
watermarks.
On-Chain Proof
Hotkey: 5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN
Coldkey: 5EAdYiz1M8BznMNE9WvTajEq64UpGKBFGZDJBNvMDj5fWpeG
Block: 7,670,707 (finney, ~March 4 2026)
Extrinsic: SubtensorModule::swap_hotkey (extrinsic #8 in block)
| Metric |
Before swap (block 7,670,706) |
After swap (block 7,670,707) |
15 days later |
RootClaimable rate_sum |
2.139 (127 subnets) |
0 (null — wiped) |
0.296 (re-accumulating) |
RootClaimed total |
~35,692 α |
~35,692 α (unchanged) |
~35,692 α |
| Root stake |
~17,065 TAO |
~17,065 TAO (unchanged) |
~17,065 TAO |
Claimable (rate × stake) |
~36,500 α |
0 |
~5,051 α |
Owed (claimable - claimed) |
~800 α |
0 |
~43 α |
| Overclaimed subnets |
0 / 126 |
126 / 126 |
122 / 126 |
The swap transferred the old hotkey to a new hotkey on a single subnet (not root).
Root stake remained on the old hotkey. But all RootClaimable rates were wiped.
Root Cause
File: pallets/subtensor/src/swap/swap_hotkey.rs
In perform_hotkey_swap_on_one_subnet (line 321), which is called for both
single-subnet swaps and as a loop body for all-subnet swaps:
// Line 510: Transfers ALL subnets' accumulated rates — entire BTreeMap
Self::transfer_root_claimable_for_new_hotkey(old_hotkey, new_hotkey);
// Lines 513-517: Transfers RootClaimed for ONLY the one subnet being swapped
for ((coldkey, netuid_alpha), alpha) in old_alpha_values {
if netuid == netuid_alpha {
Self::transfer_root_claimed_for_new_keys(
netuid, old_hotkey, new_hotkey, &coldkey, &coldkey,
);
// ...
}
}
The mismatch
| Storage |
What gets transferred |
Scope |
RootClaimable (line 510) |
Entire BTreeMap (all subnets' rates) |
All subnets |
RootClaimed (line 515) |
One entry per coldkey |
Only the swapped subnet |
transfer_root_claimable_for_new_hotkey (claim_root.rs:373-389)
pub fn transfer_root_claimable_for_new_hotkey(
old_hotkey: &T::AccountId,
new_hotkey: &T::AccountId,
) {
let src_root_claimable = RootClaimable::<T>::get(old_hotkey);
let mut dst_root_claimable = RootClaimable::<T>::get(new_hotkey);
RootClaimable::<T>::remove(old_hotkey); // ← Removes ENTIRE map
for (netuid, claimable_rate) in src_root_claimable.into_iter() {
dst_root_claimable
.entry(netuid)
.and_modify(|total| *total = total.saturating_add(claimable_rate))
.or_insert(claimable_rate);
}
RootClaimable::<T>::insert(new_hotkey, dst_root_claimable);
}
transfer_root_claimed_for_new_keys (claim_root.rs:359-372)
pub fn transfer_root_claimed_for_new_keys(
netuid: NetUid, // ← Only one subnet
old_hotkey: &T::AccountId,
new_hotkey: &T::AccountId,
old_coldkey: &T::AccountId,
new_coldkey: &T::AccountId,
) {
let old_root_claimed = RootClaimed::<T>::get((netuid, old_hotkey, old_coldkey));
RootClaimed::<T>::remove((netuid, old_hotkey, old_coldkey));
RootClaimed::<T>::mutate((netuid, new_hotkey, new_coldkey), |new_root_claimed| {
*new_root_claimed = old_root_claimed.saturating_add(*new_root_claimed);
});
}
Why it works for all-subnet swaps (but not single-subnet)
In perform_hotkey_swap_on_all_subnets (line 158), the per-subnet function is
called in a loop:
for netuid in Self::get_all_subnet_netuids() {
Self::perform_hotkey_swap_on_one_subnet(old_hotkey, new_hotkey, weight, netuid)?;
}
- First iteration:
transfer_root_claimable_for_new_hotkey transfers all rates.
transfer_root_claimed_for_new_keys transfers RootClaimed for subnet 0.
- Subsequent iterations:
transfer_root_claimable_for_new_hotkey is a no-op
(old hotkey already empty). transfer_root_claimed_for_new_keys transfers
RootClaimed for each subsequent subnet.
- End result: Both
RootClaimable and RootClaimed fully transferred. ✅
For single-subnet swap via swap_hotkey_on_subnet (line 239):
Self::perform_hotkey_swap_on_one_subnet(old_hotkey, new_hotkey, &mut weight, netuid)?;
transfer_root_claimable_for_new_hotkey transfers all rates → old hotkey wiped
transfer_root_claimed_for_new_keys transfers RootClaimed for one subnet only
- Old hotkey keeps root stake +
RootClaimed for ~125 other subnets
- End result:
claimed >> claimable → zero dividends ❌
Proposed Fix
Code change
Move transfer_root_claimable_for_new_hotkey out of perform_hotkey_swap_on_one_subnet
and into perform_hotkey_swap_on_all_subnets only. For single-subnet swaps, the old
hotkey retains its root stake, so it must retain its accumulated RootClaimable rates.
--- a/pallets/subtensor/src/swap/swap_hotkey.rs
+++ b/pallets/subtensor/src/swap/swap_hotkey.rs
@@ -189,6 +189,9 @@ impl<T: Config> Pallet<T> {
// 5. execute the hotkey swap on all subnets
for netuid in Self::get_all_subnet_netuids() {
Self::perform_hotkey_swap_on_one_subnet(old_hotkey, new_hotkey, weight, netuid)?;
}
+
+ // 5.1. Transfer root claimable (all subnets at once, only for full swap)
+ Self::transfer_root_claimable_for_new_hotkey(old_hotkey, new_hotkey);
@@ -507,9 +510,6 @@ impl<T: Config> Pallet<T> {
- // 9.1. Transfer root claimable
-
- Self::transfer_root_claimable_for_new_hotkey(old_hotkey, new_hotkey);
-
// 9.2. Insert the new alpha values.
Migration
A storage migration is needed to repair currently-affected hotkeys:
For each hotkey where RootClaimed(netuid, hotkey, coldkey) > RootClaimable(hotkey)[netuid] × root_stake(hotkey, coldkey):
- Reset
RootClaimed down to the current claimable value, so owed returns to zero
(rather than staying stuck in overclaimed state).
Alternatively, simply clear RootClaimed for affected hotkeys — they will lose any
small pending owed amounts but will immediately resume earning dividends.
Investigation Timeline
| Step |
Finding |
Queried RootClaimable + RootClaimed for hotkey |
122/126 subnets overclaimed; total claimed 7× claimable |
| Historical root stake check |
Root stake was never > 25k TAO — cannot explain 35k claimed |
Historical RootClaimable rate check |
rate_sum was 1.86 at 30d ago, 0.296 now — rates were reset |
| Binary search for reset block |
Narrowed to block 7,670,707 |
| Block-by-block trace |
RootClaimable present at 7,670,706, null at 7,670,707 |
| Other hotkeys check |
NOT affected — this was hotkey-specific, not global |
| Extrinsic decode |
Block 7,670,707 extrinsic #8 = SubtensorModule::swap_hotkey |
| Code analysis |
transfer_root_claimable_for_new_hotkey transfers all subnets; transfer_root_claimed_for_new_keys transfers only one |
Bug:
swap_hotkeyon a single subnet wipes all root dividend accumulationsSummary
perform_hotkey_swap_on_one_subnet(swap_hotkey.rs:510) unconditionally callstransfer_root_claimable_for_new_hotkey, which transfers the entireRootClaimableBTreeMap (accumulated dividend rates for all subnets) from theold hotkey to the new hotkey — even when only swapping on a single non-root subnet.
Meanwhile,
RootClaimedis only transferred for the one subnet being swapped(line 515). The old hotkey retains its root stake and
RootClaimedentries for allother subnets, putting it into a permanently overclaimed state where
claimed >> claimable, yielding effectively zero root dividends.Impact
Any validator who performs a single-subnet hotkey swap (not an all-subnet swap)
will have their root dividends frozen to near-zero for weeks or months until the
re-accumulating
RootClaimablerates catch up to the orphanedRootClaimedwatermarks.
On-Chain Proof
Hotkey:
5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeNColdkey:
5EAdYiz1M8BznMNE9WvTajEq64UpGKBFGZDJBNvMDj5fWpeGBlock: 7,670,707 (finney, ~March 4 2026)
Extrinsic:
SubtensorModule::swap_hotkey(extrinsic #8 in block)RootClaimablerate_sumRootClaimedtotalrate × stake)claimable - claimed)The swap transferred the old hotkey to a new hotkey on a single subnet (not root).
Root stake remained on the old hotkey. But all
RootClaimablerates were wiped.Root Cause
File:
pallets/subtensor/src/swap/swap_hotkey.rsIn
perform_hotkey_swap_on_one_subnet(line 321), which is called for bothsingle-subnet swaps and as a loop body for all-subnet swaps:
The mismatch
RootClaimable(line 510)RootClaimed(line 515)transfer_root_claimable_for_new_hotkey(claim_root.rs:373-389)transfer_root_claimed_for_new_keys(claim_root.rs:359-372)Why it works for all-subnet swaps (but not single-subnet)
In
perform_hotkey_swap_on_all_subnets(line 158), the per-subnet function iscalled in a loop:
transfer_root_claimable_for_new_hotkeytransfers all rates.transfer_root_claimed_for_new_keystransfersRootClaimedfor subnet 0.transfer_root_claimable_for_new_hotkeyis a no-op(old hotkey already empty).
transfer_root_claimed_for_new_keystransfersRootClaimedfor each subsequent subnet.RootClaimableandRootClaimedfully transferred. ✅For single-subnet swap via
swap_hotkey_on_subnet(line 239):transfer_root_claimable_for_new_hotkeytransfers all rates → old hotkey wipedtransfer_root_claimed_for_new_keystransfersRootClaimedfor one subnet onlyRootClaimedfor ~125 other subnetsclaimed >> claimable→ zero dividends ❌Proposed Fix
Code change
Move
transfer_root_claimable_for_new_hotkeyout ofperform_hotkey_swap_on_one_subnetand into
perform_hotkey_swap_on_all_subnetsonly. For single-subnet swaps, the oldhotkey retains its root stake, so it must retain its accumulated
RootClaimablerates.Migration
A storage migration is needed to repair currently-affected hotkeys:
For each hotkey where
RootClaimed(netuid, hotkey, coldkey) > RootClaimable(hotkey)[netuid] × root_stake(hotkey, coldkey):RootClaimeddown to the currentclaimablevalue, soowedreturns to zero(rather than staying stuck in overclaimed state).
Alternatively, simply clear
RootClaimedfor affected hotkeys — they will lose anysmall pending owed amounts but will immediately resume earning dividends.
Investigation Timeline
RootClaimable+RootClaimedfor hotkeyRootClaimablerate checkRootClaimablepresent at 7,670,706, null at 7,670,707SubtensorModule::swap_hotkeytransfer_root_claimable_for_new_hotkeytransfers all subnets;transfer_root_claimed_for_new_keystransfers only one