Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 93 additions & 3 deletions pallets/subtensor/src/coinbase/run_coinbase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@ use super::*;
use alloc::collections::BTreeMap;
use safe_math::*;
use substrate_fixed::types::U96F32;
use frame_support::traits::{
Imbalance,
tokens::{
Precision, Preservation,
fungible::{Balanced, Inspect, Mutate},
},
};
use subtensor_runtime_common::{AlphaCurrency, Currency, NetUid, TaoCurrency};
use subtensor_swap_interface::SwapHandler;
use frame_support::traits::tokens::imbalance::Credit;

// Distribute dividends to each hotkey
macro_rules! asfloat {
Expand Down Expand Up @@ -43,8 +51,11 @@ impl<T: Config> Pallet<T> {
let root_sell_flag = Self::get_network_root_sell_flag(&subnets_to_emit_to);
log::debug!("Root sell flag: {root_sell_flag:?}");

// --- 4. Emit to subnets for this block.
Self::emit_to_subnets(&subnets_to_emit_to, &subnet_emissions, root_sell_flag);
// --- 4. Mint emission and emit to subnets for this block.
// We mint the emission here using the Imbalance trait to ensure conservation.
let emission_u64: u64 = block_emission.saturating_to_num::<u64>();
let imbalance = T::Currency::issue(emission_u64);
Self::emit_to_subnets(&subnets_to_emit_to, &subnet_emissions, root_sell_flag, imbalance);

// --- 5. Drain pending emissions.
let emissions_to_distribute = Self::drain_pending(&subnets, current_block);
Expand Down Expand Up @@ -101,6 +112,77 @@ impl<T: Config> Pallet<T> {
*total = total.saturating_add(injected_tao);
});

pub fn inject_and_maybe_swap(
subnets_to_emit_to: &[NetUid],
tao_in: &BTreeMap<NetUid, U96F32>,
alpha_in: &BTreeMap<NetUid, U96F32>,
excess_tao: &BTreeMap<NetUid, U96F32>,
mut imbalance: Credit<T::AccountId, T::Currency>,
) {
for netuid_i in subnets_to_emit_to.iter() {
let tao_in_i: TaoCurrency =
tou64!(*tao_in.get(netuid_i).unwrap_or(&asfloat!(0))).into();
let alpha_in_i: AlphaCurrency =
tou64!(*alpha_in.get(netuid_i).unwrap_or(&asfloat!(0))).into();
let tao_to_swap_with: TaoCurrency =
tou64!(excess_tao.get(netuid_i).unwrap_or(&asfloat!(0))).into();

// Handle Imbalance Split
// We need to account for both directly injected TAO and TAO used for swapping.
let total_tao_needed = tao_in_i.saturating_add(tao_to_swap_with);
let total_tao_u64: u64 = total_tao_needed.into();

// Extract the credit needed for this subnet.
let (subnet_credit, remaining) = imbalance.split(total_tao_u64.into());
imbalance = remaining;

// Burn/Drop the credit to "deposit" it into the virtual staking system.
// This ensures we successfully minted the required amount.
// In a future vault-based system, we would deposit this into a subnet account.
if subnet_credit.peek() < total_tao_u64.into() {
log::error!(
"CRITICAL: Insufficient emission imbalance for netuid {:?}. Needed: {:?}, Got: {:?}",
netuid_i,
total_tao_u64,
subnet_credit.peek()
);
}
let _ = subnet_credit;

T::SwapInterface::adjust_protocol_liquidity(*netuid_i, tao_in_i, alpha_in_i);

if tao_to_swap_with > TaoCurrency::ZERO {
let buy_swap_result = Self::swap_tao_for_alpha(
*netuid_i,
tao_to_swap_with,
T::SwapInterface::max_price(),
true,
);
if let Ok(buy_swap_result_ok) = buy_swap_result {
let bought_alpha: AlphaCurrency = buy_swap_result_ok.amount_paid_out.into();
Self::recycle_subnet_alpha(*netuid_i, bought_alpha);
}
}

// Inject Alpha in.
let alpha_in_i =
AlphaCurrency::from(tou64!(*alpha_in.get(netuid_i).unwrap_or(&asfloat!(0))));
SubnetAlphaInEmission::<T>::insert(*netuid_i, alpha_in_i);
SubnetAlphaIn::<T>::mutate(*netuid_i, |total| {
*total = total.saturating_add(alpha_in_i);
});

// Inject TAO in.
let injected_tao: TaoCurrency =
tou64!(*tao_in.get(netuid_i).unwrap_or(&asfloat!(0))).into();
SubnetTaoInEmission::<T>::insert(*netuid_i, injected_tao);
SubnetTAO::<T>::mutate(*netuid_i, |total| {
*total = total.saturating_add(injected_tao);
});
TotalStake::<T>::mutate(|total| {
*total = total.saturating_add(injected_tao);
});

// Update total TAO issuance.
let difference_tao = tou64!(*excess_tao.get(netuid_i).unwrap_or(&asfloat!(0)));
TotalIssuance::<T>::mutate(|total| {
Expand Down Expand Up @@ -168,6 +250,7 @@ impl<T: Config> Pallet<T> {
subnets_to_emit_to: &[NetUid],
subnet_emissions: &BTreeMap<NetUid, U96F32>,
root_sell_flag: bool,
mut imbalance: Credit<T::AccountId, T::Currency>,
) {
// --- 1. Get subnet terms (tao_in, alpha_in, and alpha_out)
// and excess_tao amounts.
Expand All @@ -179,10 +262,17 @@ impl<T: Config> Pallet<T> {
log::debug!("excess_amount: {excess_amount:?}");

// --- 2. Inject TAO and ALPHA to pool and swap with excess TAO.
Self::inject_and_maybe_swap(subnets_to_emit_to, &tao_in, &alpha_in, &excess_amount);
Self::inject_and_maybe_swap(
subnets_to_emit_to,
&tao_in,
&alpha_in,
&excess_amount,
imbalance,
);

// --- 3. Inject ALPHA for participants.
let cut_percent: U96F32 = Self::get_float_subnet_owner_cut();
// ... (rest of logic) ...

// Get total TAO on root.
let root_tao: U96F32 = asfloat!(SubnetTAO::<T>::get(NetUid::ROOT));
Expand Down
1 change: 1 addition & 0 deletions pallets/subtensor/src/coinbase/subnet_emissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ impl<T: Config> Pallet<T> {
Self::get_network_registration_allowed(*netuid)
|| Self::get_network_pow_registration_allowed(*netuid)
})
.filter(|netuid| !crate::pallet::SubnetEmissionPaused::<T>::get(*netuid))
.copied()
.collect()
}
Expand Down
76 changes: 76 additions & 0 deletions pallets/subtensor/src/invariants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use super::*;
extern crate alloc;
use frame_support::pallet_prelude::*;
use sp_std::vec::Vec;
use sp_runtime::traits::Zero;
use crate::pallet::*;
use subtensor_runtime_common::{NetUid, AlphaCurrency, TaoCurrency};

impl<T: Config> Pallet<T> {
/// Checks invariants and handles violations.
/// Should be called in on_finalize.
pub fn check_invariants() {
// 1. Check Emission Invariant (Global vs Sum of Subnets)
// Handled by Imbalance trait in run_coinbase.

// 2. Check Stake Invariant (Per Subnet)
// Invariant: sum(neuron_stake in subnet) == stored subnet_total_stake
// We check this only at epoch boundaries (tempo) to save weight.
Self::check_stake_invariant();
}

fn check_stake_invariant() {
for netuid in Self::get_all_subnet_netuids() {
// Check if emission is paused to avoid repeated spam.
if SubnetEmissionPaused::<T>::get(netuid) {
continue;
}

// Only check at epoch boundary (when BlocksSinceLastStep was reset to 0 in this block)
// This ensures we verify the state right after pending emissions are drained and stake is "settled" for the epoch.
if BlocksSinceLastStep::<T>::get(netuid) != 0 {
continue;
}

// SubnetAlphaOut represents the total outstanding Alpha shares in the subnet.
let stored_total_alpha = SubnetAlphaOut::<T>::get(netuid);
let mut calculated_total_alpha = AlphaCurrency::zero();

// Iterate all hotkeys in subnet.
// Keys::iter_prefix(netuid) gives us all (uid, hotkey) pairs in the subnet.
// We sum the TotalHotkeyAlpha for each hotkey.
// Requirement matches: "sum(neuron_stake in subnet) == stored subnet_total_stake"
for (_, hotkey) in Keys::<T>::iter_prefix(netuid) {
let alpha = TotalHotkeyAlpha::<T>::get(&hotkey, netuid);
calculated_total_alpha = calculated_total_alpha.saturating_add(alpha);
}

// We expect strict equality. Alpha represents shares, which are integers.
if stored_total_alpha != calculated_total_alpha {
let msg = alloc::format!("Stake (Alpha) mismatch: stored={:?}, calc={:?}", stored_total_alpha, calculated_total_alpha);
Self::handle_invariant_violation(netuid, &msg);
}
}
}

fn handle_invariant_violation(netuid: NetUid, details: &str) {
log::error!("CRITICAL INVARIANT VIOLATION on subnet {}: {}", netuid, details);

// Pause emissions for this subnet to prevent further economic corruption.
SubnetEmissionPaused::<T>::insert(netuid, true);

// Emit event for off-chain monitoring.
let details_bytes = details.as_bytes().to_vec();
Self::deposit_event(Event::CriticalInvariantViolation(netuid, details_bytes));

// Panic only in test environments or debug builds.
// In production, we assume the paused state is sufficient protection and we do NOT want to brick the chain.
#[cfg(any(test, feature = "std", debug_assertions))]
{
// We only panic if we are in a test or strictly debugging.
// Note: 'feature = "std"' is often enabled in dev nodes, but careful with production wasm builds (usually no-std).
// 'debug_assertions' is the standard way to detect dev/debug profile.
panic!("Invariant violation: subnet {}, {}", netuid, details);
}
}
}
6 changes: 6 additions & 0 deletions pallets/subtensor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub mod staking;
pub mod subnets;
pub mod swap;
pub mod utils;
pub mod invariants;
use crate::utils::rate_limiting::{Hyperparameter, TransactionType};
use macros::{config, dispatches, errors, events, genesis, hooks};

Expand Down Expand Up @@ -1551,6 +1552,11 @@ pub mod pallet {
pub type SubnetLocked<T: Config> =
StorageMap<_, Identity, NetUid, TaoCurrency, ValueQuery, DefaultZeroTao<T>>;

/// --- MAP ( netuid ) --> emission_paused
#[pallet::storage]
pub type SubnetEmissionPaused<T: Config> =
StorageMap<_, Identity, NetUid, bool, ValueQuery>;

/// --- MAP ( netuid ) --> largest_locked
#[pallet::storage]
pub type LargestLocked<T: Config> =
Expand Down
22 changes: 22 additions & 0 deletions pallets/subtensor/src/macros/dispatches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2431,5 +2431,27 @@ mod dispatches {

Ok(())
}

/// Unpauses emission for a subnet.
/// Can only be called by root (sudo).
#[pallet::call_index(125)]
#[pallet::weight((
Weight::from_parts(10_000_000, 0)
.saturating_add(T::DbWeight::get().writes(1_u64)),
DispatchClass::Operational,
Pays::No
))]
pub fn unpause_subnet_emission(
origin: OriginFor<T>,
netuid: NetUid
) -> DispatchResult {
ensure_root(origin)?;

if crate::pallet::SubnetEmissionPaused::<T>::take(netuid) {
Self::deposit_event(Event::SubnetEmissionResumed(netuid));
}

Ok(())
}
}
}
5 changes: 5 additions & 0 deletions pallets/subtensor/src/macros/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -477,5 +477,10 @@ mod events {
/// The amount of alpha distributed
alpha: AlphaCurrency,
},

/// A critical invariant violation has been detected.
CriticalInvariantViolation(NetUid, Vec<u8>),
/// Subnet emission has been resumed after a pause.
SubnetEmissionResumed(NetUid),
}
}
3 changes: 2 additions & 1 deletion pallets/subtensor/src/macros/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@ mod hooks {
// # Args:
// * 'n': (BlockNumberFor<T>):
// - The number of the block we are finalizing.
fn on_finalize(_block_number: BlockNumberFor<T>) {
fn on_finalize(block_number: BlockNumberFor<T>) {
for _ in StakingOperationRateLimiter::<T>::drain() {
// Clear all entries each block
}
Self::check_invariants();
}

fn on_runtime_upgrade() -> frame_support::weights::Weight {
Expand Down
72 changes: 72 additions & 0 deletions pallets/subtensor/src/tests/imbalance_invariants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use crate::tests::mock::*;
use crate::*;
use frame_support::traits::Currency;
use substrate_fixed::types::U96F32;
use subtensor_runtime_common::{AlphaCurrency, TaoCurrency};

#[test]
fn test_imbalance_conservation_burn_mint() {
new_test_ext(1).execute_with(|| {
let netuid = NetUid::from(1);
add_network(netuid, 1, 0);

// Setup initial flows to ensure non-zero emission logic runs
SubnetTaoFlow::<Test>::insert(netuid, 100_000_000_i64);
SubnetMechanism::<Test>::insert(netuid, 1);

// Ensure subnets to emit to calculates correctly
let subnets = SubtensorModule::get_all_subnet_netuids();
let to_emit = SubtensorModule::get_subnets_to_emit_to(&subnets);
assert!(to_emit.contains(&netuid));

let emission_u64: u64 = 1_000_000;
let emission = U96F32::from_num(emission_u64);

// Capture initial state
let initial_balances_issuance = Balances::total_issuance();
let initial_subtensor_issuance = TotalIssuance::<Test>::get();
let initial_stake = TotalStake::<Test>::get();

log::info!("Initial Balances Issuance: {:?}", initial_balances_issuance);
log::info!("Initial Subtensor Issuance: {:?}", initial_subtensor_issuance);
log::info!("Initial Stake: {:?}", initial_stake);

// Run Coinbase
// This should:
// 1. Issue Imbalance (Balances::TotalIssuance + 1M)
// 2. Split Imbalance
// 3. Drop/Burn Imbalance (Balances::TotalIssuance - 1M)
// 4. Update Subtensor::TotalIssuance (+1M)
// 5. Update TotalStake (+1M)

SubtensorModule::run_coinbase(emission);

let final_balances_issuance = Balances::total_issuance();
let final_subtensor_issuance = TotalIssuance::<Test>::get();
let final_stake = TotalStake::<Test>::get();

log::info!("Final Balances Issuance: {:?}", final_balances_issuance);
log::info!("Final Subtensor Issuance: {:?}", final_subtensor_issuance);
log::info!("Final Stake: {:?}", final_stake);

// CHECK 1: Real balances logic (Imbalance usage)
// Since we drop/burn the imbalance, the real issuance should return to original.
assert_eq!(
initial_balances_issuance, final_balances_issuance,
"Real Balance Issuance should be unchanged (Imbalance Mint -> Drop/Burn pattern)"
);

// CHECK 2: Virtual accounting logic
assert_eq!(
final_subtensor_issuance,
initial_subtensor_issuance + TaoCurrency::from(emission_u64),
"Virtual Subtensor Issuance should increase by emission"
);

assert_eq!(
final_stake,
initial_stake + TaoCurrency::from(emission_u64),
"Virtual Total Stake should increase by emission"
);
});
}
Loading