diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 2854777abc..3f6fc41a07 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -102,7 +102,9 @@ impl Pallet { if let Ok(buy_swap_result_ok) = buy_swap_result { let bought_alpha: AlphaBalance = buy_swap_result_ok.amount_paid_out.into(); - Self::recycle_subnet_alpha(*netuid_i, bought_alpha); + SubnetProtocolAlpha::::mutate(*netuid_i, |total| { + *total = total.saturating_add(bought_alpha); + }); // Record actual excess TAO that entered pool. let actual_excess: TaoBalance = buy_swap_result_ok.amount_paid_in; diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 75735c7471..2286f996fd 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1368,6 +1368,10 @@ pub mod pallet { #[pallet::storage] pub type SubnetAlphaOut = StorageMap<_, Identity, NetUid, AlphaBalance, ValueQuery, DefaultZeroAlpha>; + /// --- MAP ( netuid ) --> protocol_alpha | Returns the protocol-owned alpha cached for the subnet. + #[pallet::storage] + pub type SubnetProtocolAlpha = + StorageMap<_, Identity, NetUid, AlphaBalance, ValueQuery, DefaultZeroAlpha>; /// --- MAP ( cold ) --> Vec | Maps coldkey to hotkeys that stake to it #[pallet::storage] diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index f2d07189a4..c3ed9e1b74 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -473,7 +473,11 @@ impl Pallet { // - track hotkeys to clear pool totals. let mut keys_to_remove: Vec<(T::AccountId, T::AccountId)> = Vec::new(); let mut stakers: Vec<(T::AccountId, T::AccountId, u128)> = Vec::new(); - let mut total_alpha_value_u128: u128 = 0; + let protocol_alpha_value_u128: u128 = SubnetAlphaIn::::get(netuid) + .saturating_add(SubnetProtocolAlpha::::get(netuid)) + .to_u64() as u128; + let mut total_alpha_value_u128: u128 = protocol_alpha_value_u128; + let mut protocol_tao_share = TaoBalance::ZERO; let hotkeys_in_subnet: Vec = TotalHotkeyAlpha::::iter_keys() .filter(|(_, this_netuid)| *this_netuid == netuid) @@ -517,16 +521,18 @@ impl Pallet { // 6) Pro‑rata distribution of the pot by α value (largest‑remainder), // **credited directly to each staker's COLDKEY free balance**. - if pot_u64 > 0 && total_alpha_value_u128 > 0 && !stakers.is_empty() { + if pot_u64 > 0 && total_alpha_value_u128 > 0 { struct Portion { _hot: A, cold: C, + is_protocol: bool, share: u64, // TAO to credit to coldkey balance rem: u128, // remainder for largest‑remainder method } let pot_u128: u128 = pot_u64 as u128; - let mut portions: Vec> = Vec::with_capacity(stakers.len()); + let mut portions: Vec> = + Vec::with_capacity(stakers.len().saturating_add(1)); let mut distributed: u128 = 0; for (hot, cold, alpha_val) in &stakers { @@ -539,6 +545,22 @@ impl Pallet { portions.push(Portion { _hot: hot.clone(), cold: cold.clone(), + is_protocol: false, + share: share_u64, + rem, + }); + } + + if protocol_alpha_value_u128 > 0 { + let prod: u128 = pot_u128.saturating_mul(protocol_alpha_value_u128); + let share_u128: u128 = prod.checked_div(total_alpha_value_u128).unwrap_or_default(); + let share_u64: u64 = share_u128.min(u128::from(u64::MAX)) as u64; + distributed = distributed.saturating_add(u128::from(share_u64)); + let rem: u128 = prod.checked_rem(total_alpha_value_u128).unwrap_or_default(); + portions.push(Portion { + _hot: owner_coldkey.clone(), + cold: owner_coldkey.clone(), + is_protocol: true, share: share_u64, rem, }); @@ -555,7 +577,9 @@ impl Pallet { // Credit each share directly to coldkey free balance. for p in portions { - if p.share > 0 { + if p.is_protocol { + protocol_tao_share = protocol_tao_share.saturating_add(p.share.into()); + } else if p.share > 0 { // Cannot fail the whole transaction if this transfer fails let _ = Self::transfer_tao_from_subnet(netuid, &p.cold, p.share.into()); } @@ -578,6 +602,7 @@ impl Pallet { SubnetAlphaIn::::remove(netuid); SubnetAlphaInProvided::::remove(netuid); SubnetAlphaOut::::remove(netuid); + SubnetProtocolAlpha::::remove(netuid); // Clear the locked balance on the subnet. Self::set_subnet_locked_balance(netuid, TaoBalance::ZERO); @@ -596,7 +621,8 @@ impl Pallet { && let Some(subnet_account) = Self::get_subnet_account_id(netuid) { // Transfer maximum transferrable up to refund to owner - let transferrable = Self::get_coldkey_balance(&subnet_account); + let transferrable = + Self::get_coldkey_balance(&subnet_account).saturating_sub(protocol_tao_share); // We do our best effort to refund owner to as full amount of refund as possible, but // we cannot fail new subnet registration, so the result is ignored. let _ = Self::transfer_tao(&subnet_account, &owner_coldkey, refund.min(transferrable)); diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index 6199aa9952..7fbf18b32b 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -622,6 +622,13 @@ fn test_coinbase_alpha_issuance_with_cap_trigger_and_block_emission() { // Run coinbase 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() + ); + // 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); @@ -631,11 +638,13 @@ fn test_coinbase_alpha_issuance_with_cap_trigger_and_block_emission() { assert_eq!( u64::from(SubnetAlphaOut::::get(netuid2)), 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(SubnetAlphaOut::::get(netuid2)), 21_000_000_000_000_000_u64 + .saturating_add(u64::from(SubnetProtocolAlpha::::get(netuid2))) ); assert!(price_1_after > price_1_before); diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index c4efc75825..50814b6b4d 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -110,6 +110,8 @@ fn dissolve_single_alpha_out_staker_gets_all_tao() { let owner_hot = U256::from(20); let net = add_dynamic_network(&owner_hot, &owner_cold); remove_owner_registration_stake(net); + SubnetAlphaIn::::insert(net, AlphaBalance::ZERO); + SubnetProtocolAlpha::::insert(net, AlphaBalance::ZERO); // 2. Single α-out staker let (s_hot, s_cold) = (U256::from(100), U256::from(200)); @@ -146,6 +148,8 @@ fn dissolve_two_stakers_pro_rata_distribution() { let oh = U256::from(51); let net = add_dynamic_network(&oh, &oc); remove_owner_registration_stake(net); + SubnetAlphaIn::::insert(net, AlphaBalance::ZERO); + SubnetProtocolAlpha::::insert(net, AlphaBalance::ZERO); // Mark this subnet as *legacy* so owner refund path is enabled. let reg_at = NetworkRegisteredAt::::get(net); @@ -366,6 +370,7 @@ fn dissolve_clears_all_per_subnet_storages() { // Items now REMOVED (not zeroed) by dissolution SubnetAlphaIn::::insert(net, AlphaBalance::from(2)); SubnetAlphaOut::::insert(net, AlphaBalance::from(3)); + SubnetProtocolAlpha::::insert(net, AlphaBalance::from(4)); // Prefix / double-map collections Keys::::insert(net, 0u16, owner_hot); @@ -521,6 +526,7 @@ fn dissolve_clears_all_per_subnet_storages() { // These are now REMOVED assert!(!SubnetAlphaIn::::contains_key(net)); assert!(!SubnetAlphaOut::::contains_key(net)); + assert!(!SubnetProtocolAlpha::::contains_key(net)); // Collections fully cleared assert!(Keys::::iter_prefix(net).next().is_none()); @@ -688,6 +694,8 @@ fn dissolve_rounding_remainder_distribution() { let oh = U256::from(62); let net = add_dynamic_network(&oh, &oc); remove_owner_registration_stake(net); + SubnetAlphaIn::::insert(net, AlphaBalance::ZERO); + SubnetProtocolAlpha::::insert(net, AlphaBalance::ZERO); let (s1h, s1c) = (U256::from(63), U256::from(64)); let (s2h, s2c) = (U256::from(65), U256::from(66)); @@ -721,6 +729,40 @@ fn dissolve_rounding_remainder_distribution() { }); } +#[test] +fn dissolve_protocol_alpha_share_is_not_paid_to_users() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(610); + let owner_hot = U256::from(620); + let net = add_dynamic_network(&owner_hot, &owner_cold); + remove_owner_registration_stake(net); + SubtensorModule::set_subnet_locked_balance(net, TaoBalance::ZERO); + + // Protocol owns both alpha-in and cached chain-buy alpha on dereg. + SubnetAlphaIn::::insert(net, AlphaBalance::from(100u64)); + SubnetProtocolAlpha::::insert(net, AlphaBalance::from(50u64)); + + let staker_hot = U256::from(630); + let staker_cold = U256::from(640); + AlphaV2::::insert((staker_hot, staker_cold, net), sf_from_u64(50u64)); + TotalHotkeyAlpha::::insert(staker_hot, net, AlphaBalance::from(50u64)); + + let pot: u64 = 200; + SubnetTAO::::insert(net, TaoBalance::from(pot)); + + let staker_before = SubtensorModule::get_coldkey_balance(&staker_cold); + assert_ok!(SubtensorModule::do_dissolve_network(net)); + + // User gets 50 / (100 alpha-in + 50 cached protocol alpha + 50 user alpha) + // of the TAO pot. The protocol share is withheld from user/owner payout. + assert_eq!( + SubtensorModule::get_coldkey_balance(&staker_cold), + staker_before + 50.into() + ); + assert!(!SubnetProtocolAlpha::::contains_key(net)); + }); +} + #[test] fn destroy_alpha_out_multiple_stakers_pro_rata() { new_test_ext(0).execute_with(|| { @@ -763,6 +805,9 @@ fn destroy_alpha_out_multiple_stakers_pro_rata() { )); // 4. α-out snapshot + + SubnetAlphaIn::::insert(netuid, AlphaBalance::ZERO); + SubnetProtocolAlpha::::insert(netuid, AlphaBalance::ZERO); let a1: u128 = sf_to_u128(&AlphaV2::::get((h1, c1, netuid))); let a2: u128 = sf_to_u128(&AlphaV2::::get((h2, c2, netuid))); let atotal = a1 + a2; @@ -896,6 +941,9 @@ fn destroy_alpha_out_many_stakers_complex_distribution() { let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); // ── 5) expected τ share per pallet algorithm (incl. remainder) ───── + + SubnetAlphaIn::::insert(netuid, AlphaBalance::ZERO); + SubnetProtocolAlpha::::insert(netuid, AlphaBalance::ZERO); let mut share = [0u64; N]; let mut rem = [0u128; N]; let mut paid: u128 = 0; @@ -1967,6 +2015,11 @@ fn massive_dissolve_refund_and_reregistration_flow_is_lossless_and_cleans_state( // 5) Compute Hamilton-apportionment BASE shares per cold and total leftover // from the **pair-level** pre‑LP α snapshot; also count pairs per cold. // ──────────────────────────────────────────────────────────────────── + for &net in nets.iter() { + SubnetAlphaIn::::insert(net, AlphaBalance::ZERO); + SubnetProtocolAlpha::::insert(net, AlphaBalance::ZERO); + } + let mut base_share_cold: BTreeMap = cold_lps.iter().copied().map(|c| (c, 0_u64)).collect(); let mut pair_count_cold: BTreeMap =