From c00f12e60f3bab96b8d3b567baa6f5b66daee996 Mon Sep 17 00:00:00 2001 From: Pawel Polewicz Date: Tue, 10 Feb 2026 22:53:08 +0000 Subject: [PATCH 01/17] Add EmissionSuppression storage + share zeroing - Add EmissionSuppression storage map (NetUid -> U64F64) for stake-weighted suppression fraction per subnet - Add normalize_shares() helper to re-normalize shares to sum to 1.0 - Add apply_emission_suppression() to zero shares of suppressed subnets (suppression > 0.5) and re-normalize remaining shares - Call apply_emission_suppression in get_subnet_block_emissions after get_shares - Clean up EmissionSuppression in remove_network Co-Authored-By: Claude Opus 4.6 --- pallets/subtensor/src/coinbase/root.rs | 3 ++ .../src/coinbase/subnet_emissions.rs | 33 ++++++++++++++++++- pallets/subtensor/src/lib.rs | 7 ++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index 83567b6f57..7653001394 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -302,6 +302,9 @@ impl Pallet { SubnetEmaTaoFlow::::remove(netuid); SubnetTaoProvided::::remove(netuid); + // --- 12b. Emission suppression. + EmissionSuppression::::remove(netuid); + // --- 13. Token / mechanism / registration toggles. TokenSymbol::::remove(netuid); SubnetMechanism::::remove(netuid); diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs index 477a678864..8fab26a5d4 100644 --- a/pallets/subtensor/src/coinbase/subnet_emissions.rs +++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs @@ -27,7 +27,8 @@ impl Pallet { block_emission: U96F32, ) -> BTreeMap { // Get subnet TAO emissions. - let shares = Self::get_shares(subnets_to_emit_to); + let mut shares = Self::get_shares(subnets_to_emit_to); + Self::apply_emission_suppression(&mut shares); log::debug!("Subnet emission shares = {shares:?}"); shares @@ -246,4 +247,34 @@ impl Pallet { }) .collect::>() } + + /// Normalize shares so they sum to 1.0. + pub(crate) fn normalize_shares(shares: &mut BTreeMap) { + let sum: U64F64 = shares.values().copied().fold( + U64F64::saturating_from_num(0), + |acc, v| acc.saturating_add(v), + ); + if sum > U64F64::saturating_from_num(0) { + for s in shares.values_mut() { + *s = s.safe_div(sum); + } + } + } + + /// Zero the emission share of any subnet whose suppression fraction exceeds 50%, + /// then re-normalize the remaining shares. + pub(crate) fn apply_emission_suppression(shares: &mut BTreeMap) { + let half = U64F64::saturating_from_num(0.5); + let zero = U64F64::saturating_from_num(0); + let mut any_zeroed = false; + for (netuid, share) in shares.iter_mut() { + if EmissionSuppression::::get(netuid) > half { + *share = zero; + any_zeroed = true; + } + } + if any_zeroed { + Self::normalize_shares(shares); + } + } } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 69645c0419..dfe5302bb5 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -2419,6 +2419,13 @@ pub mod pallet { pub type PendingChildKeyCooldown = StorageValue<_, u64, ValueQuery, DefaultPendingChildKeyCooldown>; + /// Stake-weighted suppression fraction for each subnet. + /// Updated every epoch from root validator votes. + /// When this value exceeds 0.5, the subnet's emission share is zeroed. + #[pallet::storage] + pub type EmissionSuppression = + StorageMap<_, Identity, NetUid, U64F64, ValueQuery>; + #[pallet::genesis_config] pub struct GenesisConfig { /// Stakes record in genesis. From 3a6c098fbc8449dfbf5fe641123d3ca1dfe7120a Mon Sep 17 00:00:00 2001 From: Pawel Polewicz Date: Tue, 10 Feb 2026 23:11:03 +0000 Subject: [PATCH 02/17] Add EmissionSuppressionOverride (root-only) - Add EmissionSuppressionOverride storage map (NetUid -> Option) for root override of suppression per subnet - Modify apply_emission_suppression to check override first: Some(true) forces suppression, Some(false) forces unsuppression, None falls back to vote-based EmissionSuppression value - Add sudo_set_emission_suppression_override extrinsic (call_index 133) - Clean up EmissionSuppressionOverride in remove_network Co-Authored-By: Claude Opus 4.6 --- pallets/subtensor/src/coinbase/root.rs | 1 + .../src/coinbase/subnet_emissions.rs | 11 +++++--- pallets/subtensor/src/lib.rs | 7 +++++ pallets/subtensor/src/macros/dispatches.rs | 27 +++++++++++++++++++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index 7653001394..6ba3e77f01 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -304,6 +304,7 @@ impl Pallet { // --- 12b. Emission suppression. EmissionSuppression::::remove(netuid); + EmissionSuppressionOverride::::remove(netuid); // --- 13. Token / mechanism / registration toggles. TokenSymbol::::remove(netuid); diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs index 8fab26a5d4..1147b7dbc5 100644 --- a/pallets/subtensor/src/coinbase/subnet_emissions.rs +++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs @@ -261,14 +261,19 @@ impl Pallet { } } - /// Zero the emission share of any subnet whose suppression fraction exceeds 50%, - /// then re-normalize the remaining shares. + /// Zero the emission share of any subnet whose suppression fraction exceeds 50% + /// (or is force-suppressed via override), then re-normalize the remaining shares. pub(crate) fn apply_emission_suppression(shares: &mut BTreeMap) { let half = U64F64::saturating_from_num(0.5); let zero = U64F64::saturating_from_num(0); let mut any_zeroed = false; for (netuid, share) in shares.iter_mut() { - if EmissionSuppression::::get(netuid) > half { + let suppressed = match EmissionSuppressionOverride::::get(netuid) { + Some(true) => true, + Some(false) => false, + None => EmissionSuppression::::get(netuid) > half, + }; + if suppressed { *share = zero; any_zeroed = true; } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index dfe5302bb5..b686ea3b62 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -2426,6 +2426,13 @@ pub mod pallet { pub type EmissionSuppression = StorageMap<_, Identity, NetUid, U64F64, ValueQuery>; + /// Root override for emission suppression per subnet. + /// Some(true) = force suppressed, Some(false) = force unsuppressed, + /// None = use vote-based EmissionSuppression value. + #[pallet::storage] + pub type EmissionSuppressionOverride = + StorageMap<_, Identity, NetUid, bool, OptionQuery>; + #[pallet::genesis_config] pub struct GenesisConfig { /// Stakes record in genesis. diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 7cfb224722..11603c7b7f 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -2580,6 +2580,33 @@ mod dispatches { Self::do_set_voting_power_ema_alpha(netuid, alpha) } + /// --- Set or clear the root override for emission suppression on a subnet. + /// Some(true) forces suppression, Some(false) forces unsuppression, + /// None removes the override and falls back to vote-based suppression. + #[pallet::call_index(133)] + #[pallet::weight(( + Weight::from_parts(5_000_000, 0) + .saturating_add(T::DbWeight::get().writes(1)), + DispatchClass::Operational, + Pays::No + ))] + pub fn sudo_set_emission_suppression_override( + origin: OriginFor, + netuid: NetUid, + override_value: Option, + ) -> DispatchResult { + ensure_root(origin)?; + ensure!( + Self::if_subnet_exist(netuid), + Error::::SubnetNotExists + ); + match override_value { + Some(val) => EmissionSuppressionOverride::::insert(netuid, val), + None => EmissionSuppressionOverride::::remove(netuid), + } + Ok(()) + } + /// --- The extrinsic is a combination of add_stake(add_stake_limit) and burn_alpha. We buy /// alpha token first and immediately burn the acquired amount of alpha (aka Subnet buyback). #[pallet::call_index(132)] From b7982b8667c1b51f7ad87bcdf666773f73e93602 Mon Sep 17 00:00:00 2001 From: Pawel Polewicz Date: Tue, 10 Feb 2026 23:13:03 +0000 Subject: [PATCH 03/17] Add voting extrinsic + on-epoch vote collection - Add EmissionSuppressionVote storage double map (NetUid, AccountId -> bool) for per-coldkey votes on subnet emission suppression - Add vote_emission_suppression extrinsic (call_index 134): requires signed coldkey owning root-registered hotkey with stake >= threshold - Add collect_emission_suppression_votes() called per-subnet on epoch: iterates root validators, accumulates stake-weighted votes, updates EmissionSuppression fraction - Add transfer_emission_suppression_votes in do_swap_coldkey for coldkey swap migration - Clean up EmissionSuppressionVote in remove_network - New errors: CannotVoteOnRootSubnet, NotEnoughStakeToVote - New event: EmissionSuppressionVoteCast Co-Authored-By: Claude Opus 4.6 --- pallets/subtensor/src/coinbase/root.rs | 1 + .../subtensor/src/coinbase/run_coinbase.rs | 3 ++ .../src/coinbase/subnet_emissions.rs | 28 ++++++++++++ pallets/subtensor/src/lib.rs | 14 ++++++ pallets/subtensor/src/macros/dispatches.rs | 45 +++++++++++++++++++ pallets/subtensor/src/macros/errors.rs | 4 ++ pallets/subtensor/src/macros/events.rs | 10 +++++ pallets/subtensor/src/swap/swap_coldkey.rs | 15 +++++++ 8 files changed, 120 insertions(+) diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index 6ba3e77f01..e3059a2f75 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -305,6 +305,7 @@ impl Pallet { // --- 12b. Emission suppression. EmissionSuppression::::remove(netuid); EmissionSuppressionOverride::::remove(netuid); + let _ = EmissionSuppressionVote::::clear_prefix(netuid, u32::MAX, None); // --- 13. Token / mechanism / registration toggles. TokenSymbol::::remove(netuid); diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 2091946598..4b8d2fafcb 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -265,6 +265,9 @@ impl Pallet { if Self::should_run_epoch(netuid, current_block) && Self::is_epoch_input_state_consistent(netuid) { + // Collect emission suppression votes for this subnet. + Self::collect_emission_suppression_votes(netuid); + // Restart counters. BlocksSinceLastStep::::insert(netuid, 0); LastMechansimStepBlock::::insert(netuid, current_block); diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs index 1147b7dbc5..7152bd1cda 100644 --- a/pallets/subtensor/src/coinbase/subnet_emissions.rs +++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs @@ -282,4 +282,32 @@ impl Pallet { Self::normalize_shares(shares); } } + + /// Collect emission suppression votes from root validators for a subnet + /// and update the EmissionSuppression storage. + /// Called once per subnet per epoch. + pub(crate) fn collect_emission_suppression_votes(netuid: NetUid) { + let root_n = SubnetworkN::::get(NetUid::ROOT); + let mut suppress_stake = U64F64::saturating_from_num(0u64); + let mut total_root_stake = U64F64::saturating_from_num(0u64); + + for uid in 0..root_n { + let hotkey = Keys::::get(NetUid::ROOT, uid); + let root_stake = Self::get_stake_for_hotkey_on_subnet(&hotkey, NetUid::ROOT); + let stake_u64f64 = U64F64::saturating_from_num(u64::from(root_stake)); + total_root_stake = total_root_stake.saturating_add(stake_u64f64); + + let coldkey = Owner::::get(&hotkey); + if let Some(true) = EmissionSuppressionVote::::get(netuid, &coldkey) { + suppress_stake = suppress_stake.saturating_add(stake_u64f64); + } + } + + let suppression = if total_root_stake > U64F64::saturating_from_num(0u64) { + suppress_stake.safe_div(total_root_stake) + } else { + U64F64::saturating_from_num(0u64) + }; + EmissionSuppression::::insert(netuid, suppression); + } } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index b686ea3b62..74e9cf2c86 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -2433,6 +2433,20 @@ pub mod pallet { pub type EmissionSuppressionOverride = StorageMap<_, Identity, NetUid, bool, OptionQuery>; + /// Per-(netuid, coldkey) vote on whether to suppress a subnet's emissions. + /// Keyed by coldkey because coldkey swaps must migrate votes and one coldkey + /// can control multiple root hotkeys. + #[pallet::storage] + pub type EmissionSuppressionVote = StorageDoubleMap< + _, + Identity, + NetUid, + Blake2_128Concat, + T::AccountId, + bool, + OptionQuery, + >; + #[pallet::genesis_config] pub struct GenesisConfig { /// Stakes record in genesis. diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 11603c7b7f..0112b13ca8 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -2607,6 +2607,51 @@ mod dispatches { Ok(()) } + /// --- Vote to suppress or unsuppress emissions for a subnet. + /// The caller must be a coldkey that owns at least one hotkey registered on root + /// with stake >= StakeThreshold. Pass suppress=None to clear the vote. + #[pallet::call_index(134)] + #[pallet::weight(( + Weight::from_parts(20_000_000, 0) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(1)), + DispatchClass::Normal, + Pays::Yes + ))] + pub fn vote_emission_suppression( + origin: OriginFor, + netuid: NetUid, + suppress: Option, + ) -> DispatchResult { + let coldkey = ensure_signed(origin)?; + ensure!( + Self::if_subnet_exist(netuid), + Error::::SubnetNotExists + ); + ensure!(!netuid.is_root(), Error::::CannotVoteOnRootSubnet); + + // Coldkey must own at least one hotkey registered on root with enough stake. + let stake_threshold = Self::get_stake_threshold(); + let hotkeys = StakingHotkeys::::get(&coldkey); + let has_qualifying_hotkey = hotkeys.iter().any(|hk| { + Self::is_hotkey_registered_on_network(NetUid::ROOT, hk) + && u64::from(Self::get_stake_for_hotkey_on_subnet(hk, NetUid::ROOT)) + >= stake_threshold + }); + ensure!(has_qualifying_hotkey, Error::::NotEnoughStakeToVote); + + match suppress { + Some(val) => EmissionSuppressionVote::::insert(netuid, &coldkey, val), + None => EmissionSuppressionVote::::remove(netuid, &coldkey), + } + Self::deposit_event(Event::EmissionSuppressionVoteCast { + coldkey, + netuid, + suppress, + }); + Ok(()) + } + /// --- The extrinsic is a combination of add_stake(add_stake_limit) and burn_alpha. We buy /// alpha token first and immediately burn the acquired amount of alpha (aka Subnet buyback). #[pallet::call_index(132)] diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index 7d50373f19..2fee340e9c 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -282,5 +282,9 @@ mod errors { Deprecated, /// "Add stake and burn" exceeded the operation rate limit AddStakeBurnRateLimitExceeded, + /// Cannot vote on emission suppression for the root subnet. + CannotVoteOnRootSubnet, + /// Coldkey does not own a root-registered hotkey with enough stake. + NotEnoughStakeToVote, } } diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index 65c33aee87..2e6718eb10 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -517,6 +517,16 @@ mod events { alpha: AlphaCurrency, }, + /// A root validator cast (or cleared) an emission suppression vote. + EmissionSuppressionVoteCast { + /// The coldkey that cast the vote + coldkey: T::AccountId, + /// The subnet voted on + netuid: NetUid, + /// The vote: Some(true) = suppress, Some(false) = unsuppress, None = cleared + suppress: Option, + }, + /// "Add stake and burn" event: alpha token was purchased and burned. AddStakeBurn { /// The subnet ID diff --git a/pallets/subtensor/src/swap/swap_coldkey.rs b/pallets/subtensor/src/swap/swap_coldkey.rs index 54b07d9dbf..591340786a 100644 --- a/pallets/subtensor/src/swap/swap_coldkey.rs +++ b/pallets/subtensor/src/swap/swap_coldkey.rs @@ -31,6 +31,7 @@ impl Pallet { } Self::transfer_staking_hotkeys(old_coldkey, new_coldkey); Self::transfer_hotkeys_ownership(old_coldkey, new_coldkey); + Self::transfer_emission_suppression_votes(old_coldkey, new_coldkey); // Transfer any remaining balance from old_coldkey to new_coldkey let remaining_balance = Self::get_coldkey_balance(old_coldkey); @@ -159,4 +160,18 @@ impl Pallet { OwnedHotkeys::::remove(old_coldkey); OwnedHotkeys::::insert(new_coldkey, new_owned_hotkeys); } + + /// Transfer emission suppression votes from the old coldkey to the new coldkey. + /// Since EmissionSuppressionVote is keyed by (netuid, coldkey), we must iterate + /// all subnets to find votes belonging to the old coldkey. + fn transfer_emission_suppression_votes( + old_coldkey: &T::AccountId, + new_coldkey: &T::AccountId, + ) { + for netuid in Self::get_all_subnet_netuids() { + if let Some(vote) = EmissionSuppressionVote::::take(netuid, old_coldkey) { + EmissionSuppressionVote::::insert(netuid, new_coldkey, vote); + } + } + } } From 24b53af3db605c74b811f81b9990785d522064e9 Mon Sep 17 00:00:00 2001 From: Pawel Polewicz Date: Tue, 10 Feb 2026 23:17:21 +0000 Subject: [PATCH 04/17] Add emission suppression tests 13 test scenarios covering: - Share zeroing and renormalization with majority suppression - No effect when suppression is below 50% - Root override force suppress/unsuppress - Override=None falls back to votes - Vote requires root registration and minimum stake - Vote clearing removes suppression - Votes collected only on epoch - Coldkey swap migrates votes - Network dissolution clears all suppression state - Share renormalization across 3 subnets - Unstaked TAO not counted in suppression denominator Co-Authored-By: Claude Opus 4.6 --- .../src/tests/emission_suppression.rs | 463 ++++++++++++++++++ pallets/subtensor/src/tests/mod.rs | 1 + 2 files changed, 464 insertions(+) create mode 100644 pallets/subtensor/src/tests/emission_suppression.rs diff --git a/pallets/subtensor/src/tests/emission_suppression.rs b/pallets/subtensor/src/tests/emission_suppression.rs new file mode 100644 index 0000000000..8549c74d80 --- /dev/null +++ b/pallets/subtensor/src/tests/emission_suppression.rs @@ -0,0 +1,463 @@ +#![allow(unused, clippy::indexing_slicing, clippy::panic, clippy::unwrap_used)] +use super::mock::*; +use crate::*; +use alloc::collections::BTreeMap; +use frame_support::{assert_err, assert_ok}; +use sp_core::U256; +use substrate_fixed::types::U64F64; +use subtensor_runtime_common::NetUid; + +/// Helper: set up root network + register a hotkey on root with given stake. +/// Returns (hotkey, coldkey). +fn setup_root_validator( + hotkey_seed: u64, + coldkey_seed: u64, + root_stake: u64, +) -> (U256, U256) { + let hotkey = U256::from(hotkey_seed); + let coldkey = U256::from(coldkey_seed); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(coldkey), + hotkey, + )); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + NetUid::ROOT, + root_stake.into(), + ); + (hotkey, coldkey) +} + +/// Helper: create a non-root subnet with TAO flow so it gets shares. +fn setup_subnet_with_flow(netuid: NetUid, tempo: u16, tao_flow: i64) { + add_network(netuid, tempo, 0); + SubnetTaoFlow::::insert(netuid, tao_flow); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 1: >50% stake votes suppress → share=0, rest renormalized +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_suppression_zeroes_share_majority() { + new_test_ext(1).execute_with(|| { + let sn1 = NetUid::from(1); + let sn2 = NetUid::from(2); + setup_subnet_with_flow(sn1, 10, 100_000_000); + setup_subnet_with_flow(sn2, 10, 100_000_000); + + // Directly set suppression > 0.5 for sn1. + EmissionSuppression::::insert(sn1, U64F64::from_num(0.6)); + + let mut shares = SubtensorModule::get_shares(&[sn1, sn2]); + SubtensorModule::apply_emission_suppression(&mut shares); + + // sn1 should be zeroed. + assert_eq!( + shares.get(&sn1).copied().unwrap_or(U64F64::from_num(0)), + U64F64::from_num(0) + ); + // sn2 should get the full share (renormalized to 1.0). + let sn2_share = shares.get(&sn2).copied().unwrap_or(U64F64::from_num(0)); + assert!( + sn2_share > U64F64::from_num(0.99), + "sn2 share should be ~1.0, got {sn2_share:?}" + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 2: <50% stake votes suppress → share unchanged +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_suppression_no_effect_below_half() { + new_test_ext(1).execute_with(|| { + let sn1 = NetUid::from(1); + let sn2 = NetUid::from(2); + setup_subnet_with_flow(sn1, 10, 100_000_000); + setup_subnet_with_flow(sn2, 10, 100_000_000); + + // Set suppression <= 0.5 for sn1. + EmissionSuppression::::insert(sn1, U64F64::from_num(0.4)); + + let mut shares = SubtensorModule::get_shares(&[sn1, sn2]); + let shares_before = shares.clone(); + SubtensorModule::apply_emission_suppression(&mut shares); + + // Both shares should be unchanged. + assert_eq!(shares, shares_before); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 3: Root override=Some(true), no votes → suppressed +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_override_force_suppress() { + new_test_ext(1).execute_with(|| { + let sn1 = NetUid::from(1); + let sn2 = NetUid::from(2); + setup_subnet_with_flow(sn1, 10, 100_000_000); + setup_subnet_with_flow(sn2, 10, 100_000_000); + + // No votes, suppression is 0. But override forces suppression. + EmissionSuppressionOverride::::insert(sn1, true); + + let mut shares = SubtensorModule::get_shares(&[sn1, sn2]); + SubtensorModule::apply_emission_suppression(&mut shares); + + assert_eq!( + shares.get(&sn1).copied().unwrap_or(U64F64::from_num(0)), + U64F64::from_num(0) + ); + let sn2_share = shares.get(&sn2).copied().unwrap_or(U64F64::from_num(0)); + assert!( + sn2_share > U64F64::from_num(0.99), + "sn2 share should be ~1.0, got {sn2_share:?}" + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 4: Majority votes suppress, override=Some(false) → not suppressed +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_override_force_unsuppress() { + new_test_ext(1).execute_with(|| { + let sn1 = NetUid::from(1); + let sn2 = NetUid::from(2); + setup_subnet_with_flow(sn1, 10, 100_000_000); + setup_subnet_with_flow(sn2, 10, 100_000_000); + + // Set high suppression fraction. + EmissionSuppression::::insert(sn1, U64F64::from_num(0.9)); + // But override forces unsuppression. + EmissionSuppressionOverride::::insert(sn1, false); + + let mut shares = SubtensorModule::get_shares(&[sn1, sn2]); + let shares_before = shares.clone(); + SubtensorModule::apply_emission_suppression(&mut shares); + + // Shares should be unchanged (not suppressed). + assert_eq!(shares, shares_before); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 5: Override=None, votes determine outcome +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_override_none_uses_votes() { + new_test_ext(1).execute_with(|| { + let sn1 = NetUid::from(1); + let sn2 = NetUid::from(2); + setup_subnet_with_flow(sn1, 10, 100_000_000); + setup_subnet_with_flow(sn2, 10, 100_000_000); + + // No override (default). + // Set suppression > 0.5. + EmissionSuppression::::insert(sn1, U64F64::from_num(0.7)); + + let mut shares = SubtensorModule::get_shares(&[sn1, sn2]); + SubtensorModule::apply_emission_suppression(&mut shares); + + assert_eq!( + shares.get(&sn1).copied().unwrap_or(U64F64::from_num(0)), + U64F64::from_num(0) + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 6: Non-root validator → error +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_vote_requires_root_registration() { + new_test_ext(1).execute_with(|| { + add_network(NetUid::ROOT, 1, 0); + let sn1 = NetUid::from(1); + setup_subnet_with_flow(sn1, 10, 100_000_000); + + // coldkey with no root-registered hotkey. + let coldkey = U256::from(999); + + assert_err!( + SubtensorModule::vote_emission_suppression( + RuntimeOrigin::signed(coldkey), + sn1, + Some(true), + ), + Error::::NotEnoughStakeToVote + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 7: Below threshold → error +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_vote_requires_minimum_stake() { + new_test_ext(1).execute_with(|| { + add_network(NetUid::ROOT, 1, 0); + let sn1 = NetUid::from(1); + setup_subnet_with_flow(sn1, 10, 100_000_000); + + // Set a non-zero stake threshold. + StakeThreshold::::put(1_000_000); + + let hotkey = U256::from(10); + let coldkey = U256::from(11); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(coldkey), + hotkey, + )); + // Stake below threshold. + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + NetUid::ROOT, + 999_999u64.into(), + ); + + assert_err!( + SubtensorModule::vote_emission_suppression( + RuntimeOrigin::signed(coldkey), + sn1, + Some(true), + ), + Error::::NotEnoughStakeToVote + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 8: Vote then clear (None) → suppression drops +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_vote_clear() { + new_test_ext(1).execute_with(|| { + add_network(NetUid::ROOT, 1, 0); + let sn1 = NetUid::from(1); + setup_subnet_with_flow(sn1, 10, 100_000_000); + + let (hotkey, coldkey) = setup_root_validator(10, 11, 1_000_000); + + // Vote to suppress. + assert_ok!(SubtensorModule::vote_emission_suppression( + RuntimeOrigin::signed(coldkey), + sn1, + Some(true), + )); + assert_eq!( + EmissionSuppressionVote::::get(sn1, &coldkey), + Some(true) + ); + + // Clear vote. + assert_ok!(SubtensorModule::vote_emission_suppression( + RuntimeOrigin::signed(coldkey), + sn1, + None, + )); + assert_eq!( + EmissionSuppressionVote::::get(sn1, &coldkey), + None + ); + + // Collect votes - should result in 0 suppression. + SubtensorModule::collect_emission_suppression_votes(sn1); + let suppression = EmissionSuppression::::get(sn1); + assert_eq!(suppression, U64F64::from_num(0)); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 9: Suppression only updates on epoch +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_votes_collected_on_epoch() { + new_test_ext(1).execute_with(|| { + add_network(NetUid::ROOT, 1, 0); + let sn1 = NetUid::from(1); + setup_subnet_with_flow(sn1, 10, 100_000_000); + + let (hotkey, coldkey) = setup_root_validator(10, 11, 1_000_000); + + // Vote to suppress. + assert_ok!(SubtensorModule::vote_emission_suppression( + RuntimeOrigin::signed(coldkey), + sn1, + Some(true), + )); + + // Before epoch, suppression should still be 0 (default). + assert_eq!(EmissionSuppression::::get(sn1), U64F64::from_num(0)); + + // Run epochs so vote collection occurs. + step_epochs(1, sn1); + + // After epoch, suppression should be updated. + let suppression = EmissionSuppression::::get(sn1); + assert!( + suppression > U64F64::from_num(0), + "suppression should be > 0 after epoch, got {suppression:?}" + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 10: Swap coldkey → votes follow +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_coldkey_swap_migrates_votes() { + new_test_ext(1).execute_with(|| { + add_network(NetUid::ROOT, 1, 0); + let sn1 = NetUid::from(1); + setup_subnet_with_flow(sn1, 10, 100_000_000); + + let (hotkey, old_coldkey) = setup_root_validator(10, 11, 1_000_000); + + // Vote to suppress. + assert_ok!(SubtensorModule::vote_emission_suppression( + RuntimeOrigin::signed(old_coldkey), + sn1, + Some(true), + )); + assert_eq!( + EmissionSuppressionVote::::get(sn1, &old_coldkey), + Some(true) + ); + + // Perform coldkey swap. + let new_coldkey = U256::from(999); + assert_ok!(SubtensorModule::do_swap_coldkey(&old_coldkey, &new_coldkey)); + + // Vote should be on new coldkey. + assert_eq!( + EmissionSuppressionVote::::get(sn1, &new_coldkey), + Some(true) + ); + // Old coldkey should have no vote. + assert_eq!( + EmissionSuppressionVote::::get(sn1, &old_coldkey), + None + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 11: Dissolve subnet → votes + suppression cleaned +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_dissolution_clears_all() { + new_test_ext(1).execute_with(|| { + add_network(NetUid::ROOT, 1, 0); + let sn1 = NetUid::from(1); + setup_subnet_with_flow(sn1, 10, 100_000_000); + + let (hotkey, coldkey) = setup_root_validator(10, 11, 1_000_000); + + // Vote and set suppression. + assert_ok!(SubtensorModule::vote_emission_suppression( + RuntimeOrigin::signed(coldkey), + sn1, + Some(true), + )); + EmissionSuppression::::insert(sn1, U64F64::from_num(0.8)); + EmissionSuppressionOverride::::insert(sn1, true); + + // Remove the network. + SubtensorModule::remove_network(sn1); + + // Everything should be cleaned up. + assert_eq!(EmissionSuppression::::get(sn1), U64F64::from_num(0)); + assert_eq!(EmissionSuppressionOverride::::get(sn1), None); + assert_eq!( + EmissionSuppressionVote::::get(sn1, &coldkey), + None + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 12: 3 subnets, suppress 1 → others sum to 1.0 +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_shares_renormalize() { + new_test_ext(1).execute_with(|| { + let sn1 = NetUid::from(1); + let sn2 = NetUid::from(2); + let sn3 = NetUid::from(3); + setup_subnet_with_flow(sn1, 10, 100_000_000); + setup_subnet_with_flow(sn2, 10, 200_000_000); + setup_subnet_with_flow(sn3, 10, 300_000_000); + + // Suppress sn2. + EmissionSuppression::::insert(sn2, U64F64::from_num(0.9)); + + let mut shares = SubtensorModule::get_shares(&[sn1, sn2, sn3]); + SubtensorModule::apply_emission_suppression(&mut shares); + + // sn2 should be 0. + assert_eq!( + shares.get(&sn2).copied().unwrap_or(U64F64::from_num(0)), + U64F64::from_num(0) + ); + + // Remaining shares should sum to ~1.0. + let sum: U64F64 = shares + .values() + .copied() + .fold(U64F64::from_num(0), |a, b| a.saturating_add(b)); + let sum_f64: f64 = sum.to_num(); + assert!( + (sum_f64 - 1.0).abs() < 1e-9, + "shares should sum to 1.0, got {sum_f64}" + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 13: Extra unstaked TAO → no effect on suppression fraction +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_unstaked_tao_not_in_denominator() { + new_test_ext(1).execute_with(|| { + add_network(NetUid::ROOT, 1, 0); + let sn1 = NetUid::from(1); + setup_subnet_with_flow(sn1, 10, 100_000_000); + + // Two root validators: one votes suppress, one doesn't. + let (_hk1, ck1) = setup_root_validator(10, 11, 1_000_000); + let (_hk2, ck2) = setup_root_validator(20, 21, 1_000_000); + + // Only ck1 votes to suppress. + assert_ok!(SubtensorModule::vote_emission_suppression( + RuntimeOrigin::signed(ck1), + sn1, + Some(true), + )); + + // Collect votes. + SubtensorModule::collect_emission_suppression_votes(sn1); + + // Suppression should be 0.5 (1M / 2M). + let suppression: f64 = EmissionSuppression::::get(sn1).to_num(); + assert!( + (suppression - 0.5).abs() < 1e-6, + "suppression should be 0.5, got {suppression}" + ); + + // Adding free balance (unstaked TAO) to some account should NOT affect denominator. + let random_account = U256::from(999); + SubtensorModule::add_balance_to_coldkey_account(&random_account, 100_000_000_000); + + // Re-collect. + SubtensorModule::collect_emission_suppression_votes(sn1); + let suppression2: f64 = EmissionSuppression::::get(sn1).to_num(); + assert!( + (suppression2 - 0.5).abs() < 1e-6, + "suppression should still be 0.5 after adding unstaked TAO, got {suppression2}" + ); + }); +} diff --git a/pallets/subtensor/src/tests/mod.rs b/pallets/subtensor/src/tests/mod.rs index 8f07572e25..6402bd6b31 100644 --- a/pallets/subtensor/src/tests/mod.rs +++ b/pallets/subtensor/src/tests/mod.rs @@ -7,6 +7,7 @@ mod consensus; mod delegate_info; mod difficulty; mod emission; +mod emission_suppression; mod ensure; mod epoch; mod epoch_logs; From f4fda367730869f4721ae4f17657da5845bc82b0 Mon Sep 17 00:00:00 2001 From: Pawel Polewicz Date: Tue, 10 Feb 2026 23:52:59 +0000 Subject: [PATCH 05/17] Add KeepRootSellPressureOnSuppressedSubnets - Add KeepRootSellPressureOnSuppressedSubnets storage (default true): controls whether root validators receive alpha dividends from suppressed subnets - Add sudo_set_keep_root_sell_pressure_on_suppressed_subnets extrinsic (call_index 135) - Modify emit_to_subnets: when flag is false and subnet is suppressed, root_alpha is zeroed and all validator alpha goes to subnet validators - Add is_subnet_emission_suppressed() helper to deduplicate suppression check logic - Refactor apply_emission_suppression to use the new helper - Add 3 tests (14-16): root alpha on suppressed subnet with flag on/off, and unsuppressed subnet unaffected by flag Co-Authored-By: Claude Opus 4.6 --- .../subtensor/src/coinbase/run_coinbase.rs | 14 +- .../src/coinbase/subnet_emissions.rs | 27 ++- pallets/subtensor/src/lib.rs | 27 ++- pallets/subtensor/src/macros/dispatches.rs | 30 ++- pallets/subtensor/src/swap/swap_coldkey.rs | 5 +- .../src/tests/emission_suppression.rs | 195 ++++++++++++++++-- 6 files changed, 239 insertions(+), 59 deletions(-) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 4b8d2fafcb..05a6f6e43a 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -209,9 +209,17 @@ impl Pallet { log::debug!("root_proportion: {root_proportion:?}"); // Get root alpha from root prop. - let root_alpha: U96F32 = root_proportion - .saturating_mul(alpha_out_i) // Total alpha emission per block remaining. - .saturating_mul(asfloat!(0.5)); // 50% to validators. + // If the subnet is suppressed and KeepRootSellPressureOnSuppressedSubnets + // is false, zero out root alpha so all validator alpha goes to subnet validators. + let root_alpha: U96F32 = if Self::is_subnet_emission_suppressed(*netuid_i) + && !KeepRootSellPressureOnSuppressedSubnets::::get() + { + asfloat!(0) + } else { + root_proportion + .saturating_mul(alpha_out_i) // Total alpha emission per block remaining. + .saturating_mul(asfloat!(0.5)) // 50% to validators. + }; log::debug!("root_alpha: {root_alpha:?}"); // Get pending server alpha, which is the miner cut of the alpha out. diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs index 7152bd1cda..a378a8c0de 100644 --- a/pallets/subtensor/src/coinbase/subnet_emissions.rs +++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs @@ -250,10 +250,12 @@ impl Pallet { /// Normalize shares so they sum to 1.0. pub(crate) fn normalize_shares(shares: &mut BTreeMap) { - let sum: U64F64 = shares.values().copied().fold( - U64F64::saturating_from_num(0), - |acc, v| acc.saturating_add(v), - ); + let sum: U64F64 = shares + .values() + .copied() + .fold(U64F64::saturating_from_num(0), |acc, v| { + acc.saturating_add(v) + }); if sum > U64F64::saturating_from_num(0) { for s in shares.values_mut() { *s = s.safe_div(sum); @@ -261,19 +263,22 @@ impl Pallet { } } + /// Check if a subnet is currently emission-suppressed, considering override first. + pub(crate) fn is_subnet_emission_suppressed(netuid: NetUid) -> bool { + match EmissionSuppressionOverride::::get(netuid) { + Some(true) => true, + Some(false) => false, + None => EmissionSuppression::::get(netuid) > U64F64::saturating_from_num(0.5), + } + } + /// Zero the emission share of any subnet whose suppression fraction exceeds 50% /// (or is force-suppressed via override), then re-normalize the remaining shares. pub(crate) fn apply_emission_suppression(shares: &mut BTreeMap) { - let half = U64F64::saturating_from_num(0.5); let zero = U64F64::saturating_from_num(0); let mut any_zeroed = false; for (netuid, share) in shares.iter_mut() { - let suppressed = match EmissionSuppressionOverride::::get(netuid) { - Some(true) => true, - Some(false) => false, - None => EmissionSuppression::::get(netuid) > half, - }; - if suppressed { + if Self::is_subnet_emission_suppressed(*netuid) { *share = zero; any_zeroed = true; } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 74e9cf2c86..877418e07c 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -2423,8 +2423,7 @@ pub mod pallet { /// Updated every epoch from root validator votes. /// When this value exceeds 0.5, the subnet's emission share is zeroed. #[pallet::storage] - pub type EmissionSuppression = - StorageMap<_, Identity, NetUid, U64F64, ValueQuery>; + pub type EmissionSuppression = StorageMap<_, Identity, NetUid, U64F64, ValueQuery>; /// Root override for emission suppression per subnet. /// Some(true) = force suppressed, Some(false) = force unsuppressed, @@ -2437,15 +2436,21 @@ pub mod pallet { /// Keyed by coldkey because coldkey swaps must migrate votes and one coldkey /// can control multiple root hotkeys. #[pallet::storage] - pub type EmissionSuppressionVote = StorageDoubleMap< - _, - Identity, - NetUid, - Blake2_128Concat, - T::AccountId, - bool, - OptionQuery, - >; + pub type EmissionSuppressionVote = + StorageDoubleMap<_, Identity, NetUid, Blake2_128Concat, T::AccountId, bool, OptionQuery>; + + /// Whether root validators continue receiving alpha dividends (sell pressure) + /// from suppressed subnets. Default: true (maintain sell pressure). + /// When false, all alpha goes to subnet validators instead. + #[pallet::storage] + pub type KeepRootSellPressureOnSuppressedSubnets = + StorageValue<_, bool, ValueQuery, KeepRootSellPressureDefault>; + + /// Default value for KeepRootSellPressureOnSuppressedSubnets (true). + #[pallet::type_value] + pub fn KeepRootSellPressureDefault() -> bool { + true + } #[pallet::genesis_config] pub struct GenesisConfig { diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 0112b13ca8..3c3ffd1155 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -2596,10 +2596,7 @@ mod dispatches { override_value: Option, ) -> DispatchResult { ensure_root(origin)?; - ensure!( - Self::if_subnet_exist(netuid), - Error::::SubnetNotExists - ); + ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); match override_value { Some(val) => EmissionSuppressionOverride::::insert(netuid, val), None => EmissionSuppressionOverride::::remove(netuid), @@ -2607,6 +2604,26 @@ mod dispatches { Ok(()) } + /// --- Set whether root validators continue receiving alpha dividends (sell pressure) + /// from emission-suppressed subnets. When true (default), root validators still + /// accumulate alpha on suppressed subnets. When false, all alpha goes to subnet + /// validators instead. + #[pallet::call_index(135)] + #[pallet::weight(( + Weight::from_parts(5_000_000, 0) + .saturating_add(T::DbWeight::get().writes(1)), + DispatchClass::Operational, + Pays::No + ))] + pub fn sudo_set_keep_root_sell_pressure_on_suppressed_subnets( + origin: OriginFor, + value: bool, + ) -> DispatchResult { + ensure_root(origin)?; + KeepRootSellPressureOnSuppressedSubnets::::put(value); + Ok(()) + } + /// --- Vote to suppress or unsuppress emissions for a subnet. /// The caller must be a coldkey that owns at least one hotkey registered on root /// with stake >= StakeThreshold. Pass suppress=None to clear the vote. @@ -2624,10 +2641,7 @@ mod dispatches { suppress: Option, ) -> DispatchResult { let coldkey = ensure_signed(origin)?; - ensure!( - Self::if_subnet_exist(netuid), - Error::::SubnetNotExists - ); + ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); ensure!(!netuid.is_root(), Error::::CannotVoteOnRootSubnet); // Coldkey must own at least one hotkey registered on root with enough stake. diff --git a/pallets/subtensor/src/swap/swap_coldkey.rs b/pallets/subtensor/src/swap/swap_coldkey.rs index 591340786a..832b513541 100644 --- a/pallets/subtensor/src/swap/swap_coldkey.rs +++ b/pallets/subtensor/src/swap/swap_coldkey.rs @@ -164,10 +164,7 @@ impl Pallet { /// Transfer emission suppression votes from the old coldkey to the new coldkey. /// Since EmissionSuppressionVote is keyed by (netuid, coldkey), we must iterate /// all subnets to find votes belonging to the old coldkey. - fn transfer_emission_suppression_votes( - old_coldkey: &T::AccountId, - new_coldkey: &T::AccountId, - ) { + fn transfer_emission_suppression_votes(old_coldkey: &T::AccountId, new_coldkey: &T::AccountId) { for netuid in Self::get_all_subnet_netuids() { if let Some(vote) = EmissionSuppressionVote::::take(netuid, old_coldkey) { EmissionSuppressionVote::::insert(netuid, new_coldkey, vote); diff --git a/pallets/subtensor/src/tests/emission_suppression.rs b/pallets/subtensor/src/tests/emission_suppression.rs index 8549c74d80..03d08b8e6f 100644 --- a/pallets/subtensor/src/tests/emission_suppression.rs +++ b/pallets/subtensor/src/tests/emission_suppression.rs @@ -4,16 +4,12 @@ use crate::*; use alloc::collections::BTreeMap; use frame_support::{assert_err, assert_ok}; use sp_core::U256; -use substrate_fixed::types::U64F64; -use subtensor_runtime_common::NetUid; +use substrate_fixed::types::{U64F64, U96F32}; +use subtensor_runtime_common::{AlphaCurrency, NetUid, TaoCurrency}; /// Helper: set up root network + register a hotkey on root with given stake. /// Returns (hotkey, coldkey). -fn setup_root_validator( - hotkey_seed: u64, - coldkey_seed: u64, - root_stake: u64, -) -> (U256, U256) { +fn setup_root_validator(hotkey_seed: u64, coldkey_seed: u64, root_stake: u64) -> (U256, U256) { let hotkey = U256::from(hotkey_seed); let coldkey = U256::from(coldkey_seed); assert_ok!(SubtensorModule::root_register( @@ -249,7 +245,7 @@ fn test_vote_clear() { Some(true), )); assert_eq!( - EmissionSuppressionVote::::get(sn1, &coldkey), + EmissionSuppressionVote::::get(sn1, coldkey), Some(true) ); @@ -259,10 +255,7 @@ fn test_vote_clear() { sn1, None, )); - assert_eq!( - EmissionSuppressionVote::::get(sn1, &coldkey), - None - ); + assert_eq!(EmissionSuppressionVote::::get(sn1, coldkey), None); // Collect votes - should result in 0 suppression. SubtensorModule::collect_emission_suppression_votes(sn1); @@ -324,7 +317,7 @@ fn test_coldkey_swap_migrates_votes() { Some(true), )); assert_eq!( - EmissionSuppressionVote::::get(sn1, &old_coldkey), + EmissionSuppressionVote::::get(sn1, old_coldkey), Some(true) ); @@ -334,14 +327,11 @@ fn test_coldkey_swap_migrates_votes() { // Vote should be on new coldkey. assert_eq!( - EmissionSuppressionVote::::get(sn1, &new_coldkey), + EmissionSuppressionVote::::get(sn1, new_coldkey), Some(true) ); // Old coldkey should have no vote. - assert_eq!( - EmissionSuppressionVote::::get(sn1, &old_coldkey), - None - ); + assert_eq!(EmissionSuppressionVote::::get(sn1, old_coldkey), None); }); } @@ -372,10 +362,7 @@ fn test_dissolution_clears_all() { // Everything should be cleaned up. assert_eq!(EmissionSuppression::::get(sn1), U64F64::from_num(0)); assert_eq!(EmissionSuppressionOverride::::get(sn1), None); - assert_eq!( - EmissionSuppressionVote::::get(sn1, &coldkey), - None - ); + assert_eq!(EmissionSuppressionVote::::get(sn1, coldkey), None); }); } @@ -461,3 +448,167 @@ fn test_unstaked_tao_not_in_denominator() { ); }); } + +/// Helper: set up root + subnet with proper SubnetTAO and alpha issuance +/// so that root_proportion returns a meaningful nonzero value. +fn setup_root_with_tao(sn: NetUid) { + // Set SubnetTAO for root so root_proportion numerator is nonzero. + SubnetTAO::::insert(NetUid::ROOT, TaoCurrency::from(1_000_000_000)); + // Set alpha issuance for subnet so denominator is meaningful. + SubnetAlphaOut::::insert(sn, AlphaCurrency::from(1_000_000_000)); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 14: Suppress subnet, default flag=true → root still gets alpha +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_suppressed_subnet_root_alpha_by_default() { + new_test_ext(1).execute_with(|| { + add_network(NetUid::ROOT, 1, 0); + let sn1 = NetUid::from(1); + setup_subnet_with_flow(sn1, 10, 100_000_000); + + // Register a root validator and add stake on root so root_proportion > 0. + let hotkey = U256::from(10); + let coldkey = U256::from(11); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(coldkey), + hotkey, + )); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + NetUid::ROOT, + 1_000_000_000u64.into(), + ); + // Set TAO weight so root_proportion is nonzero. + SubtensorModule::set_tao_weight(u64::MAX); + setup_root_with_tao(sn1); + + // Force-suppress sn1. + EmissionSuppressionOverride::::insert(sn1, true); + + // Default: KeepRootSellPressureOnSuppressedSubnets = true. + assert!(KeepRootSellPressureOnSuppressedSubnets::::get()); + + // Clear any pending emissions. + PendingRootAlphaDivs::::insert(sn1, AlphaCurrency::ZERO); + + // Build emission map with some emission for sn1. + let mut subnet_emissions = BTreeMap::new(); + subnet_emissions.insert(sn1, U96F32::from_num(1_000_000)); + + SubtensorModule::emit_to_subnets(&[sn1], &subnet_emissions, true); + + // Root should have received some alpha (pending root alpha divs > 0). + let pending_root = PendingRootAlphaDivs::::get(sn1); + assert!( + pending_root > AlphaCurrency::ZERO, + "with flag=true, root should still get alpha on suppressed subnet" + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 15: Suppress subnet, flag=false → root gets no alpha +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_suppressed_subnet_no_root_alpha_flag_off() { + new_test_ext(1).execute_with(|| { + add_network(NetUid::ROOT, 1, 0); + let sn1 = NetUid::from(1); + setup_subnet_with_flow(sn1, 10, 100_000_000); + + // Register a root validator and add stake on root so root_proportion > 0. + let hotkey = U256::from(10); + let coldkey = U256::from(11); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(coldkey), + hotkey, + )); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + NetUid::ROOT, + 1_000_000_000u64.into(), + ); + SubtensorModule::set_tao_weight(u64::MAX); + setup_root_with_tao(sn1); + + // Force-suppress sn1. + EmissionSuppressionOverride::::insert(sn1, true); + + // Set flag to false: no root sell pressure on suppressed subnets. + KeepRootSellPressureOnSuppressedSubnets::::put(false); + + // Clear any pending emissions. + PendingRootAlphaDivs::::insert(sn1, AlphaCurrency::ZERO); + PendingValidatorEmission::::insert(sn1, AlphaCurrency::ZERO); + + // Build emission map. + let mut subnet_emissions = BTreeMap::new(); + subnet_emissions.insert(sn1, U96F32::from_num(1_000_000)); + + SubtensorModule::emit_to_subnets(&[sn1], &subnet_emissions, true); + + // Root should get NO alpha. + let pending_root = PendingRootAlphaDivs::::get(sn1); + assert_eq!( + pending_root, + AlphaCurrency::ZERO, + "with flag=false, root should get no alpha on suppressed subnet" + ); + + // But validator emission should be non-zero (all alpha goes to validators). + let pending_validator = PendingValidatorEmission::::get(sn1); + assert!( + pending_validator > AlphaCurrency::ZERO, + "validators should receive all alpha when root alpha is zeroed" + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 16: Non-suppressed subnet → root alpha normal regardless of flag +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_unsuppressed_subnet_unaffected_by_flag() { + new_test_ext(1).execute_with(|| { + add_network(NetUid::ROOT, 1, 0); + let sn1 = NetUid::from(1); + setup_subnet_with_flow(sn1, 10, 100_000_000); + + let hotkey = U256::from(10); + let coldkey = U256::from(11); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(coldkey), + hotkey, + )); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + NetUid::ROOT, + 1_000_000_000u64.into(), + ); + SubtensorModule::set_tao_weight(u64::MAX); + setup_root_with_tao(sn1); + + // sn1 is NOT suppressed. + // Set flag to false (should not matter for unsuppressed subnets). + KeepRootSellPressureOnSuppressedSubnets::::put(false); + + PendingRootAlphaDivs::::insert(sn1, AlphaCurrency::ZERO); + + let mut subnet_emissions = BTreeMap::new(); + subnet_emissions.insert(sn1, U96F32::from_num(1_000_000)); + + SubtensorModule::emit_to_subnets(&[sn1], &subnet_emissions, true); + + // Root should still get alpha since subnet is not suppressed. + let pending_root = PendingRootAlphaDivs::::get(sn1); + assert!( + pending_root > AlphaCurrency::ZERO, + "non-suppressed subnet should still give root alpha regardless of flag" + ); + }); +} From 6694b52af280921d9007d72c2461567e174405a9 Mon Sep 17 00:00:00 2001 From: Pawel Polewicz Date: Wed, 11 Feb 2026 22:21:13 +0000 Subject: [PATCH 06/17] Address code review: events, root guard, coldkey swap conflict - Add events for sudo_set_emission_suppression_override and sudo_set_keep_root_sell_pressure_on_suppressed_subnets - Fix missing read in sudo_set_emission_suppression_override weight - Add early return guard in collect_emission_suppression_votes for root - Fail coldkey swap if destination already has emission suppression votes (new error: DestinationColdkeyHasExistingVotes) Co-Authored-By: Claude Opus 4.6 --- .../subtensor/src/coinbase/subnet_emissions.rs | 5 ++++- pallets/subtensor/src/macros/dispatches.rs | 6 ++++++ pallets/subtensor/src/macros/errors.rs | 2 ++ pallets/subtensor/src/macros/events.rs | 14 ++++++++++++++ pallets/subtensor/src/swap/swap_coldkey.rs | 16 ++++++++++++++-- 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs index a378a8c0de..1f63e31506 100644 --- a/pallets/subtensor/src/coinbase/subnet_emissions.rs +++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs @@ -290,8 +290,11 @@ impl Pallet { /// Collect emission suppression votes from root validators for a subnet /// and update the EmissionSuppression storage. - /// Called once per subnet per epoch. + /// Called once per subnet per epoch. No-op for root subnet. pub(crate) fn collect_emission_suppression_votes(netuid: NetUid) { + if netuid.is_root() { + return; + } let root_n = SubnetworkN::::get(NetUid::ROOT); let mut suppress_stake = U64F64::saturating_from_num(0u64); let mut total_root_stake = U64F64::saturating_from_num(0u64); diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 3c3ffd1155..9414af4fa2 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -2586,6 +2586,7 @@ mod dispatches { #[pallet::call_index(133)] #[pallet::weight(( Weight::from_parts(5_000_000, 0) + .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)), DispatchClass::Operational, Pays::No @@ -2601,6 +2602,10 @@ mod dispatches { Some(val) => EmissionSuppressionOverride::::insert(netuid, val), None => EmissionSuppressionOverride::::remove(netuid), } + Self::deposit_event(Event::EmissionSuppressionOverrideSet { + netuid, + override_value, + }); Ok(()) } @@ -2621,6 +2626,7 @@ mod dispatches { ) -> DispatchResult { ensure_root(origin)?; KeepRootSellPressureOnSuppressedSubnets::::put(value); + Self::deposit_event(Event::KeepRootSellPressureOnSuppressedSubnetsSet { value }); Ok(()) } diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index 2fee340e9c..49f1975af4 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -286,5 +286,7 @@ mod errors { CannotVoteOnRootSubnet, /// Coldkey does not own a root-registered hotkey with enough stake. NotEnoughStakeToVote, + /// Coldkey swap destination already has emission suppression votes. + DestinationColdkeyHasExistingVotes, } } diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index 2e6718eb10..e1c928285c 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -527,6 +527,20 @@ mod events { suppress: Option, }, + /// Root set or cleared the emission suppression override for a subnet. + EmissionSuppressionOverrideSet { + /// The subnet affected + netuid: NetUid, + /// The override value: Some(true) = force suppress, Some(false) = force unsuppress, None = cleared + override_value: Option, + }, + + /// Root set the KeepRootSellPressureOnSuppressedSubnets flag. + KeepRootSellPressureOnSuppressedSubnetsSet { + /// The new value + value: bool, + }, + /// "Add stake and burn" event: alpha token was purchased and burned. AddStakeBurn { /// The subnet ID diff --git a/pallets/subtensor/src/swap/swap_coldkey.rs b/pallets/subtensor/src/swap/swap_coldkey.rs index 832b513541..ae9bf8cab8 100644 --- a/pallets/subtensor/src/swap/swap_coldkey.rs +++ b/pallets/subtensor/src/swap/swap_coldkey.rs @@ -31,7 +31,7 @@ impl Pallet { } Self::transfer_staking_hotkeys(old_coldkey, new_coldkey); Self::transfer_hotkeys_ownership(old_coldkey, new_coldkey); - Self::transfer_emission_suppression_votes(old_coldkey, new_coldkey); + Self::transfer_emission_suppression_votes(old_coldkey, new_coldkey)?; // Transfer any remaining balance from old_coldkey to new_coldkey let remaining_balance = Self::get_coldkey_balance(old_coldkey); @@ -164,11 +164,23 @@ impl Pallet { /// Transfer emission suppression votes from the old coldkey to the new coldkey. /// Since EmissionSuppressionVote is keyed by (netuid, coldkey), we must iterate /// all subnets to find votes belonging to the old coldkey. - fn transfer_emission_suppression_votes(old_coldkey: &T::AccountId, new_coldkey: &T::AccountId) { + /// Fails if the new coldkey already has any emission suppression votes. + fn transfer_emission_suppression_votes( + old_coldkey: &T::AccountId, + new_coldkey: &T::AccountId, + ) -> DispatchResult { + // First pass: verify the destination has no existing votes. + for netuid in Self::get_all_subnet_netuids() { + if EmissionSuppressionVote::::get(netuid, new_coldkey).is_some() { + return Err(Error::::DestinationColdkeyHasExistingVotes.into()); + } + } + // Second pass: move votes. for netuid in Self::get_all_subnet_netuids() { if let Some(vote) = EmissionSuppressionVote::::take(netuid, old_coldkey) { EmissionSuppressionVote::::insert(netuid, new_coldkey, vote); } } + Ok(()) } } From 11b0d5f7bab1a06481e008a77d1939ace3328d2b Mon Sep 17 00:00:00 2001 From: Pawel Polewicz Date: Wed, 11 Feb 2026 23:43:22 +0000 Subject: [PATCH 07/17] Add 8 additional test scenarios from code review - test_vote_on_root_subnet_rejected: CannotVoteOnRootSubnet error - test_vote_explicit_false: Some(false) stored, produces 0 suppression - test_all_subnets_suppressed: all shares zeroed, zero total emission - test_coldkey_swap_blocked_by_existing_votes: swap fails with error - test_multi_hotkey_coldkey_vote_weight: 3 hotkeys, weight = sum stakes - test_sudo_override_emits_event: EmissionSuppressionOverrideSet event - test_sudo_sell_pressure_emits_event: KeepRootSellPressureSet event - test_collect_votes_skips_root: ROOT no-op guard Co-Authored-By: Claude Opus 4.6 --- .../src/tests/emission_suppression.rs | 284 ++++++++++++++++++ 1 file changed, 284 insertions(+) diff --git a/pallets/subtensor/src/tests/emission_suppression.rs b/pallets/subtensor/src/tests/emission_suppression.rs index 03d08b8e6f..6be75ef28a 100644 --- a/pallets/subtensor/src/tests/emission_suppression.rs +++ b/pallets/subtensor/src/tests/emission_suppression.rs @@ -3,6 +3,7 @@ use super::mock::*; use crate::*; use alloc::collections::BTreeMap; use frame_support::{assert_err, assert_ok}; +use frame_system::pallet_prelude::BlockNumberFor; use sp_core::U256; use substrate_fixed::types::{U64F64, U96F32}; use subtensor_runtime_common::{AlphaCurrency, NetUid, TaoCurrency}; @@ -612,3 +613,286 @@ fn test_unsuppressed_subnet_unaffected_by_flag() { ); }); } + +// ───────────────────────────────────────────────────────────────────────────── +// Test 17: Voting on root subnet returns CannotVoteOnRootSubnet +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_vote_on_root_subnet_rejected() { + new_test_ext(1).execute_with(|| { + add_network(NetUid::ROOT, 1, 0); + let sn1 = NetUid::from(1); + setup_subnet_with_flow(sn1, 10, 100_000_000); + + let (_hk, ck) = setup_root_validator(10, 11, 1_000_000); + + assert_err!( + SubtensorModule::vote_emission_suppression( + RuntimeOrigin::signed(ck), + NetUid::ROOT, + Some(true), + ), + Error::::CannotVoteOnRootSubnet + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 18: Some(false) vote is stored and treated as no-suppress weight +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_vote_explicit_false() { + new_test_ext(1).execute_with(|| { + add_network(NetUid::ROOT, 1, 0); + let sn1 = NetUid::from(1); + setup_subnet_with_flow(sn1, 10, 100_000_000); + + // Single root validator votes Some(false). + let (_hk, ck) = setup_root_validator(10, 11, 1_000_000); + + assert_ok!(SubtensorModule::vote_emission_suppression( + RuntimeOrigin::signed(ck), + sn1, + Some(false), + )); + assert_eq!( + EmissionSuppressionVote::::get(sn1, ck), + Some(false) + ); + + // Collect votes: sole validator voted false → suppression should be 0. + SubtensorModule::collect_emission_suppression_votes(sn1); + let suppression: f64 = EmissionSuppression::::get(sn1).to_num(); + assert!( + suppression.abs() < 1e-9, + "explicit false vote should produce 0 suppression, got {suppression}" + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 19: All subnets suppressed → all zeroed, no panic +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_all_subnets_suppressed() { + new_test_ext(1).execute_with(|| { + let sn1 = NetUid::from(1); + let sn2 = NetUid::from(2); + setup_subnet_with_flow(sn1, 10, 100_000_000); + setup_subnet_with_flow(sn2, 10, 100_000_000); + + // Suppress both. + EmissionSuppression::::insert(sn1, U64F64::from_num(0.9)); + EmissionSuppression::::insert(sn2, U64F64::from_num(0.8)); + + let mut shares = SubtensorModule::get_shares(&[sn1, sn2]); + SubtensorModule::apply_emission_suppression(&mut shares); + + // Both should be zero. + let s1 = shares.get(&sn1).copied().unwrap_or(U64F64::from_num(0)); + let s2 = shares.get(&sn2).copied().unwrap_or(U64F64::from_num(0)); + assert_eq!(s1, U64F64::from_num(0)); + assert_eq!(s2, U64F64::from_num(0)); + + // Total emission via get_subnet_block_emissions should be zero. + let emissions = + SubtensorModule::get_subnet_block_emissions(&[sn1, sn2], U96F32::from_num(1_000_000)); + let total: u64 = emissions + .values() + .map(|e| e.saturating_to_num::()) + .sum(); + assert_eq!(total, 0, "all-suppressed should yield zero total emission"); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 20: Coldkey swap blocked by existing votes on destination +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_coldkey_swap_blocked_by_existing_votes() { + new_test_ext(1).execute_with(|| { + add_network(NetUid::ROOT, 1, 0); + let sn1 = NetUid::from(1); + setup_subnet_with_flow(sn1, 10, 100_000_000); + + // Set up old coldkey with a vote. + let (_hk_old, old_ck) = setup_root_validator(10, 11, 1_000_000); + assert_ok!(SubtensorModule::vote_emission_suppression( + RuntimeOrigin::signed(old_ck), + sn1, + Some(true), + )); + + // Set up new coldkey that already has a vote via direct storage insert. + let new_ck = U256::from(999); + EmissionSuppressionVote::::insert(sn1, new_ck, false); + + // Swap should fail. + assert_err!( + SubtensorModule::do_swap_coldkey(&old_ck, &new_ck), + Error::::DestinationColdkeyHasExistingVotes + ); + + // Old coldkey's vote should still be intact (no partial state change). + assert_eq!( + EmissionSuppressionVote::::get(sn1, old_ck), + Some(true) + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 21: Coldkey with multiple root hotkeys → vote weight = sum of stakes +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_multi_hotkey_coldkey_vote_weight() { + new_test_ext(1).execute_with(|| { + add_network(NetUid::ROOT, 1, 0); + let sn1 = NetUid::from(1); + setup_subnet_with_flow(sn1, 10, 100_000_000); + + let coldkey = U256::from(100); + let hk1 = U256::from(1); + let hk2 = U256::from(2); + let hk3 = U256::from(3); + + // Register all 3 hotkeys on root under the same coldkey. + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(coldkey), + hk1, + )); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(coldkey), + hk2, + )); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(coldkey), + hk3, + )); + + // Stake: hk1=100, hk2=200, hk3=300 → total root stake = 600. + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hk1, + &coldkey, + NetUid::ROOT, + 100u64.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hk2, + &coldkey, + NetUid::ROOT, + 200u64.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hk3, + &coldkey, + NetUid::ROOT, + 300u64.into(), + ); + + // Vote to suppress. + assert_ok!(SubtensorModule::vote_emission_suppression( + RuntimeOrigin::signed(coldkey), + sn1, + Some(true), + )); + + // Collect votes. Only coldkey's hotkeys exist on root, + // and all stakes belong to the suppressing coldkey. + SubtensorModule::collect_emission_suppression_votes(sn1); + + // Suppression should be 1.0 (all stake voted suppress). + let suppression: f64 = EmissionSuppression::::get(sn1).to_num(); + assert!( + (suppression - 1.0).abs() < 1e-6, + "suppression should be 1.0 when all root stake votes suppress, got {suppression}" + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 22: sudo_set_emission_suppression_override emits event +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_sudo_override_emits_event() { + new_test_ext(1).execute_with(|| { + let sn1 = NetUid::from(1); + setup_subnet_with_flow(sn1, 10, 100_000_000); + + System::set_block_number(1); + System::reset_events(); + + assert_ok!(SubtensorModule::sudo_set_emission_suppression_override( + RuntimeOrigin::root(), + sn1, + Some(true), + )); + + assert!( + System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule( + Event::EmissionSuppressionOverrideSet { netuid, override_value } + ) if *netuid == sn1 && *override_value == Some(true) + ) + }), + "should emit EmissionSuppressionOverrideSet event" + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 23: sudo_set_keep_root_sell_pressure emits event +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_sudo_sell_pressure_emits_event() { + new_test_ext(1).execute_with(|| { + System::set_block_number(1); + System::reset_events(); + + assert_ok!( + SubtensorModule::sudo_set_keep_root_sell_pressure_on_suppressed_subnets( + RuntimeOrigin::root(), + false, + ) + ); + + assert!( + System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule( + Event::KeepRootSellPressureOnSuppressedSubnetsSet { value } + ) if !(*value) + ) + }), + "should emit KeepRootSellPressureOnSuppressedSubnetsSet event" + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 24: collect_emission_suppression_votes(ROOT) is a no-op +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_collect_votes_skips_root() { + new_test_ext(1).execute_with(|| { + add_network(NetUid::ROOT, 1, 0); + + // Ensure no EmissionSuppression entry for ROOT. + assert_eq!( + EmissionSuppression::::get(NetUid::ROOT), + U64F64::from_num(0) + ); + + // Call collect on ROOT — should be a no-op. + SubtensorModule::collect_emission_suppression_votes(NetUid::ROOT); + + // Still no entry. + assert_eq!( + EmissionSuppression::::get(NetUid::ROOT), + U64F64::from_num(0) + ); + }); +} From 68748b990054592b9239cd82fe69c2f87c9614dd Mon Sep 17 00:00:00 2001 From: Pawel Polewicz Date: Thu, 12 Feb 2026 20:06:58 +0000 Subject: [PATCH 08/17] Add /fix and /ship Claude Code skills for automated lint, test, and ship workflow Co-Authored-By: Claude Opus 4.6 --- .claude/skills/fix.md | 45 +++++++++ .claude/skills/ship.md | 94 +++++++++++++++++++ .../src/tests/emission_suppression.rs | 5 +- 3 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 .claude/skills/fix.md create mode 100644 .claude/skills/ship.md diff --git a/.claude/skills/fix.md b/.claude/skills/fix.md new file mode 100644 index 0000000000..15d8adebf9 --- /dev/null +++ b/.claude/skills/fix.md @@ -0,0 +1,45 @@ +--- +name: fix +description: Commit changes, run Rust fix tools, run tests, and amend with any fixes +--- + +# Fix Skill + +Commit current changes with a descriptive message, then run Rust fix tools one by one, amending the commit after each tool if it produced changes, then run unit tests and fix any failures. + +## Steps + +1. **Initial commit**: Stage all changes and create a commit with a descriptive message summarizing the changes (use `git add -A && git commit -m ""`). If there are no changes to commit, create no commit but still proceed with the fix tools below. + +2. **Run each fix tool in order**. After EACH tool, check `git status --porcelain` for changes. If there are changes, stage them and amend the commit (`git add -A && git commit --amend --no-edit`). + + The tools to run in order: + + a. `cargo check --workspace` + b. `cargo clippy --fix --workspace --all-features --all-targets --allow-dirty` + c. `cargo fix --workspace --all-features --all-targets --allow-dirty` + d. `cargo fmt --all` + +3. **Run unit tests in a Sonnet subagent**: Launch a Task subagent (subagent_type: `general-purpose`, model: `sonnet`) that runs: + ``` + cargo test -p pallet-subtensor --lib + ``` + The subagent must: + - Run the test command and capture full output. + - If all tests pass, report success and return. + - If any tests fail, analyze the failures: read the failing test code AND the source code it tests, determine the root cause, apply fixes using Edit tools, and re-run the tests to confirm the fix works. + - After fixing, if there are further failures, repeat (up to 3 fix-and-retest cycles). + - Return a summary of: which tests failed, what was fixed, and whether all tests pass now. + +4. **Amend commit with test fixes**: After the subagent returns, if any code changes were made (check `git status --porcelain`), stage and amend the commit (`git add -A && git commit --amend --no-edit`). Then re-run the fix tools from step 2 (since code changes from test fixes may need formatting/clippy cleanup), amending after each if there are changes. + +5. **Final output**: Show `git log --oneline -1` so the user can see the resulting commit. + +## Important + +- Use `--allow-dirty` flags on clippy --fix and cargo fix since the working tree may have unstaged changes between steps. +- If a fix tool fails (step 2/4), stop and report the error to the user rather than continuing. +- Do NOT run `scripts/fix_rust.sh` itself — run the individual commands listed above instead. +- Do NOT skip any step. Run all four fix tools even if earlier ones produced no changes. +- The test subagent must fix source code to make tests pass, NOT modify tests to make them pass (unless the test itself is clearly wrong). +- If the test subagent cannot fix all failures after 3 cycles, it must return the remaining failures so the main agent can report them to the user. diff --git a/.claude/skills/ship.md b/.claude/skills/ship.md new file mode 100644 index 0000000000..58ca7e0e32 --- /dev/null +++ b/.claude/skills/ship.md @@ -0,0 +1,94 @@ +--- +name: ship +description: Ship the current branch: fix, push, create PR, watch CI, fix failures, code review +--- + +# Ship Skill + +Ship the current branch: fix, push, create PR if needed, watch CI, fix failures, and perform code review. + +## Phase 1: Fix and Push + +1. **Run `/fix`** — invoke the fix skill to commit, lint, and format. +2. **Push the branch** to origin: `git push -u origin HEAD`. +3. **Create a PR if none exists**: + - Check: `gh pr view --json number 2>/dev/null` — if it fails, no PR exists yet. + - If no PR exists, create one: + - Use `git log main..HEAD --oneline` to understand all commits on the branch. + - Read the changed files with `git diff main...HEAD --stat` to understand scope. + - Create the PR with `gh pr create --title "" --body "" --label "skip-cargo-audit"`. + - The description must include: a **Summary** section (bullet points of what changed and why), a **Changes** section (key files/modules affected), and a **Test plan** section. + - If a PR already exists, just note its number/URL. + +## Phase 2: Watch CI and Fix Failures + +4. **Poll CI status** in a loop: + - Run: `gh pr checks --json name,state,conclusion,link --watch --fail-fast 2>/dev/null || gh pr checks` + - If `--watch` is not available, poll manually every 90 seconds using `gh pr checks --json name,state,conclusion,link` until all checks have completed (no checks with state "pending" or conclusion ""). + - **Ignore these known-flaky/irrelevant checks** — treat them as passing even if they fail: + - `validate-benchmarks` (benchmark CI — not relevant) + - Any `Contract E2E Tests` check that failed only due to a timeout (look for timeout in the failure link/logs) + - `cargo-audit` (we already added the skip label) + - Also ignore any checks related to `check-spec-version` and `e2e` tests — these are environment-dependent and not fixable from code. + +5. **If there are real CI failures** (failures NOT in the ignore list above): + - For EACH distinct failing check, launch a **separate Task subagent** (subagent_type: `general-purpose`, model: `sonnet`) in parallel. Each subagent must: + - Fetch the failed check's logs: use `gh run view --log-failed` or the check link to get failure details. + - Investigate the root cause by reading relevant source files. + - Return a **fix plan**: a description of what needs to change and in which files, with specific code snippets showing the fix. + - **Wait for all subagents** to return their fix plans. + +6. **Aggregate and apply fixes**: + - Review all returned fix plans for conflicts or overlaps. + - Apply the fixes using Edit/Write tools. + - Run `/fix` again (invoke the fix skill) to commit, lint, and format the fixes. + - Push: `git push`. + +7. **Re-check CI**: Go back to step 4 and poll again. Repeat the fix cycle up to **3 times**. If CI still fails after 3 rounds, report the remaining failures to the user and stop. + +## Phase 3: Code Review + +8. **Once CI is green** (or only ignored checks are failing), perform a thorough code review. + +9. **Launch a single Opus subagent** (subagent_type: `general-purpose`, model: `opus`) for the review: + - It must get the full PR diff: `git diff main...HEAD`. + - It must read every changed file in full. + - It must produce a numbered list of **issues** found, where each issue has: + - A unique sequential ID (e.g., `R-1`, `R-2`, ...). + - **Severity**: critical / major / minor / nit. + - **File and line(s)** affected. + - **Description** of the problem. + - The review must check for: correctness, safety (no panics, no unchecked arithmetic, no indexing), edge cases, naming, documentation gaps, test coverage, and adherence to Substrate/Rust best practices. + - Return the full list of issues. + +10. **For each issue**, launch TWO subagents **in parallel**: + - **Fix designer** (subagent_type: `general-purpose`, model: `sonnet`): Given the issue description and relevant code context, design a concrete proposed fix with exact code changes (old code -> new code). Return the fix as a structured plan. + - **Fix reviewer** (subagent_type: `general-purpose`, model: `opus`): Given the issue description, the relevant code context, and the proposed fix (once the fix designer returns — so the reviewer runs AFTER the designer, but reviewers for different issues run in parallel with each other). The reviewer must check: + - Does the fix actually solve the issue? + - Does it introduce new problems? + - Is it the simplest correct fix? + - Return: approved / rejected with reasoning. + + Implementation note: For each issue, first launch the fix designer. Once the fix designer for that issue returns, launch the fix reviewer for that issue. But all issues should be processed in parallel — i.e., launch all fix designers at once, then as each designer returns, launch its corresponding reviewer. You may batch reviewers if designers finish close together. + +11. **Report to user**: Present a formatted summary: + ``` + ## Code Review Results + + ### R-1: [severity] + **File**: path/to/file.rs:42 + **Issue**: <description> + **Proposed fix**: <summary of fix> + **Review**: Approved / Rejected — <reasoning> + + ### R-2: ... + ``` + Ask the user which fixes to apply (all approved ones, specific ones by ID, or none). + +## Important Rules + +- Never force-push. Always use regular `git push`. +- All CI polling must have a maximum total wall-clock timeout of 45 minutes. If CI hasn't finished by then, report current status and stop waiting. +- When fetching CI logs, if `gh run view` output is very long, focus on the failed step output only. +- Do NOT apply code review fixes automatically — always present them for user approval first. +- Use HEREDOC syntax for PR body and commit messages to preserve formatting. diff --git a/pallets/subtensor/src/tests/emission_suppression.rs b/pallets/subtensor/src/tests/emission_suppression.rs index 6be75ef28a..6e2774ed20 100644 --- a/pallets/subtensor/src/tests/emission_suppression.rs +++ b/pallets/subtensor/src/tests/emission_suppression.rs @@ -655,10 +655,7 @@ fn test_vote_explicit_false() { sn1, Some(false), )); - assert_eq!( - EmissionSuppressionVote::<Test>::get(sn1, ck), - Some(false) - ); + assert_eq!(EmissionSuppressionVote::<Test>::get(sn1, ck), Some(false)); // Collect votes: sole validator voted false → suppression should be 0. SubtensorModule::collect_emission_suppression_votes(sn1); From c3283ab01d308505523a560985f8e85ea8e91829 Mon Sep 17 00:00:00 2001 From: Pawel Polewicz <p.polewicz@gmail.com> Date: Thu, 12 Feb 2026 22:05:18 +0000 Subject: [PATCH 09/17] Address code review: R-1 fail-fast validation, R-2 weight fix, R-5 OwnedHotkeys, R-9 root guard, R-11 cache netuids, R-12 call_index order, R-13 saturating fold, R-14 narrow lints, R-16 allow vote cleanup - R-1: Move DestinationColdkeyHasExistingVotes check to top of do_swap_coldkey before any mutations; make transfer_emission_suppression_votes infallible - R-2: Increase vote_emission_suppression weight from reads(5) to reads(131) to account for up to 64 hotkeys at 2 reads each plus 3 base reads - R-5: Use OwnedHotkeys instead of StakingHotkeys for vote eligibility so only hotkeys owned by the coldkey qualify - R-9: Add root subnet guard to sudo_set_emission_suppression_override - R-11: Cache get_all_subnet_netuids in transfer_emission_suppression_votes - R-12: Reorder extrinsics so call indices appear in ascending order (132-135) - R-13: Replace .sum() with saturating_add fold in test - R-14: Remove blanket #![allow(unused)] from test file - R-16: Skip stake threshold check when clearing a vote (suppress=None) so deregistered coldkeys can clean up stale entries Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- pallets/subtensor/src/macros/dispatches.rs | 99 ++++++++++--------- pallets/subtensor/src/swap/swap_coldkey.rs | 27 +++-- .../src/tests/emission_suppression.rs | 5 +- 3 files changed, 67 insertions(+), 64 deletions(-) diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 9414af4fa2..807ea2200c 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -2580,6 +2580,26 @@ mod dispatches { Self::do_set_voting_power_ema_alpha(netuid, alpha) } + /// --- The extrinsic is a combination of add_stake(add_stake_limit) and burn_alpha. We buy + /// alpha token first and immediately burn the acquired amount of alpha (aka Subnet buyback). + #[pallet::call_index(132)] + #[pallet::weight(( + Weight::from_parts(368_000_000, 8556) + .saturating_add(T::DbWeight::get().reads(28_u64)) + .saturating_add(T::DbWeight::get().writes(17_u64)), + DispatchClass::Normal, + Pays::Yes + ))] + pub fn add_stake_burn( + origin: T::RuntimeOrigin, + hotkey: T::AccountId, + netuid: NetUid, + amount: TaoCurrency, + limit: Option<TaoCurrency>, + ) -> DispatchResult { + Self::do_add_stake_burn(origin, hotkey, netuid, amount, limit) + } + /// --- Set or clear the root override for emission suppression on a subnet. /// Some(true) forces suppression, Some(false) forces unsuppression, /// None removes the override and falls back to vote-based suppression. @@ -2598,6 +2618,7 @@ mod dispatches { ) -> DispatchResult { ensure_root(origin)?; ensure!(Self::if_subnet_exist(netuid), Error::<T>::SubnetNotExists); + ensure!(!netuid.is_root(), Error::<T>::CannotVoteOnRootSubnet); match override_value { Some(val) => EmissionSuppressionOverride::<T>::insert(netuid, val), None => EmissionSuppressionOverride::<T>::remove(netuid), @@ -2609,34 +2630,14 @@ mod dispatches { Ok(()) } - /// --- Set whether root validators continue receiving alpha dividends (sell pressure) - /// from emission-suppressed subnets. When true (default), root validators still - /// accumulate alpha on suppressed subnets. When false, all alpha goes to subnet - /// validators instead. - #[pallet::call_index(135)] - #[pallet::weight(( - Weight::from_parts(5_000_000, 0) - .saturating_add(T::DbWeight::get().writes(1)), - DispatchClass::Operational, - Pays::No - ))] - pub fn sudo_set_keep_root_sell_pressure_on_suppressed_subnets( - origin: OriginFor<T>, - value: bool, - ) -> DispatchResult { - ensure_root(origin)?; - KeepRootSellPressureOnSuppressedSubnets::<T>::put(value); - Self::deposit_event(Event::KeepRootSellPressureOnSuppressedSubnetsSet { value }); - Ok(()) - } - /// --- Vote to suppress or unsuppress emissions for a subnet. /// The caller must be a coldkey that owns at least one hotkey registered on root /// with stake >= StakeThreshold. Pass suppress=None to clear the vote. #[pallet::call_index(134)] #[pallet::weight(( Weight::from_parts(20_000_000, 0) - .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().reads(128)) .saturating_add(T::DbWeight::get().writes(1)), DispatchClass::Normal, Pays::Yes @@ -2650,15 +2651,20 @@ mod dispatches { ensure!(Self::if_subnet_exist(netuid), Error::<T>::SubnetNotExists); ensure!(!netuid.is_root(), Error::<T>::CannotVoteOnRootSubnet); - // Coldkey must own at least one hotkey registered on root with enough stake. - let stake_threshold = Self::get_stake_threshold(); - let hotkeys = StakingHotkeys::<T>::get(&coldkey); - let has_qualifying_hotkey = hotkeys.iter().any(|hk| { - Self::is_hotkey_registered_on_network(NetUid::ROOT, hk) - && u64::from(Self::get_stake_for_hotkey_on_subnet(hk, NetUid::ROOT)) - >= stake_threshold - }); - ensure!(has_qualifying_hotkey, Error::<T>::NotEnoughStakeToVote); + // Only require root registration + stake threshold when *setting* a vote. + // Clearing (None) is always allowed so that deregistered/understaked coldkeys + // can clean up their own stale entries. + if suppress.is_some() { + // Coldkey must own at least one hotkey registered on root with enough stake. + let stake_threshold = Self::get_stake_threshold(); + let hotkeys = OwnedHotkeys::<T>::get(&coldkey); + let has_qualifying_hotkey = hotkeys.iter().any(|hk| { + Self::is_hotkey_registered_on_network(NetUid::ROOT, hk) + && u64::from(Self::get_stake_for_hotkey_on_subnet(hk, NetUid::ROOT)) + >= stake_threshold + }); + ensure!(has_qualifying_hotkey, Error::<T>::NotEnoughStakeToVote); + } match suppress { Some(val) => EmissionSuppressionVote::<T>::insert(netuid, &coldkey, val), @@ -2672,24 +2678,25 @@ mod dispatches { Ok(()) } - /// --- The extrinsic is a combination of add_stake(add_stake_limit) and burn_alpha. We buy - /// alpha token first and immediately burn the acquired amount of alpha (aka Subnet buyback). - #[pallet::call_index(132)] + /// --- Set whether root validators continue receiving alpha dividends (sell pressure) + /// from emission-suppressed subnets. When true (default), root validators still + /// accumulate alpha on suppressed subnets. When false, all alpha goes to subnet + /// validators instead. + #[pallet::call_index(135)] #[pallet::weight(( - Weight::from_parts(368_000_000, 8556) - .saturating_add(T::DbWeight::get().reads(28_u64)) - .saturating_add(T::DbWeight::get().writes(17_u64)), - DispatchClass::Normal, - Pays::Yes + Weight::from_parts(5_000_000, 0) + .saturating_add(T::DbWeight::get().writes(1)), + DispatchClass::Operational, + Pays::No ))] - pub fn add_stake_burn( - origin: T::RuntimeOrigin, - hotkey: T::AccountId, - netuid: NetUid, - amount: TaoCurrency, - limit: Option<TaoCurrency>, + pub fn sudo_set_keep_root_sell_pressure_on_suppressed_subnets( + origin: OriginFor<T>, + value: bool, ) -> DispatchResult { - Self::do_add_stake_burn(origin, hotkey, netuid, amount, limit) + ensure_root(origin)?; + KeepRootSellPressureOnSuppressedSubnets::<T>::put(value); + Self::deposit_event(Event::KeepRootSellPressureOnSuppressedSubnetsSet { value }); + Ok(()) } } } diff --git a/pallets/subtensor/src/swap/swap_coldkey.rs b/pallets/subtensor/src/swap/swap_coldkey.rs index ae9bf8cab8..4491f732d8 100644 --- a/pallets/subtensor/src/swap/swap_coldkey.rs +++ b/pallets/subtensor/src/swap/swap_coldkey.rs @@ -16,6 +16,13 @@ impl<T: Config> Pallet<T> { !Self::hotkey_account_exists(new_coldkey), Error::<T>::NewColdKeyIsHotkey ); + // Verify the destination coldkey has no existing emission suppression votes. + for netuid in Self::get_all_subnet_netuids() { + ensure!( + EmissionSuppressionVote::<T>::get(netuid, new_coldkey).is_none(), + Error::<T>::DestinationColdkeyHasExistingVotes + ); + } // Swap the identity if the old coldkey has one and the new coldkey doesn't if IdentitiesV2::<T>::get(new_coldkey).is_none() @@ -31,7 +38,7 @@ impl<T: Config> Pallet<T> { } Self::transfer_staking_hotkeys(old_coldkey, new_coldkey); Self::transfer_hotkeys_ownership(old_coldkey, new_coldkey); - Self::transfer_emission_suppression_votes(old_coldkey, new_coldkey)?; + Self::transfer_emission_suppression_votes(old_coldkey, new_coldkey); // Transfer any remaining balance from old_coldkey to new_coldkey let remaining_balance = Self::get_coldkey_balance(old_coldkey); @@ -164,23 +171,13 @@ impl<T: Config> Pallet<T> { /// Transfer emission suppression votes from the old coldkey to the new coldkey. /// Since EmissionSuppressionVote is keyed by (netuid, coldkey), we must iterate /// all subnets to find votes belonging to the old coldkey. - /// Fails if the new coldkey already has any emission suppression votes. - fn transfer_emission_suppression_votes( - old_coldkey: &T::AccountId, - new_coldkey: &T::AccountId, - ) -> DispatchResult { - // First pass: verify the destination has no existing votes. - for netuid in Self::get_all_subnet_netuids() { - if EmissionSuppressionVote::<T>::get(netuid, new_coldkey).is_some() { - return Err(Error::<T>::DestinationColdkeyHasExistingVotes.into()); - } - } - // Second pass: move votes. - for netuid in Self::get_all_subnet_netuids() { + /// NOTE: The caller must verify the new coldkey has no existing votes before calling this. + fn transfer_emission_suppression_votes(old_coldkey: &T::AccountId, new_coldkey: &T::AccountId) { + let all_netuids = Self::get_all_subnet_netuids(); + for netuid in all_netuids { if let Some(vote) = EmissionSuppressionVote::<T>::take(netuid, old_coldkey) { EmissionSuppressionVote::<T>::insert(netuid, new_coldkey, vote); } } - Ok(()) } } diff --git a/pallets/subtensor/src/tests/emission_suppression.rs b/pallets/subtensor/src/tests/emission_suppression.rs index 6e2774ed20..b4f251156d 100644 --- a/pallets/subtensor/src/tests/emission_suppression.rs +++ b/pallets/subtensor/src/tests/emission_suppression.rs @@ -1,9 +1,8 @@ -#![allow(unused, clippy::indexing_slicing, clippy::panic, clippy::unwrap_used)] +#![allow(clippy::indexing_slicing, clippy::panic, clippy::unwrap_used)] use super::mock::*; use crate::*; use alloc::collections::BTreeMap; use frame_support::{assert_err, assert_ok}; -use frame_system::pallet_prelude::BlockNumberFor; use sp_core::U256; use substrate_fixed::types::{U64F64, U96F32}; use subtensor_runtime_common::{AlphaCurrency, NetUid, TaoCurrency}; @@ -697,7 +696,7 @@ fn test_all_subnets_suppressed() { let total: u64 = emissions .values() .map(|e| e.saturating_to_num::<u64>()) - .sum(); + .fold(0u64, |a, b| a.saturating_add(b)); assert_eq!(total, 0, "all-suppressed should yield zero total emission"); }); } From df8f8f41ebc89fa3f3b2bc3fff7e0c201b8b1d0d Mon Sep 17 00:00:00 2001 From: Pawel Polewicz <p.polewicz@gmail.com> Date: Thu, 12 Feb 2026 22:27:43 +0000 Subject: [PATCH 10/17] Fix unused variable warnings in emission_suppression tests Fix compiler warnings for unused variables by prefixing them with underscores. All tests pass successfully. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- pallets/subtensor/src/tests/emission_suppression.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pallets/subtensor/src/tests/emission_suppression.rs b/pallets/subtensor/src/tests/emission_suppression.rs index b4f251156d..4f24889968 100644 --- a/pallets/subtensor/src/tests/emission_suppression.rs +++ b/pallets/subtensor/src/tests/emission_suppression.rs @@ -236,7 +236,7 @@ fn test_vote_clear() { let sn1 = NetUid::from(1); setup_subnet_with_flow(sn1, 10, 100_000_000); - let (hotkey, coldkey) = setup_root_validator(10, 11, 1_000_000); + let (_hotkey, coldkey) = setup_root_validator(10, 11, 1_000_000); // Vote to suppress. assert_ok!(SubtensorModule::vote_emission_suppression( @@ -274,7 +274,7 @@ fn test_votes_collected_on_epoch() { let sn1 = NetUid::from(1); setup_subnet_with_flow(sn1, 10, 100_000_000); - let (hotkey, coldkey) = setup_root_validator(10, 11, 1_000_000); + let (_hotkey, coldkey) = setup_root_validator(10, 11, 1_000_000); // Vote to suppress. assert_ok!(SubtensorModule::vote_emission_suppression( @@ -308,7 +308,7 @@ fn test_coldkey_swap_migrates_votes() { let sn1 = NetUid::from(1); setup_subnet_with_flow(sn1, 10, 100_000_000); - let (hotkey, old_coldkey) = setup_root_validator(10, 11, 1_000_000); + let (_hotkey, old_coldkey) = setup_root_validator(10, 11, 1_000_000); // Vote to suppress. assert_ok!(SubtensorModule::vote_emission_suppression( @@ -345,7 +345,7 @@ fn test_dissolution_clears_all() { let sn1 = NetUid::from(1); setup_subnet_with_flow(sn1, 10, 100_000_000); - let (hotkey, coldkey) = setup_root_validator(10, 11, 1_000_000); + let (_hotkey, coldkey) = setup_root_validator(10, 11, 1_000_000); // Vote and set suppression. assert_ok!(SubtensorModule::vote_emission_suppression( @@ -416,7 +416,7 @@ fn test_unstaked_tao_not_in_denominator() { // Two root validators: one votes suppress, one doesn't. let (_hk1, ck1) = setup_root_validator(10, 11, 1_000_000); - let (_hk2, ck2) = setup_root_validator(20, 21, 1_000_000); + let (_hk2, _ck2) = setup_root_validator(20, 21, 1_000_000); // Only ck1 votes to suppress. assert_ok!(SubtensorModule::vote_emission_suppression( From b9dbe951a3e9546d4a3d59e6f5c987b383ff4eb9 Mon Sep 17 00:00:00 2001 From: Pawel Polewicz <p.polewicz@gmail.com> Date: Thu, 12 Feb 2026 23:39:49 +0000 Subject: [PATCH 11/17] Fix skills location --- .claude/skills/{fix.md => fix/SKILL.md} | 0 .claude/skills/{ship.md => ship/SKILL.md} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename .claude/skills/{fix.md => fix/SKILL.md} (100%) rename .claude/skills/{ship.md => ship/SKILL.md} (100%) diff --git a/.claude/skills/fix.md b/.claude/skills/fix/SKILL.md similarity index 100% rename from .claude/skills/fix.md rename to .claude/skills/fix/SKILL.md diff --git a/.claude/skills/ship.md b/.claude/skills/ship/SKILL.md similarity index 100% rename from .claude/skills/ship.md rename to .claude/skills/ship/SKILL.md From bf0b57458c48b790a90640da10708dec51cff81c Mon Sep 17 00:00:00 2001 From: Pawel Polewicz <p.polewicz@gmail.com> Date: Fri, 13 Feb 2026 00:10:34 +0000 Subject: [PATCH 12/17] Replace KeepRootSellPressure bool with RootSellPressureOnSuppressedSubnetsMode enum Introduce a 3-mode enum (Disable/Enable/Recycle) to control how root alpha dividends are handled on emission-suppressed subnets. Recycle mode (new default) swaps root alpha to TAO via AMM and burns it, creating sell pressure without benefiting root validators. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- .../subtensor/src/coinbase/run_coinbase.rs | 33 +- pallets/subtensor/src/lib.rs | 38 +- pallets/subtensor/src/macros/dispatches.rs | 16 +- pallets/subtensor/src/macros/events.rs | 8 +- .../src/tests/emission_suppression.rs | 401 +++++++++++++++++- 5 files changed, 449 insertions(+), 47 deletions(-) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 05a6f6e43a..d9666dd89e 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -183,6 +183,7 @@ impl<T: Config> Pallet<T> { // --- 3. Inject ALPHA for participants. let cut_percent: U96F32 = Self::get_float_subnet_owner_cut(); + let suppression_mode = KeepRootSellPressureOnSuppressedSubnets::<T>::get(); for netuid_i in subnets_to_emit_to.iter() { // Get alpha_out for this block. @@ -209,10 +210,10 @@ impl<T: Config> Pallet<T> { log::debug!("root_proportion: {root_proportion:?}"); // Get root alpha from root prop. - // If the subnet is suppressed and KeepRootSellPressureOnSuppressedSubnets - // is false, zero out root alpha so all validator alpha goes to subnet validators. + // When mode is Disable and subnet is suppressed, zero out root alpha + // so all validator alpha goes to subnet validators. let root_alpha: U96F32 = if Self::is_subnet_emission_suppressed(*netuid_i) - && !KeepRootSellPressureOnSuppressedSubnets::<T>::get() + && suppression_mode == RootSellPressureOnSuppressedSubnetsMode::Disable { asfloat!(0) } else { @@ -243,10 +244,28 @@ impl<T: Config> Pallet<T> { }); if root_sell_flag { - // Only accumulate root alpha divs if root sell is allowed. - PendingRootAlphaDivs::<T>::mutate(*netuid_i, |total| { - *total = total.saturating_add(tou64!(root_alpha).into()); - }); + // Determine disposition of root alpha based on suppression mode. + let is_suppressed = Self::is_subnet_emission_suppressed(*netuid_i); + if is_suppressed + && suppression_mode == RootSellPressureOnSuppressedSubnetsMode::Recycle + { + // Recycle mode: swap alpha → TAO via AMM, then burn the TAO. + let root_alpha_currency = + AlphaCurrency::from(tou64!(root_alpha)); + if let Ok(swap_result) = Self::swap_alpha_for_tao( + *netuid_i, + root_alpha_currency, + TaoCurrency::ZERO, // no price limit + true, // drop fees + ) { + Self::recycle_tao(swap_result.amount_paid_out); + } + } else { + // Enable mode (or non-suppressed subnet): accumulate for root validators. + PendingRootAlphaDivs::<T>::mutate(*netuid_i, |total| { + *total = total.saturating_add(tou64!(root_alpha).into()); + }); + } } else { // If we are not selling the root alpha, we should recycle it. Self::recycle_subnet_alpha(*netuid_i, AlphaCurrency::from(tou64!(root_alpha))); diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 877418e07c..df3ed5a5fa 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -344,6 +344,29 @@ pub mod pallet { }, } + /// Controls how root alpha dividends are handled on emission-suppressed subnets. + #[derive( + Encode, + Decode, + Default, + TypeInfo, + Clone, + Copy, + PartialEq, + Eq, + Debug, + DecodeWithMemTracking, + )] + pub enum RootSellPressureOnSuppressedSubnetsMode { + /// Root gets no alpha on suppressed subnets; alpha recycled to subnet. + Disable, + /// Root still accumulates alpha on suppressed subnets (old `true`). + Enable, + /// Root alpha is swapped to TAO via AMM and the TAO is burned. + #[default] + Recycle, + } + /// Default minimum root claim amount. /// This is the minimum amount of root claim that can be made. /// Any amount less than this will not be claimed. @@ -2439,18 +2462,13 @@ pub mod pallet { pub type EmissionSuppressionVote<T: Config> = StorageDoubleMap<_, Identity, NetUid, Blake2_128Concat, T::AccountId, bool, OptionQuery>; - /// Whether root validators continue receiving alpha dividends (sell pressure) - /// from suppressed subnets. Default: true (maintain sell pressure). - /// When false, all alpha goes to subnet validators instead. + /// Controls how root alpha dividends are handled on emission-suppressed subnets. + /// - Disable (0x00): root gets no alpha; alpha recycled to subnet validators. + /// - Enable (0x01): root still accumulates alpha (old behaviour). + /// - Recycle (0x02, default): root alpha swapped to TAO and TAO burned. #[pallet::storage] pub type KeepRootSellPressureOnSuppressedSubnets<T: Config> = - StorageValue<_, bool, ValueQuery, KeepRootSellPressureDefault>; - - /// Default value for KeepRootSellPressureOnSuppressedSubnets (true). - #[pallet::type_value] - pub fn KeepRootSellPressureDefault() -> bool { - true - } + StorageValue<_, RootSellPressureOnSuppressedSubnetsMode, ValueQuery>; #[pallet::genesis_config] pub struct GenesisConfig<T: Config> { diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 807ea2200c..c853409fbc 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -2678,10 +2678,10 @@ mod dispatches { Ok(()) } - /// --- Set whether root validators continue receiving alpha dividends (sell pressure) - /// from emission-suppressed subnets. When true (default), root validators still - /// accumulate alpha on suppressed subnets. When false, all alpha goes to subnet - /// validators instead. + /// --- Set the mode for root alpha dividends on emission-suppressed subnets. + /// - Disable: root gets no alpha; alpha recycled to subnet validators. + /// - Enable: root still accumulates alpha (old behaviour). + /// - Recycle: root alpha swapped to TAO via AMM, TAO burned. #[pallet::call_index(135)] #[pallet::weight(( Weight::from_parts(5_000_000, 0) @@ -2689,13 +2689,13 @@ mod dispatches { DispatchClass::Operational, Pays::No ))] - pub fn sudo_set_keep_root_sell_pressure_on_suppressed_subnets( + pub fn sudo_set_root_sell_pressure_on_suppressed_subnets_mode( origin: OriginFor<T>, - value: bool, + mode: RootSellPressureOnSuppressedSubnetsMode, ) -> DispatchResult { ensure_root(origin)?; - KeepRootSellPressureOnSuppressedSubnets::<T>::put(value); - Self::deposit_event(Event::KeepRootSellPressureOnSuppressedSubnetsSet { value }); + KeepRootSellPressureOnSuppressedSubnets::<T>::put(mode); + Self::deposit_event(Event::RootSellPressureOnSuppressedSubnetsModeSet { mode }); Ok(()) } } diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index e1c928285c..3225970f6e 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -535,10 +535,10 @@ mod events { override_value: Option<bool>, }, - /// Root set the KeepRootSellPressureOnSuppressedSubnets flag. - KeepRootSellPressureOnSuppressedSubnetsSet { - /// The new value - value: bool, + /// Root set the RootSellPressureOnSuppressedSubnetsMode. + RootSellPressureOnSuppressedSubnetsModeSet { + /// The new mode + mode: RootSellPressureOnSuppressedSubnetsMode, }, /// "Add stake and burn" event: alpha token was purchased and burned. diff --git a/pallets/subtensor/src/tests/emission_suppression.rs b/pallets/subtensor/src/tests/emission_suppression.rs index 4f24889968..13b85a0368 100644 --- a/pallets/subtensor/src/tests/emission_suppression.rs +++ b/pallets/subtensor/src/tests/emission_suppression.rs @@ -459,7 +459,7 @@ fn setup_root_with_tao(sn: NetUid) { } // ───────────────────────────────────────────────────────────────────────────── -// Test 14: Suppress subnet, default flag=true → root still gets alpha +// Test 14: Suppress subnet, Enable mode → root still gets alpha // ───────────────────────────────────────────────────────────────────────────── #[test] fn test_suppressed_subnet_root_alpha_by_default() { @@ -488,8 +488,14 @@ fn test_suppressed_subnet_root_alpha_by_default() { // Force-suppress sn1. EmissionSuppressionOverride::<Test>::insert(sn1, true); - // Default: KeepRootSellPressureOnSuppressedSubnets = true. - assert!(KeepRootSellPressureOnSuppressedSubnets::<Test>::get()); + // Default mode is Recycle; verify that, then set to Enable for this test. + assert_eq!( + KeepRootSellPressureOnSuppressedSubnets::<Test>::get(), + RootSellPressureOnSuppressedSubnetsMode::Recycle, + ); + KeepRootSellPressureOnSuppressedSubnets::<Test>::put( + RootSellPressureOnSuppressedSubnetsMode::Enable, + ); // Clear any pending emissions. PendingRootAlphaDivs::<Test>::insert(sn1, AlphaCurrency::ZERO); @@ -504,13 +510,13 @@ fn test_suppressed_subnet_root_alpha_by_default() { let pending_root = PendingRootAlphaDivs::<Test>::get(sn1); assert!( pending_root > AlphaCurrency::ZERO, - "with flag=true, root should still get alpha on suppressed subnet" + "with Enable mode, root should still get alpha on suppressed subnet" ); }); } // ───────────────────────────────────────────────────────────────────────────── -// Test 15: Suppress subnet, flag=false → root gets no alpha +// Test 15: Suppress subnet, Disable mode → root gets no alpha // ───────────────────────────────────────────────────────────────────────────── #[test] fn test_suppressed_subnet_no_root_alpha_flag_off() { @@ -538,8 +544,10 @@ fn test_suppressed_subnet_no_root_alpha_flag_off() { // Force-suppress sn1. EmissionSuppressionOverride::<Test>::insert(sn1, true); - // Set flag to false: no root sell pressure on suppressed subnets. - KeepRootSellPressureOnSuppressedSubnets::<Test>::put(false); + // Set mode to Disable: no root sell pressure on suppressed subnets. + KeepRootSellPressureOnSuppressedSubnets::<Test>::put( + RootSellPressureOnSuppressedSubnetsMode::Disable, + ); // Clear any pending emissions. PendingRootAlphaDivs::<Test>::insert(sn1, AlphaCurrency::ZERO); @@ -556,7 +564,7 @@ fn test_suppressed_subnet_no_root_alpha_flag_off() { assert_eq!( pending_root, AlphaCurrency::ZERO, - "with flag=false, root should get no alpha on suppressed subnet" + "with Disable mode, root should get no alpha on suppressed subnet" ); // But validator emission should be non-zero (all alpha goes to validators). @@ -569,7 +577,7 @@ fn test_suppressed_subnet_no_root_alpha_flag_off() { } // ───────────────────────────────────────────────────────────────────────────── -// Test 16: Non-suppressed subnet → root alpha normal regardless of flag +// Test 16: Non-suppressed subnet → root alpha normal regardless of mode // ───────────────────────────────────────────────────────────────────────────── #[test] fn test_unsuppressed_subnet_unaffected_by_flag() { @@ -594,8 +602,10 @@ fn test_unsuppressed_subnet_unaffected_by_flag() { setup_root_with_tao(sn1); // sn1 is NOT suppressed. - // Set flag to false (should not matter for unsuppressed subnets). - KeepRootSellPressureOnSuppressedSubnets::<Test>::put(false); + // Set mode to Disable (should not matter for unsuppressed subnets). + KeepRootSellPressureOnSuppressedSubnets::<Test>::put( + RootSellPressureOnSuppressedSubnetsMode::Disable, + ); PendingRootAlphaDivs::<Test>::insert(sn1, AlphaCurrency::ZERO); @@ -608,7 +618,7 @@ fn test_unsuppressed_subnet_unaffected_by_flag() { let pending_root = PendingRootAlphaDivs::<Test>::get(sn1); assert!( pending_root > AlphaCurrency::ZERO, - "non-suppressed subnet should still give root alpha regardless of flag" + "non-suppressed subnet should still give root alpha regardless of mode" ); }); } @@ -839,7 +849,7 @@ fn test_sudo_override_emits_event() { } // ───────────────────────────────────────────────────────────────────────────── -// Test 23: sudo_set_keep_root_sell_pressure emits event +// Test 23: sudo_set_root_sell_pressure_on_suppressed_subnets_mode emits event // ───────────────────────────────────────────────────────────────────────────── #[test] fn test_sudo_sell_pressure_emits_event() { @@ -848,9 +858,9 @@ fn test_sudo_sell_pressure_emits_event() { System::reset_events(); assert_ok!( - SubtensorModule::sudo_set_keep_root_sell_pressure_on_suppressed_subnets( + SubtensorModule::sudo_set_root_sell_pressure_on_suppressed_subnets_mode( RuntimeOrigin::root(), - false, + RootSellPressureOnSuppressedSubnetsMode::Disable, ) ); @@ -859,11 +869,11 @@ fn test_sudo_sell_pressure_emits_event() { matches!( &e.event, RuntimeEvent::SubtensorModule( - Event::KeepRootSellPressureOnSuppressedSubnetsSet { value } - ) if !(*value) + Event::RootSellPressureOnSuppressedSubnetsModeSet { mode } + ) if *mode == RootSellPressureOnSuppressedSubnetsMode::Disable ) }), - "should emit KeepRootSellPressureOnSuppressedSubnetsSet event" + "should emit RootSellPressureOnSuppressedSubnetsModeSet event" ); }); } @@ -892,3 +902,358 @@ fn test_collect_votes_skips_root() { ); }); } + +// ───────────────────────────────────────────────────────────────────────────── +// Test 25: default mode is Recycle +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_default_mode_is_recycle() { + new_test_ext(1).execute_with(|| { + assert_eq!( + KeepRootSellPressureOnSuppressedSubnets::<Test>::get(), + RootSellPressureOnSuppressedSubnetsMode::Recycle, + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 26: Recycle mode, suppressed subnet → alpha swapped to TAO, TAO burned +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_recycle_mode_suppressed_subnet_swaps_and_recycles() { + new_test_ext(1).execute_with(|| { + add_network(NetUid::ROOT, 1, 0); + let sn1 = NetUid::from(1); + setup_subnet_with_flow(sn1, 10, 100_000_000); + + // Make it a dynamic subnet so swap_alpha_for_tao actually works via AMM. + SubnetMechanism::<Test>::insert(sn1, 1); + + // Seed the pool with TAO and alpha reserves. + let initial_tao = TaoCurrency::from(500_000_000u64); + let initial_alpha_in = AlphaCurrency::from(500_000_000u64); + SubnetTAO::<Test>::insert(sn1, initial_tao); + SubnetAlphaIn::<Test>::insert(sn1, initial_alpha_in); + + // Also set root TAO so root_proportion is nonzero. + SubnetTAO::<Test>::insert(NetUid::ROOT, TaoCurrency::from(1_000_000_000)); + SubnetAlphaOut::<Test>::insert(sn1, AlphaCurrency::from(1_000_000_000)); + + // Register a root validator. + let hotkey = U256::from(10); + let coldkey = U256::from(11); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(coldkey), + hotkey, + )); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + NetUid::ROOT, + 1_000_000_000u64.into(), + ); + SubtensorModule::set_tao_weight(u64::MAX); + + // Force-suppress sn1. + EmissionSuppressionOverride::<Test>::insert(sn1, true); + + // Default mode is Recycle. + assert_eq!( + KeepRootSellPressureOnSuppressedSubnets::<Test>::get(), + RootSellPressureOnSuppressedSubnetsMode::Recycle, + ); + + // Record TotalIssuance before emission. + let issuance_before = TotalIssuance::<Test>::get(); + + // Clear pending. + PendingRootAlphaDivs::<Test>::insert(sn1, AlphaCurrency::ZERO); + + // Build emission map. + let mut subnet_emissions = BTreeMap::new(); + subnet_emissions.insert(sn1, U96F32::from_num(1_000_000)); + + SubtensorModule::emit_to_subnets(&[sn1], &subnet_emissions, true); + + // PendingRootAlphaDivs should be 0 (root did NOT accumulate alpha). + let pending_root = PendingRootAlphaDivs::<Test>::get(sn1); + assert_eq!( + pending_root, + AlphaCurrency::ZERO, + "in Recycle mode, PendingRootAlphaDivs should be 0" + ); + + // SubnetAlphaIn should have increased (alpha was swapped into pool). + let alpha_in_after = SubnetAlphaIn::<Test>::get(sn1); + assert!( + alpha_in_after > initial_alpha_in, + "SubnetAlphaIn should increase after swap" + ); + + // TotalIssuance should have decreased (TAO was recycled/burned). + let issuance_after = TotalIssuance::<Test>::get(); + assert!( + issuance_after < issuance_before, + "TotalIssuance should decrease (TAO recycled)" + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 27: Recycle mode on non-suppressed subnet → normal PendingRootAlphaDivs +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_recycle_mode_non_suppressed_subnet_normal() { + new_test_ext(1).execute_with(|| { + add_network(NetUid::ROOT, 1, 0); + let sn1 = NetUid::from(1); + setup_subnet_with_flow(sn1, 10, 100_000_000); + + let hotkey = U256::from(10); + let coldkey = U256::from(11); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(coldkey), + hotkey, + )); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + NetUid::ROOT, + 1_000_000_000u64.into(), + ); + SubtensorModule::set_tao_weight(u64::MAX); + setup_root_with_tao(sn1); + + // sn1 is NOT suppressed. Mode is Recycle (default). + assert_eq!( + KeepRootSellPressureOnSuppressedSubnets::<Test>::get(), + RootSellPressureOnSuppressedSubnetsMode::Recycle, + ); + + PendingRootAlphaDivs::<Test>::insert(sn1, AlphaCurrency::ZERO); + + let mut subnet_emissions = BTreeMap::new(); + subnet_emissions.insert(sn1, U96F32::from_num(1_000_000)); + + SubtensorModule::emit_to_subnets(&[sn1], &subnet_emissions, true); + + // Root should still get alpha — Recycle only affects suppressed subnets. + let pending_root = PendingRootAlphaDivs::<Test>::get(sn1); + assert!( + pending_root > AlphaCurrency::ZERO, + "non-suppressed subnet should still give root alpha in Recycle mode" + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 28: Recycle mode ignores RootClaimType (alpha never enters claim flow) +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_recycle_mode_ignores_root_claim_type() { + new_test_ext(1).execute_with(|| { + add_network(NetUid::ROOT, 1, 0); + let sn1 = NetUid::from(1); + setup_subnet_with_flow(sn1, 10, 100_000_000); + + // Dynamic subnet for AMM swap. + SubnetMechanism::<Test>::insert(sn1, 1); + SubnetTAO::<Test>::insert(sn1, TaoCurrency::from(500_000_000u64)); + SubnetAlphaIn::<Test>::insert(sn1, AlphaCurrency::from(500_000_000u64)); + SubnetTAO::<Test>::insert(NetUid::ROOT, TaoCurrency::from(1_000_000_000)); + SubnetAlphaOut::<Test>::insert(sn1, AlphaCurrency::from(1_000_000_000)); + + let hotkey = U256::from(10); + let coldkey = U256::from(11); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(coldkey), + hotkey, + )); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + NetUid::ROOT, + 1_000_000_000u64.into(), + ); + SubtensorModule::set_tao_weight(u64::MAX); + + // Force-suppress sn1. + EmissionSuppressionOverride::<Test>::insert(sn1, true); + + // Set RootClaimType to Keep — in normal flow this would keep alpha. + // But Recycle mode should override and swap+burn regardless. + RootClaimType::<Test>::insert(coldkey, RootClaimTypeEnum::Keep); + + // Default mode is Recycle. + assert_eq!( + KeepRootSellPressureOnSuppressedSubnets::<Test>::get(), + RootSellPressureOnSuppressedSubnetsMode::Recycle, + ); + + PendingRootAlphaDivs::<Test>::insert(sn1, AlphaCurrency::ZERO); + + let issuance_before = TotalIssuance::<Test>::get(); + + let mut subnet_emissions = BTreeMap::new(); + subnet_emissions.insert(sn1, U96F32::from_num(1_000_000)); + + SubtensorModule::emit_to_subnets(&[sn1], &subnet_emissions, true); + + // PendingRootAlphaDivs should still be 0 (recycled, not claimed). + let pending_root = PendingRootAlphaDivs::<Test>::get(sn1); + assert_eq!( + pending_root, + AlphaCurrency::ZERO, + "Recycle mode should swap+burn regardless of RootClaimType" + ); + + // TAO was burned. + let issuance_after = TotalIssuance::<Test>::get(); + assert!( + issuance_after < issuance_before, + "TotalIssuance should decrease even with RootClaimType::Keep" + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 29: sudo_set_mode all 3 variants emit events +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_sudo_set_mode_all_variants_emit_events() { + new_test_ext(1).execute_with(|| { + System::set_block_number(1); + + for mode in [ + RootSellPressureOnSuppressedSubnetsMode::Disable, + RootSellPressureOnSuppressedSubnetsMode::Enable, + RootSellPressureOnSuppressedSubnetsMode::Recycle, + ] { + System::reset_events(); + + assert_ok!( + SubtensorModule::sudo_set_root_sell_pressure_on_suppressed_subnets_mode( + RuntimeOrigin::root(), + mode, + ) + ); + + assert_eq!( + KeepRootSellPressureOnSuppressedSubnets::<Test>::get(), + mode, + ); + + assert!( + System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule( + Event::RootSellPressureOnSuppressedSubnetsModeSet { mode: m } + ) if *m == mode + ) + }), + "should emit RootSellPressureOnSuppressedSubnetsModeSet for {mode:?}" + ); + } + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 30: Recycle mode decreases price and flow EMA; Disable/Enable do not +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_recycle_mode_decreases_price_and_flow_ema() { + new_test_ext(1).execute_with(|| { + add_network(NetUid::ROOT, 1, 0); + let sn1 = NetUid::from(1); + setup_subnet_with_flow(sn1, 10, 100_000_000); + + // Dynamic subnet. + SubnetMechanism::<Test>::insert(sn1, 1); + + // Large pool reserves to ensure swaps produce measurable effects. + let pool_reserve = 1_000_000_000u64; + SubnetTAO::<Test>::insert(sn1, TaoCurrency::from(pool_reserve)); + SubnetAlphaIn::<Test>::insert(sn1, AlphaCurrency::from(pool_reserve)); + SubnetTAO::<Test>::insert(NetUid::ROOT, TaoCurrency::from(pool_reserve)); + SubnetAlphaOut::<Test>::insert(sn1, AlphaCurrency::from(pool_reserve)); + + let hotkey = U256::from(10); + let coldkey = U256::from(11); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(coldkey), + hotkey, + )); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + NetUid::ROOT, + 1_000_000_000u64.into(), + ); + SubtensorModule::set_tao_weight(u64::MAX); + + // Force-suppress sn1. + EmissionSuppressionOverride::<Test>::insert(sn1, true); + + let emission_amount = U96F32::from_num(10_000_000); + let mut subnet_emissions = BTreeMap::new(); + subnet_emissions.insert(sn1, emission_amount); + + // ── First: verify that Disable and Enable modes do NOT cause TAO outflow ── + + for mode in [ + RootSellPressureOnSuppressedSubnetsMode::Disable, + RootSellPressureOnSuppressedSubnetsMode::Enable, + ] { + // Reset pool state. + SubnetTAO::<Test>::insert(sn1, TaoCurrency::from(pool_reserve)); + SubnetAlphaIn::<Test>::insert(sn1, AlphaCurrency::from(pool_reserve)); + SubnetTaoFlow::<Test>::insert(sn1, 0i64); + PendingRootAlphaDivs::<Test>::insert(sn1, AlphaCurrency::ZERO); + SubnetAlphaOut::<Test>::insert(sn1, AlphaCurrency::from(pool_reserve)); + + KeepRootSellPressureOnSuppressedSubnets::<Test>::put(mode); + + SubtensorModule::emit_to_subnets(&[sn1], &subnet_emissions, true); + + let flow = SubnetTaoFlow::<Test>::get(sn1); + assert!( + flow >= 0, + "mode {mode:?}: SubnetTaoFlow should not be negative, got {flow}" + ); + } + + // ── Now: verify that Recycle mode DOES cause TAO outflow ── + + // Reset pool state. + SubnetTAO::<Test>::insert(sn1, TaoCurrency::from(pool_reserve)); + SubnetAlphaIn::<Test>::insert(sn1, AlphaCurrency::from(pool_reserve)); + SubnetTaoFlow::<Test>::insert(sn1, 0i64); + PendingRootAlphaDivs::<Test>::insert(sn1, AlphaCurrency::ZERO); + SubnetAlphaOut::<Test>::insert(sn1, AlphaCurrency::from(pool_reserve)); + + // Set Recycle mode. + KeepRootSellPressureOnSuppressedSubnets::<Test>::put( + RootSellPressureOnSuppressedSubnetsMode::Recycle, + ); + + // Record price before. + let price_before = SubnetMovingPrice::<Test>::get(sn1); + + SubtensorModule::emit_to_subnets(&[sn1], &subnet_emissions, true); + + // SubnetTaoFlow should be negative (TAO left the pool via swap). + let flow_after = SubnetTaoFlow::<Test>::get(sn1); + assert!( + flow_after < 0, + "Recycle mode: SubnetTaoFlow should be negative (TAO outflow), got {flow_after}" + ); + + // Moving price should have decreased (alpha was sold into pool for TAO). + let price_after = SubnetMovingPrice::<Test>::get(sn1); + assert!( + price_after < price_before, + "Recycle mode: SubnetMovingPrice should decrease, before={price_before:?} after={price_after:?}" + ); + }); +} From 0cc440f4efd3a80695ae6df61158992d7437eaaf Mon Sep 17 00:00:00 2001 From: Pawel Polewicz <p.polewicz@gmail.com> Date: Fri, 13 Feb 2026 00:29:23 +0000 Subject: [PATCH 13/17] cargo fmt --- pallets/subtensor/src/coinbase/run_coinbase.rs | 3 +-- pallets/subtensor/src/lib.rs | 11 +---------- pallets/subtensor/src/tests/emission_suppression.rs | 5 +---- 3 files changed, 3 insertions(+), 16 deletions(-) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index d9666dd89e..8754f7d281 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -250,8 +250,7 @@ impl<T: Config> Pallet<T> { && suppression_mode == RootSellPressureOnSuppressedSubnetsMode::Recycle { // Recycle mode: swap alpha → TAO via AMM, then burn the TAO. - let root_alpha_currency = - AlphaCurrency::from(tou64!(root_alpha)); + let root_alpha_currency = AlphaCurrency::from(tou64!(root_alpha)); if let Ok(swap_result) = Self::swap_alpha_for_tao( *netuid_i, root_alpha_currency, diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index df3ed5a5fa..0364cdb1ba 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -346,16 +346,7 @@ pub mod pallet { /// Controls how root alpha dividends are handled on emission-suppressed subnets. #[derive( - Encode, - Decode, - Default, - TypeInfo, - Clone, - Copy, - PartialEq, - Eq, - Debug, - DecodeWithMemTracking, + Encode, Decode, Default, TypeInfo, Clone, Copy, PartialEq, Eq, Debug, DecodeWithMemTracking, )] pub enum RootSellPressureOnSuppressedSubnetsMode { /// Root gets no alpha on suppressed subnets; alpha recycled to subnet. diff --git a/pallets/subtensor/src/tests/emission_suppression.rs b/pallets/subtensor/src/tests/emission_suppression.rs index 13b85a0368..f6e1f72ca2 100644 --- a/pallets/subtensor/src/tests/emission_suppression.rs +++ b/pallets/subtensor/src/tests/emission_suppression.rs @@ -1138,10 +1138,7 @@ fn test_sudo_set_mode_all_variants_emit_events() { ) ); - assert_eq!( - KeepRootSellPressureOnSuppressedSubnets::<Test>::get(), - mode, - ); + assert_eq!(KeepRootSellPressureOnSuppressedSubnets::<Test>::get(), mode,); assert!( System::events().iter().any(|e| { From 884ac91aabebef7a0e89f44ddf6b2a8a43f8667a Mon Sep 17 00:00:00 2001 From: Pawel Polewicz <p.polewicz@gmail.com> Date: Fri, 13 Feb 2026 00:45:50 +0000 Subject: [PATCH 14/17] Fix recycle mode tests: use add_dynamic_network, add tao_outflow tracking - Use add_dynamic_network to properly initialize AMM for swap tests - Add record_tao_outflow call after swap_alpha_for_tao in recycle path - Check SubnetTAO decrease instead of SubnetMovingPrice (needs epoch) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- .../subtensor/src/coinbase/run_coinbase.rs | 1 + .../src/tests/emission_suppression.rs | 44 ++++++++++--------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 8754f7d281..f8b6ffb7a8 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -257,6 +257,7 @@ impl<T: Config> Pallet<T> { TaoCurrency::ZERO, // no price limit true, // drop fees ) { + Self::record_tao_outflow(*netuid_i, swap_result.amount_paid_out); Self::recycle_tao(swap_result.amount_paid_out); } } else { diff --git a/pallets/subtensor/src/tests/emission_suppression.rs b/pallets/subtensor/src/tests/emission_suppression.rs index f6e1f72ca2..d850ebc273 100644 --- a/pallets/subtensor/src/tests/emission_suppression.rs +++ b/pallets/subtensor/src/tests/emission_suppression.rs @@ -923,17 +923,17 @@ fn test_default_mode_is_recycle() { fn test_recycle_mode_suppressed_subnet_swaps_and_recycles() { new_test_ext(1).execute_with(|| { add_network(NetUid::ROOT, 1, 0); - let sn1 = NetUid::from(1); - setup_subnet_with_flow(sn1, 10, 100_000_000); - - // Make it a dynamic subnet so swap_alpha_for_tao actually works via AMM. - SubnetMechanism::<Test>::insert(sn1, 1); + // Use add_dynamic_network to properly initialize the AMM. + let owner_hk = U256::from(50); + let owner_ck = U256::from(51); + let sn1 = add_dynamic_network(&owner_hk, &owner_ck); // Seed the pool with TAO and alpha reserves. let initial_tao = TaoCurrency::from(500_000_000u64); let initial_alpha_in = AlphaCurrency::from(500_000_000u64); SubnetTAO::<Test>::insert(sn1, initial_tao); SubnetAlphaIn::<Test>::insert(sn1, initial_alpha_in); + SubnetTaoFlow::<Test>::insert(sn1, 100_000_000i64); // Also set root TAO so root_proportion is nonzero. SubnetTAO::<Test>::insert(NetUid::ROOT, TaoCurrency::from(1_000_000_000)); @@ -1053,13 +1053,14 @@ fn test_recycle_mode_non_suppressed_subnet_normal() { fn test_recycle_mode_ignores_root_claim_type() { new_test_ext(1).execute_with(|| { add_network(NetUid::ROOT, 1, 0); - let sn1 = NetUid::from(1); - setup_subnet_with_flow(sn1, 10, 100_000_000); + // Use add_dynamic_network to properly initialize the AMM. + let owner_hk = U256::from(50); + let owner_ck = U256::from(51); + let sn1 = add_dynamic_network(&owner_hk, &owner_ck); - // Dynamic subnet for AMM swap. - SubnetMechanism::<Test>::insert(sn1, 1); SubnetTAO::<Test>::insert(sn1, TaoCurrency::from(500_000_000u64)); SubnetAlphaIn::<Test>::insert(sn1, AlphaCurrency::from(500_000_000u64)); + SubnetTaoFlow::<Test>::insert(sn1, 100_000_000i64); SubnetTAO::<Test>::insert(NetUid::ROOT, TaoCurrency::from(1_000_000_000)); SubnetAlphaOut::<Test>::insert(sn1, AlphaCurrency::from(1_000_000_000)); @@ -1162,11 +1163,10 @@ fn test_sudo_set_mode_all_variants_emit_events() { fn test_recycle_mode_decreases_price_and_flow_ema() { new_test_ext(1).execute_with(|| { add_network(NetUid::ROOT, 1, 0); - let sn1 = NetUid::from(1); - setup_subnet_with_flow(sn1, 10, 100_000_000); - - // Dynamic subnet. - SubnetMechanism::<Test>::insert(sn1, 1); + // Use add_dynamic_network to properly initialize the AMM. + let owner_hk = U256::from(50); + let owner_ck = U256::from(51); + let sn1 = add_dynamic_network(&owner_hk, &owner_ck); // Large pool reserves to ensure swaps produce measurable effects. let pool_reserve = 1_000_000_000u64; @@ -1174,6 +1174,7 @@ fn test_recycle_mode_decreases_price_and_flow_ema() { SubnetAlphaIn::<Test>::insert(sn1, AlphaCurrency::from(pool_reserve)); SubnetTAO::<Test>::insert(NetUid::ROOT, TaoCurrency::from(pool_reserve)); SubnetAlphaOut::<Test>::insert(sn1, AlphaCurrency::from(pool_reserve)); + SubnetTaoFlow::<Test>::insert(sn1, 100_000_000i64); let hotkey = U256::from(10); let coldkey = U256::from(11); @@ -1234,8 +1235,8 @@ fn test_recycle_mode_decreases_price_and_flow_ema() { RootSellPressureOnSuppressedSubnetsMode::Recycle, ); - // Record price before. - let price_before = SubnetMovingPrice::<Test>::get(sn1); + // Record TAO reserve before. + let tao_before = SubnetTAO::<Test>::get(sn1); SubtensorModule::emit_to_subnets(&[sn1], &subnet_emissions, true); @@ -1246,11 +1247,14 @@ fn test_recycle_mode_decreases_price_and_flow_ema() { "Recycle mode: SubnetTaoFlow should be negative (TAO outflow), got {flow_after}" ); - // Moving price should have decreased (alpha was sold into pool for TAO). - let price_after = SubnetMovingPrice::<Test>::get(sn1); + // SubnetTAO should have decreased (TAO left the pool in the swap). + // Note: emit_to_subnets injects some TAO via inject_and_maybe_swap, + // but the swap_alpha_for_tao pulls TAO back out. The net flow recorded + // as negative proves outflow dominated. + let tao_after = SubnetTAO::<Test>::get(sn1); assert!( - price_after < price_before, - "Recycle mode: SubnetMovingPrice should decrease, before={price_before:?} after={price_after:?}" + tao_after < tao_before, + "Recycle mode: SubnetTAO should decrease (TAO outflow), before={tao_before:?} after={tao_after:?}" ); }); } From 6d8ca771ee83a954a2fbf56a7a9aab8a13b43e5a Mon Sep 17 00:00:00 2001 From: Pawel Polewicz <p.polewicz@gmail.com> Date: Fri, 13 Feb 2026 08:01:58 +0000 Subject: [PATCH 15/17] Address code review: R-1 swap fallback, R-3 cache suppression, R-8 codec indices, R-10 doc fix - R-1: Add else fallback to recycle alpha when swap_alpha_for_tao fails in Recycle mode, preventing orphaned tokens - R-3: Cache is_subnet_emission_suppressed result to avoid double storage read per subnet per block - R-8: Add explicit #[codec(index)] annotations to enum variants for migration safety - R-10: Fix misleading doc comment on Disable variant Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- pallets/subtensor/src/coinbase/run_coinbase.rs | 9 +++++++-- pallets/subtensor/src/lib.rs | 5 ++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index f8b6ffb7a8..8451919d3c 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -209,10 +209,13 @@ impl<T: Config> Pallet<T> { let root_proportion = Self::root_proportion(*netuid_i); log::debug!("root_proportion: {root_proportion:?}"); + // Check if subnet emission is suppressed (compute once to avoid double storage read). + let is_suppressed = Self::is_subnet_emission_suppressed(*netuid_i); + // Get root alpha from root prop. // When mode is Disable and subnet is suppressed, zero out root alpha // so all validator alpha goes to subnet validators. - let root_alpha: U96F32 = if Self::is_subnet_emission_suppressed(*netuid_i) + let root_alpha: U96F32 = if is_suppressed && suppression_mode == RootSellPressureOnSuppressedSubnetsMode::Disable { asfloat!(0) @@ -245,7 +248,6 @@ impl<T: Config> Pallet<T> { if root_sell_flag { // Determine disposition of root alpha based on suppression mode. - let is_suppressed = Self::is_subnet_emission_suppressed(*netuid_i); if is_suppressed && suppression_mode == RootSellPressureOnSuppressedSubnetsMode::Recycle { @@ -259,6 +261,9 @@ impl<T: Config> Pallet<T> { ) { Self::record_tao_outflow(*netuid_i, swap_result.amount_paid_out); Self::recycle_tao(swap_result.amount_paid_out); + } else { + // Swap failed: recycle alpha back to subnet to prevent loss. + Self::recycle_subnet_alpha(*netuid_i, root_alpha_currency); } } else { // Enable mode (or non-suppressed subnet): accumulate for root validators. diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 0364cdb1ba..adb1a7c60a 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -349,12 +349,15 @@ pub mod pallet { Encode, Decode, Default, TypeInfo, Clone, Copy, PartialEq, Eq, Debug, DecodeWithMemTracking, )] pub enum RootSellPressureOnSuppressedSubnetsMode { - /// Root gets no alpha on suppressed subnets; alpha recycled to subnet. + /// Root gets no alpha on suppressed subnets; all validator alpha goes to subnet validators. + #[codec(index = 0)] Disable, /// Root still accumulates alpha on suppressed subnets (old `true`). + #[codec(index = 1)] Enable, /// Root alpha is swapped to TAO via AMM and the TAO is burned. #[default] + #[codec(index = 2)] Recycle, } From 684169a4b0d864ea85fe766aaf0aeb14a69e0edb Mon Sep 17 00:00:00 2001 From: Pawel Polewicz <p.polewicz@gmail.com> Date: Fri, 13 Feb 2026 20:31:37 +0000 Subject: [PATCH 16/17] Add basic CLAUDE.md --- CLAUDE.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..f095488a7b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,2 @@ +- never use slice indexing like `arr[n..]` or `arr[i]`; use `.get(n..)`, `.get(i)` etc. instead to avoid panics (clippy::indexing_slicing) +- never use `*`, `+`, `-`, `/` for arithmetic; use `.saturating_mul()`, `.saturating_add()`, `.saturating_sub()`, `.saturating_div()` or checked variants instead (clippy::arithmetic_side_effects) From 136c1a49f5696b930d34e08a7b0bb6c4fcdbd115 Mon Sep 17 00:00:00 2001 From: Pawel Polewicz <p.polewicz@gmail.com> Date: Tue, 17 Feb 2026 21:43:10 +0000 Subject: [PATCH 17/17] Address code review: Disable mode recycles root alpha to validators, add constant, fix weight MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R-1: Disable mode now actually recycles root_alpha to PendingValidatorEmission instead of zeroing it before computation. Validators get explicitly more. R-7: Extract EMISSION_SUPPRESSION_THRESHOLD constant (0.5) to avoid magic number. R-9: Rename suppression_mode → root_sell_pressure_mode for clarity. R-12: Fix weight for sudo_set_emission_suppression_override: reads(1) → reads(2). Adds test_disable_mode_recycles_root_alpha_to_validators which verifies that Disable mode validators receive more than Enable mode by exactly the root alpha amount. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- .../subtensor/src/coinbase/run_coinbase.rs | 25 +++-- .../src/coinbase/subnet_emissions.rs | 9 +- pallets/subtensor/src/lib.rs | 4 +- pallets/subtensor/src/macros/dispatches.rs | 4 +- .../src/tests/emission_suppression.rs | 93 ++++++++++++++++++- 5 files changed, 114 insertions(+), 21 deletions(-) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 05e1dc6ea3..307821e62c 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -187,7 +187,7 @@ impl<T: Config> Pallet<T> { // --- 3. Inject ALPHA for participants. let cut_percent: U96F32 = Self::get_float_subnet_owner_cut(); - let suppression_mode = KeepRootSellPressureOnSuppressedSubnets::<T>::get(); + let root_sell_pressure_mode = KeepRootSellPressureOnSuppressedSubnets::<T>::get(); for netuid_i in subnets_to_emit_to.iter() { // Get alpha_out for this block. @@ -217,17 +217,9 @@ impl<T: Config> Pallet<T> { let is_suppressed = Self::is_subnet_emission_suppressed(*netuid_i); // Get root alpha from root prop. - // When mode is Disable and subnet is suppressed, zero out root alpha - // so all validator alpha goes to subnet validators. - let root_alpha: U96F32 = if is_suppressed - && suppression_mode == RootSellPressureOnSuppressedSubnetsMode::Disable - { - asfloat!(0) - } else { - root_proportion - .saturating_mul(alpha_out_i) // Total alpha emission per block remaining. - .saturating_mul(asfloat!(0.5)) // 50% to validators. - }; + let root_alpha: U96F32 = root_proportion + .saturating_mul(alpha_out_i) // Total alpha emission per block remaining. + .saturating_mul(asfloat!(0.5)); // 50% to validators. log::debug!("root_alpha: {root_alpha:?}"); // Get pending server alpha, which is the miner cut of the alpha out. @@ -253,7 +245,14 @@ impl<T: Config> Pallet<T> { if root_sell_flag { // Determine disposition of root alpha based on suppression mode. if is_suppressed - && suppression_mode == RootSellPressureOnSuppressedSubnetsMode::Recycle + && root_sell_pressure_mode == RootSellPressureOnSuppressedSubnetsMode::Disable + { + // Disable mode: recycle root alpha back to subnet validators. + PendingValidatorEmission::<T>::mutate(*netuid_i, |total| { + *total = total.saturating_add(tou64!(root_alpha).into()); + }); + } else if is_suppressed + && root_sell_pressure_mode == RootSellPressureOnSuppressedSubnetsMode::Recycle { // Recycle mode: swap alpha → TAO via AMM, then burn the TAO. let root_alpha_currency = AlphaCurrency::from(tou64!(root_alpha)); diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs index 1f63e31506..5395187c72 100644 --- a/pallets/subtensor/src/coinbase/subnet_emissions.rs +++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs @@ -4,6 +4,10 @@ use safe_math::FixedExt; use substrate_fixed::transcendental::{exp, ln}; use substrate_fixed::types::{I32F32, I64F64, U64F64, U96F32}; +/// Emission suppression threshold (50%). Subnets with suppression fraction +/// above this value are considered emission-suppressed. +const EMISSION_SUPPRESSION_THRESHOLD: f64 = 0.5; + impl<T: Config> Pallet<T> { pub fn get_subnets_to_emit_to(subnets: &[NetUid]) -> Vec<NetUid> { // Filter out root subnet. @@ -268,7 +272,10 @@ impl<T: Config> Pallet<T> { match EmissionSuppressionOverride::<T>::get(netuid) { Some(true) => true, Some(false) => false, - None => EmissionSuppression::<T>::get(netuid) > U64F64::saturating_from_num(0.5), + None => { + EmissionSuppression::<T>::get(netuid) + > U64F64::saturating_from_num(EMISSION_SUPPRESSION_THRESHOLD) + } } } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 664264f24c..cd524f5d0e 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -349,7 +349,7 @@ pub mod pallet { Encode, Decode, Default, TypeInfo, Clone, Copy, PartialEq, Eq, Debug, DecodeWithMemTracking, )] pub enum RootSellPressureOnSuppressedSubnetsMode { - /// Root gets no alpha on suppressed subnets; all validator alpha goes to subnet validators. + /// Root gets no alpha on suppressed subnets; root alpha recycled to subnet validators. #[codec(index = 0)] Disable, /// Root still accumulates alpha on suppressed subnets (old `true`). @@ -2447,7 +2447,7 @@ pub mod pallet { StorageDoubleMap<_, Identity, NetUid, Blake2_128Concat, T::AccountId, bool, OptionQuery>; /// Controls how root alpha dividends are handled on emission-suppressed subnets. - /// - Disable (0x00): root gets no alpha; alpha recycled to subnet validators. + /// - Disable (0x00): root gets no alpha; root alpha recycled to subnet validators. /// - Enable (0x01): root still accumulates alpha (old behaviour). /// - Recycle (0x02, default): root alpha swapped to TAO and TAO burned. #[pallet::storage] diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index d996831c82..75a00083f1 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -2606,7 +2606,7 @@ mod dispatches { #[pallet::call_index(133)] #[pallet::weight(( Weight::from_parts(5_000_000, 0) - .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(1)), DispatchClass::Operational, Pays::No @@ -2679,7 +2679,7 @@ mod dispatches { } /// --- Set the mode for root alpha dividends on emission-suppressed subnets. - /// - Disable: root gets no alpha; alpha recycled to subnet validators. + /// - Disable: root gets no alpha; root alpha recycled to subnet validators. /// - Enable: root still accumulates alpha (old behaviour). /// - Recycle: root alpha swapped to TAO via AMM, TAO burned. #[pallet::call_index(135)] diff --git a/pallets/subtensor/src/tests/emission_suppression.rs b/pallets/subtensor/src/tests/emission_suppression.rs index d850ebc273..71c18a2b08 100644 --- a/pallets/subtensor/src/tests/emission_suppression.rs +++ b/pallets/subtensor/src/tests/emission_suppression.rs @@ -516,7 +516,7 @@ fn test_suppressed_subnet_root_alpha_by_default() { } // ───────────────────────────────────────────────────────────────────────────── -// Test 15: Suppress subnet, Disable mode → root gets no alpha +// Test 15: Suppress subnet, Disable mode → root gets no alpha, validators get more // ───────────────────────────────────────────────────────────────────────────── #[test] fn test_suppressed_subnet_no_root_alpha_flag_off() { @@ -567,11 +567,98 @@ fn test_suppressed_subnet_no_root_alpha_flag_off() { "with Disable mode, root should get no alpha on suppressed subnet" ); - // But validator emission should be non-zero (all alpha goes to validators). + // Validator emission should be non-zero (root alpha recycled to validators). let pending_validator = PendingValidatorEmission::<Test>::get(sn1); assert!( pending_validator > AlphaCurrency::ZERO, - "validators should receive all alpha when root alpha is zeroed" + "validators should receive recycled root alpha" + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Test 15b: Disable mode actually recycles root alpha to validators +// (validators get more than with Enable mode) +// ───────────────────────────────────────────────────────────────────────────── +#[test] +fn test_disable_mode_recycles_root_alpha_to_validators() { + new_test_ext(1).execute_with(|| { + add_network(NetUid::ROOT, 1, 0); + let sn1 = NetUid::from(1); + setup_subnet_with_flow(sn1, 10, 100_000_000); + + let hotkey = U256::from(10); + let coldkey = U256::from(11); + assert_ok!(SubtensorModule::root_register( + RuntimeOrigin::signed(coldkey), + hotkey, + )); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + NetUid::ROOT, + 1_000_000_000u64.into(), + ); + SubtensorModule::set_tao_weight(u64::MAX); + setup_root_with_tao(sn1); + + // Force-suppress sn1. + EmissionSuppressionOverride::<Test>::insert(sn1, true); + + let mut subnet_emissions = BTreeMap::new(); + subnet_emissions.insert(sn1, U96F32::from_num(1_000_000)); + + // ── Run with Enable mode first to get baseline ── + KeepRootSellPressureOnSuppressedSubnets::<Test>::put( + RootSellPressureOnSuppressedSubnetsMode::Enable, + ); + PendingRootAlphaDivs::<Test>::insert(sn1, AlphaCurrency::ZERO); + PendingValidatorEmission::<Test>::insert(sn1, AlphaCurrency::ZERO); + + SubtensorModule::emit_to_subnets(&[sn1], &subnet_emissions, true); + + let enable_validator = PendingValidatorEmission::<Test>::get(sn1); + let enable_root = PendingRootAlphaDivs::<Test>::get(sn1); + + // In Enable mode, root should accumulate some alpha. + assert!( + enable_root > AlphaCurrency::ZERO, + "Enable mode: root should get alpha" + ); + + // ── Now run with Disable mode ── + KeepRootSellPressureOnSuppressedSubnets::<Test>::put( + RootSellPressureOnSuppressedSubnetsMode::Disable, + ); + PendingRootAlphaDivs::<Test>::insert(sn1, AlphaCurrency::ZERO); + PendingValidatorEmission::<Test>::insert(sn1, AlphaCurrency::ZERO); + + SubtensorModule::emit_to_subnets(&[sn1], &subnet_emissions, true); + + let disable_validator = PendingValidatorEmission::<Test>::get(sn1); + let disable_root = PendingRootAlphaDivs::<Test>::get(sn1); + + // In Disable mode, root should get nothing. + assert_eq!( + disable_root, + AlphaCurrency::ZERO, + "Disable mode: root should get no alpha" + ); + + // Disable validators should get MORE than Enable validators because + // root alpha is recycled to them instead of going to root. + assert!( + disable_validator > enable_validator, + "Disable mode validators ({disable_validator:?}) should get more \ + than Enable mode ({enable_validator:?}) because root alpha is recycled" + ); + + // The difference should equal the root alpha from Enable mode + // (root alpha is recycled to validators instead). + assert_eq!( + disable_validator.saturating_sub(enable_validator), + enable_root, + "difference should equal the root alpha that was recycled" ); }); }