From c1f351a7ac770a4a5ea9a010604493c1b80eaac4 Mon Sep 17 00:00:00 2001 From: igoraxz Date: Tue, 9 Jun 2026 19:14:38 +0100 Subject: [PATCH] Miner incentive tao outflow Count miner emission as virtual user outflow in net TaoFlow, so a subnet that routes miner emission to wallets it controls and holds it does not gain emission share without the sell pressure a subnet pays when its miners sell. Genuine sales of that alpha are counted exactly once. - Every miner incentive (all UIDs) is counted as outflow at emission, valued at the moving (EMA) alpha price (smoothed -> resists epoch-block dump/pump), into the signed SubnetMinerIncentiveFlow accumulator (EMA SubnetEmaMinerIncentiveFlow). - Cost normalization: one shared factor discounts both the protocol cost and the miner-incentive cost before they are subtracted from user flow: factor = sum(max(user,0)) / sum(max(protocol,0) + max(miner,0)) (<= 1) The total cost charged is capped at total positive user demand and split pro-rata by each subnet's (protocol + miner) cost, so the miner term cannot push most subnets below eligibility. The penalty is relative: it redistributes share toward low-miner-emission subnets rather than an absolute deduction. - Miner alpha staked to a wallet is tagged with a per-position TAO credit MinerOriginCredit((netuid,hotkey,coldkey)) equal to the value counted. - On a genuine sale (unstake_from_subnet) the credit is consumed pro-rata and reversed out of SubnetMinerIncentiveFlow; the real sale is recorded at realized price, so each emission unit counts once. Pro-rata + TAO reversal is exact for fungible share-pool alpha (no clamp); the price-limit refund restores credit for the unsold portion (rounded so reversal never over-counts). - decrease_stake_for_hotkey_and_coldkey_on_subnet returns the consumed credit; callers route it -- sale reverses, same-subnet transfer and hotkey/coldkey swaps carry it to the destination (merging), burn/recycle/dust/AMM-removal discard it. - Owner/burn-hotkey emission is counted but uncredited (destroyed, never sold). - MinerIncentiveFlowEnabled (default on) gates the miner cost; all flow EMAs are kept warm. sudo_set_miner_incentive_flow_enabled (root, call index 96). Dereg clears the credit map; positions with no credit default to zero. Co-Authored-By: Claude Opus 4 (1M context) --- pallets/admin-utils/src/lib.rs | 18 ++ pallets/subtensor/src/coinbase/root.rs | 3 + .../subtensor/src/coinbase/run_coinbase.rs | 21 ++- .../src/coinbase/subnet_emissions.rs | 166 +++++++++++++++--- pallets/subtensor/src/lib.rs | 41 +++++ pallets/subtensor/src/staking/stake_utils.rs | 69 +++++++- pallets/subtensor/src/swap/swap_coldkey.rs | 4 +- pallets/subtensor/src/swap/swap_hotkey.rs | 5 +- pallets/subtensor/src/utils/misc.rs | 5 + 9 files changed, 296 insertions(+), 36 deletions(-) diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index f972facca6..18c4fbe130 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -2027,6 +2027,24 @@ pub mod pallet { Ok(()) } + /// Enables or disables counting miner emission as virtual user outflow in net flow. + /// When enabled, miner emission (all UIDs) is valued at the moving alpha price and + /// subtracted from the subnet's user flow EMA, with the count reversed when miner-origin + /// alpha is genuinely sold. + #[pallet::call_index(96)] + #[pallet::weight(Weight::from_parts(7_343_000, 0) + .saturating_add(::DbWeight::get().reads(0)) + .saturating_add(::DbWeight::get().writes(1)))] + pub fn sudo_set_miner_incentive_flow_enabled( + origin: OriginFor, + enabled: bool, + ) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_miner_incentive_flow_enabled(enabled); + log::debug!("set_miner_incentive_flow_enabled( {enabled:?} ) "); + Ok(()) + } + /// Sets the global maximum number of mechanisms in a subnet #[pallet::call_index(88)] #[pallet::weight(Weight::from_parts(15_000_000, 0) diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index b64043a4f5..ceb405da52 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -300,6 +300,9 @@ impl Pallet { SubnetEmaTaoFlow::::remove(netuid); SubnetProtocolFlow::::remove(netuid); SubnetEmaProtocolFlow::::remove(netuid); + SubnetMinerIncentiveFlow::::remove(netuid); + SubnetEmaMinerIncentiveFlow::::remove(netuid); + let _ = MinerOriginCredit::::clear_prefix((netuid,), u32::MAX, None); SubnetExcessTao::::remove(netuid); SubnetRootSellTao::::remove(netuid); SubnetTaoProvided::::remove(netuid); diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index a6c988feea..3ffa4d1707 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -614,12 +614,26 @@ impl Pallet { for (hotkey, incentive) in incentives { log::debug!("incentives: hotkey: {incentive:?}"); - // Skip/burn miner-emission for immune keys + // Count every miner incentive (all UIDs) as virtual user outflow at emission: + // emission to a wallet the subnet controls and holds gives an emission-share + // advantage with no sell pressure, regardless of which UID receives it. + // Value the alpha at the moving (EMA) price, which is smoothed over a window and so + // cannot be shifted by a flash dump/pump at the epoch block (resists manipulation in + // both directions). When the alpha later reaches a real seller, the credit recorded + // below is reversed on that sale so the same emission is not counted twice. + let miner_outflow_tao: u64 = Self::get_moving_alpha_price(netuid) + .saturating_mul(U96F32::saturating_from_num(incentive)) + .saturating_to_num::(); + Self::record_miner_incentive_outflow(netuid, TaoBalance::from(miner_outflow_tao)); + + // Owner/associated hotkeys: the emission is already counted as outflow above; do not + // stake it to a wallet -- recycle or burn the alpha instead (no credit, never sold). if owner_hotkeys.contains(&hotkey) { log::debug!( "incentives: hotkey: {hotkey:?} is SN owner hotkey or associated hotkey, skipping {incentive:?}" ); - // Check if we should recycle or burn the incentive + // Burned/recycled: alpha is destroyed, never reaches a position, so no credit is + // recorded -- the at-emission outflow stands permanently (never reversed). match RecycleOrBurn::::try_get(netuid) { Ok(RecycleOrBurnEnum::Recycle) => { log::debug!("recycling {incentive:?}"); @@ -656,6 +670,9 @@ impl Pallet { netuid, incentive, ); + // Tag the staked alpha with the TAO already counted as outflow at emission, so its + // eventual genuine sale reverses the count (pro-rata) instead of double-counting. + Self::add_miner_origin_credit(netuid, &destination, &owner, TaoBalance::from(miner_outflow_tao)); } // Distribute alpha divs. diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs index 63dbf36e5a..a6aa16a11e 100644 --- a/pallets/subtensor/src/coinbase/subnet_emissions.rs +++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs @@ -93,6 +93,103 @@ impl Pallet { SubnetProtocolFlow::::remove(netuid); } + /// Count miner emission as virtual user outflow at emission (all UIDs, valued in TAO at the + /// moving price). Added to the signed per-block accumulator. + pub fn record_miner_incentive_outflow(netuid: NetUid, tao: TaoBalance) { + SubnetMinerIncentiveFlow::::mutate(netuid, |flow| { + *flow = flow.saturating_add(u64::from(tao) as i64); + }); + } + + /// Reverse previously-counted miner-emission outflow (when miner-origin alpha is genuinely + /// sold, the real sell already hits the user-flow outflow, so the at-emission count is undone + /// to avoid double-counting). The per-block accumulator is signed and may go negative. + pub fn record_miner_incentive_inflow(netuid: NetUid, tao: TaoBalance) { + SubnetMinerIncentiveFlow::::mutate(netuid, |flow| { + *flow = flow.saturating_sub(u64::from(tao) as i64); + }); + } + + /// Add miner-origin outflow credit to a position (TAO counted at emission for alpha staked + /// to a real miner wallet). + pub fn add_miner_origin_credit( + netuid: NetUid, + hotkey: &T::AccountId, + coldkey: &T::AccountId, + tao: TaoBalance, + ) { + if tao.is_zero() { + return; + } + MinerOriginCredit::::mutate((netuid, hotkey, coldkey), |c| { + *c = c.saturating_add(tao); + }); + } + + /// Consume miner-origin credit pro-rata when `removed` alpha leaves a position holding + /// `alpha_before` alpha. Returns the TAO credit consumed (delta-C). Pro-rata is the precise + /// convention for fungible share-pool alpha and is robust to appreciation/dilution (it never + /// needs a clamp). Floors the consumed amount so any rounding leaves credit on the position + /// (conservative: never under-counts future outflow). + pub fn consume_miner_origin_credit( + netuid: NetUid, + hotkey: &T::AccountId, + coldkey: &T::AccountId, + removed: AlphaBalance, + alpha_before: AlphaBalance, + ) -> TaoBalance { + let before = u64::from(alpha_before); + let rem = u64::from(removed).min(before); + if before == 0 || rem == 0 { + return TaoBalance::ZERO; + } + let key = (netuid, hotkey.clone(), coldkey.clone()); + let credit = u64::from(MinerOriginCredit::::get(&key)); + if credit == 0 { + return TaoBalance::ZERO; + } + // delta = floor(credit * rem / before) + let delta = (credit as u128) + .saturating_mul(rem as u128) + .checked_div(before as u128) + .unwrap_or(0) as u64; + let delta = delta.min(credit); + let new_credit = credit.saturating_sub(delta); + if new_credit == 0 { + MinerOriginCredit::::remove(&key); + } else { + MinerOriginCredit::::insert(&key, TaoBalance::from(new_credit)); + } + TaoBalance::from(delta) + } + + pub fn reset_miner_incentive_flow(netuid: NetUid) { + SubnetMinerIncentiveFlow::::remove(netuid); + } + + fn update_ema_miner_incentive_flow(netuid: NetUid) -> I64F64 { + let current_block: u64 = Self::get_current_block_as_u64(); + + let block_flow = I64F64::saturating_from_num(SubnetMinerIncentiveFlow::::get(netuid)); + let (last_block, last_block_ema) = SubnetEmaMinerIncentiveFlow::::get(netuid) + .unwrap_or((0, I64F64::saturating_from_num(0))); + + if last_block != current_block { + let flow_alpha = I64F64::saturating_from_num(FlowEmaSmoothingFactor::::get()) + .safe_div(I64F64::saturating_from_num(i64::MAX)); + let one = I64F64::saturating_from_num(1); + let ema_flow = (one.saturating_sub(flow_alpha)) + .saturating_mul(last_block_ema) + .saturating_add(flow_alpha.saturating_mul(block_flow)); + SubnetEmaMinerIncentiveFlow::::insert(netuid, (current_block, ema_flow)); + + Self::reset_miner_incentive_flow(netuid); + ema_flow + } else { + last_block_ema + } + } + fn update_ema_protocol_flow(netuid: NetUid) -> I64F64 { let current_block: u64 = Self::get_current_block_as_u64(); @@ -245,37 +342,49 @@ impl Pallet { let net_flow_enabled = NetTaoFlowEnabled::::get(); let zero = I64F64::saturating_from_num(0); - // Always update both EMAs (keeps protocol EMA warm for when toggled on). - // Fixes #2667: protocol EMA accumulator was only drained when enabled, - // causing a shock on toggle. - let subnet_emas: Vec<(NetUid, I64F64, I64F64)> = subnets_to_emit_to + // Drain all three flow EMAs every block (user, miner-incentive, protocol) so each stays + // warm and is drained regardless of toggle state. Fixes #2667: a flow accumulator drained + // only while its feature was enabled caused a shock on toggle. + // `miner_on`: when enabled, miner incentive (all UIDs) is a cost alongside protocol cost. + let miner_incentive_enabled = MinerIncentiveFlowEnabled::::get(); + let miner_on = net_flow_enabled && miner_incentive_enabled; + let subnet_emas: Vec<(NetUid, I64F64, I64F64, I64F64)> = subnets_to_emit_to .iter() .map(|netuid| { let user_ema = Self::get_ema_flow(*netuid); + let miner_incentive_ema = Self::update_ema_miner_incentive_flow(*netuid); let protocol_ema = Self::update_ema_protocol_flow(*netuid); - (*netuid, user_ema, protocol_ema) + (*netuid, user_ema, protocol_ema, miner_incentive_ema) }) .collect(); - // When net flow is enabled, normalize protocol EMA so that its - // positive total matches the user EMA positive total. This prevents - // subsidy concentration: as emissions concentrate on fewer subnets, - // their protocol EMA grows, but the normalization factor shrinks to - // compensate, keeping the deduction proportional to user demand. + // Cost normalization. Both the protocol cost and the miner-incentive cost are discounted + // by ONE shared factor before being subtracted from user flow: + // + // factor = sum_i max(user_i,0) / sum_i ( max(protocol_i,0) + max(miner_i,0) ) (<= 1) + // + // i.e. the total cost charged across all subnets is capped at total positive user demand, + // split pro-rata by each subnet's (protocol + miner) cost. This keeps the deduction + // proportional to demand and prevents the (large) miner-incentive term from pushing most + // subnets below the eligibility floor. Consequence: as the combined cost grows the factor + // shrinks, so the miner penalty is RELATIVE -- it redistributes emission share toward + // low-miner-emission subnets rather than applying an absolute, un-normalized deduction. let norm_factor = if net_flow_enabled { - let (user_positive_ema_sum, protocol_positive_ema_sum) = + let (user_positive_ema_sum, cost_positive_ema_sum) = subnet_emas .iter() - .fold((zero, zero), |(su, sp), (_, u, p)| { - ( - su.saturating_add((*u).max(zero)), - sp.saturating_add((*p).max(zero)), - ) + .fold((zero, zero), |(su, sc), (_, u, p, m)| { + let cost = (*p).max(zero).saturating_add(if miner_on { + (*m).max(zero) + } else { + zero + }); + (su.saturating_add((*u).max(zero)), sc.saturating_add(cost)) }); let one = I64F64::saturating_from_num(1); - if protocol_positive_ema_sum > zero { + if cost_positive_ema_sum > zero { user_positive_ema_sum - .safe_div(protocol_positive_ema_sum) + .safe_div(cost_positive_ema_sum) .min(one) } else { zero @@ -283,20 +392,31 @@ impl Pallet { } else { zero }; - log::debug!("Protocol normalization factor: {norm_factor:?}"); + log::debug!("Net flow normalization factor (protocol+miner): {norm_factor:?}"); let ema_flows: BTreeMap = subnet_emas .into_iter() - .map(|(netuid, user_ema, protocol_ema)| { + .map(|(netuid, user_ema, protocol_ema, miner_incentive_ema)| { let net = if net_flow_enabled { - // Only scale positive protocol cost by norm_factor. Negative - // protocol cost (root drain > emissions) is a benefit, kept as-is. + // Scale the positive protocol and miner-incentive costs by the same factor. + // Negative protocol cost (root drain > emissions) is a real benefit, kept + // as-is. Negative miner-incentive EMA is floored to 0 -- NOT a benefit: a + // net-negative means more miner-origin alpha was sold than freshly emitted, + // and that sale already hit user flow as real outflow, so crediting it here + // would double-count. let scaled_protocol = if protocol_ema > zero { norm_factor.saturating_mul(protocol_ema) } else { protocol_ema }; - user_ema.saturating_sub(scaled_protocol) + let scaled_miner = if miner_on && miner_incentive_ema > zero { + norm_factor.saturating_mul(miner_incentive_ema) + } else { + zero + }; + user_ema + .saturating_sub(scaled_protocol) + .saturating_sub(scaled_miner) } else { user_ema }; diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 97ba77a92a..a6de59ab8e 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1656,6 +1656,47 @@ pub mod pallet { pub type SubnetEmaProtocolFlow = StorageMap<_, Identity, NetUid, (u64, I64F64), OptionQuery>; + /// --- MAP ( netuid ) --> subnet_miner_incentive_flow | Signed per-block accumulator for miner + /// emission counted as virtual outflow (all UIDs, valued in TAO at the moving price). Reduced + /// (may go negative within a block) when miner-origin alpha is genuinely sold, reversing the + /// at-emission count so the same emission is not counted twice. + #[pallet::storage] + pub type SubnetMinerIncentiveFlow = + StorageMap<_, Identity, NetUid, i64, ValueQuery, DefaultZeroI64>; + + /// --- MAP ( netuid ) --> subnet_ema_miner_incentive_flow | EMA of the per-block miner-incentive + /// flow accumulator (emission counted at the moving price, minus reversals on genuine sale), + /// same smoothing as SubnetEmaTaoFlow. + #[pallet::storage] + pub type SubnetEmaMinerIncentiveFlow = + StorageMap<_, Identity, NetUid, (u64, I64F64), OptionQuery>; + + /// --- NMAP ( netuid, hotkey, coldkey ) --> miner_origin_credit | TAO already counted as + /// miner-emission outflow for the miner-origin alpha still held on this position. Reduced + /// pro-rata whenever the position's alpha leaves; on a genuine sale the consumed credit is + /// reversed out of SubnetMinerIncentiveFlow so the same emission is not counted twice. + #[pallet::storage] + pub type MinerOriginCredit = StorageNMap< + _, + ( + NMapKey, // subnet (first, for dereg clear_prefix) + NMapKey, // hotkey + NMapKey, // coldkey + ), + TaoBalance, + ValueQuery, + DefaultZeroTao, + >; + + /// --- ITEM --> miner_incentive_flow_enabled | When true, miner emission counts as virtual user outflow in net flow. + #[pallet::type_value] + pub fn DefaultMinerIncentiveFlowEnabled() -> bool { + true + } + #[pallet::storage] + pub type MinerIncentiveFlowEnabled = + StorageValue<_, bool, ValueQuery, DefaultMinerIncentiveFlowEnabled>; + /// Default value for flow cutoff. #[pallet::type_value] pub fn DefaultFlowCutoff() -> I64F64 { diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index 0f6a553c91..70de2f2948 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -612,22 +612,38 @@ impl Pallet { /// * `hotkey` - The account ID of the hotkey. /// * `coldkey` - The account ID of the coldkey (owner). /// * `netuid` - The unique identifier of the subnet. - /// * `amount` - The amount of alpha to be added. + /// * `amount` - The amount of alpha to be removed. /// + /// Returns the miner-origin outflow credit consumed pro-rata by this decrease (delta-C, in + /// TAO). Callers route it: a genuine sale reverses it out of SubnetMinerIncentiveFlow; a transfer + /// or key-swap carries it to the destination position; burn/recycle/dust/AMM-liquidity removal + /// discard it (the at-emission count stands). Callers that don't move alpha to another position + /// can ignore it. pub fn decrease_stake_for_hotkey_and_coldkey_on_subnet( hotkey: &T::AccountId, coldkey: &T::AccountId, netuid: NetUid, amount: AlphaBalance, - ) { + ) -> TaoBalance { let mut alpha_share_pool = Self::get_alpha_share_pool(hotkey.clone(), netuid); - let amount = amount.to_u64(); + let amount_u = amount.to_u64(); // We expect a negative value here if let Ok(value) = alpha_share_pool.try_get_value(coldkey) - && value >= amount + && value >= amount_u { - alpha_share_pool.update_value_for_one(coldkey, (amount as i64).neg()); + // Consume miner-origin credit pro-rata against pre-decrease holdings. + let delta_c = Self::consume_miner_origin_credit( + netuid, + hotkey, + coldkey, + amount, + AlphaBalance::from(value), + ); + alpha_share_pool.update_value_for_one(coldkey, (amount_u as i64).neg()); + delta_c + } else { + TaoBalance::ZERO } } @@ -747,8 +763,9 @@ impl Pallet { price_limit: TaoBalance, drop_fees: bool, ) -> Result { - // Decrease alpha on subnet - Self::decrease_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid, alpha); + // Decrease alpha on subnet (consumes miner-origin outflow credit pro-rata) + let delta_credit = + Self::decrease_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid, alpha); // Swap the alpha for TAO. let swap_result = Self::swap_alpha_for_tao(netuid, alpha, price_limit, drop_fees)?; @@ -764,6 +781,37 @@ impl Pallet { Self::increase_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid, refund); } + // The decrease consumed credit for the full `alpha`; restore the portion of credit for + // any refunded (un-sold) alpha so only the genuinely sold portion is reversed below. + let alpha_u = u64::from(alpha); + let sold_credit: u64 = if alpha_u == 0 { + 0 + } else { + let dc = u64::from(delta_credit); + // Restore credit for the refunded (un-sold) alpha, rounding UP so that the reversed + // `sold_credit` is rounded DOWN -- strictly conservative (never reverses more than the + // genuinely-sold fraction, i.e. never under-counts outflow). + let restore = ((dc as u128).saturating_mul(u64::from(refund) as u128)) + .saturating_add(alpha_u as u128 - 1) + .checked_div(alpha_u as u128) + .unwrap_or(0) + .min(dc as u128) as u64; + if restore > 0 { + Self::add_miner_origin_credit(netuid, hotkey, coldkey, TaoBalance::from(restore)); + } + dc.saturating_sub(restore) + }; + // Reverse the at-emission count for the sold miner-origin alpha: its real sale is recorded + // as user outflow below, so the emission-time count must be undone to avoid double-count. + // The reversal uses the emission-time valuation (the stored credit), while the real sale + // records realized TAO; the difference is genuine price drift and is intended to remain. + if sold_credit > 0 { + log::debug!( + "miner incentive flow: reversing {sold_credit:?} (sold miner-origin alpha) for netuid={netuid:?}" + ); + Self::record_miner_incentive_inflow(netuid, TaoBalance::from(sold_credit)); + } + // Transfer unstaked TAO from subnet account to the coldkey. Self::transfer_tao_from_subnet(netuid, beneficiary, swap_result.amount_paid_out.into())?; @@ -963,8 +1011,8 @@ impl Pallet { // Transfer lock (may fail if destination coldkey has a conflicting lock) Self::transfer_lock(origin_coldkey, destination_coldkey, netuid, alpha)?; - // Decrease alpha on origin keys - Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( + // Decrease alpha on origin keys (consumes miner-origin credit pro-rata) + let delta_credit = Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( origin_hotkey, origin_coldkey, netuid, @@ -985,6 +1033,9 @@ impl Pallet { netuid, alpha, ); + // A transfer is not a sale: carry the miner-origin credit to the destination so the tag + // follows the alpha and its eventual sale is still reversed (no outflow recorded here). + Self::add_miner_origin_credit(netuid, destination_hotkey, destination_coldkey, delta_credit); if netuid == NetUid::ROOT { Self::add_stake_adjust_root_claimed_for_hotkey_and_coldkey( destination_hotkey, diff --git a/pallets/subtensor/src/swap/swap_coldkey.rs b/pallets/subtensor/src/swap/swap_coldkey.rs index 2358fcecf1..b78809e5e5 100644 --- a/pallets/subtensor/src/swap/swap_coldkey.rs +++ b/pallets/subtensor/src/swap/swap_coldkey.rs @@ -92,7 +92,7 @@ impl Pallet { // Swap let alpha_old = Self::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, old_coldkey, netuid); - Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( + let delta_credit = Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( &hotkey, old_coldkey, netuid, @@ -104,6 +104,8 @@ impl Pallet { netuid, alpha_old, ); + // Carry miner-origin credit to the new coldkey position (coldkey swap is not a sale). + Self::add_miner_origin_credit(netuid, &hotkey, new_coldkey, delta_credit); let new_dest_alpha = Self::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, new_coldkey, netuid); diff --git a/pallets/subtensor/src/swap/swap_hotkey.rs b/pallets/subtensor/src/swap/swap_hotkey.rs index 944ea5877f..cfb9539598 100644 --- a/pallets/subtensor/src/swap/swap_hotkey.rs +++ b/pallets/subtensor/src/swap/swap_hotkey.rs @@ -571,12 +571,15 @@ impl Pallet { for coldkey in unique_coldkeys { let alpha_old = Self::get_stake_for_hotkey_and_coldkey_on_subnet(old_hotkey, &coldkey, netuid); - Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( + let delta_credit = Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( old_hotkey, &coldkey, netuid, alpha_old, ); Self::increase_stake_for_hotkey_and_coldkey_on_subnet( new_hotkey, &coldkey, netuid, alpha_old, ); + // Carry miner-origin credit to the new hotkey position (hotkey swap is not a + // sale); merges into any existing credit there. + Self::add_miner_origin_credit(netuid, new_hotkey, &coldkey, delta_credit); weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); let mut staking_hotkeys = StakingHotkeys::::get(&coldkey); diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index a1c0309b24..80dd29d807 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -923,6 +923,11 @@ impl Pallet { NetTaoFlowEnabled::::set(enabled); } + /// Enables or disables counting miner emission as virtual user outflow in net flow. + pub fn set_miner_incentive_flow_enabled(enabled: bool) { + MinerIncentiveFlowEnabled::::set(enabled); + } + /// Multiply an integer `value` by a Q32 fixed-point factor. /// /// Q32 means: