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);