Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions pallets/subtensor/src/swap/swap_hotkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ impl<T: Config> Pallet<T> {
)?;
}

// 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the code before, only call transfer_root_claimable_for_new_hotkey if not keep_stake. should we also add it here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original code had this inside perform_hotkey_swap (single-subnet path), where it ran on every per-subnet swap — including single-subnet swaps. That was the bug: a single-subnet swap would wipe RootClaimable even though the old hotkey still holds root stake on other subnets.

Moving it up to perform_hotkey_swap_on_all_subnets means it only runs during a full swap, which is the only case where all root stake actually moves to the new hotkey. The keep_stake guard isn't needed here because this function is the all-subnets path — by definition it transfers everything.


// 6. Swap LastTxBlock
// LastTxBlock( hotkey ) --> u64 -- the last transaction block for the hotkey.
Self::remove_last_tx_block(old_hotkey);
Expand Down Expand Up @@ -543,10 +548,7 @@ impl<T: Config> Pallet<T> {
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(
Expand Down
90 changes: 86 additions & 4 deletions pallets/subtensor/src/tests/claim_root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1295,11 +1295,93 @@ fn test_claim_root_with_swap_hotkey() {
RootClaimed::<Test>::get((netuid, &new_hotkey, &coldkey,))
);

assert!(!RootClaimable::<Test>::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::<Test>::get(hotkey);
assert!(
old_claimable_after.contains_key(&netuid),
"single-subnet swap must not wipe RootClaimable from the old hotkey"
);
assert!(
!RootClaimable::<Test>::get(new_hotkey).contains_key(&netuid),
"single-subnet swap must not move RootClaimable to the new hotkey"
);
});
}

let _new_claimable = *RootClaimable::<Test>::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::<Test>::insert(netuid, 1);

let tao_reserve = TaoBalance::from(50_000_000_000u64);
let alpha_in = AlphaBalance::from(100_000_000_000u64);
SubnetTAO::<Test>::insert(netuid, tao_reserve);
SubnetAlphaIn::<Test>::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::<Test>::get(hotkey).contains_key(&netuid));
assert!(!RootClaimable::<Test>::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::<Test>::get(hotkey).contains_key(&netuid),
"all-subnet swap must clear RootClaimable from the old hotkey"
);
assert!(
RootClaimable::<Test>::get(new_hotkey).contains_key(&netuid),
"all-subnet swap must move RootClaimable to the new hotkey"
);
});
}

Expand Down
19 changes: 14 additions & 5 deletions pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Test>::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::<Test>::get(hk2).get(&netuid).unwrap(),
*RootClaimable::<Test>::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::<Test>::get(hk2).contains_key(&netuid),
"hk2 must not receive RootClaimable from single-subnet swap"
);

// Revert: hk2 -> hk1
Expand All @@ -2434,11 +2439,15 @@ fn test_revert_claim_root_with_swap_hotkey() {
"hk1 RootClaimed must be restored after revert"
);

assert!(!RootClaimable::<Test>::get(hk2).contains_key(&netuid));
// RootClaimable stays on hk1 throughout — single-subnet swaps don't move it.
assert_eq!(
*RootClaimable::<Test>::get(hk1).get(&netuid).unwrap(),
hk1_claimable,
"hk1 RootClaimable must be restored after revert"
"hk1 RootClaimable must remain after revert"
);
assert!(
!RootClaimable::<Test>::get(hk2).contains_key(&netuid),
"hk2 must not have RootClaimable after revert"
);
});
}