diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs index 400a14a5c8..63dbf36e5a 100644 --- a/pallets/subtensor/src/coinbase/subnet_emissions.rs +++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs @@ -243,19 +243,64 @@ impl Pallet { #[allow(dead_code)] fn get_shares_flow(subnets_to_emit_to: &[NetUid]) -> BTreeMap { let net_flow_enabled = NetTaoFlowEnabled::::get(); + let zero = I64F64::saturating_from_num(0); - // Always update the protocol EMA (keeps it warm for when toggled on) - let ema_flows: BTreeMap = subnets_to_emit_to + // 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 .iter() .map(|netuid| { let user_ema = Self::get_ema_flow(*netuid); let protocol_ema = Self::update_ema_protocol_flow(*netuid); + (*netuid, user_ema, protocol_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. + let norm_factor = if net_flow_enabled { + let (user_positive_ema_sum, protocol_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)), + ) + }); + let one = I64F64::saturating_from_num(1); + if protocol_positive_ema_sum > zero { + user_positive_ema_sum + .safe_div(protocol_positive_ema_sum) + .min(one) + } else { + zero + } + } else { + zero + }; + log::debug!("Protocol normalization factor: {norm_factor:?}"); + + let ema_flows: BTreeMap = subnet_emas + .into_iter() + .map(|(netuid, user_ema, protocol_ema)| { let net = if net_flow_enabled { - user_ema.saturating_sub(protocol_ema) + // Only scale positive protocol cost by norm_factor. Negative + // protocol cost (root drain > emissions) is a benefit, kept as-is. + let scaled_protocol = if protocol_ema > zero { + norm_factor.saturating_mul(protocol_ema) + } else { + protocol_ema + }; + user_ema.saturating_sub(scaled_protocol) } else { user_ema }; - (*netuid, net) + (netuid, net) }) .collect(); log::debug!("EMA flows (net_flow_enabled={net_flow_enabled}): {ema_flows:?}");