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
4 changes: 3 additions & 1 deletion pallets/subtensor/src/coinbase/run_coinbase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ impl<T: Config> Pallet<T> {
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::<T>::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;
Expand Down
4 changes: 4 additions & 0 deletions pallets/subtensor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1368,6 +1368,10 @@ pub mod pallet {
#[pallet::storage]
pub type SubnetAlphaOut<T: Config> =
StorageMap<_, Identity, NetUid, AlphaBalance, ValueQuery, DefaultZeroAlpha<T>>;
/// --- MAP ( netuid ) --> protocol_alpha | Returns the protocol-owned alpha cached for the subnet.
#[pallet::storage]
pub type SubnetProtocolAlpha<T: Config> =
StorageMap<_, Identity, NetUid, AlphaBalance, ValueQuery, DefaultZeroAlpha<T>>;

/// --- MAP ( cold ) --> Vec<hot> | Maps coldkey to hotkeys that stake to it
#[pallet::storage]
Expand Down
36 changes: 31 additions & 5 deletions pallets/subtensor/src/staking/remove_stake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,11 @@ impl<T: Config> Pallet<T> {
// - 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::<T>::get(netuid)
.saturating_add(SubnetProtocolAlpha::<T>::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<T::AccountId> = TotalHotkeyAlpha::<T>::iter_keys()
.filter(|(_, this_netuid)| *this_netuid == netuid)
Expand Down Expand Up @@ -517,16 +521,18 @@ impl<T: Config> Pallet<T> {

// 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<A, C> {
_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<Portion<_, _>> = Vec::with_capacity(stakers.len());
let mut portions: Vec<Portion<_, _>> =
Vec::with_capacity(stakers.len().saturating_add(1));
let mut distributed: u128 = 0;

for (hot, cold, alpha_val) in &stakers {
Expand All @@ -539,6 +545,22 @@ impl<T: Config> Pallet<T> {
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,
});
Expand All @@ -555,7 +577,9 @@ impl<T: Config> Pallet<T> {

// 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());
}
Expand All @@ -578,6 +602,7 @@ impl<T: Config> Pallet<T> {
SubnetAlphaIn::<T>::remove(netuid);
SubnetAlphaInProvided::<T>::remove(netuid);
SubnetAlphaOut::<T>::remove(netuid);
SubnetProtocolAlpha::<T>::remove(netuid);

// Clear the locked balance on the subnet.
Self::set_subnet_locked_balance(netuid, TaoBalance::ZERO);
Expand All @@ -596,7 +621,8 @@ impl<T: Config> Pallet<T> {
&& 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));
Expand Down
9 changes: 9 additions & 0 deletions pallets/subtensor/src/tests/coinbase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Test>::get(netuid1).is_zero()
|| !SubnetProtocolAlpha::<Test>::get(netuid2).is_zero()
);

// Get the prices after the run_coinbase
let price_1_after = <Test as pallet::Config>::SwapInterface::current_alpha_price(netuid1);
let price_2_after = <Test as pallet::Config>::SwapInterface::current_alpha_price(netuid2);
Expand All @@ -631,11 +638,13 @@ fn test_coinbase_alpha_issuance_with_cap_trigger_and_block_emission() {
assert_eq!(
u64::from(SubnetAlphaOut::<Test>::get(netuid2)),
21_000_000_000_000_000_u64
.saturating_add(u64::from(SubnetProtocolAlpha::<Test>::get(netuid2)))
);
assert!(u64::from(SubnetAlphaIn::<Test>::get(netuid2)) < initial_alpha);
assert_eq!(
u64::from(SubnetAlphaOut::<Test>::get(netuid2)),
21_000_000_000_000_000_u64
.saturating_add(u64::from(SubnetProtocolAlpha::<Test>::get(netuid2)))
);

assert!(price_1_after > price_1_before);
Expand Down
53 changes: 53 additions & 0 deletions pallets/subtensor/src/tests/networks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Test>::insert(net, AlphaBalance::ZERO);
SubnetProtocolAlpha::<Test>::insert(net, AlphaBalance::ZERO);

// 2. Single α-out staker
let (s_hot, s_cold) = (U256::from(100), U256::from(200));
Expand Down Expand Up @@ -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::<Test>::insert(net, AlphaBalance::ZERO);
SubnetProtocolAlpha::<Test>::insert(net, AlphaBalance::ZERO);

// Mark this subnet as *legacy* so owner refund path is enabled.
let reg_at = NetworkRegisteredAt::<Test>::get(net);
Expand Down Expand Up @@ -366,6 +370,7 @@ fn dissolve_clears_all_per_subnet_storages() {
// Items now REMOVED (not zeroed) by dissolution
SubnetAlphaIn::<Test>::insert(net, AlphaBalance::from(2));
SubnetAlphaOut::<Test>::insert(net, AlphaBalance::from(3));
SubnetProtocolAlpha::<Test>::insert(net, AlphaBalance::from(4));

// Prefix / double-map collections
Keys::<Test>::insert(net, 0u16, owner_hot);
Expand Down Expand Up @@ -521,6 +526,7 @@ fn dissolve_clears_all_per_subnet_storages() {
// These are now REMOVED
assert!(!SubnetAlphaIn::<Test>::contains_key(net));
assert!(!SubnetAlphaOut::<Test>::contains_key(net));
assert!(!SubnetProtocolAlpha::<Test>::contains_key(net));

// Collections fully cleared
assert!(Keys::<Test>::iter_prefix(net).next().is_none());
Expand Down Expand Up @@ -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::<Test>::insert(net, AlphaBalance::ZERO);
SubnetProtocolAlpha::<Test>::insert(net, AlphaBalance::ZERO);

let (s1h, s1c) = (U256::from(63), U256::from(64));
let (s2h, s2c) = (U256::from(65), U256::from(66));
Expand Down Expand Up @@ -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::<Test>::insert(net, AlphaBalance::from(100u64));
SubnetProtocolAlpha::<Test>::insert(net, AlphaBalance::from(50u64));

let staker_hot = U256::from(630);
let staker_cold = U256::from(640);
AlphaV2::<Test>::insert((staker_hot, staker_cold, net), sf_from_u64(50u64));
TotalHotkeyAlpha::<Test>::insert(staker_hot, net, AlphaBalance::from(50u64));

let pot: u64 = 200;
SubnetTAO::<Test>::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::<Test>::contains_key(net));
});
}

#[test]
fn destroy_alpha_out_multiple_stakers_pro_rata() {
new_test_ext(0).execute_with(|| {
Expand Down Expand Up @@ -763,6 +805,9 @@ fn destroy_alpha_out_multiple_stakers_pro_rata() {
));

// 4. α-out snapshot

SubnetAlphaIn::<Test>::insert(netuid, AlphaBalance::ZERO);
SubnetProtocolAlpha::<Test>::insert(netuid, AlphaBalance::ZERO);
let a1: u128 = sf_to_u128(&AlphaV2::<Test>::get((h1, c1, netuid)));
let a2: u128 = sf_to_u128(&AlphaV2::<Test>::get((h2, c2, netuid)));
let atotal = a1 + a2;
Expand Down Expand Up @@ -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::<Test>::insert(netuid, AlphaBalance::ZERO);
SubnetProtocolAlpha::<Test>::insert(netuid, AlphaBalance::ZERO);
let mut share = [0u64; N];
let mut rem = [0u128; N];
let mut paid: u128 = 0;
Expand Down Expand Up @@ -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::<Test>::insert(net, AlphaBalance::ZERO);
SubnetProtocolAlpha::<Test>::insert(net, AlphaBalance::ZERO);
}

let mut base_share_cold: BTreeMap<U256, u64> =
cold_lps.iter().copied().map(|c| (c, 0_u64)).collect();
let mut pair_count_cold: BTreeMap<U256, u32> =
Expand Down
Loading