From ea465d011d1007d3bf6b79a48b277c4ebebee112 Mon Sep 17 00:00:00 2001 From: igoraxz Date: Sun, 3 May 2026 11:31:56 +0100 Subject: [PATCH] feat: implement net TAO flow for emission allocation Replace gross user flow (buys - sells) with net flow (user flow - protocol cost) for emission share computation. This ensures subnets only receive emissions when they generate positive net TAO inflow for the network, preventing subsidized extraction where a subnet receives more in protocol emissions than it attracts in external capital. New per-block storage (auditable base values): - SubnetExcessTao: excess TAO swapped (chain buys) per block - SubnetRootSellTao: TAO from root dividend sells per block New protocol cost tracking: - SubnetProtocolFlow: per-block accumulator (emit + chain_buys - root_sells) - SubnetEmaProtocolFlow: EMA of protocol cost, same smoothing as user flow EMA Emission share computation: - net_ema = ema(user_flow) - ema(protocol_cost) - Protocol EMA starts from zero and scales in over ~30 days - Sudo toggle NetTaoFlowEnabled (default: on) to switch between net and gross flow Sudo call: sudo_set_net_tao_flow_enabled (call_index 91) Cleanup: all new storage items removed on subnet deregistration. Co-Authored-By: Claude Opus 4.6 (1M context) --- pallets/admin-utils/src/lib.rs | 17 ++++++ pallets/subtensor/src/coinbase/root.rs | 4 ++ .../subtensor/src/coinbase/run_coinbase.rs | 14 +++++ .../src/coinbase/subnet_emissions.rs | 58 +++++++++++++++++-- pallets/subtensor/src/lib.rs | 29 ++++++++++ pallets/subtensor/src/staking/claim_root.rs | 7 +++ pallets/subtensor/src/utils/misc.rs | 5 ++ 7 files changed, 130 insertions(+), 4 deletions(-) diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index e100feb787..4688b1f22f 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -1963,6 +1963,23 @@ pub mod pallet { Ok(()) } + /// Enables or disables net TAO flow (protocol cost deduction from emission shares). + /// When enabled, emission shares use net flow = user flow - protocol cost. + /// When disabled, emission shares use gross user flow only (current behavior). + #[pallet::call_index(91)] + #[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_net_tao_flow_enabled( + origin: OriginFor, + enabled: bool, + ) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_net_tao_flow_enabled(enabled); + log::debug!("set_net_tao_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 49f9e67d58..b2926323db 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -298,6 +298,10 @@ impl Pallet { SubnetMovingPrice::::remove(netuid); SubnetTaoFlow::::remove(netuid); SubnetEmaTaoFlow::::remove(netuid); + SubnetProtocolFlow::::remove(netuid); + SubnetEmaProtocolFlow::::remove(netuid); + SubnetExcessTao::::remove(netuid); + SubnetRootSellTao::::remove(netuid); SubnetTaoProvided::::remove(netuid); // --- 13. Token / mechanism / registration toggles. diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index a6026b0556..60abfd1145 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -28,6 +28,12 @@ impl Pallet { log::debug!( "Running coinbase for block {current_block:?} with block emission: {block_emission:?}" ); + + // Reset per-block root sell counters from the previous block. + // Root sells (step 8 in block_step) happen after coinbase, so their + // accumulated values are consumed here at the start of the next block. + let _ = SubnetRootSellTao::::clear(u32::MAX, None); + // --- 1. Get all subnets (excluding root). let subnets: Vec = Self::get_all_subnet_netuids() .into_iter() @@ -97,6 +103,11 @@ impl Pallet { let bought_alpha: AlphaBalance = buy_swap_result_ok.amount_paid_out.into(); Self::recycle_subnet_alpha(*netuid_i, bought_alpha); + + // Record actual excess TAO that entered pool. + let actual_excess: TaoBalance = buy_swap_result_ok.amount_paid_in; + SubnetExcessTao::::insert(*netuid_i, actual_excess); + Self::record_protocol_inflow(*netuid_i, actual_excess); } } Err(remainder) => { @@ -132,6 +143,9 @@ impl Pallet { TotalStake::::mutate(|total| { *total = total.saturating_add(injected_tao); }); + + // Record emission injection as protocol inflow. + Self::record_protocol_inflow(*netuid_i, injected_tao); } Err(remainder) => { remaining_credit = remainder; diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs index b856ff3d7f..44f7cdb163 100644 --- a/pallets/subtensor/src/coinbase/subnet_emissions.rs +++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs @@ -51,6 +51,45 @@ impl Pallet { SubnetTaoFlow::::remove(netuid); } + pub fn record_protocol_inflow(netuid: NetUid, tao: TaoBalance) { + SubnetProtocolFlow::::mutate(netuid, |flow| { + *flow = flow.saturating_add(u64::from(tao) as i64); + }); + } + + pub fn record_protocol_outflow(netuid: NetUid, tao: TaoBalance) { + SubnetProtocolFlow::::mutate(netuid, |flow| { + *flow = flow.saturating_sub(u64::from(tao) as i64); + }); + } + + pub fn reset_protocol_flow(netuid: NetUid) { + SubnetProtocolFlow::::remove(netuid); + } + + fn get_ema_protocol_flow(netuid: NetUid) -> I64F64 { + let current_block: u64 = Self::get_current_block_as_u64(); + + let block_flow = I64F64::saturating_from_num(SubnetProtocolFlow::::get(netuid)); + let (last_block, last_block_ema) = + SubnetEmaProtocolFlow::::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)); + SubnetEmaProtocolFlow::::insert(netuid, (current_block, ema_flow)); + + Self::reset_protocol_flow(netuid); + ema_flow + } else { + last_block_ema + } + } + // Update SubnetEmaTaoFlow if needed and return its value for // the current block #[allow(dead_code)] @@ -177,12 +216,23 @@ impl Pallet { // Implementation of shares that uses TAO flow #[allow(dead_code)] fn get_shares_flow(subnets_to_emit_to: &[NetUid]) -> BTreeMap { - // Get raw flows - let ema_flows = subnets_to_emit_to + let net_flow_enabled = NetTaoFlowEnabled::::get(); + + // Always update the protocol EMA (keeps it warm for when toggled on) + let ema_flows: BTreeMap = subnets_to_emit_to .iter() - .map(|netuid| (*netuid, Self::get_ema_flow(*netuid))) + .map(|netuid| { + let user_ema = Self::get_ema_flow(*netuid); + let protocol_ema = Self::get_ema_protocol_flow(*netuid); + let net = if net_flow_enabled { + user_ema.saturating_sub(protocol_ema) + } else { + user_ema + }; + (*netuid, net) + }) .collect(); - log::debug!("EMA flows: {ema_flows:?}"); + log::debug!("EMA flows (net_flow_enabled={net_flow_enabled}): {ema_flows:?}"); // Clip the EMA flow with lower limit L // z[i] = max{S[i] − L, 0} diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index d720aed56a..75735c7471 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1344,6 +1344,16 @@ pub mod pallet { pub type SubnetTaoInEmission = StorageMap<_, Identity, NetUid, TaoBalance, ValueQuery, DefaultZeroTao>; + /// --- MAP ( netuid ) --> excess_tao | Returns the excess TAO swapped (chain buys) into this subnet on the last block. + #[pallet::storage] + pub type SubnetExcessTao = + StorageMap<_, Identity, NetUid, TaoBalance, ValueQuery, DefaultZeroTao>; + + /// --- MAP ( netuid ) --> root_sell_tao | Returns the TAO received from root dividend sells on this subnet on the last block. + #[pallet::storage] + pub type SubnetRootSellTao = + StorageMap<_, Identity, NetUid, TaoBalance, ValueQuery, DefaultZeroTao>; + /// --- MAP ( netuid ) --> alpha_supply_in_pool | Returns the amount of alpha in the pool. #[pallet::storage] pub type SubnetAlphaIn = @@ -1576,6 +1586,25 @@ pub mod pallet { pub type SubnetEmaTaoFlow = StorageMap<_, Identity, NetUid, (u64, I64F64), OptionQuery>; + /// --- ITEM --> net_tao_flow_enabled | When true, emission shares use net flow (user - protocol). When false, uses gross user flow only. + #[pallet::type_value] + pub fn DefaultNetTaoFlowEnabled() -> bool { + true + } + #[pallet::storage] + pub type NetTaoFlowEnabled = + StorageValue<_, bool, ValueQuery, DefaultNetTaoFlowEnabled>; + + /// --- MAP ( netuid ) --> subnet_protocol_flow | Per-block accumulator for protocol cost (emission + chain buys - root sells). + #[pallet::storage] + pub type SubnetProtocolFlow = + StorageMap<_, Identity, NetUid, i64, ValueQuery, DefaultZeroI64>; + + /// --- MAP ( netuid ) --> subnet_ema_protocol_flow | EMA of protocol cost flow, same smoothing as SubnetEmaTaoFlow. + #[pallet::storage] + pub type SubnetEmaProtocolFlow = + StorageMap<_, Identity, NetUid, (u64, I64F64), OptionQuery>; + /// Default value for flow cutoff. #[pallet::type_value] pub fn DefaultFlowCutoff() -> I64F64 { diff --git a/pallets/subtensor/src/staking/claim_root.rs b/pallets/subtensor/src/staking/claim_root.rs index f3f13c4679..304eb37e5b 100644 --- a/pallets/subtensor/src/staking/claim_root.rs +++ b/pallets/subtensor/src/staking/claim_root.rs @@ -179,6 +179,13 @@ impl Pallet { } }; + // Record root sell as protocol outflow (reduces protocol cost). + let root_sell_tao: TaoBalance = owed_tao.amount_paid_out; + SubnetRootSellTao::::mutate(netuid, |total| { + *total = total.saturating_add(root_sell_tao); + }); + Self::record_protocol_outflow(netuid, root_sell_tao); + // Transfer unstaked TAO from subnet account to the root subnet account // and increase root stake. if let Some(root_subnet_account_id) = Self::get_subnet_account_id(NetUid::ROOT) diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index 718173bfaa..f6b24db36b 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -908,6 +908,11 @@ impl Pallet { FlowEmaSmoothingFactor::::set(smoothing_factor); } + /// Enables or disables net TAO flow (protocol cost deduction from emission shares). + pub fn set_net_tao_flow_enabled(enabled: bool) { + NetTaoFlowEnabled::::set(enabled); + } + /// Multiply an integer `value` by a Q32 fixed-point factor. /// /// Q32 means: