Skip to content
Closed
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
97 changes: 46 additions & 51 deletions pallets/subtensor/src/staking/stake_utils.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::*;
use safe_math::*;
use share_pool::{SafeFloat, SharePool, SharePoolDataOperations};
use sp_std::{collections::btree_map::BTreeMap, ops::Neg};
use sp_std::ops::Neg;
use substrate_fixed::types::{I64F64, I96F32, U96F32};
use subtensor_runtime_common::{AlphaBalance, AuthorshipInfo, NetUid, TaoBalance, Token};
use subtensor_swap_interface::{Order, SwapHandler, SwapResult};
Expand Down Expand Up @@ -70,73 +70,68 @@ impl<T: Config> Pallet<T> {
}

/// Gets the Median Subnet Alpha Price
pub fn get_median_subnet_alpha_price() -> U96F32 {
pub fn get_bottom_half_median_subnet_alpha_price() -> U96F32 {
let default_price = U96F32::saturating_from_num(1_u64);
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.

if it's something that could be reused, it's better to make it a constant instead of hardcoding, to simplify refactoring in case we would need to change the value.

let zero_price = U96F32::saturating_from_num(0_u64);
let two = U96F32::saturating_from_num(2_u64);

let mut price_counts: BTreeMap<U96F32, usize> = BTreeMap::new();
let mut total_prices: usize = 0;

for (netuid, added) in NetworksAdded::<T>::iter() {
if !added || netuid == NetUid::ROOT {
continue;
}

let price = T::SwapInterface::current_alpha_price(netuid);
if price <= zero_price {
continue;
}

total_prices = total_prices.saturating_add(1);

if let Some(count) = price_counts.get_mut(&price) {
*count = count.saturating_add(1);
} else {
price_counts.insert(price, 1usize);
}
}
let mut prices: Vec<U96F32> = NetworksAdded::<T>::iter()
.filter_map(|(netuid, added)| {
if added && netuid != NetUid::ROOT {
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.

!netuid.is_root() could be used here instead

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.

also match or reverse if (guard return) would improve readability, but it's probably a personal preference.

let price = T::SwapInterface::current_alpha_price(netuid);
if price > zero_price {
Some(price)
} else {
None
}
} else {
None
}
})
.collect();

if total_prices == 0 {
if prices.is_empty() {
return default_price;
}

let Some(last_index) = total_prices.checked_sub(1) else {
prices.sort_unstable();

let len = prices.len();
let Some(bottom_half_len) = len.checked_add(1).and_then(|value| value.checked_div(2))
else {
return default_price;
};
let Some(lower_target) = last_index.checked_div(2) else {

let bottom_half_prices: Vec<U96F32> = prices.into_iter().take(bottom_half_len).collect();
let bottom_len = bottom_half_prices.len();

let Some(mid_index) = bottom_len.checked_div(2) else {
return default_price;
};
let Some(upper_target) = total_prices.checked_div(2) else {

let Some(remainder) = bottom_len.checked_rem(2) else {
return default_price;
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.

since your failure cases all return default_price, you could wrap the code into a function returning an Option and then just use unwrap_or

};

let mut cumulative: usize = 0;
let mut lower_price: Option<U96F32> = None;
let mut upper_price: Option<U96F32> = None;

for (price, count) in price_counts.into_iter() {
let next_cumulative = cumulative.saturating_add(count);

if lower_price.is_none() && lower_target < next_cumulative {
lower_price = Some(price);
}

if upper_price.is_none() && upper_target < next_cumulative {
upper_price = Some(price);
if remainder == 0 {
let Some(left_index) = mid_index.checked_sub(1) else {
return default_price;
};

match (
bottom_half_prices.get(left_index).copied(),
bottom_half_prices.get(mid_index).copied(),
) {
(Some(left_price), Some(right_price)) => {
left_price.saturating_add(right_price).safe_div(two)
}
_ => default_price,
}

if lower_price.is_some() && upper_price.is_some() {
break;
} else {
match bottom_half_prices.get(mid_index).copied() {
Some(price) => price,
None => default_price,
}

cumulative = next_cumulative;
}

match (lower_price, upper_price) {
(Some(_), Some(upper)) if lower_target == upper_target => upper,
(Some(lower), Some(upper)) => lower.saturating_add(upper).safe_div(two),
_ => default_price,
}
}

Expand Down
44 changes: 30 additions & 14 deletions pallets/subtensor/src/subnets/subnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,10 @@ impl<T: Config> Pallet<T> {
None => Self::get_next_netuid(),
};

// --- 11. Snapshot the current median subnet alpha price before creating the new subnet.
let median_subnet_alpha_price = Self::get_median_subnet_alpha_price();
// --- 11. Snapshot the current bottom-half median subnet alpha price before
// creating the new subnet.
let bottom_half_median_subnet_alpha_price =
Self::get_bottom_half_median_subnet_alpha_price();

// --- 12. Set initial and custom parameters for the network.
let default_tempo = DefaultTempo::<T>::get();
Expand All @@ -209,17 +211,31 @@ impl<T: Config> Pallet<T> {
let symbol = Self::get_next_available_symbol(netuid_to_register);
TokenSymbol::<T>::insert(netuid_to_register, symbol);

// Seed the new subnet pool at a 1:1 reserve ratio.
// Separately, grant the subnet owner outstanding alpha based on the TAO they actually spent
// on registration converted by the current median subnet alpha price.
let pool_initial_tao: TaoBalance = Self::get_network_min_lock();
let pool_initial_alpha: AlphaBalance = pool_initial_tao.to_u64().into();
let owner_alpha_stake: AlphaBalance =
U96F32::saturating_from_num(actual_tao_lock_amount.to_u64())
.safe_div(median_subnet_alpha_price)
.saturating_floor()
.saturating_to_num::<u64>()
.into();
// Seed the new subnet with:
// * 25% of the actual lock as TAO in the pool
// * alpha priced off that TAO amount at the bottom-half median snapshot price
// * the same alpha amount again to the owner as staked alpha
//
// This keeps the opening pool price aligned with the snapshot price and avoids
// ratcheting the bottom-half median downward on sequential registrations.
let actual_tao_lock_amount_u64 = actual_tao_lock_amount.to_u64();

let pool_initial_tao_u64 = actual_tao_lock_amount_u64
.checked_div(4)
.unwrap_or_default();
let pool_initial_tao: TaoBalance = pool_initial_tao_u64.into();

let pool_initial_alpha_u64 = U96F32::saturating_from_num(pool_initial_tao_u64)
.safe_div(bottom_half_median_subnet_alpha_price)
.saturating_floor()
.saturating_to_num::<u64>();
let pool_initial_alpha: AlphaBalance = pool_initial_alpha_u64.into();

// Give the owner the same alpha amount as the pool-side alpha so the launch
// price remains equal to the snapshot price.
let owner_alpha_stake_u64 = pool_initial_alpha_u64;
let owner_alpha_stake: AlphaBalance = owner_alpha_stake_u64.into();

let actual_tao_lock_amount_less_pool_tao =
actual_tao_lock_amount.saturating_sub(pool_initial_tao);

Expand Down Expand Up @@ -280,7 +296,7 @@ impl<T: Config> Pallet<T> {
log::info!("NetworkAdded( netuid:{netuid_to_register:?}, mechanism:{mechid:?} )");
Self::deposit_event(Event::NetworkAdded(netuid_to_register, mechid));

// --- 19. Return success.
// --- 20. Return success.
Ok(())
}

Expand Down
87 changes: 69 additions & 18 deletions pallets/subtensor/src/tests/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use sp_runtime::{
};
use sp_std::{cell::RefCell, cmp::Ordering, sync::OnceLock};
use sp_tracing::tracing_subscriber;
use substrate_fixed::types::U64F64;
use substrate_fixed::types::{U64F64, U96F32};
use subtensor_runtime_common::{AuthorshipInfo, NetUid, TaoBalance};
use subtensor_swap_interface::{Order, SwapHandler};
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
Expand Down Expand Up @@ -842,6 +842,7 @@ pub fn add_network_disable_subtoken(netuid: NetUid, tempo: u16, _modality: u16)
pub fn add_dynamic_network(hotkey: &U256, coldkey: &U256) -> NetUid {
let netuid = SubtensorModule::get_next_netuid();
let lock_cost = SubtensorModule::get_network_lock_cost();

SubtensorModule::add_balance_to_coldkey_account(coldkey, lock_cost.into());
TotalIssuance::<Test>::mutate(|total_issuance| {
*total_issuance = total_issuance.saturating_add(lock_cost);
Expand All @@ -851,6 +852,10 @@ pub fn add_dynamic_network(hotkey: &U256, coldkey: &U256) -> NetUid {
RawOrigin::Signed(*coldkey).into(),
*hotkey
));

// Normalize the freshly registered subnet back to the legacy 1:1 mock shape.
remove_owner_registration_stake(netuid);

NetworkRegistrationAllowed::<Test>::insert(netuid, true);
FirstEmissionBlockNumber::<Test>::insert(netuid, 0);
SubtokenEnabled::<Test>::insert(netuid, true);
Expand Down Expand Up @@ -1111,26 +1116,38 @@ pub fn remove_owner_registration_stake(netuid: NetUid) {
let owner_hotkey = SubnetOwnerHotkey::<Test>::get(netuid);
let owner_coldkey = SubnetOwner::<Test>::get(netuid);

let owner_stake = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(
&owner_hotkey,
&owner_coldkey,
netuid,
);

if owner_stake.is_zero() {
return;
}

let alpha_out_before = SubnetAlphaOut::<Test>::get(netuid);

SubtensorModule::decrease_stake_for_hotkey_and_coldkey_on_subnet(
&owner_hotkey,
&owner_coldkey,
let legacy_pool_tao = SubtensorModule::get_network_min_lock()
.min(SubtensorModule::get_subnet_locked_balance(netuid));
let legacy_pool_alpha: AlphaBalance = legacy_pool_tao.to_u64().into();

let current_pool_tao = SubnetTAO::<Test>::get(netuid);

// Hard-reset the owner stake/sharepool state so the subnet matches the legacy
// post-registration shape used by the original base-branch test suite.
Alpha::<Test>::remove((owner_hotkey, owner_coldkey, netuid));
AlphaV2::<Test>::remove((owner_hotkey, owner_coldkey, netuid));
TotalHotkeyShares::<Test>::remove(owner_hotkey, netuid);
TotalHotkeySharesV2::<Test>::remove(owner_hotkey, netuid);
TotalHotkeyAlpha::<Test>::remove(owner_hotkey, netuid);
TotalHotkeyAlphaLastEpoch::<Test>::remove(owner_hotkey, netuid);
AlphaDividendsPerSubnet::<Test>::remove(netuid, owner_hotkey);

// Restore the original 1:1 seeded pool used by the legacy tests.
SubnetTAO::<Test>::insert(netuid, legacy_pool_tao);
SubnetAlphaIn::<Test>::insert(netuid, legacy_pool_alpha);
SubnetAlphaOut::<Test>::insert(netuid, AlphaBalance::ZERO);
SubnetTaoProvided::<Test>::insert(netuid, TaoBalance::ZERO);
SubnetAlphaInProvided::<Test>::insert(netuid, AlphaBalance::ZERO);
RAORecycledForRegistration::<Test>::insert(
netuid,
owner_stake,
SubtensorModule::get_subnet_locked_balance(netuid).saturating_sub(legacy_pool_tao),
);

SubnetAlphaOut::<Test>::insert(netuid, alpha_out_before.saturating_sub(owner_stake));
TotalStake::<Test>::mutate(|total| {
*total = total
.saturating_sub(current_pool_tao)
.saturating_add(legacy_pool_tao);
});

assert_eq!(
SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(
Expand All @@ -1144,4 +1161,38 @@ pub fn remove_owner_registration_stake(netuid: NetUid) {
TotalHotkeyAlpha::<Test>::get(owner_hotkey, netuid),
AlphaBalance::ZERO
);
assert_eq!(SubnetAlphaOut::<Test>::get(netuid), AlphaBalance::ZERO);
assert_eq!(SubnetTAO::<Test>::get(netuid), legacy_pool_tao);
assert_eq!(SubnetAlphaIn::<Test>::get(netuid), legacy_pool_alpha);
}

pub fn total_registration_alpha_from_lock_and_price(lock_cost_u64: u64, price: U96F32) -> u64 {
let pool_tao_u64 = lock_cost_u64.checked_div(4).unwrap_or_default();

let alpha = (U96F32::from_num(pool_tao_u64)
.checked_div(price)
.unwrap_or_default())
.floor();

if alpha > U96F32::from_num(u64::MAX) {
u64::MAX
} else {
alpha.to_num::<u64>()
}
}

pub fn owner_alpha_from_lock_and_price(lock_cost_u64: u64, price: U96F32) -> u64 {
total_registration_alpha_from_lock_and_price(lock_cost_u64, price)
}

pub fn pool_alpha_from_lock_and_price(lock_cost_u64: u64, price: U96F32) -> u64 {
total_registration_alpha_from_lock_and_price(lock_cost_u64, price)
}

pub fn pool_tao_from_lock(lock_cost_u64: u64) -> u64 {
lock_cost_u64.checked_div(4).unwrap_or_default()
}

pub fn recycled_tao_from_lock(lock_cost_u64: u64) -> u64 {
lock_cost_u64.saturating_sub(pool_tao_from_lock(lock_cost_u64))
}
Loading
Loading