diff --git a/docs/concepts/emissions.mdx b/docs/concepts/emissions.mdx index 1a313056d4..e86903bd10 100644 --- a/docs/concepts/emissions.mdx +++ b/docs/concepts/emissions.mdx @@ -63,9 +63,88 @@ capped at `root_proportion × alpha_emission`, where As a subnet ages its alpha issuance grows, the cap falls, and the TAO that can no longer be injected as liquidity is instead swapped for alpha on the subnet's own pool — buying pressure that transitions mature subnets from -liquidity injection to chain buybacks. Alpha bought this way accumulates as +liquidity injection to chain buybacks. A subnet's chain buy is capped at the +spot-TAO value of its unburned miner alpha: + +``` +unburned_miner_alpha_i = + alpha_out_i × (1 − owner_cut_i) × 50% × (1 − miner_burned_i) + +chain_buy_cap_i = unburned_miner_alpha_i × spot_price_i +initial_chain_buy_i = min(local_excess_tao_i, chain_buy_cap_i) +``` + +`miner_burned` is the proportion recorded by the subnet's last settled tempo +for miner incentive directed to owner/immune burn or recycle UIDs. Those +withheld emissions do not support chain-buy capacity; at 100% miner burn, the +cap is zero. If owner cut is disabled, the miner half is calculated from the +full `alpha_out`. + +TAO rejected by a subnet's local cap spills over to other subnets with +uncovered chain-buy capacity. Eligible recipients are ordered by emission +weight, highest first, with netuid ascending as the deterministic tie-breaker. +The allocator preserves the normal liquidity-first ordering for each recipient: +it fills the subnet's remaining liquidity-injection capacity before assigning +any spillover to chain buys. It moves to the next subnet after both the +liquidity and unburned-miner-emission caps are full, or when spillover runs out: + +``` +spillover = Σ_i (local_excess_tao_i − initial_chain_buy_i) + +for j in descending_emission_weight: + added_liquidity_j = min(spillover, liquidity_cap_j − tao_in_j) + tao_in_j += added_liquidity_j + alpha_in_j = min(tao_in_j / spot_price_j, alpha_injection_cap_j) + spillover -= added_liquidity_j + + added_chain_buy_j = min(spillover, chain_buy_cap_j − chain_buy_j) + chain_buy_j += added_chain_buy_j + spillover -= added_chain_buy_j +``` + +If all eligible liquidity and chain-buy caps are full and TAO still remains, +that terminal remainder stays issued and is allocated across emitting subnets +in proportion to their emission weights. It is held in each subnet's protocol +TAO reservoir, where it cannot create an uncapped chain buy or an unpaired +spot-price change. The balancer may activate reservoir TAO in a later block +when pool weights permit. Alpha bought by the chain-buy path accumulates as protocol-owned alpha. +### Illustrative mainnet snapshot + +At mainnet block 8,725,138 on July 29, 2026, the top 32 subnets received 97.85% +of the original TAO allocation. With liquidity-first spillover, they still +receive 84.20% of block issuance and retain full buyback coverage for their +unburned miner emissions. Their combined allocation falls by 13.96%. + +That change redirects 0.06749 TAO per block to ranks 33 through 66. About +0.02688 TAO first fills their remaining liquidity capacity, then 0.04061 TAO +funds chain buybacks. This fully covers the TAO value of unburned miner +emissions for all 34 subnets. The remaining spillover fills rank 67's liquidity +and partially funds its buyback. + +This is the practical tradeoff: the largest subnets keep most issuance and full +miner coverage, while a limited reallocation gives lower-ranked networks enough +liquidity and buyback support to fund the miners they depend on. + +This is a one-block example, not a guarantee of the cutoff under future prices, +emission weights, burn rates, or liquidity requirements. + +The chain records each subnet's final current-block TAO allocation after this +planning and execution: + +``` +subnet_tao_emission_i = + current_block_liquidity_i + + successful_chain_buys_i + + current_block_reservoir_allocation_i +``` + +This value populates the existing `emission` field in dynamic subnet info and +`subnet_emission` in metagraph responses. Raw emission weight remains separate +and is used only to rank spillover recipients and divide any terminal remainder. +TAO activated from a reservoir in a later block is not counted again. + ## Subnet emission shares diff --git a/pallets/subtensor/src/benchmarks/helpers.rs b/pallets/subtensor/src/benchmarks/helpers.rs index 2f45e98e80..2a1274c35b 100644 --- a/pallets/subtensor/src/benchmarks/helpers.rs +++ b/pallets/subtensor/src/benchmarks/helpers.rs @@ -297,6 +297,9 @@ pub(super) fn setup_block_step_benchmark() { Subtensor::::init_new_network(netuid, TEMPO); SubtokenEnabled::::insert(netuid, true); SubnetEmissionEnabled::::insert(netuid, true); + // Seed the prior block's reported TAO allocation so the benchmark + // measures clearing all 128 per-block entries before writing new ones. + SubnetTaoEmission::::insert(netuid, TaoBalance::from(1_u64)); SubnetOwner::::insert(netuid, subnet_owner); Subtensor::::set_network_registration_allowed(netuid, true); Subtensor::::set_max_allowed_uids(netuid, MAINNET_NEURONS_PER_SUBNET); diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index e3425addba..ea30b290eb 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -48,7 +48,7 @@ impl Pallet { // --- 3. Get emissions for subnets to emit to let subnet_emissions = Self::get_subnet_block_emissions(&subnets_to_emit_to, block_emission); - log::debug!("Subnet emissions: {subnet_emissions:?}"); + log::debug!("Raw subnet emission allocations: {subnet_emissions:?}"); let root_sell_flag = Self::get_network_root_sell_flag(&subnets_to_emit_to); log::debug!("Root sell flag: {root_sell_flag:?}"); @@ -69,11 +69,17 @@ impl Pallet { pub fn inject_and_maybe_swap( subnets_to_emit_to: &[NetUid], + subnet_emissions: &BTreeMap, tao_in: &BTreeMap, alpha_in: &BTreeMap, excess_tao: &BTreeMap, credit: CreditOf, ) { + // This map reports only the current block's final materialized allocation. + // Clearing the complete prefix also prevents an ineligible subnet from + // retaining a stale value from the previous block. + let _ = SubnetTaoEmission::::clear(u32::MAX, None); + let mut remaining_credit = credit; for netuid_i in subnets_to_emit_to.iter() { let maybe_subnet_account_id = Self::get_subnet_account_id(*netuid_i); @@ -114,6 +120,7 @@ impl Pallet { let actual_excess: TaoBalance = buy_swap_result_ok.amount_paid_in; SubnetExcessTao::::insert(*netuid_i, actual_excess); + Self::record_subnet_tao_emission(*netuid_i, tao_to_swap_with); Self::record_protocol_inflow(*netuid_i, actual_excess); } Err(error) => { @@ -126,6 +133,13 @@ impl Pallet { remaining_credit.merge(refund_credit); } Err(withdraw_error) => { + // The credit was already materialized on the subnet + // account and could not be recovered, so it remains + // part of this subnet's final TAO allocation. + Self::record_subnet_tao_emission( + *netuid_i, + tao_to_swap_with, + ); log::error!( "Failed to revert excess TAO deposit after swap failure: netuid_i = {netuid_i:?}, tao_to_swap_with = {tao_to_swap_with:?}, swap_error = {error:?}, withdraw_error = {withdraw_error:?}" ); @@ -153,6 +167,7 @@ impl Pallet { match Self::spend_tao(&subnet_account_id, remaining_credit, tao_in_i) { Ok(remainder) => { remaining_credit = remainder; + Self::record_subnet_tao_emission(*netuid_i, tao_in_i); tao_in_i } Err(remainder) => { @@ -198,10 +213,88 @@ impl Pallet { } } - // Remaining imbalance should be zero at this point. If not, log error and burn. - let remaining_balance = remaining_credit.peek(); - if !remaining_balance.is_zero() { - // log::error!("Unspent imbalance remains: remaining_balance = {remaining_balance:?}"); + // After spillover has filled every available chain-buy cap, the block + // emission can still contain unspent TAO. Allocate that terminal remainder + // across all emitting subnets in proportion to their emission weights. It + // is materialized into each subnet's protocol TAO reservoir rather than + // sent through another chain buy, which preserves the caps and avoids an + // unpaired liquidity injection changing the spot price. + Self::allocate_remaining_tao(subnets_to_emit_to, subnet_emissions, remaining_credit); + } + + fn record_subnet_tao_emission(netuid: NetUid, amount: TaoBalance) { + if amount.is_zero() { + return; + } + + SubnetTaoEmission::::mutate(netuid, |total| { + *total = total.saturating_add(amount); + }); + } + + fn allocate_remaining_tao( + subnets_to_emit_to: &[NetUid], + subnet_emissions: &BTreeMap, + credit: CreditOf, + ) { + let mut remaining_credit = credit; + if remaining_credit.peek().is_zero() { + return; + } + + let zero = asfloat!(0); + let recipients: Vec<(NetUid, T::AccountId, U96F32)> = subnets_to_emit_to + .iter() + .filter_map(|netuid| { + let weight = *subnet_emissions.get(netuid).unwrap_or(&zero); + if weight == zero { + return None; + } + Self::get_subnet_account_id(*netuid).map(|account| (*netuid, account, weight)) + }) + .collect(); + let total_weight = recipients + .iter() + .fold(zero, |total, (_, _, weight)| total.saturating_add(*weight)); + + if total_weight == zero { + log::debug!("No weighted emitting subnet is available for remaining TAO"); + Self::recycle_credit(remaining_credit); + return; + } + + let distributable = asfloat!(remaining_credit.peek()); + let last_index = recipients.len().saturating_sub(1); + for (index, (netuid, account, weight)) in recipients.into_iter().enumerate() { + let amount: TaoBalance = if index == last_index { + remaining_credit.peek() + } else { + tou64!(distributable.saturating_mul(weight.safe_div(total_weight))).into() + }; + if amount.is_zero() { + continue; + } + + match Self::spend_tao(&account, remaining_credit, amount) { + Ok(remainder) => { + remaining_credit = remainder; + T::SwapInterface::reserve_protocol_tao(netuid, amount); + Self::record_subnet_tao_emission(netuid, amount); + } + Err(remainder) => { + remaining_credit = remainder; + log::error!( + "Failed to allocate remaining TAO: netuid = {netuid:?}, amount = {amount:?}" + ); + } + } + } + + if !remaining_credit.peek().is_zero() { + log::error!( + "Unable to allocate all remaining TAO: remaining = {:?}", + remaining_credit.peek() + ); Self::recycle_credit(remaining_credit); } } @@ -214,11 +307,20 @@ impl Pallet { BTreeMap, BTreeMap, ) { - // Computation is described in detail in the dtao whitepaper. + // Build the complete pre-execution emission plan. `subnet_emissions` + // contains raw allocations and remains the authoritative priority signal + // for spillover; the returned TAO maps contain the post-cap plan. let mut tao_in: BTreeMap = BTreeMap::new(); let mut alpha_in: BTreeMap = BTreeMap::new(); let mut alpha_out: BTreeMap = BTreeMap::new(); let mut excess_tao: BTreeMap = BTreeMap::new(); + let mut chain_buy_caps: BTreeMap = BTreeMap::new(); + let mut alpha_injection_caps: BTreeMap = BTreeMap::new(); + let mut subnet_prices: BTreeMap = BTreeMap::new(); + let mut spillover_tao = asfloat!(0); + let owner_cut_percent = Self::get_float_subnet_owner_cut(); + let one = asfloat!(1); + let miner_share = asfloat!(0.5); // Only calculate for subnets that we are emitting to. for (&netuid_i, &tao_emission_i) in subnet_emissions.iter() { @@ -251,14 +353,171 @@ impl Pallet { tao_in_i = alpha_in_i.saturating_mul(price_i); } - let excess_amount: U96F32 = tao_emission_i.saturating_sub(tao_in_i); - excess_tao.insert(netuid_i, excess_amount); + // Chain buys may consume at most the TAO value of unburned miner alpha. + // Start from the miner half of alpha_out after owner cut, then exclude + // the proportion withheld from miners by burn/recycle UIDs in the last + // settled tempo. If owner cut is disabled, miners receive half of the + // full alpha_out before the burn adjustment. + let participant_alpha_i = if Self::get_owner_cut_enabled(netuid_i) { + alpha_emission_i.saturating_sub(alpha_emission_i.saturating_mul(owner_cut_percent)) + } else { + alpha_emission_i + }; + let miner_burned_i = MinerBurned::::get(netuid_i).min(one); + let unburned_miner_alpha_i = participant_alpha_i + .saturating_mul(miner_share) + .saturating_mul(one.saturating_sub(miner_burned_i)); + let chain_buy_cap_i = unburned_miner_alpha_i.saturating_mul(price_i); + let available_chain_buy_i = tao_emission_i.saturating_sub(tao_in_i); + let chain_buy_i = available_chain_buy_i.min(chain_buy_cap_i); + spillover_tao = + spillover_tao.saturating_add(available_chain_buy_i.saturating_sub(chain_buy_i)); + excess_tao.insert(netuid_i, chain_buy_i); + chain_buy_caps.insert(netuid_i, chain_buy_cap_i); + alpha_injection_caps.insert(netuid_i, alpha_injection_cap); + subnet_prices.insert(netuid_i, price_i); // Insert values into maps tao_in.insert(netuid_i, tao_in_i); alpha_in.insert(netuid_i, alpha_in_i); alpha_out.insert(netuid_i, alpha_out_i); } + + // Reassign TAO rejected by a subnet's local cap to the highest-emission + // subnet that still has uncovered unburned-miner-emission capacity. + // Preserve the normal emission ordering for each recipient: first fill + // its remaining liquidity-injection capacity, then fill its chain buy. + // Move to the next subnet only after both are full or spillover is + // exhausted. Netuid ascending is the deterministic tie-breaker for equal + // emission weights. + let mut spillover_priority: Vec<(NetUid, U96F32)> = subnet_emissions + .iter() + .filter(|(_, emission)| **emission > asfloat!(0)) + .map(|(netuid, emission)| (*netuid, *emission)) + .collect(); + spillover_priority.sort_by(|(netuid_a, emission_a), (netuid_b, emission_b)| { + emission_b + .cmp(emission_a) + .then_with(|| netuid_a.cmp(netuid_b)) + }); + + for (netuid, _) in spillover_priority.iter().copied() { + if spillover_tao == asfloat!(0) { + break; + } + + let current_chain_buy = *excess_tao.get(&netuid).unwrap_or(&asfloat!(0)); + let chain_buy_cap = *chain_buy_caps.get(&netuid).unwrap_or(&asfloat!(0)); + if current_chain_buy >= chain_buy_cap { + continue; + } + + let current_tao_in = *tao_in.get(&netuid).unwrap_or(&asfloat!(0)); + let alpha_injection_cap = *alpha_injection_caps.get(&netuid).unwrap_or(&asfloat!(0)); + let price = *subnet_prices.get(&netuid).unwrap_or(&asfloat!(0)); + let tao_injection_cap = alpha_injection_cap.saturating_mul(price); + let uncovered_liquidity = tao_injection_cap.saturating_sub(current_tao_in); + let spillover_liquidity = spillover_tao.min(uncovered_liquidity); + if spillover_liquidity > asfloat!(0) { + let updated_tao_in = current_tao_in.saturating_add(spillover_liquidity); + let updated_alpha_in = if spillover_liquidity == uncovered_liquidity { + alpha_injection_cap + } else { + updated_tao_in.safe_div_or(price, asfloat!(0)) + }; + tao_in.insert(netuid, updated_tao_in); + alpha_in.insert(netuid, updated_alpha_in); + spillover_tao = spillover_tao.saturating_sub(spillover_liquidity); + } + + if spillover_tao == asfloat!(0) { + break; + } + + let uncovered_chain_buy = chain_buy_cap.saturating_sub(current_chain_buy); + let spillover_chain_buy = spillover_tao.min(uncovered_chain_buy); + if spillover_chain_buy == asfloat!(0) { + continue; + } + + excess_tao.insert( + netuid, + current_chain_buy.saturating_add(spillover_chain_buy), + ); + spillover_tao = spillover_tao.saturating_sub(spillover_chain_buy); + } + + // Converting each fixed-point plan entry to an integer balance can leave + // a small number of rao unassigned even when the fixed-point spillover + // was exhausted. Reconcile that integer remainder through the same + // liquidity-first priority before allowing it to reach the terminal + // reservoir. + let total_emission_rao = subnet_emissions + .values() + .fold(asfloat!(0), |total, emission| { + total.saturating_add(*emission) + }) + .saturating_to_num::(); + let planned_tao_rao = tao_in + .values() + .chain(excess_tao.values()) + .fold(0_u64, |total, amount| { + total.saturating_add(amount.saturating_to_num::()) + }); + let mut integer_spillover_rao = total_emission_rao.saturating_sub(planned_tao_rao); + + for (netuid, _) in spillover_priority { + if integer_spillover_rao == 0 { + break; + } + + let current_chain_buy = *excess_tao.get(&netuid).unwrap_or(&asfloat!(0)); + let chain_buy_cap = *chain_buy_caps.get(&netuid).unwrap_or(&asfloat!(0)); + let current_chain_buy_rao = current_chain_buy.saturating_to_num::(); + let chain_buy_cap_rao = chain_buy_cap.saturating_to_num::(); + if current_chain_buy_rao >= chain_buy_cap_rao { + continue; + } + + let current_tao_in = *tao_in.get(&netuid).unwrap_or(&asfloat!(0)); + let alpha_injection_cap = *alpha_injection_caps.get(&netuid).unwrap_or(&asfloat!(0)); + let price = *subnet_prices.get(&netuid).unwrap_or(&asfloat!(0)); + let tao_injection_cap = alpha_injection_cap.saturating_mul(price); + let current_tao_in_rao = current_tao_in.saturating_to_num::(); + let tao_injection_cap_rao = tao_injection_cap.saturating_to_num::(); + let uncovered_liquidity_rao = tao_injection_cap_rao.saturating_sub(current_tao_in_rao); + let spillover_liquidity_rao = integer_spillover_rao.min(uncovered_liquidity_rao); + if spillover_liquidity_rao > 0 { + let updated_tao_in_rao = current_tao_in_rao.saturating_add(spillover_liquidity_rao); + let updated_tao_in = asfloat!(updated_tao_in_rao); + tao_in.insert(netuid, updated_tao_in); + alpha_in.insert( + netuid, + updated_tao_in + .safe_div_or(price, asfloat!(0)) + .min(alpha_injection_cap), + ); + integer_spillover_rao = + integer_spillover_rao.saturating_sub(spillover_liquidity_rao); + } + + if integer_spillover_rao == 0 { + break; + } + + let uncovered_chain_buy_rao = chain_buy_cap_rao.saturating_sub(current_chain_buy_rao); + let spillover_chain_buy_rao = integer_spillover_rao.min(uncovered_chain_buy_rao); + if spillover_chain_buy_rao == 0 { + continue; + } + + excess_tao.insert( + netuid, + asfloat!(current_chain_buy_rao.saturating_add(spillover_chain_buy_rao)), + ); + integer_spillover_rao = integer_spillover_rao.saturating_sub(spillover_chain_buy_rao); + } + (tao_in, alpha_in, alpha_out, excess_tao) } @@ -280,6 +539,7 @@ impl Pallet { // --- 2. Inject TAO and ALPHA to pool and swap with excess TAO. Self::inject_and_maybe_swap( subnets_to_emit_to, + subnet_emissions, &tao_in, &alpha_in, &excess_amount, diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 3cf4318e9d..1744960969 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1531,6 +1531,13 @@ pub mod pallet { pub type SubnetTaoInEmission = StorageMap<_, Identity, NetUid, TaoBalance, ValueQuery, DefaultZeroTao>; + /// MAP ( netuid ) --> tao_emission | Returns the total TAO allocated to this subnet from the + /// most recent block emission. This includes current-block liquidity, successful chain buys + /// after spillover, and current-block TAO placed in the protocol reservoir. + #[pallet::storage] + pub type SubnetTaoEmission = + 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 = diff --git a/pallets/subtensor/src/rpc_info/dynamic_info.rs b/pallets/subtensor/src/rpc_info/dynamic_info.rs index b143295425..05aa7e2526 100644 --- a/pallets/subtensor/src/rpc_info/dynamic_info.rs +++ b/pallets/subtensor/src/rpc_info/dynamic_info.rs @@ -55,7 +55,7 @@ impl Pallet { tempo: Tempo::::get(netuid).into(), last_step: last_step.into(), blocks_since_last_step: blocks_since_last_step.into(), - emission: 0.into(), + emission: u64::from(SubnetTaoEmission::::get(netuid)).into(), alpha_in: SubnetAlphaIn::::get(netuid).into(), alpha_out: SubnetAlphaOut::::get(netuid).into(), tao_in: SubnetTAO::::get(netuid).into(), @@ -81,3 +81,23 @@ impl Pallet { dynamic_info } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::mock::{SubtensorModule, Test, add_dynamic_network, new_test_ext}; + use sp_core::U256; + + #[test] + fn dynamic_info_reports_final_tao_emission() { + new_test_ext(1).execute_with(|| { + let netuid = add_dynamic_network(&U256::from(1), &U256::from(2)); + let final_emission = TaoBalance::from(123_u64); + SubnetTaoEmission::::insert(netuid, final_emission); + + let info = SubtensorModule::get_dynamic_info(netuid).unwrap(); + + assert_eq!(info.emission.0, u64::from(final_emission)); + }); + } +} diff --git a/pallets/subtensor/src/rpc_info/metagraph.rs b/pallets/subtensor/src/rpc_info/metagraph.rs index a627a4eff3..c3218801c3 100644 --- a/pallets/subtensor/src/rpc_info/metagraph.rs +++ b/pallets/subtensor/src/rpc_info/metagraph.rs @@ -718,7 +718,7 @@ impl Pallet { blocks_since_last_step: blocks_since_last_step.into(), // blocks since last epoch. // Subnet emission terms - subnet_emission: 0.into(), // DEPRECATED + subnet_emission: u64::from(SubnetTaoEmission::::get(netuid)).into(), // final TAO allocated on the most recent block alpha_in: SubnetAlphaIn::::get(netuid).into(), // amount of alpha in reserve alpha_out: SubnetAlphaOut::::get(netuid).into(), // amount of alpha outstanding tao_in: SubnetTAO::::get(netuid).into(), // amount of tao injected per block @@ -994,7 +994,7 @@ impl Pallet { // Subnet emission terms Some(SelectiveMetagraphIndex::SubnetEmission) => SelectiveMetagraph { netuid: netuid.into(), - subnet_emission: Some(0.into()), + subnet_emission: Some(u64::from(SubnetTaoEmission::::get(netuid)).into()), ..Default::default() }, Some(SelectiveMetagraphIndex::AlphaIn) => SelectiveMetagraph { @@ -1725,3 +1725,32 @@ fn test_selective_metagraph() { metagraph.merge_value(&metagraph_alpha_low, alph_low_index); assert!(metagraph.alpha_low.is_some()); } + +#[cfg(test)] +mod emission_tests { + use super::*; + use crate::tests::mock::{SubtensorModule, Test, add_dynamic_network, new_test_ext}; + use sp_core::U256; + + #[test] + fn metagraphs_report_final_tao_emission() { + new_test_ext(1).execute_with(|| { + let netuid = add_dynamic_network(&U256::from(1), &U256::from(2)); + let final_emission = TaoBalance::from(456_u64); + SubnetTaoEmission::::insert(netuid, final_emission); + + let metagraph = SubtensorModule::get_metagraph(netuid).unwrap(); + let selective = SubtensorModule::get_selective_metagraph( + netuid, + vec![SelectiveMetagraphIndex::SubnetEmission as u16], + ) + .unwrap(); + + assert_eq!(metagraph.subnet_emission.0, u64::from(final_emission)); + assert_eq!( + selective.subnet_emission, + Some(u64::from(final_emission).into()) + ); + }); + } +} diff --git a/pallets/subtensor/src/subnets/dissolution.rs b/pallets/subtensor/src/subnets/dissolution.rs index 760d24aaa5..45af2ce024 100644 --- a/pallets/subtensor/src/subnets/dissolution.rs +++ b/pallets/subtensor/src/subnets/dissolution.rs @@ -291,6 +291,7 @@ impl Pallet { SubnetAlphaInEmission::::remove(netuid); SubnetAlphaOutEmission::::remove(netuid); SubnetTaoInEmission::::remove(netuid); + SubnetTaoEmission::::remove(netuid); SubnetVolume::::remove(netuid); SubnetMovingPrice::::remove(netuid); SubnetTaoFlow::::remove(netuid); diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index 4a54157b63..2130c84395 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -92,6 +92,7 @@ fn test_coinbase_tao_issuance_base() { let emission_credit = SubtensorModule::mint_tao(emission); SubtensorModule::run_coinbase(emission_credit); assert_eq!(SubnetTAO::::get(netuid), tao_in_before + emission); + assert_eq!(SubnetTaoEmission::::get(netuid), emission); assert_eq!( TotalIssuance::::get(), total_issuance_before + emission @@ -282,17 +283,17 @@ fn test_sudo_set_subnet_emission_enabled_multiple_subnets_multiple_toggles() { assert_abs_diff_eq!( SubnetTaoInEmission::::get(netuid1), TaoBalance::from(expected1), - epsilon = 2.into(), + epsilon = 10.into(), ); assert_abs_diff_eq!( SubnetTaoInEmission::::get(netuid2), TaoBalance::from(expected2), - epsilon = 2.into(), + epsilon = 10.into(), ); assert_abs_diff_eq!( SubnetTaoInEmission::::get(netuid3), TaoBalance::from(expected3), - epsilon = 2.into(), + epsilon = 10.into(), ); assert_eq!( @@ -742,7 +743,6 @@ fn test_coinbase_alpha_issuance_with_cap_trigger_and_block_emission() { let netuid1 = NetUid::from(1); let netuid2 = NetUid::from(2); let emission: u64 = 1_000_000; - let emission_credit = SubtensorModule::mint_tao(emission.into()); add_network(netuid1, 1, 0); add_network(netuid2, 1, 0); @@ -777,35 +777,56 @@ fn test_coinbase_alpha_issuance_with_cap_trigger_and_block_emission() { SubnetAlphaOut::::insert(netuid2, AlphaBalance::from(21_000_000_000_000_000_u64)); // Set issuance above 21M // Run coinbase + let issuance_before = TotalIssuance::::get(); + let emission_credit = SubtensorModule::mint_tao(emission.into()); SubtensorModule::run_coinbase(emission_credit); - // New behavior: chain-bought alpha is cached instead of recycled. - // The cached amount remains part of outstanding alpha supply. - assert!( - !SubnetProtocolAlpha::::get(netuid1).is_zero() - || !SubnetProtocolAlpha::::get(netuid2).is_zero() + // At the alpha supply cap there is no miner or validator alpha emission, + // so the chain-buy cap is zero and all TAO is allocated to subnet + // reservoirs by emission weight. + assert_eq!( + SubnetProtocolAlpha::::get(netuid1), + AlphaBalance::ZERO + ); + assert_eq!( + SubnetProtocolAlpha::::get(netuid2), + AlphaBalance::ZERO + ); + assert_eq!( + TotalIssuance::::get(), + issuance_before.saturating_add(TaoBalance::from(emission)) + ); + assert_eq!( + pallet_subtensor_swap::BalancerTaoReservoir::::get(netuid1).saturating_add( + pallet_subtensor_swap::BalancerTaoReservoir::::get(netuid2) + ), + TaoBalance::from(emission) ); // Get the prices after the run_coinbase let price_1_after = ::SwapInterface::current_alpha_price(netuid1); let price_2_after = ::SwapInterface::current_alpha_price(netuid2); - // AlphaIn gets decreased beacuse of a buy - assert!(u64::from(SubnetAlphaIn::::get(netuid1)) < initial_alpha); + // No chain buy means pool reserves and prices remain unchanged. assert_eq!( - u64::from(SubnetAlphaOut::::get(netuid2)), + u64::from(SubnetAlphaIn::::get(netuid1)), + initial_alpha + ); + assert_eq!( + u64::from(SubnetAlphaOut::::get(netuid1)), 21_000_000_000_000_000_u64 - .saturating_add(u64::from(SubnetProtocolAlpha::::get(netuid2))) ); - assert!(u64::from(SubnetAlphaIn::::get(netuid2)) < initial_alpha); + assert_eq!( + u64::from(SubnetAlphaIn::::get(netuid2)), + initial_alpha + ); assert_eq!( u64::from(SubnetAlphaOut::::get(netuid2)), 21_000_000_000_000_000_u64 - .saturating_add(u64::from(SubnetProtocolAlpha::::get(netuid2))) ); - assert!(price_1_after > price_1_before); - assert!(price_2_after > price_2_before); + assert_eq!(price_1_after, price_1_before); + assert_eq!(price_2_after, price_2_before); }); } @@ -2039,6 +2060,7 @@ fn test_incentive_to_subnet_owner_is_burned() { assert_eq!(subnet_owner_stake_after, 0.into()); let other_stake_after = SubtensorModule::get_stake_for_hotkey_on_subnet(&other_hk, netuid); assert!(other_stake_after > 0.into()); + assert_eq!(MinerBurned::::get(netuid), U96F32::from_num(0.5)); }); } @@ -2094,6 +2116,7 @@ fn test_incentive_to_subnet_owners_hotkey_is_burned() { assert_eq!(subnet_owner_stake_after, 0.into()); let other_stake_after = SubtensorModule::get_stake_for_hotkey_on_subnet(&other_hk, netuid); assert_eq!(other_stake_after, 0.into()); + assert_eq!(MinerBurned::::get(netuid), U96F32::from_num(1)); }); } @@ -3680,6 +3703,645 @@ fn test_coinbase_subnet_terms_with_alpha_in_lte_alpha_emission() { }); } +#[test] +fn test_coinbase_chain_buy_is_capped_at_unburned_miner_alpha_value() { + new_test_ext(1).execute_with(|| { + let netuid = add_dynamic_network(&U256::from(1), &U256::from(2)); + mock::setup_reserves( + netuid, + TaoBalance::from(1_000_000_000_000_000_u64), + AlphaBalance::from(1_000_000_000_000_000_u64), + ); + Swap::maybe_initialize_palswap(netuid, None); + + // Force root_proportion to zero so the full subnet TAO allocation is + // eligible for the chain-buy path. + SubnetTAO::::insert(NetUid::ROOT, TaoBalance::ZERO); + SubnetOwnerCut::::set(u16::MAX / 10); + SubtensorModule::set_owner_cut_enabled_flag(netuid, true); + MinerBurned::::insert(netuid, U96F32::from_num(0)); + + let alpha_emission = U96F32::saturating_from_num( + SubtensorModule::get_block_emission_for_issuance( + SubtensorModule::get_alpha_issuance(netuid).into(), + ) + .unwrap_or(0), + ); + let price = U96F32::saturating_from_num(Swap::current_alpha_price(netuid)); + let owner_cut = + alpha_emission.saturating_mul(SubtensorModule::get_float_subnet_owner_cut()); + let participant_alpha = alpha_emission.saturating_sub(owner_cut); + let miner_alpha = participant_alpha.saturating_mul(U96F32::from_num(0.5)); + let chain_buy_cap = miner_alpha.saturating_mul(price); + let tao_emission = chain_buy_cap.saturating_mul(U96F32::from_num(2)); + + let subnet_emissions = BTreeMap::from([(netuid, tao_emission)]); + let (tao_in, _, _, excess_tao) = SubtensorModule::get_subnet_terms(&subnet_emissions); + + assert_eq!(tao_in[&netuid], U96F32::from_num(0)); + assert_eq!(excess_tao[&netuid], chain_buy_cap); + + // A chain buy already below the participant-emission value is unchanged. + let below_cap = chain_buy_cap.saturating_div(U96F32::from_num(2)); + let subnet_emissions = BTreeMap::from([(netuid, below_cap)]); + let (_, _, _, excess_tao) = SubtensorModule::get_subnet_terms(&subnet_emissions); + assert_eq!(excess_tao[&netuid], below_cap); + + // Miner emission withheld by burn/recycle UIDs reduces chain-buy + // capacity by the same proportion. + MinerBurned::::insert(netuid, U96F32::from_num(0.25)); + let partially_burned_cap = chain_buy_cap.saturating_mul(U96F32::from_num(0.75)); + let subnet_emissions = BTreeMap::from([( + netuid, + partially_burned_cap.saturating_mul(U96F32::from_num(2)), + )]); + let (_, _, _, excess_tao) = SubtensorModule::get_subnet_terms(&subnet_emissions); + assert_eq!(excess_tao[&netuid], partially_burned_cap); + + // A subnet withholding all miner emission gets no chain-buy capacity. + MinerBurned::::insert(netuid, U96F32::from_num(1)); + let subnet_emissions = BTreeMap::from([(netuid, chain_buy_cap)]); + let (_, _, _, excess_tao) = SubtensorModule::get_subnet_terms(&subnet_emissions); + assert_eq!(excess_tao[&netuid], U96F32::from_num(0)); + + // With owner cut disabled, the miner half is calculated from the full + // alpha_out amount before applying the burn adjustment. + SubtensorModule::set_owner_cut_enabled_flag(netuid, false); + MinerBurned::::insert(netuid, U96F32::from_num(0)); + let full_miner_cap = alpha_emission + .saturating_mul(U96F32::from_num(0.5)) + .saturating_mul(price); + let subnet_emissions = + BTreeMap::from([(netuid, full_miner_cap.saturating_mul(U96F32::from_num(2)))]); + let (_, _, _, excess_tao) = SubtensorModule::get_subnet_terms(&subnet_emissions); + assert_eq!(excess_tao[&netuid], full_miner_cap); + }); +} + +#[test] +fn test_coinbase_chain_buy_spillover_fills_highest_weight_uncovered_capacity_first() { + new_test_ext(1).execute_with(|| { + let netuid1 = add_dynamic_network(&U256::from(1), &U256::from(2)); + let netuid2 = add_dynamic_network(&U256::from(3), &U256::from(4)); + let netuid3 = add_dynamic_network(&U256::from(5), &U256::from(6)); + let netuid4 = add_dynamic_network(&U256::from(7), &U256::from(8)); + let netuids = [netuid1, netuid2, netuid3, netuid4]; + + for netuid in netuids { + mock::setup_reserves( + netuid, + TaoBalance::from(1_000_000_000_000_000_u64), + AlphaBalance::from(1_000_000_000_000_000_u64), + ); + Swap::maybe_initialize_palswap(netuid, None); + SubtensorModule::set_owner_cut_enabled_flag(netuid, false); + MinerBurned::::insert(netuid, U96F32::from_num(0)); + } + + // Make the full subnet TAO allocation eligible for chain buys. + SubnetTAO::::insert(NetUid::ROOT, TaoBalance::ZERO); + + let alpha_emission = U96F32::saturating_from_num( + SubtensorModule::get_block_emission_for_issuance( + SubtensorModule::get_alpha_issuance(netuid1).into(), + ) + .unwrap_or(0), + ); + let price = U96F32::saturating_from_num(Swap::current_alpha_price(netuid1)); + let chain_buy_cap = alpha_emission + .saturating_mul(U96F32::from_num(0.5)) + .saturating_mul(price); + let tenth = chain_buy_cap.saturating_div(U96F32::from_num(10)); + + // netuid1 produces 0.2 cap of spillover. netuid2 has greater weight than + // netuid3 but is already capped, so it is skipped. netuid3 is filled + // first, and the remaining 0.1 cap goes to netuid4. + let subnet_emissions = BTreeMap::from([ + ( + netuid1, + chain_buy_cap.saturating_add(tenth.saturating_mul(U96F32::from_num(2))), + ), + (netuid2, chain_buy_cap), + (netuid3, chain_buy_cap.saturating_sub(tenth)), + ( + netuid4, + chain_buy_cap.saturating_sub(tenth.saturating_mul(U96F32::from_num(2))), + ), + ]); + + let (_, _, _, chain_buys) = SubtensorModule::get_subnet_terms(&subnet_emissions); + + assert_eq!(chain_buys[&netuid1], chain_buy_cap); + assert_eq!(chain_buys[&netuid2], chain_buy_cap); + assert_eq!(chain_buys[&netuid3], chain_buy_cap); + assert_eq!(chain_buys[&netuid4], chain_buy_cap.saturating_sub(tenth)); + assert!(chain_buys[&netuid4] < chain_buy_cap); + for chain_buy in chain_buys.values() { + assert!(*chain_buy <= chain_buy_cap); + } + assert_eq!( + chain_buys + .values() + .fold(U96F32::from_num(0), |total, amount| { + total.saturating_add(*amount) + }), + subnet_emissions + .values() + .fold(U96F32::from_num(0), |total, amount| { + total.saturating_add(*amount) + }) + ); + }); +} + +#[test] +fn test_coinbase_chain_buy_spillover_fills_liquidity_before_buyback() { + new_test_ext(1).execute_with(|| { + let source = add_dynamic_network(&U256::from(1), &U256::from(2)); + let recipient = add_dynamic_network(&U256::from(3), &U256::from(4)); + + for netuid in [source, recipient] { + mock::setup_reserves( + netuid, + TaoBalance::from(1_000_000_000_000_000_u64), + AlphaBalance::from(1_000_000_000_000_000_u64), + ); + Swap::maybe_initialize_palswap(netuid, None); + SubtensorModule::set_owner_cut_enabled_flag(netuid, false); + MinerBurned::::insert(netuid, U96F32::from_num(0)); + } + + // Keep root proportion near one so the recipient's original emission is + // below its liquidity-injection cap and leaves liquidity headroom. + SubnetTAO::::insert(NetUid::ROOT, TaoBalance::from(u64::MAX)); + SubtensorModule::set_tao_weight(u64::MAX); + + let alpha_emission = U96F32::saturating_from_num( + SubtensorModule::get_block_emission_for_issuance( + SubtensorModule::get_alpha_issuance(source).into(), + ) + .unwrap_or(0), + ); + let price = U96F32::saturating_from_num(Swap::current_alpha_price(source)); + let alpha_injection_cap = + SubtensorModule::root_proportion(source).saturating_mul(alpha_emission); + let tao_injection_cap = alpha_injection_cap.saturating_mul(price); + let chain_buy_cap = alpha_emission + .saturating_mul(U96F32::from_num(0.5)) + .saturating_mul(price); + let tenth = chain_buy_cap.saturating_div(U96F32::from_num(10)); + + // The source contributes 0.3 cap of spillover. The recipient needs 0.1 + // cap to finish liquidity, so only the remaining 0.2 cap becomes a buy. + let source_emission = tao_injection_cap + .saturating_add(chain_buy_cap) + .saturating_add(tenth.saturating_mul(U96F32::from_num(3))); + let recipient_emission = tao_injection_cap.saturating_sub(tenth); + let subnet_emissions = + BTreeMap::from([(source, source_emission), (recipient, recipient_emission)]); + + let (tao_in, alpha_in, _, chain_buys) = + SubtensorModule::get_subnet_terms(&subnet_emissions); + + assert_eq!(tao_in[&source], tao_injection_cap); + assert_eq!(chain_buys[&source], chain_buy_cap); + assert_eq!(tao_in[&recipient], tao_injection_cap); + assert_eq!(alpha_in[&recipient], alpha_injection_cap); + let continuous_recipient_chain_buy = tenth.saturating_mul(U96F32::from_num(2)); + assert!(chain_buys[&recipient] >= continuous_recipient_chain_buy); + assert!( + chain_buys[&recipient].saturating_sub(continuous_recipient_chain_buy) + <= U96F32::from_num(1) + ); + assert_eq!( + tao_in + .values() + .chain(chain_buys.values()) + .fold(0_u64, |total, amount| { + total.saturating_add(amount.saturating_to_num::()) + }), + source_emission + .saturating_add(recipient_emission) + .saturating_to_num::() + ); + + let total_emission = TaoBalance::from( + source_emission + .saturating_add(recipient_emission) + .saturating_to_num::(), + ); + let expected_source_liquidity = + TaoBalance::from(tao_in[&source].saturating_to_num::()); + let expected_recipient_liquidity = + TaoBalance::from(tao_in[&recipient].saturating_to_num::()); + let expected_source_alpha = + AlphaBalance::from(alpha_in[&source].saturating_to_num::()); + let expected_recipient_alpha = + AlphaBalance::from(alpha_in[&recipient].saturating_to_num::()); + let issuance_before = TotalIssuance::::get(); + + let credit = SubtensorModule::mint_tao(total_emission); + SubtensorModule::emit_to_subnets(&[source, recipient], &subnet_emissions, credit, false); + + assert_eq!( + SubnetTaoInEmission::::get(source), + expected_source_liquidity + ); + assert_eq!( + SubnetTaoInEmission::::get(recipient), + expected_recipient_liquidity + ); + assert_eq!( + SubnetAlphaInEmission::::get(source), + expected_source_alpha + ); + assert_eq!( + SubnetAlphaInEmission::::get(recipient), + expected_recipient_alpha + ); + assert_eq!( + SubnetExcessTao::::get(source), + TaoBalance::from(chain_buys[&source].saturating_to_num::()) + ); + assert_eq!( + SubnetExcessTao::::get(recipient), + TaoBalance::from(chain_buys[&recipient].saturating_to_num::()) + ); + assert_eq!( + pallet_subtensor_swap::BalancerTaoReservoir::::get(source).saturating_add( + pallet_subtensor_swap::BalancerTaoReservoir::::get(recipient,) + ), + TaoBalance::ZERO + ); + assert_eq!( + SubnetTaoEmission::::get(source) + .saturating_add(SubnetTaoEmission::::get(recipient)), + total_emission + ); + assert_eq!( + TotalIssuance::::get(), + issuance_before.saturating_add(total_emission) + ); + }); +} + +#[test] +fn test_coinbase_chain_buy_spillover_stops_partway_through_liquidity() { + new_test_ext(1).execute_with(|| { + let source = add_dynamic_network(&U256::from(1), &U256::from(2)); + let first_recipient = add_dynamic_network(&U256::from(3), &U256::from(4)); + let second_recipient = add_dynamic_network(&U256::from(5), &U256::from(6)); + + for netuid in [source, first_recipient, second_recipient] { + mock::setup_reserves( + netuid, + TaoBalance::from(1_000_000_000_000_000_u64), + AlphaBalance::from(1_000_000_000_000_000_u64), + ); + Swap::maybe_initialize_palswap(netuid, None); + SubtensorModule::set_owner_cut_enabled_flag(netuid, false); + MinerBurned::::insert(netuid, U96F32::from_num(0)); + } + + SubnetTAO::::insert(NetUid::ROOT, TaoBalance::from(u64::MAX)); + SubtensorModule::set_tao_weight(u64::MAX); + + let alpha_emission = U96F32::saturating_from_num( + SubtensorModule::get_block_emission_for_issuance( + SubtensorModule::get_alpha_issuance(source).into(), + ) + .unwrap_or(0), + ); + let price = U96F32::saturating_from_num(Swap::current_alpha_price(source)); + let alpha_injection_cap = + SubtensorModule::root_proportion(source).saturating_mul(alpha_emission); + let tao_injection_cap = alpha_injection_cap.saturating_mul(price); + let chain_buy_cap = alpha_emission + .saturating_mul(U96F32::from_num(0.5)) + .saturating_mul(price); + let tenth = chain_buy_cap.saturating_div(U96F32::from_num(10)); + let partial_spillover = tenth.saturating_div(U96F32::from_num(2)); + let recipient_emission = tao_injection_cap.saturating_sub(tenth); + let subnet_emissions = BTreeMap::from([ + ( + source, + tao_injection_cap + .saturating_add(chain_buy_cap) + .saturating_add(partial_spillover), + ), + (first_recipient, recipient_emission), + (second_recipient, recipient_emission), + ]); + + let (tao_in, _, _, chain_buys) = SubtensorModule::get_subnet_terms(&subnet_emissions); + + let continuous_first_recipient_tao = recipient_emission.saturating_add(partial_spillover); + assert!(tao_in[&first_recipient] >= continuous_first_recipient_tao); + assert!( + tao_in[&first_recipient].saturating_sub(continuous_first_recipient_tao) + <= U96F32::from_num(1) + ); + assert!(tao_in[&first_recipient] < tao_injection_cap); + assert_eq!(chain_buys[&first_recipient], U96F32::from_num(0)); + assert_eq!(tao_in[&second_recipient], recipient_emission); + assert_eq!(chain_buys[&second_recipient], U96F32::from_num(0)); + assert_eq!( + tao_in + .values() + .chain(chain_buys.values()) + .fold(0_u64, |total, amount| { + total.saturating_add(amount.saturating_to_num::()) + }), + subnet_emissions + .values() + .fold(U96F32::from_num(0), |total, emission| { + total.saturating_add(*emission) + }) + .saturating_to_num::() + ); + }); +} + +#[test] +fn test_coinbase_chain_buy_spillover_uses_netuid_to_break_weight_ties() { + new_test_ext(1).execute_with(|| { + let source = add_dynamic_network(&U256::from(1), &U256::from(2)); + let lower_netuid = add_dynamic_network(&U256::from(3), &U256::from(4)); + let higher_netuid = add_dynamic_network(&U256::from(5), &U256::from(6)); + + for netuid in [source, lower_netuid, higher_netuid] { + mock::setup_reserves( + netuid, + TaoBalance::from(1_000_000_000_000_000_u64), + AlphaBalance::from(1_000_000_000_000_000_u64), + ); + Swap::maybe_initialize_palswap(netuid, None); + SubtensorModule::set_owner_cut_enabled_flag(netuid, false); + MinerBurned::::insert(netuid, U96F32::from_num(0)); + } + SubnetTAO::::insert(NetUid::ROOT, TaoBalance::ZERO); + + let alpha_emission = U96F32::saturating_from_num( + SubtensorModule::get_block_emission_for_issuance( + SubtensorModule::get_alpha_issuance(source).into(), + ) + .unwrap_or(0), + ); + let price = U96F32::saturating_from_num(Swap::current_alpha_price(source)); + let chain_buy_cap = alpha_emission + .saturating_mul(U96F32::from_num(0.5)) + .saturating_mul(price); + let tenth = chain_buy_cap.saturating_div(U96F32::from_num(10)); + let tied_emission = chain_buy_cap.saturating_sub(tenth); + let subnet_emissions = BTreeMap::from([ + (source, chain_buy_cap.saturating_add(tenth)), + (lower_netuid, tied_emission), + (higher_netuid, tied_emission), + ]); + + let (_, _, _, chain_buys) = SubtensorModule::get_subnet_terms(&subnet_emissions); + + assert_eq!(chain_buys[&source], chain_buy_cap); + assert_eq!(chain_buys[&lower_netuid], chain_buy_cap); + assert_eq!(chain_buys[&higher_netuid], tied_emission); + }); +} + +#[test] +fn test_coinbase_chain_buy_spillover_keeps_zero_emission_subnets_ineligible() { + new_test_ext(1).execute_with(|| { + let source = add_dynamic_network(&U256::from(1), &U256::from(2)); + let recipient = add_dynamic_network(&U256::from(3), &U256::from(4)); + let zero_emission = add_dynamic_network(&U256::from(5), &U256::from(6)); + + for netuid in [source, recipient, zero_emission] { + mock::setup_reserves( + netuid, + TaoBalance::from(1_000_000_000_000_000_u64), + AlphaBalance::from(1_000_000_000_000_000_u64), + ); + Swap::maybe_initialize_palswap(netuid, None); + SubtensorModule::set_owner_cut_enabled_flag(netuid, false); + MinerBurned::::insert(netuid, U96F32::from_num(0)); + } + SubnetTAO::::insert(NetUid::ROOT, TaoBalance::ZERO); + + let alpha_emission = U96F32::saturating_from_num( + SubtensorModule::get_block_emission_for_issuance( + SubtensorModule::get_alpha_issuance(source).into(), + ) + .unwrap_or(0), + ); + let price = U96F32::saturating_from_num(Swap::current_alpha_price(source)); + let chain_buy_cap = alpha_emission + .saturating_mul(U96F32::from_num(0.5)) + .saturating_mul(price); + let tenth = chain_buy_cap.saturating_div(U96F32::from_num(10)); + let subnet_emissions = BTreeMap::from([ + (source, chain_buy_cap.saturating_add(tenth)), + (recipient, chain_buy_cap.saturating_sub(tenth)), + (zero_emission, U96F32::from_num(0)), + ]); + + let (tao_in, _, _, chain_buys) = SubtensorModule::get_subnet_terms(&subnet_emissions); + + assert_eq!(tao_in[&zero_emission], U96F32::from_num(0)); + assert_eq!(chain_buys[&source], chain_buy_cap); + assert_eq!(chain_buys[&recipient], chain_buy_cap); + assert_eq!(chain_buys[&zero_emission], U96F32::from_num(0)); + }); +} + +#[test] +fn test_coinbase_materializes_spillover_as_chain_buy_after_liquidity_before_reservoir() { + new_test_ext(1).execute_with(|| { + let source = add_dynamic_network(&U256::from(1), &U256::from(2)); + let recipient = add_dynamic_network(&U256::from(3), &U256::from(4)); + + for netuid in [source, recipient] { + mock::setup_reserves( + netuid, + TaoBalance::from(1_000_000_000_000_000_u64), + AlphaBalance::from(1_000_000_000_000_000_u64), + ); + Swap::maybe_initialize_palswap(netuid, None); + SubtensorModule::set_owner_cut_enabled_flag(netuid, false); + MinerBurned::::insert(netuid, U96F32::from_num(0)); + } + SubnetTAO::::insert(NetUid::ROOT, TaoBalance::ZERO); + + let alpha_emission = U96F32::saturating_from_num( + SubtensorModule::get_block_emission_for_issuance( + SubtensorModule::get_alpha_issuance(source).into(), + ) + .unwrap_or(0), + ); + let price = U96F32::saturating_from_num(Swap::current_alpha_price(source)); + let chain_buy_cap = alpha_emission + .saturating_mul(U96F32::from_num(0.5)) + .saturating_mul(price); + let tenth = chain_buy_cap.saturating_div(U96F32::from_num(10)); + let subnet_emissions = BTreeMap::from([ + (source, chain_buy_cap.saturating_add(tenth)), + (recipient, chain_buy_cap.saturating_sub(tenth)), + ]); + let total_emission = TaoBalance::from( + subnet_emissions + .values() + .fold(U96F32::from_num(0), |total, amount| { + total.saturating_add(*amount) + }) + .saturating_to_num::(), + ); + let expected_chain_buy = TaoBalance::from(chain_buy_cap.saturating_to_num::()); + let issuance_before = TotalIssuance::::get(); + + let credit = SubtensorModule::mint_tao(total_emission); + SubtensorModule::emit_to_subnets(&[source, recipient], &subnet_emissions, credit, false); + + assert_eq!(SubnetExcessTao::::get(source), expected_chain_buy); + assert_eq!(SubnetExcessTao::::get(recipient), expected_chain_buy); + assert_eq!(SubnetTaoEmission::::get(source), expected_chain_buy); + assert_eq!( + SubnetTaoEmission::::get(recipient), + expected_chain_buy + ); + assert_eq!( + SubnetTaoEmission::::get(source) + .saturating_add(SubnetTaoEmission::::get(recipient)), + total_emission + ); + assert_eq!( + pallet_subtensor_swap::BalancerTaoReservoir::::get(source), + TaoBalance::ZERO + ); + assert_eq!( + pallet_subtensor_swap::BalancerTaoReservoir::::get(recipient), + TaoBalance::ZERO + ); + assert_eq!( + TotalIssuance::::get(), + issuance_before.saturating_add(total_emission) + ); + }); +} + +#[test] +fn test_coinbase_allocates_tao_above_chain_buy_cap() { + new_test_ext(1).execute_with(|| { + let netuid = add_dynamic_network(&U256::from(1), &U256::from(2)); + mock::setup_reserves( + netuid, + TaoBalance::from(1_000_000_000_000_000_u64), + AlphaBalance::from(1_000_000_000_000_000_u64), + ); + Swap::maybe_initialize_palswap(netuid, None); + + SubnetTAO::::insert(NetUid::ROOT, TaoBalance::ZERO); + SubnetOwnerCut::::set(u16::MAX / 10); + SubtensorModule::set_owner_cut_enabled_flag(netuid, true); + MinerBurned::::insert(netuid, U96F32::from_num(0.25)); + + let alpha_emission = U96F32::saturating_from_num( + SubtensorModule::get_block_emission_for_issuance( + SubtensorModule::get_alpha_issuance(netuid).into(), + ) + .unwrap_or(0), + ); + let price = U96F32::saturating_from_num(Swap::current_alpha_price(netuid)); + let miner_alpha = alpha_emission + .saturating_sub( + alpha_emission.saturating_mul(SubtensorModule::get_float_subnet_owner_cut()), + ) + .saturating_mul(U96F32::from_num(0.5)) + .saturating_mul(U96F32::from_num(0.75)); + let chain_buy_cap = miner_alpha.saturating_mul(price); + let tao_emission = chain_buy_cap.saturating_mul(U96F32::from_num(2)); + let tao_emission_balance = TaoBalance::from(tao_emission.saturating_to_num::()); + let expected_chain_buy = TaoBalance::from(chain_buy_cap.saturating_to_num::()); + let expected_reservoir = tao_emission_balance.saturating_sub(expected_chain_buy); + let issuance_before = TotalIssuance::::get(); + + let credit = SubtensorModule::mint_tao(tao_emission_balance); + let subnet_emissions = BTreeMap::from([(netuid, tao_emission)]); + SubtensorModule::emit_to_subnets(&[netuid], &subnet_emissions, credit, false); + + assert_eq!(SubnetExcessTao::::get(netuid), expected_chain_buy); + assert_eq!(SubnetTaoEmission::::get(netuid), tao_emission_balance); + assert_eq!( + TotalIssuance::::get(), + issuance_before.saturating_add(tao_emission_balance) + ); + assert_eq!( + pallet_subtensor_swap::BalancerTaoReservoir::::get(netuid), + expected_reservoir + ); + }); +} + +#[test] +fn test_coinbase_allocates_capped_tao_across_subnets_by_emission_weight() { + new_test_ext(1).execute_with(|| { + let netuid1 = add_dynamic_network(&U256::from(1), &U256::from(2)); + let netuid2 = add_dynamic_network(&U256::from(3), &U256::from(4)); + let initial_tao = TaoBalance::from(1_000_000_000_000_000_u64); + let initial_alpha = AlphaBalance::from(1_000_000_000_000_000_u64); + + for netuid in [netuid1, netuid2] { + mock::setup_reserves(netuid, initial_tao, initial_alpha); + Swap::maybe_initialize_palswap(netuid, None); + SubtensorModule::set_owner_cut_enabled_flag(netuid, false); + MinerBurned::::insert(netuid, U96F32::from_num(1)); + } + SubnetTAO::::insert(NetUid::ROOT, TaoBalance::ZERO); + + let emission1 = TaoBalance::from(100_u64); + let emission2 = TaoBalance::from(300_u64); + let total_emission = emission1.saturating_add(emission2); + let subnet_emissions = BTreeMap::from([ + (netuid1, U96F32::saturating_from_num(emission1)), + (netuid2, U96F32::saturating_from_num(emission2)), + ]); + let subnet_account1 = SubtensorModule::get_subnet_account_id(netuid1).unwrap(); + let subnet_account2 = SubtensorModule::get_subnet_account_id(netuid2).unwrap(); + let balance1_before = Balances::free_balance(&subnet_account1); + let balance2_before = Balances::free_balance(&subnet_account2); + let issuance_before = TotalIssuance::::get(); + + let credit = SubtensorModule::mint_tao(total_emission); + SubtensorModule::emit_to_subnets(&[netuid1, netuid2], &subnet_emissions, credit, false); + + // Both subnets have zero chain-buy capacity, so the entire emission is + // allocated to their TAO reservoirs in the original 1:3 emission ratio. + assert_eq!(SubnetExcessTao::::get(netuid1), TaoBalance::ZERO); + assert_eq!(SubnetExcessTao::::get(netuid2), TaoBalance::ZERO); + assert_eq!(SubnetTaoEmission::::get(netuid1), emission1); + assert_eq!(SubnetTaoEmission::::get(netuid2), emission2); + assert_eq!( + pallet_subtensor_swap::BalancerTaoReservoir::::get(netuid1), + emission1 + ); + assert_eq!( + pallet_subtensor_swap::BalancerTaoReservoir::::get(netuid2), + emission2 + ); + assert_eq!( + Balances::free_balance(&subnet_account1), + balance1_before.saturating_add(emission1) + ); + assert_eq!( + Balances::free_balance(&subnet_account2), + balance2_before.saturating_add(emission2) + ); + assert_eq!( + TotalIssuance::::get(), + issuance_before.saturating_add(total_emission) + ); + assert_eq!(SubnetTAO::::get(netuid1), initial_tao); + assert_eq!(SubnetTAO::::get(netuid2), initial_tao); + }); +} + // Tests for the inject and swap are in the right order. #[test] fn test_coinbase_inject_and_maybe_swap_does_not_skew_reserves() { @@ -3698,10 +4360,19 @@ fn test_coinbase_inject_and_maybe_swap_does_not_skew_reserves() { let alpha_in = BTreeMap::from([(netuid0, U96F32::saturating_from_num(456))]); // We have excess TAO, so we will be swapping with it. let excess_tao = BTreeMap::from([(netuid0, U96F32::saturating_from_num(789100))]); + let subnet_emissions = + BTreeMap::from([(netuid0, U96F32::saturating_from_num(123 + 789100))]); // Run the inject and maybe swap let credit = SubtensorModule::mint_tao((123 + 789100).into()); - SubtensorModule::inject_and_maybe_swap(&[netuid0], &tao_in, &alpha_in, &excess_tao, credit); + SubtensorModule::inject_and_maybe_swap( + &[netuid0], + &subnet_emissions, + &tao_in, + &alpha_in, + &excess_tao, + credit, + ); let tao_in_after = SubnetTAO::::get(netuid0); let alpha_in_after = SubnetAlphaIn::::get(netuid0); @@ -3737,9 +4408,17 @@ fn test_coinbase_failed_tao_materialization_does_not_activate_current_tao() { let tao_in = BTreeMap::from([(netuid, U96F32::saturating_from_num(current_tao))]); let alpha_in = BTreeMap::from([(netuid, U96F32::saturating_from_num(current_alpha))]); let excess_tao = BTreeMap::new(); + let subnet_emissions = BTreeMap::from([(netuid, U96F32::saturating_from_num(current_tao))]); let credit = SubtensorModule::mint_tao(TaoBalance::ZERO); - SubtensorModule::inject_and_maybe_swap(&[netuid], &tao_in, &alpha_in, &excess_tao, credit); + SubtensorModule::inject_and_maybe_swap( + &[netuid], + &subnet_emissions, + &tao_in, + &alpha_in, + &excess_tao, + credit, + ); assert_eq!( SubnetTAO::::get(netuid), @@ -3754,6 +4433,9 @@ fn test_coinbase_failed_tao_materialization_does_not_activate_current_tao() { pallet_subtensor_swap::BalancerTaoReservoir::::get(netuid), TaoBalance::ZERO ); + // Activating TAO from a prior-block reservoir must not be reported as + // allocation from this block when no current credit materialized. + assert_eq!(SubnetTaoEmission::::get(netuid), TaoBalance::ZERO); }); } @@ -3786,7 +4468,7 @@ fn test_alpha_reservoir_counts_toward_subnet_issuance_across_blocks() { } #[test] -fn test_coinbase_inject_and_maybe_swap_reverts_excess_tao_deposit_on_swap_failure() { +fn test_coinbase_inject_and_maybe_swap_reserves_excess_tao_on_swap_failure() { new_test_ext(1).execute_with(|| { let zero = U96F32::saturating_from_num(0); let netuid = add_dynamic_network(&U256::from(1), &U256::from(2)); @@ -3823,15 +4505,61 @@ fn test_coinbase_inject_and_maybe_swap_reverts_excess_tao_deposit_on_swap_failur let tao_in = BTreeMap::from([(netuid, zero)]); let alpha_in = BTreeMap::from([(netuid, zero)]); let excess_tao = BTreeMap::from([(netuid, U96F32::saturating_from_num(tao_to_swap))]); + let subnet_emissions = BTreeMap::from([(netuid, U96F32::saturating_from_num(tao_to_swap))]); let credit = SubtensorModule::mint_tao(tao_to_swap); - SubtensorModule::inject_and_maybe_swap(&[netuid], &tao_in, &alpha_in, &excess_tao, credit); + SubtensorModule::inject_and_maybe_swap( + &[netuid], + &subnet_emissions, + &tao_in, + &alpha_in, + &excess_tao, + credit, + ); - assert_eq!(Balances::free_balance(subnet_account), chain_before); + assert_eq!( + Balances::free_balance(subnet_account), + chain_before.saturating_add(tao_to_swap) + ); assert_eq!(SubnetTAO::::get(netuid), subnet_tao_before); assert_eq!(SubnetExcessTao::::get(netuid), TaoBalance::ZERO); - assert_eq!(TotalIssuance::::get(), total_issuance_before); - assert_eq!(Balances::total_issuance(), balances_issuance_before); + assert_eq!(SubnetTaoEmission::::get(netuid), tao_to_swap); + assert_eq!( + pallet_subtensor_swap::BalancerTaoReservoir::::get(netuid), + tao_to_swap + ); + assert_eq!( + TotalIssuance::::get(), + total_issuance_before.saturating_add(tao_to_swap) + ); + assert_eq!( + Balances::total_issuance(), + balances_issuance_before.saturating_add(tao_to_swap) + ); + }); +} + +#[test] +fn test_coinbase_final_tao_emission_clears_stale_subnet_values() { + new_test_ext(1).execute_with(|| { + let emitting = add_dynamic_network(&U256::from(1), &U256::from(2)); + let ineligible = add_dynamic_network(&U256::from(3), &U256::from(4)); + let stale = TaoBalance::from(123_u64); + + SubnetTaoEmission::::insert(emitting, stale); + SubnetTaoEmission::::insert(ineligible, stale); + + SubtensorModule::inject_and_maybe_swap( + &[emitting], + &BTreeMap::from([(emitting, U96F32::from_num(0))]), + &BTreeMap::new(), + &BTreeMap::new(), + &BTreeMap::new(), + SubtensorModule::mint_tao(TaoBalance::ZERO), + ); + + assert_eq!(SubnetTaoEmission::::get(emitting), TaoBalance::ZERO); + assert_eq!(SubnetTaoEmission::::get(ineligible), TaoBalance::ZERO); }); } diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index a1af973eb6..957b64cf18 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -420,6 +420,7 @@ fn dissolve_clears_all_per_subnet_storages() { SubnetAlphaInEmission::::insert(net, AlphaBalance::from(1)); SubnetAlphaOutEmission::::insert(net, AlphaBalance::from(1)); SubnetTaoInEmission::::insert(net, TaoBalance::from(1)); + SubnetTaoEmission::::insert(net, TaoBalance::from(1)); SubnetVolume::::insert(net, 1u128); // Items now REMOVED (not zeroed) by dissolution @@ -573,6 +574,7 @@ fn dissolve_clears_all_per_subnet_storages() { assert!(!SubnetAlphaInEmission::::contains_key(net)); assert!(!SubnetAlphaOutEmission::::contains_key(net)); assert!(!SubnetTaoInEmission::::contains_key(net)); + assert!(!SubnetTaoEmission::::contains_key(net)); assert!(!SubnetVolume::::contains_key(net)); assert!(!pallet_subtensor_swap::BalancerTaoReservoir::::contains_key(net)); assert!(!pallet_subtensor_swap::BalancerAlphaReservoir::::contains_key(net)); diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index 3b841169ed..18c3abe6fb 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -510,6 +510,12 @@ impl SwapHandler for Pallet { BalancerTaoReservoir::::get(netuid) } + fn reserve_protocol_tao(netuid: NetUid, amount: TaoBalance) { + BalancerTaoReservoir::::mutate(netuid, |total| { + *total = total.saturating_add(amount); + }); + } + fn clear_protocol_liquidity_reservoirs(netuid: NetUid) { BalancerTaoReservoir::::remove(netuid); BalancerAlphaReservoir::::remove(netuid); diff --git a/pallets/swap/src/pallet/tests.rs b/pallets/swap/src/pallet/tests.rs index dd41c84269..7fcdaa8847 100644 --- a/pallets/swap/src/pallet/tests.rs +++ b/pallets/swap/src/pallet/tests.rs @@ -185,6 +185,27 @@ mod dispatchables { }); } + #[test] + fn test_reserve_protocol_tao_accumulates_without_activating_reserves() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let initial_tao = TaoBalance::from(1_000_u64); + let initial_alpha = AlphaBalance::from(2_000_u64); + TaoReserve::set_mock_reserve(netuid, initial_tao); + AlphaReserve::set_mock_reserve(netuid, initial_alpha); + + Swap::reserve_protocol_tao(netuid, TaoBalance::from(100_u64)); + Swap::reserve_protocol_tao(netuid, TaoBalance::from(50_u64)); + + assert_eq!( + Swap::protocol_tao_reservoir(netuid), + TaoBalance::from(150_u64) + ); + assert_eq!(TaoReserve::reserve(netuid), initial_tao); + assert_eq!(AlphaReserve::reserve(netuid), initial_alpha); + }); + } + #[test] fn test_adjust_protocol_liquidity_materializes_alpha_when_reservoiring_alpha() { new_test_ext().execute_with(|| { diff --git a/primitives/swap-interface/src/lib.rs b/primitives/swap-interface/src/lib.rs index ed76dcda43..59db577be9 100644 --- a/primitives/swap-interface/src/lib.rs +++ b/primitives/swap-interface/src/lib.rs @@ -49,6 +49,9 @@ pub trait SwapHandler { ) -> (TaoBalance, AlphaBalance); fn protocol_alpha_reservoir(netuid: NetUid) -> AlphaBalance; fn protocol_tao_reservoir(netuid: NetUid) -> TaoBalance; + /// Record protocol TAO that the caller has already materialized into the + /// subnet account, without making it immediately price-active. + fn reserve_protocol_tao(netuid: NetUid, amount: TaoBalance); fn clear_protocol_liquidity_reservoirs(netuid: NetUid); fn clear_protocol_liquidity(netuid: NetUid, weight_meter: &mut WeightMeter) -> bool; fn init_swap(netuid: NetUid, maybe_price: Option);