Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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);
Comment thread
gztensor marked this conversation as resolved.
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;
Comment on lines +476 to +478
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Update dissolve weights for protocol-alpha storage

This adds a new SubnetProtocolAlpha read to the dissolve path, and the same function now also removes that map later in the cleanup. The dispatch weights for both dissolve_network and root_dissolve_network in pallets/subtensor/src/macros/dispatches.rs still charge the old reads(6) / writes(31), so the runtime undercharges this path by at least the new protocol-alpha read/write. Please update those annotations or regenerate the relevant weight so the storage proof and DB ops match the new dissolve accounting.

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