From 9fd36b8d7a2cf982761104073c7e748b0f6122d7 Mon Sep 17 00:00:00 2001 From: moktamd Date: Thu, 19 Mar 2026 21:52:24 +0000 Subject: [PATCH 1/3] fix: only transfer RootClaimable during all-subnet hotkey swap perform_hotkey_swap_on_one_subnet unconditionally called transfer_root_claimable_for_new_hotkey, which wiped the entire RootClaimable BTreeMap from the old hotkey. For single-subnet swaps the old hotkey retains root stake on other subnets, so losing its accumulated rates puts it into a permanently overclaimed state where RootClaimed >> RootClaimable, yielding near-zero root dividends. Move the transfer_root_claimable_for_new_hotkey call into perform_hotkey_swap_on_all_subnets, where it belongs. Fixes #2515 --- pallets/subtensor/src/swap/swap_hotkey.rs | 10 ++- pallets/subtensor/src/tests/claim_root.rs | 90 ++++++++++++++++++++++- 2 files changed, 92 insertions(+), 8 deletions(-) diff --git a/pallets/subtensor/src/swap/swap_hotkey.rs b/pallets/subtensor/src/swap/swap_hotkey.rs index ba50c78bca..b6f118b722 100644 --- a/pallets/subtensor/src/swap/swap_hotkey.rs +++ b/pallets/subtensor/src/swap/swap_hotkey.rs @@ -215,6 +215,11 @@ impl Pallet { )?; } + // 5.1. Transfer root claimable rates (all subnets at once). + // This must only happen for full swaps — single-subnet swaps leave root + // stake on the old hotkey, so the old hotkey must keep its RootClaimable. + Self::transfer_root_claimable_for_new_hotkey(old_hotkey, new_hotkey); + // 6. Swap LastTxBlock // LastTxBlock( hotkey ) --> u64 -- the last transaction block for the hotkey. Self::remove_last_tx_block(old_hotkey); @@ -543,10 +548,7 @@ impl Pallet { weight.saturating_accrue(T::DbWeight::get().reads(old_alpha_values.len() as u64)); weight.saturating_accrue(T::DbWeight::get().writes(old_alpha_values.len() as u64)); - // 9.1. Transfer root claimable - Self::transfer_root_claimable_for_new_hotkey(old_hotkey, new_hotkey); - - // 9.2. Insert the new alpha values. + // 9.1. Insert the new alpha values. for ((coldkey, netuid_alpha), alpha) in old_alpha_values { if netuid == netuid_alpha { Self::transfer_root_claimed_for_new_keys( diff --git a/pallets/subtensor/src/tests/claim_root.rs b/pallets/subtensor/src/tests/claim_root.rs index 0f628d6b86..d26b5d64e0 100644 --- a/pallets/subtensor/src/tests/claim_root.rs +++ b/pallets/subtensor/src/tests/claim_root.rs @@ -1295,11 +1295,93 @@ fn test_claim_root_with_swap_hotkey() { RootClaimed::::get((netuid, &new_hotkey, &coldkey,)) ); - assert!(!RootClaimable::::get(hotkey).contains_key(&netuid)); + // After a single-subnet swap, RootClaimable stays on the old hotkey + // because the old hotkey still holds root stake on other subnets. + // Only perform_hotkey_swap_on_all_subnets transfers RootClaimable. + let old_claimable_after = RootClaimable::::get(hotkey); + assert!( + old_claimable_after.contains_key(&netuid), + "single-subnet swap must not wipe RootClaimable from the old hotkey" + ); + assert!( + !RootClaimable::::get(new_hotkey).contains_key(&netuid), + "single-subnet swap must not move RootClaimable to the new hotkey" + ); + }); +} - let _new_claimable = *RootClaimable::::get(new_hotkey) - .get(&netuid) - .expect("claimable must exist at this point"); +#[test] +fn test_claim_root_with_swap_hotkey_all_subnets() { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(1001); + let hotkey = U256::from(1002); + let coldkey = U256::from(1003); + let netuid = add_dynamic_network(&hotkey, &owner_coldkey); + + SubtensorModule::set_tao_weight(u64::MAX); + SubnetMechanism::::insert(netuid, 1); + + let tao_reserve = TaoCurrency::from(50_000_000_000); + let alpha_in = AlphaCurrency::from(100_000_000_000); + SubnetTAO::::insert(netuid, tao_reserve); + SubnetAlphaIn::::insert(netuid, alpha_in); + + let root_stake = 2_000_000u64; + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + NetUid::ROOT, + root_stake.into(), + ); + + let initial_total_hotkey_alpha = 10_000_000u64; + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &owner_coldkey, + netuid, + initial_total_hotkey_alpha.into(), + ); + + let pending_root_alpha = 1_000_000u64; + SubtensorModule::distribute_emission( + netuid, + AlphaCurrency::ZERO, + AlphaCurrency::ZERO, + pending_root_alpha.into(), + AlphaCurrency::ZERO, + ); + + assert_ok!(SubtensorModule::set_root_claim_type( + RuntimeOrigin::signed(coldkey), + RootClaimTypeEnum::Keep + )); + assert_ok!(SubtensorModule::claim_root( + RuntimeOrigin::signed(coldkey), + BTreeSet::from([netuid]) + )); + + let new_hotkey = U256::from(10030); + assert!(RootClaimable::::get(hotkey).contains_key(&netuid)); + assert!(!RootClaimable::::get(new_hotkey).contains_key(&netuid)); + + // Swap on ALL subnets — RootClaimable should transfer + let mut weight = Weight::zero(); + assert_ok!(SubtensorModule::perform_hotkey_swap_on_all_subnets( + &hotkey, + &new_hotkey, + &coldkey, + &mut weight, + false, + )); + + assert!( + !RootClaimable::::get(hotkey).contains_key(&netuid), + "all-subnet swap must clear RootClaimable from the old hotkey" + ); + assert!( + RootClaimable::::get(new_hotkey).contains_key(&netuid), + "all-subnet swap must move RootClaimable to the new hotkey" + ); }); } From 67bceff6e002775583d02eb7707a7d7850010b37 Mon Sep 17 00:00:00 2001 From: moktamd Date: Fri, 20 Mar 2026 12:49:55 +0000 Subject: [PATCH 2/3] fix test: use TaoBalance/AlphaBalance instead of nonexistent types --- pallets/subtensor/src/tests/claim_root.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pallets/subtensor/src/tests/claim_root.rs b/pallets/subtensor/src/tests/claim_root.rs index d26b5d64e0..d56ea9395d 100644 --- a/pallets/subtensor/src/tests/claim_root.rs +++ b/pallets/subtensor/src/tests/claim_root.rs @@ -1321,8 +1321,8 @@ fn test_claim_root_with_swap_hotkey_all_subnets() { SubtensorModule::set_tao_weight(u64::MAX); SubnetMechanism::::insert(netuid, 1); - let tao_reserve = TaoCurrency::from(50_000_000_000); - let alpha_in = AlphaCurrency::from(100_000_000_000); + let tao_reserve = TaoBalance::from(50_000_000_000u64); + let alpha_in = AlphaBalance::from(100_000_000_000u64); SubnetTAO::::insert(netuid, tao_reserve); SubnetAlphaIn::::insert(netuid, alpha_in); @@ -1345,10 +1345,10 @@ fn test_claim_root_with_swap_hotkey_all_subnets() { let pending_root_alpha = 1_000_000u64; SubtensorModule::distribute_emission( netuid, - AlphaCurrency::ZERO, - AlphaCurrency::ZERO, + AlphaBalance::ZERO, + AlphaBalance::ZERO, pending_root_alpha.into(), - AlphaCurrency::ZERO, + AlphaBalance::ZERO, ); assert_ok!(SubtensorModule::set_root_claim_type( From 3b9005cb97f54b6f2aa58700801fd982466dbce7 Mon Sep 17 00:00:00 2001 From: moktamd Date: Fri, 20 Mar 2026 13:41:25 +0000 Subject: [PATCH 3/3] fix test: single-subnet swap must not transfer RootClaimable The existing test asserted the buggy behavior where a single-subnet swap would wipe RootClaimable from the old hotkey. Updated assertions to match the corrected behavior: only all-subnet swaps transfer RootClaimable. --- .../src/tests/swap_hotkey_with_subnet.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs index 6f43d46fde..f1ef096334 100644 --- a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs +++ b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs @@ -2406,11 +2406,16 @@ fn test_revert_claim_root_with_swap_hotkey() { hk1_root_claimed, "hk2 must have hk1's RootClaimed after swap" ); - assert!(!RootClaimable::::get(hk1).contains_key(&netuid)); + // Single-subnet swap must NOT transfer RootClaimable because the old + // hotkey still holds root stake on other subnets. assert_eq!( - *RootClaimable::::get(hk2).get(&netuid).unwrap(), + *RootClaimable::::get(hk1).get(&netuid).unwrap(), hk1_claimable, - "hk2 must have hk1's RootClaimable after swap" + "hk1 must keep RootClaimable after single-subnet swap" + ); + assert!( + !RootClaimable::::get(hk2).contains_key(&netuid), + "hk2 must not receive RootClaimable from single-subnet swap" ); // Revert: hk2 -> hk1 @@ -2434,11 +2439,15 @@ fn test_revert_claim_root_with_swap_hotkey() { "hk1 RootClaimed must be restored after revert" ); - assert!(!RootClaimable::::get(hk2).contains_key(&netuid)); + // RootClaimable stays on hk1 throughout — single-subnet swaps don't move it. assert_eq!( *RootClaimable::::get(hk1).get(&netuid).unwrap(), hk1_claimable, - "hk1 RootClaimable must be restored after revert" + "hk1 RootClaimable must remain after revert" + ); + assert!( + !RootClaimable::::get(hk2).contains_key(&netuid), + "hk2 must not have RootClaimable after revert" ); }); }