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..d56ea9395d 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 = 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); + + 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, + AlphaBalance::ZERO, + AlphaBalance::ZERO, + pending_root_alpha.into(), + AlphaBalance::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" + ); }); } 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" ); }); }