diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index 25877b7820..764722bf8a 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -2057,6 +2057,33 @@ pub mod pallet { Ok(()) } + /// Sets the emission depth bar (D), in rao of SubnetTAO reserve. Pools below the bar + /// have their emission-relevant EMA price capped toward the network median; the cap + /// relaxes as the pool deepens. Zero disables the cap. (Reuses the gate-exponent + /// weight; a dedicated benchmark should be added before mainnet.) + #[pallet::call_index(103)] + #[pallet::weight(::WeightInfo::sudo_set_emission_gate_exponent())] + pub fn sudo_set_emission_depth_bar(origin: OriginFor, bar: u64) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_emission_depth_bar(bar); + log::debug!("set_emission_depth_bar( {bar:?} ) "); + Ok(()) + } + + /// Sets the emission depth exponent (k): sharpness of the depth-cap falloff + #[pallet::call_index(104)] + #[pallet::weight(::WeightInfo::sudo_set_emission_gate_exponent())] + pub fn sudo_set_emission_depth_exponent( + origin: OriginFor, + exponent: u16, + ) -> DispatchResult { + ensure_root(origin)?; + ensure!((1..=8).contains(&exponent), Error::::InvalidValue); + pallet_subtensor::Pallet::::set_emission_depth_exponent(exponent); + log::debug!("set_emission_depth_exponent( {exponent:?} ) "); + Ok(()) + } + /// Sets TAO flow smoothing factor (alpha) #[pallet::call_index(83)] #[pallet::weight(::WeightInfo::sudo_set_tao_flow_smoothing_factor())] diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs index 02b01a45a5..91073570a9 100644 --- a/pallets/subtensor/src/coinbase/subnet_emissions.rs +++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs @@ -490,31 +490,98 @@ impl Pallet { } // Implementation of shares that uses subnet EMA prices (SubnetMovingPrice), - // not the active/spot alpha price. + // not the active/spot alpha price. Each subnet's EMA price is first passed through a + // depth-graduated cap (`emission_price_capped`): prices at or below the network median EMA + // price pass through unchanged, but the portion ABOVE the median is credited only in + // proportion to the pool's depth weight w(SubnetTAO). A shallow pool (w -> 0) is therefore + // capped at the median regardless of how far its price is pumped, while a deep pool + // (w -> 1) keeps its full price. This caps the TAO a small pool can earn without touching + // its alpha emission, and leaves large pools ~unchanged. fn get_shares_price_ema(subnets_to_emit_to: &[NetUid]) -> BTreeMap { - // Get sum of alpha moving prices - let total_moving_prices = subnets_to_emit_to - .iter() - .map(|netuid| U64F64::saturating_from_num(Self::get_moving_alpha_price(*netuid))) - .fold(U64F64::saturating_from_num(0.0), |acc, ema| { - acc.saturating_add(ema) - }); - log::debug!("total_moving_prices: {total_moving_prices:?}"); + // Median EMA price across the emit set = the cap anchor for shallow pools. + let median_ema = Self::get_median_moving_price(subnets_to_emit_to); - // Calculate shares. - subnets_to_emit_to + // Depth-capped price per subnet. + let capped: BTreeMap = subnets_to_emit_to .iter() - .map(|netuid| { - let moving_price = - U64F64::saturating_from_num(Self::get_moving_alpha_price(*netuid)); - log::debug!("moving_price_i: {moving_price:?}"); + .map(|netuid| (*netuid, Self::emission_price_capped(*netuid, median_ema))) + .collect(); - let share = moving_price - .checked_div(total_moving_prices) - .unwrap_or(U64F64::saturating_from_num(0)); + let total_capped = capped + .values() + .copied() + .fold(U64F64::saturating_from_num(0.0), |acc, p| { + acc.saturating_add(p) + }); + log::debug!("total_capped_moving_prices: {total_capped:?}"); - (*netuid, share) + capped + .into_iter() + .map(|(netuid, price)| { + let share = price + .checked_div(total_capped) + .unwrap_or(U64F64::saturating_from_num(0)); + (netuid, share) }) .collect::>() } + + /// Median of the EMA (moving) prices over the emit set — the anchor at which a shallow + /// pool's emission price is capped. Zero if the set is empty. + pub(crate) fn get_median_moving_price(subnets_to_emit_to: &[NetUid]) -> U64F64 { + let mut prices: Vec = subnets_to_emit_to + .iter() + .map(|netuid| Self::get_moving_alpha_price(*netuid)) + .collect(); + if prices.is_empty() { + return U64F64::saturating_from_num(0); + } + prices.sort(); + let mid = prices.len() / 2; + if prices.len() % 2 == 1 { + prices[mid] + } else { + prices[mid.saturating_sub(1)] + .saturating_add(prices[mid]) + .safe_div(U64F64::saturating_from_num(2)) + } + } + + /// A subnet's emission-relevant price after the depth-graduated cap. + /// - Prices at or below `median_ema` pass through unchanged. + /// - The portion above `median_ema` is credited only at fraction `w = emission_depth_weight`, + /// so a shallow pool (w -> 0) is capped at the median and a deep pool (w -> 1) keeps its + /// full price. + pub(crate) fn emission_price_capped(netuid: NetUid, median_ema: U64F64) -> U64F64 { + let ema = Self::get_moving_alpha_price(netuid); + if ema <= median_ema { + return ema; + } + let w = Self::emission_depth_weight(netuid); + let excess = ema.saturating_sub(median_ema); + median_ema.saturating_add(w.saturating_mul(excess)) + } + + /// Depth weight w(T) in [0, 1] where T = SubnetTAO reserve. Hill ramp + /// `w = T^k / (T^k + D^k)` with bar D = `EmissionDepthBar` (rao) and exponent + /// k = `EmissionDepthExponent`. w -> 0 for a shallow pool (hard cap at the median), + /// w -> 1 for a deep pool (full price). + pub(crate) fn emission_depth_weight(netuid: NetUid) -> U64F64 { + let d_u64 = EmissionDepthBar::::get(); + if d_u64 == 0 { + return U64F64::saturating_from_num(1); + } + let t = U64F64::saturating_from_num(SubnetTAO::::get(netuid).to_u64()); + let d = U64F64::saturating_from_num(d_u64); + let r = t.safe_div(d); + let k = u32::from(EmissionDepthExponent::::get().clamp(1, 8)); + let mut rk = r; + let mut i = 1u32; + while i < k { + rk = rk.saturating_mul(r); + i = i.saturating_add(1); + } + rk.checked_div(rk.saturating_add(U64F64::saturating_from_num(1))) + .unwrap_or(U64F64::saturating_from_num(0)) + } } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 3cf4318e9d..8866d3dd66 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1875,6 +1875,33 @@ pub mod pallet { /// same EMA-price shares that drive emission. Zero means "not yet computed" /// and disables the gate. pub type EmissionGateBar = StorageValue<_, U64F64, ValueQuery>; + + #[pallet::type_value] + /// Default emission depth bar (D): the SubnetTAO reserve, in rao, at which a subnet's + /// depth weight `w(T) = T^k/(T^k+D^k)` reaches exactly 0.5 (the half-credit pool size). + /// With the default exponent k = 3 this lands w ~= 0.89 at 1000 TAO and ~= 1.0 at 10k TAO + /// of reserve. Zero disables the depth cap (every subnet keeps its full EMA price). + pub fn DefaultEmissionDepthBar() -> u64 { + // 500 TAO in rao. Half-credit pool size (w = 0.5 at 500 TAO) at the default exponent k = 3. + 500_000_000_000 + } + #[pallet::storage] + /// ITEM --> Emission depth bar (D), in rao of SubnetTAO reserve. Small pools have their + /// emission-relevant EMA price capped toward the network median; the cap relaxes as the + /// pool deepens past this bar. + pub type EmissionDepthBar = + StorageValue<_, u64, ValueQuery, DefaultEmissionDepthBar>; + + #[pallet::type_value] + /// Default emission depth exponent (k) for the depth-weight Hill curve. Higher = sharper + /// falloff of emission below the bar. + pub fn DefaultEmissionDepthExponent() -> u16 { + 3 + } + #[pallet::storage] + /// ITEM --> Emission depth exponent (k), clamped to 1..=8 when read. + pub type EmissionDepthExponent = + StorageValue<_, u16, ValueQuery, DefaultEmissionDepthExponent>; #[pallet::type_value] /// Default value for flow EMA smoothing. pub fn DefaultFlowEmaSmoothingFactor() -> u64 { diff --git a/pallets/subtensor/src/subnets/subnet.rs b/pallets/subtensor/src/subnets/subnet.rs index 0d2e8ef52b..efe60cf524 100644 --- a/pallets/subtensor/src/subnets/subnet.rs +++ b/pallets/subtensor/src/subnets/subnet.rs @@ -369,24 +369,16 @@ impl Pallet { weight.saturating_accrue(db_weight.reads(networks_added_reads.max(1))); weight.saturating_accrue(db_weight.writes(1)); - // Keep the locked TAO in the pool instead of recycling the excess. - // Size the pool alpha reserve from the total TAO reserve at that same price. - let pool_initial_tao: TaoBalance = Self::get_network_min_lock(); + // The pool begins with exactly 1 TAO and 1 alpha regardless of the lock paid. + // The remainder of the lock is recycled below, which makes registration a + // real (non-recoverable) cost rather than a self-deposit into the founder's own pool. + let _ = median_subnet_alpha_price; + let one_unit: u64 = 1_000_000_000; // 1 TAO / 1 alpha (9 decimals) + let total_pool_tao: TaoBalance = one_unit.into(); + let total_pool_alpha: AlphaBalance = one_unit.into(); weight.saturating_accrue(db_weight.reads(1)); - let total_pool_tao: TaoBalance = if actual_tao_lock_amount >= pool_initial_tao { - actual_tao_lock_amount - } else { - pool_initial_tao - }; - - let total_pool_alpha: AlphaBalance = U64F64::saturating_from_num(total_pool_tao.to_u64()) - .safe_div(median_subnet_alpha_price) - .saturating_floor() - .saturating_to_num::() - .into(); - - // // With the full lock retained in the reserve, this will normally be zero. + // Everything above the 1-TAO seed is recycled (removed from circulation). let tao_recycled_for_registration = actual_tao_lock_amount.saturating_sub(total_pool_tao); // Core pool + ownership @@ -394,7 +386,7 @@ impl Pallet { SubnetAlphaIn::::insert(netuid_to_register, total_pool_alpha); SubnetOwner::::insert(netuid_to_register, coldkey.clone()); Self::set_subnet_owner_hotkey(netuid_to_register, hotkey)?; - SubnetLocked::::insert(netuid_to_register, actual_tao_lock_amount); + SubnetLocked::::insert(netuid_to_register, total_pool_tao); SubnetAlphaOut::::insert(netuid_to_register, AlphaBalance::ZERO); SubnetVolume::::insert(netuid_to_register, 0u128); RAORecycledForRegistration::::insert(netuid_to_register, tao_recycled_for_registration); diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index cc60a933ea..94e79c1ec8 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -1000,6 +1000,16 @@ impl Pallet { EmissionGateExponent::::set(exponent); } + /// Sets the emission depth bar (D) + pub fn set_emission_depth_bar(bar: u64) { + EmissionDepthBar::::set(bar); + } + + /// Sets the emission depth exponent (k) + pub fn set_emission_depth_exponent(exponent: u16) { + EmissionDepthExponent::::set(exponent); + } + /// Sets TAO flow smoothing factor (alpha) pub fn set_tao_flow_smoothing_factor(smoothing_factor: u64) { FlowEmaSmoothingFactor::::set(smoothing_factor);