diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index 8b4f7204c3..5569e286b9 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -1084,6 +1084,23 @@ pub mod pallet { Ok(()) } + /// The extrinsic sets the subnet limit for the network. + /// It is only callable by the root account. + /// The extrinsic will call the Subtensor pallet to set the subnet limit. + #[pallet::call_index(37)] + #[pallet::weight(( + Weight::from_parts(14_000_000, 0) + .saturating_add(::DbWeight::get().writes(1)), + DispatchClass::Operational, + Pays::No + ))] + pub fn sudo_set_subnet_limit(origin: OriginFor, max_subnets: u16) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_max_subnets(max_subnets); + log::debug!("MaxSubnets ( max_subnets: {max_subnets:?} ) "); + Ok(()) + } + /// The extrinsic sets the lock reduction interval for the network. /// It is only callable by the root account. /// The extrinsic will call the Subtensor pallet to set the lock reduction interval. diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index 558aba8657..2a8c559c23 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -129,7 +129,7 @@ parameter_types! { pub const InitialMaxDifficulty: u64 = u64::MAX; pub const InitialRAORecycledForRegistration: u64 = 0; pub const InitialSenateRequiredStakePercentage: u64 = 2; // 2 percent of total stake - pub const InitialNetworkImmunityPeriod: u64 = 7200 * 7; + pub const InitialNetworkImmunityPeriod: u64 = 1_296_000; pub const InitialNetworkMinLockCost: u64 = 100_000_000_000; pub const InitialSubnetOwnerCut: u16 = 0; // 0%. 100% of rewards go to validators + miners. pub const InitialNetworkLockReductionInterval: u64 = 2; // 2 blocks. @@ -230,6 +230,7 @@ impl pallet_subtensor::Config for Test { type LeaseDividendsDistributionInterval = LeaseDividendsDistributionInterval; type GetCommitments = (); type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; + type CommitmentsInterface = CommitmentsI; } parameter_types! { @@ -356,6 +357,11 @@ impl PrivilegeCmp for OriginPrivilegeCmp { } } +pub struct CommitmentsI; +impl pallet_subtensor::CommitmentsInterface for CommitmentsI { + fn purge_netuid(_netuid: NetUid) {} +} + pub struct GrandpaInterfaceImpl; impl crate::GrandpaInterface for GrandpaInterfaceImpl { fn schedule_change( diff --git a/pallets/commitments/src/lib.rs b/pallets/commitments/src/lib.rs index 6d0d826ab4..5fa37bf5e1 100644 --- a/pallets/commitments/src/lib.rs +++ b/pallets/commitments/src/lib.rs @@ -566,6 +566,18 @@ impl Pallet { .collect(); commitments } + + pub fn purge_netuid(netuid: NetUid) { + let _ = CommitmentOf::::clear_prefix(netuid, u32::MAX, None); + let _ = LastCommitment::::clear_prefix(netuid, u32::MAX, None); + let _ = LastBondsReset::::clear_prefix(netuid, u32::MAX, None); + let _ = RevealedCommitments::::clear_prefix(netuid, u32::MAX, None); + let _ = UsedSpaceOf::::clear_prefix(netuid, u32::MAX, None); + + TimelockedIndex::::mutate(|index| { + index.retain(|(n, _)| *n != netuid); + }); + } } pub trait GetCommitments { diff --git a/pallets/commitments/src/tests.rs b/pallets/commitments/src/tests.rs index 6866ebdeec..5f19070ea2 100644 --- a/pallets/commitments/src/tests.rs +++ b/pallets/commitments/src/tests.rs @@ -4,8 +4,9 @@ use subtensor_runtime_common::NetUid; #[cfg(test)] use crate::{ - BalanceOf, CommitmentInfo, CommitmentOf, Config, Data, Error, Event, MaxSpace, Pallet, - Registration, RevealedCommitments, TimelockedIndex, UsedSpaceOf, + BalanceOf, CommitmentInfo, CommitmentOf, Config, Data, Error, Event, LastBondsReset, + LastCommitment, MaxSpace, Pallet, Registration, RevealedCommitments, TimelockedIndex, + UsageTracker, UsedSpaceOf, mock::{ Balances, DRAND_QUICKNET_SIG_2000_HEX, DRAND_QUICKNET_SIG_HEX, RuntimeEvent, RuntimeOrigin, Test, TestMaxFields, insert_drand_pulse, new_test_ext, produce_ciphertext, @@ -2185,3 +2186,119 @@ fn mixed_timelocked_and_raw_fields_works() { ); }); } + +#[test] +fn purge_netuid_clears_only_that_netuid() { + new_test_ext().execute_with(|| { + // Setup + System::::set_block_number(1); + + let net_a = NetUid::from(42); + let net_b = NetUid::from(43); + let who_a1: u64 = 1001; + let who_a2: u64 = 1002; + let who_b: u64 = 2001; + + // Minimal commitment payload + let empty_fields: BoundedVec::MaxFields> = BoundedVec::default(); + let info_empty: CommitmentInfo<::MaxFields> = CommitmentInfo { + fields: empty_fields, + }; + let bn = System::::block_number(); + + // Seed NET A with two accounts across all tracked storages + let reg_a1 = Registration { + deposit: Default::default(), + block: bn, + info: info_empty.clone(), + }; + let reg_a2 = Registration { + deposit: Default::default(), + block: bn, + info: info_empty.clone(), + }; + CommitmentOf::::insert(net_a, who_a1, reg_a1); + CommitmentOf::::insert(net_a, who_a2, reg_a2); + LastCommitment::::insert(net_a, who_a1, bn); + LastCommitment::::insert(net_a, who_a2, bn); + LastBondsReset::::insert(net_a, who_a1, bn); + RevealedCommitments::::insert(net_a, who_a1, vec![(b"a".to_vec(), 7u64)]); + UsedSpaceOf::::insert( + net_a, + who_a1, + UsageTracker { + last_epoch: 1, + used_space: 123, + }, + ); + + // Seed NET B with one account that must remain intact + let reg_b = Registration { + deposit: Default::default(), + block: bn, + info: info_empty, + }; + CommitmentOf::::insert(net_b, who_b, reg_b); + LastCommitment::::insert(net_b, who_b, bn); + LastBondsReset::::insert(net_b, who_b, bn); + RevealedCommitments::::insert(net_b, who_b, vec![(b"b".to_vec(), 8u64)]); + UsedSpaceOf::::insert( + net_b, + who_b, + UsageTracker { + last_epoch: 9, + used_space: 999, + }, + ); + + // Timelocked index contains both nets + TimelockedIndex::::mutate(|idx| { + idx.insert((net_a, who_a1)); + idx.insert((net_a, who_a2)); + idx.insert((net_b, who_b)); + }); + + // Sanity pre-checks + assert!(CommitmentOf::::get(net_a, who_a1).is_some()); + assert!(CommitmentOf::::get(net_b, who_b).is_some()); + assert!(TimelockedIndex::::get().contains(&(net_a, who_a1))); + + // Act + Pallet::::purge_netuid(net_a); + + // NET A: everything cleared + assert_eq!(CommitmentOf::::iter_prefix(net_a).count(), 0); + assert!(CommitmentOf::::get(net_a, who_a1).is_none()); + assert!(CommitmentOf::::get(net_a, who_a2).is_none()); + + assert_eq!(LastCommitment::::iter_prefix(net_a).count(), 0); + assert!(LastCommitment::::get(net_a, who_a1).is_none()); + assert!(LastCommitment::::get(net_a, who_a2).is_none()); + + assert_eq!(LastBondsReset::::iter_prefix(net_a).count(), 0); + assert!(LastBondsReset::::get(net_a, who_a1).is_none()); + + assert_eq!(RevealedCommitments::::iter_prefix(net_a).count(), 0); + assert!(RevealedCommitments::::get(net_a, who_a1).is_none()); + + assert_eq!(UsedSpaceOf::::iter_prefix(net_a).count(), 0); + assert!(UsedSpaceOf::::get(net_a, who_a1).is_none()); + + let idx_after = TimelockedIndex::::get(); + assert!(!idx_after.contains(&(net_a, who_a1))); + assert!(!idx_after.contains(&(net_a, who_a2))); + + // NET B: untouched + assert!(CommitmentOf::::get(net_b, who_b).is_some()); + assert!(LastCommitment::::get(net_b, who_b).is_some()); + assert!(LastBondsReset::::get(net_b, who_b).is_some()); + assert!(RevealedCommitments::::get(net_b, who_b).is_some()); + assert!(UsedSpaceOf::::get(net_b, who_b).is_some()); + assert!(idx_after.contains(&(net_b, who_b))); + + // Idempotency + Pallet::::purge_netuid(net_a); + assert_eq!(CommitmentOf::::iter_prefix(net_a).count(), 0); + assert!(!TimelockedIndex::::get().contains(&(net_a, who_a1))); + }); +} diff --git a/pallets/subtensor/rpc/src/lib.rs b/pallets/subtensor/rpc/src/lib.rs index ea46695142..2a1c13c7a9 100644 --- a/pallets/subtensor/rpc/src/lib.rs +++ b/pallets/subtensor/rpc/src/lib.rs @@ -100,6 +100,8 @@ pub trait SubtensorCustomApi { metagraph_index: Vec, at: Option, ) -> RpcResult>; + #[method(name = "subnetInfo_getSubnetToPrune")] + fn get_subnet_to_prune(&self, at: Option) -> RpcResult>; } pub struct SubtensorCustom { @@ -489,4 +491,19 @@ where } } } + + fn get_subnet_to_prune( + &self, + at: Option<::Hash>, + ) -> RpcResult> { + let api = self.client.runtime_api(); + let at = at.unwrap_or_else(|| self.client.info().best_hash); + + match api.get_subnet_to_prune(at) { + Ok(result) => Ok(result), + Err(e) => { + Err(Error::RuntimeError(format!("Unable to get subnet to prune: {e:?}")).into()) + } + } + } } diff --git a/pallets/subtensor/runtime-api/src/lib.rs b/pallets/subtensor/runtime-api/src/lib.rs index 3ec76df45f..9516b4f8f4 100644 --- a/pallets/subtensor/runtime-api/src/lib.rs +++ b/pallets/subtensor/runtime-api/src/lib.rs @@ -46,6 +46,7 @@ sp_api::decl_runtime_apis! { fn get_subnet_state(netuid: NetUid) -> Option>; fn get_selective_metagraph(netuid: NetUid, metagraph_indexes: Vec) -> Option>; fn get_selective_submetagraph(netuid: NetUid, subid: SubId, metagraph_indexes: Vec) -> Option>; + fn get_subnet_to_prune() -> Option; } pub trait StakeInfoRuntimeApi { diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index 796cf5614b..e3acbf3432 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -16,12 +16,13 @@ // DEALINGS IN THE SOFTWARE. use super::*; -use frame_support::dispatch::Pays; -use frame_support::weights::Weight; +use crate::CommitmentsInterface; +use frame_support::{dispatch::Pays, weights::Weight}; use safe_math::*; use sp_core::Get; -use substrate_fixed::types::I64F64; +use substrate_fixed::types::{I64F64, U96F32}; use subtensor_runtime_common::{AlphaCurrency, Currency, NetUid, NetUidStorageIndex, TaoCurrency}; +use subtensor_swap_interface::SwapHandler; impl Pallet { /// Fetches the total count of root network validators @@ -364,52 +365,32 @@ impl Pallet { /// * 'SubNetworkDoesNotExist': If the specified network does not exist. /// * 'NotSubnetOwner': If the caller does not own the specified subnet. /// - pub fn user_remove_network(coldkey: T::AccountId, netuid: NetUid) -> dispatch::DispatchResult { - // --- 1. Ensure this subnet exists. + pub fn do_dissolve_network(netuid: NetUid) -> dispatch::DispatchResult { + // 1. --- The network exists? ensure!( - Self::if_subnet_exist(netuid), + Self::if_subnet_exist(netuid) && netuid != NetUid::ROOT, Error::::SubNetworkDoesNotExist ); - // --- 2. Ensure the caller owns this subnet. - ensure!( - SubnetOwner::::get(netuid) == coldkey, - Error::::NotSubnetOwner - ); - - // --- 4. Remove the subnet identity if it exists. - if SubnetIdentitiesV3::::take(netuid).is_some() { - Self::deposit_event(Event::SubnetIdentityRemoved(netuid)); - } + // 2. --- Perform the cleanup before removing the network. + T::SwapInterface::dissolve_all_liquidity_providers(netuid)?; + Self::destroy_alpha_in_out_stakes(netuid)?; + T::CommitmentsInterface::purge_netuid(netuid); - // --- 5. Explicitly erase the network and all its parameters. + // 3. --- Remove the network Self::remove_network(netuid); - // --- 6. Emit the NetworkRemoved event. - log::debug!("NetworkRemoved( netuid:{netuid:?} )"); + // 4. --- Emit the NetworkRemoved event + log::info!("NetworkRemoved( netuid:{netuid:?} )"); Self::deposit_event(Event::NetworkRemoved(netuid)); - // --- 7. Return success. Ok(()) } - /// Removes a network (identified by netuid) and all associated parameters. - /// - /// This function is responsible for cleaning up all the data associated with a network. - /// It ensures that all the storage values related to the network are removed, any - /// reserved balance is returned to the network owner, and the subnet identity is removed if it exists. - /// - /// # Args: - /// * 'netuid': ('u16'): The unique identifier of the network to be removed. - /// - /// # Note: - /// This function does not emit any events, nor does it raise any errors. It silently - /// returns if any internal checks fail. pub fn remove_network(netuid: NetUid) { - // --- 1. Return balance to subnet owner. + // --- 1. Get the owner and remove from SubnetOwner. let owner_coldkey: T::AccountId = SubnetOwner::::get(netuid); - let reserved_amount = Self::get_subnet_locked_balance(netuid); - let subsubnets: u8 = SubsubnetCountCurrent::::get(netuid).into(); + SubnetOwner::::remove(netuid); // --- 2. Remove network count. SubnetworkN::::remove(netuid); @@ -427,26 +408,15 @@ impl Pallet { let _ = Uids::::clear_prefix(netuid, u32::MAX, None); let keys = Keys::::iter_prefix(netuid).collect::>(); let _ = Keys::::clear_prefix(netuid, u32::MAX, None); - for subid in 0..subsubnets { - let netuid_index = Self::get_subsubnet_storage_index(netuid, subid.into()); - let _ = Bonds::::clear_prefix(netuid_index, u32::MAX, None); - } - - // --- 7. Removes the weights for this subnet (do not remove). - for subid in 0..subsubnets { - let netuid_index = Self::get_subsubnet_storage_index(netuid, subid.into()); - let _ = Weights::::clear_prefix(netuid_index, u32::MAX, None); - } // --- 8. Iterate over stored weights and fill the matrix. for (uid_i, weights_i) in Weights::::iter_prefix(NetUidStorageIndex::ROOT) { // Create a new vector to hold modified weights. let mut modified_weights = weights_i.clone(); - // Iterate over each weight entry to potentially update it. for (subnet_id, weight) in modified_weights.iter_mut() { + // If the root network had a weight pointing to this netuid, set it to 0 if subnet_id == &u16::from(netuid) { - // If the condition matches, modify the weight - *weight = 0; // Set weight to 0 for the matching subnet_id. + *weight = 0; } } Weights::::insert(NetUidStorageIndex::ROOT, uid_i, modified_weights); @@ -457,17 +427,10 @@ impl Pallet { Trust::::remove(netuid); Active::::remove(netuid); Emission::::remove(netuid); - for subid in 0..subsubnets { - let netuid_index = Self::get_subsubnet_storage_index(netuid, subid.into()); - Incentive::::remove(netuid_index); - } + Consensus::::remove(netuid); Dividends::::remove(netuid); PruningScores::::remove(netuid); - for subid in 0..subsubnets { - let netuid_index = Self::get_subsubnet_storage_index(netuid, subid.into()); - LastUpdate::::remove(netuid_index); - } ValidatorPermit::::remove(netuid); ValidatorTrust::::remove(netuid); @@ -488,16 +451,200 @@ impl Pallet { POWRegistrationsThisInterval::::remove(netuid); BurnRegistrationsThisInterval::::remove(netuid); - // --- 11. Add the balance back to the owner. - Self::add_balance_to_coldkey_account(&owner_coldkey, reserved_amount.into()); - Self::set_subnet_locked_balance(netuid, TaoCurrency::ZERO); - SubnetOwner::::remove(netuid); + // --- 11. AMM / price / accounting. + // SubnetTAO, SubnetAlpha{In,InProvided,Out} are already cleared during dissolve/destroy. + SubnetAlphaInEmission::::remove(netuid); + SubnetAlphaOutEmission::::remove(netuid); + SubnetTaoInEmission::::remove(netuid); + SubnetVolume::::remove(netuid); + SubnetMovingPrice::::remove(netuid); + SubnetTaoProvided::::remove(netuid); + + // --- 13. Token / mechanism / registration toggles. + TokenSymbol::::remove(netuid); + SubnetMechanism::::remove(netuid); + SubnetOwnerHotkey::::remove(netuid); + NetworkRegistrationAllowed::::remove(netuid); + NetworkPowRegistrationAllowed::::remove(netuid); + + // --- 14. Locks & toggles. + TransferToggle::::remove(netuid); + SubnetLocked::::remove(netuid); + LargestLocked::::remove(netuid); + + // --- 15. Mechanism step / emissions bookkeeping. + FirstEmissionBlockNumber::::remove(netuid); + PendingEmission::::remove(netuid); + PendingRootDivs::::remove(netuid); + PendingAlphaSwapped::::remove(netuid); + PendingOwnerCut::::remove(netuid); + BlocksSinceLastStep::::remove(netuid); + LastMechansimStepBlock::::remove(netuid); + LastAdjustmentBlock::::remove(netuid); + + // --- 16. Serving / rho / curves, and other per-net controls. + ServingRateLimit::::remove(netuid); + Rho::::remove(netuid); + AlphaSigmoidSteepness::::remove(netuid); + + MaxAllowedValidators::::remove(netuid); + AdjustmentInterval::::remove(netuid); + BondsMovingAverage::::remove(netuid); + BondsPenalty::::remove(netuid); + BondsResetOn::::remove(netuid); + WeightsSetRateLimit::::remove(netuid); + ValidatorPruneLen::::remove(netuid); + ScalingLawPower::::remove(netuid); + TargetRegistrationsPerInterval::::remove(netuid); + AdjustmentAlpha::::remove(netuid); + CommitRevealWeightsEnabled::::remove(netuid); + + Burn::::remove(netuid); + MinBurn::::remove(netuid); + MaxBurn::::remove(netuid); + MinDifficulty::::remove(netuid); + MaxDifficulty::::remove(netuid); + RegistrationsThisBlock::::remove(netuid); + EMAPriceHalvingBlocks::::remove(netuid); + RAORecycledForRegistration::::remove(netuid); + MaxRegistrationsPerBlock::::remove(netuid); + WeightsVersionKey::::remove(netuid); + + // --- 17. Subtoken / feature flags. + LiquidAlphaOn::::remove(netuid); + Yuma3On::::remove(netuid); + AlphaValues::::remove(netuid); + SubtokenEnabled::::remove(netuid); + ImmuneOwnerUidsLimit::::remove(netuid); + + // --- 18. Consensus aux vectors. + StakeWeight::::remove(netuid); + LoadedEmission::::remove(netuid); + + // --- 19. DMAPs where netuid is the FIRST key: clear by prefix. + let _ = BlockAtRegistration::::clear_prefix(netuid, u32::MAX, None); + let _ = Axons::::clear_prefix(netuid, u32::MAX, None); + let _ = NeuronCertificates::::clear_prefix(netuid, u32::MAX, None); + let _ = Prometheus::::clear_prefix(netuid, u32::MAX, None); + let _ = AlphaDividendsPerSubnet::::clear_prefix(netuid, u32::MAX, None); + let _ = TaoDividendsPerSubnet::::clear_prefix(netuid, u32::MAX, None); + let _ = PendingChildKeys::::clear_prefix(netuid, u32::MAX, None); + let _ = AssociatedEvmAddress::::clear_prefix(netuid, u32::MAX, None); + + // Commit-reveal / weights commits (all per-net prefixes): + let subsubnets: u8 = SubsubnetCountCurrent::::get(netuid).into(); + for subid in 0..subsubnets { + let netuid_index = Self::get_subsubnet_storage_index(netuid, subid.into()); + LastUpdate::::remove(netuid_index); + Incentive::::remove(netuid_index); + let _ = WeightCommits::::clear_prefix(netuid_index, u32::MAX, None); + let _ = TimelockedWeightCommits::::clear_prefix(netuid_index, u32::MAX, None); + let _ = CRV3WeightCommits::::clear_prefix(netuid_index, u32::MAX, None); + let _ = CRV3WeightCommitsV2::::clear_prefix(netuid_index, u32::MAX, None); + let _ = Bonds::::clear_prefix(netuid_index, u32::MAX, None); + let _ = Weights::::clear_prefix(netuid_index, u32::MAX, None); + } + RevealPeriodEpochs::::remove(netuid); + SubsubnetCountCurrent::::remove(netuid); + SubsubnetEmissionSplit::::remove(netuid); + + // Last hotkey swap (DMAP where netuid is FIRST key → easy) + let _ = LastHotkeySwapOnNetuid::::clear_prefix(netuid, u32::MAX, None); - // --- 12. Remove subnet identity if it exists. + // --- 20. Identity maps across versions (netuid-scoped). + SubnetIdentities::::remove(netuid); + SubnetIdentitiesV2::::remove(netuid); if SubnetIdentitiesV3::::contains_key(netuid) { SubnetIdentitiesV3::::remove(netuid); Self::deposit_event(Event::SubnetIdentityRemoved(netuid)); } + + // --- 21. DMAP / NMAP where netuid is NOT the first key → iterate & remove. + + // ChildkeyTake: (hot, netuid) → u16 + { + let to_rm: sp_std::vec::Vec = ChildkeyTake::::iter() + .filter_map(|(hot, n, _)| if n == netuid { Some(hot) } else { None }) + .collect(); + for hot in to_rm { + ChildkeyTake::::remove(&hot, netuid); + } + } + // ChildKeys: (parent, netuid) → Vec<...> + { + let to_rm: sp_std::vec::Vec = ChildKeys::::iter() + .filter_map(|(parent, n, _)| if n == netuid { Some(parent) } else { None }) + .collect(); + for parent in to_rm { + ChildKeys::::remove(&parent, netuid); + } + } + // ParentKeys: (child, netuid) → Vec<...> + { + let to_rm: sp_std::vec::Vec = ParentKeys::::iter() + .filter_map(|(child, n, _)| if n == netuid { Some(child) } else { None }) + .collect(); + for child in to_rm { + ParentKeys::::remove(&child, netuid); + } + } + // LastHotkeyEmissionOnNetuid: (hot, netuid) → α + { + let to_rm: sp_std::vec::Vec = LastHotkeyEmissionOnNetuid::::iter() + .filter_map(|(hot, n, _)| if n == netuid { Some(hot) } else { None }) + .collect(); + for hot in to_rm { + LastHotkeyEmissionOnNetuid::::remove(&hot, netuid); + } + } + // TotalHotkeyAlphaLastEpoch: (hot, netuid) → ... + // (TotalHotkeyAlpha and TotalHotkeyShares were already removed during dissolve.) + { + let to_rm_alpha_last: sp_std::vec::Vec = + TotalHotkeyAlphaLastEpoch::::iter() + .filter_map(|(hot, n, _)| if n == netuid { Some(hot) } else { None }) + .collect(); + for hot in to_rm_alpha_last { + TotalHotkeyAlphaLastEpoch::::remove(&hot, netuid); + } + } + // TransactionKeyLastBlock NMAP: (hot, netuid, name) → u64 + { + let to_rm: sp_std::vec::Vec<(T::AccountId, u16)> = TransactionKeyLastBlock::::iter() + .filter_map( + |((hot, n, name), _)| if n == netuid { Some((hot, name)) } else { None }, + ) + .collect(); + for (hot, name) in to_rm { + TransactionKeyLastBlock::::remove((hot, netuid, name)); + } + } + // StakingOperationRateLimiter NMAP: (hot, cold, netuid) → bool + { + let to_rm: sp_std::vec::Vec<(T::AccountId, T::AccountId)> = + StakingOperationRateLimiter::::iter() + .filter_map( + |((hot, cold, n), _)| { + if n == netuid { Some((hot, cold)) } else { None } + }, + ) + .collect(); + for (hot, cold) in to_rm { + StakingOperationRateLimiter::::remove((hot, cold, netuid)); + } + } + + // --- 22. Subnet leasing: remove mapping and any lease-scoped state linked to this netuid. + if let Some(lease_id) = SubnetUidToLeaseId::::take(netuid) { + SubnetLeases::::remove(lease_id); + let _ = SubnetLeaseShares::::clear_prefix(lease_id, u32::MAX, None); + AccumulatedLeaseDividends::::remove(lease_id); + } + + // --- Final removal logging. + log::debug!( + "remove_network: netuid={netuid}, owner={owner_coldkey:?} removed successfully" + ); } #[allow(clippy::arithmetic_side_effects)] @@ -602,4 +749,38 @@ impl Pallet { pub fn remove_rate_limited_last_block(rate_limit_key: &RateLimitKey) { LastRateLimitedBlock::::remove(rate_limit_key); } + + pub fn get_network_to_prune() -> Option { + let current_block: u64 = Self::get_current_block_as_u64(); + + let mut candidate_netuid: Option = None; + let mut candidate_price: U96F32 = U96F32::saturating_from_num(u128::MAX); + let mut candidate_timestamp: u64 = u64::MAX; + + for (netuid, added) in NetworksAdded::::iter() { + if !added || netuid == NetUid::ROOT { + continue; + } + + let registered_at = NetworkRegisteredAt::::get(netuid); + + // Skip immune networks. + if current_block < registered_at.saturating_add(Self::get_network_immunity_period()) { + continue; + } + + let price: U96F32 = Self::get_moving_alpha_price(netuid); + + // If tie on price, earliest registration wins. + if price < candidate_price + || (price == candidate_price && registered_at < candidate_timestamp) + { + candidate_netuid = Some(netuid); + candidate_price = price; + candidate_timestamp = registered_at; + } + } + + candidate_netuid + } } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 00fab25ae4..f4a65de0e1 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -546,6 +546,11 @@ pub mod pallet { T::InitialNetworkRateLimit::get() } #[pallet::type_value] + /// Default value for network rate limit. + pub fn DefaultNetworkRegistrationStartBlock() -> u64 { + 0 + } + #[pallet::type_value] /// Default value for weights version key rate limit. /// In units of tempos. pub fn DefaultWeightsVersionKeyRateLimit() -> u64 { @@ -883,6 +888,12 @@ pub mod pallet { 0 } + #[pallet::type_value] + /// Default value for subnet limit. + pub fn DefaultSubnetLimit() -> u16 { + 128 + } + #[pallet::storage] pub type MinActivityCutoff = StorageValue<_, u16, ValueQuery, DefaultMinActivityCutoff>; @@ -1069,6 +1080,9 @@ pub mod pallet { /// /// Eventually, Bittensor should migrate to using Holds afterwhich time we will not require this /// separate accounting. + + #[pallet::storage] // --- ITEM ( maximum_number_of_networks ) + pub type SubnetLimit = StorageValue<_, u16, ValueQuery, DefaultSubnetLimit>; #[pallet::storage] // --- ITEM ( total_issuance ) pub type TotalIssuance = StorageValue<_, TaoCurrency, ValueQuery, DefaultTotalIssuance>; #[pallet::storage] // --- ITEM ( total_stake ) @@ -1833,6 +1847,11 @@ pub mod pallet { pub type CommitRevealWeightsVersion = StorageValue<_, u16, ValueQuery, DefaultCommitRevealWeightsVersion>; + #[pallet::storage] + /// ITEM( NetworkRegistrationStartBlock ) + pub type NetworkRegistrationStartBlock = + StorageValue<_, u64, ValueQuery, DefaultNetworkRegistrationStartBlock>; + /// ====================== /// ==== Sub-subnets ===== /// ====================== @@ -2229,3 +2248,8 @@ impl ProxyInterface for () { Ok(()) } } + +/// Pallets that hold per-subnet commitments implement this to purge all state for `netuid`. +pub trait CommitmentsInterface { + fn purge_netuid(netuid: NetUid); +} diff --git a/pallets/subtensor/src/macros/config.rs b/pallets/subtensor/src/macros/config.rs index cb6af29728..1a64826ed4 100644 --- a/pallets/subtensor/src/macros/config.rs +++ b/pallets/subtensor/src/macros/config.rs @@ -6,6 +6,7 @@ use frame_support::pallet_macros::pallet_section; #[pallet_section] mod config { + use crate::CommitmentsInterface; use pallet_commitments::GetCommitments; use subtensor_swap_interface::SwapHandler; @@ -62,6 +63,9 @@ mod config { /// Interface to get commitments. type GetCommitments: GetCommitments; + /// Interface to clean commitments on network dissolution. + type CommitmentsInterface: CommitmentsInterface; + /// ================================= /// ==== Initial Value Constants ==== /// ================================= diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index f4583c0711..e953ac5ae3 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -1300,7 +1300,7 @@ mod dispatches { #[pallet::call_index(59)] #[pallet::weight((Weight::from_parts(235_400_000, 0) .saturating_add(T::DbWeight::get().reads(37_u64)) - .saturating_add(T::DbWeight::get().writes(51_u64)), DispatchClass::Normal, Pays::Yes))] + .saturating_add(T::DbWeight::get().writes(60_u64)), DispatchClass::Normal, Pays::No))] pub fn register_network(origin: OriginFor, hotkey: T::AccountId) -> DispatchResult { Self::do_register_network(origin, &hotkey, 1, None) } @@ -1333,11 +1333,11 @@ mod dispatches { .saturating_add(T::DbWeight::get().writes(31)), DispatchClass::Operational, Pays::No))] pub fn dissolve_network( origin: OriginFor, - coldkey: T::AccountId, + _coldkey: T::AccountId, netuid: NetUid, ) -> DispatchResult { ensure_root(origin)?; - Self::user_remove_network(coldkey, netuid) + Self::do_dissolve_network(netuid) } /// Set a single child for a given hotkey on a specified network. @@ -1586,8 +1586,8 @@ mod dispatches { /// User register a new subnetwork #[pallet::call_index(79)] #[pallet::weight((Weight::from_parts(234_200_000, 0) - .saturating_add(T::DbWeight::get().reads(36_u64)) - .saturating_add(T::DbWeight::get().writes(50_u64)), DispatchClass::Normal, Pays::Yes))] + .saturating_add(T::DbWeight::get().reads(36_u64)) + .saturating_add(T::DbWeight::get().writes(59_u64)), DispatchClass::Normal, Pays::No))] pub fn register_network_with_identity( origin: OriginFor, hotkey: T::AccountId, @@ -2343,5 +2343,16 @@ mod dispatches { commit_reveal_version, ) } + + /// Remove a user's subnetwork + /// The caller must be root + #[pallet::call_index(120)] + #[pallet::weight((Weight::from_parts(119_000_000, 0) + .saturating_add(T::DbWeight::get().reads(6)) + .saturating_add(T::DbWeight::get().writes(31)), DispatchClass::Operational, Pays::No))] + pub fn root_dissolve_network(origin: OriginFor, netuid: NetUid) -> DispatchResult { + ensure_root(origin)?; + Self::do_dissolve_network(netuid) + } } } diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index ed6ca3c002..8ae3566819 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -252,5 +252,9 @@ mod errors { RevealPeriodTooSmall, /// Generic error for out-of-range parameter value InvalidValue, + /// Subnet limit reached & there is no eligible subnet to prune + SubnetLimitReached, + /// Insufficient funds to meet the subnet lock cost + CannotAffordLockCost, } } diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index a5de146e31..f2d134c189 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -151,7 +151,7 @@ mod events { /// the network minimum locking cost is set. NetworkMinLockCostSet(TaoCurrency), /// the maximum number of subnets is set - // SubnetLimitSet(u16), + SubnetLimitSet(u16), /// the lock cost reduction is set NetworkLockCostReductionIntervalSet(u64), /// the take for a delegate is decreased. diff --git a/pallets/subtensor/src/macros/hooks.rs b/pallets/subtensor/src/macros/hooks.rs index b43f9422df..a3cb7a692f 100644 --- a/pallets/subtensor/src/macros/hooks.rs +++ b/pallets/subtensor/src/macros/hooks.rs @@ -139,7 +139,15 @@ mod hooks { // Migrate last block rate limiting storage items .saturating_add(migrations::migrate_rate_limiting_last_blocks::migrate_obsolete_rate_limiting_last_blocks_storage::()) // Migrate remove network modality - .saturating_add(migrations::migrate_remove_network_modality::migrate_remove_network_modality::()); + .saturating_add(migrations::migrate_remove_network_modality::migrate_remove_network_modality::()) + // Migrate Immunity Period + .saturating_add(migrations::migrate_network_immunity_period::migrate_network_immunity_period::()) + // Migrate Subnet Limit + .saturating_add(migrations::migrate_subnet_limit_to_default::migrate_subnet_limit_to_default::()) + // Migrate Lock Reduction Interval + .saturating_add(migrations::migrate_network_lock_reduction_interval::migrate_network_lock_reduction_interval::()) + // Migrate subnet locked balances + .saturating_add(migrations::migrate_subnet_locked::migrate_restore_subnet_locked::()); weight } diff --git a/pallets/subtensor/src/migrations/migrate_init_total_issuance.rs b/pallets/subtensor/src/migrations/migrate_init_total_issuance.rs index 042ad0fe77..6a05dc5a85 100644 --- a/pallets/subtensor/src/migrations/migrate_init_total_issuance.rs +++ b/pallets/subtensor/src/migrations/migrate_init_total_issuance.rs @@ -15,7 +15,7 @@ pub mod deprecated_loaded_emission_format { } pub(crate) fn migrate_init_total_issuance() -> Weight { - let subnets_len = crate::SubnetLocked::::iter().count() as u64; + let subnets_len = crate::NetworksAdded::::iter().count() as u64; // Retrieve the total balance of all accounts let total_account_balances = <::Currency as fungible::Inspect< diff --git a/pallets/subtensor/src/migrations/migrate_network_immunity_period.rs b/pallets/subtensor/src/migrations/migrate_network_immunity_period.rs new file mode 100644 index 0000000000..a9fcea21e3 --- /dev/null +++ b/pallets/subtensor/src/migrations/migrate_network_immunity_period.rs @@ -0,0 +1,40 @@ +use crate::{Config, Event, HasMigrationRun, NetworkImmunityPeriod, Pallet, Weight}; +use scale_info::prelude::string::String; + +pub fn migrate_network_immunity_period() -> Weight { + use frame_support::traits::Get; + + const NEW_VALUE: u64 = 864_000; + + let migration_name = b"migrate_network_immunity_period".to_vec(); + let mut weight = T::DbWeight::get().reads(1); + + // Skip if already executed + if HasMigrationRun::::get(&migration_name) { + log::info!( + target: "runtime", + "Migration '{}' already run - skipping.", + String::from_utf8_lossy(&migration_name) + ); + return weight; + } + + // ── 1) Set new value ───────────────────────────────────────────────────── + NetworkImmunityPeriod::::put(NEW_VALUE); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + Pallet::::deposit_event(Event::NetworkImmunityPeriodSet(NEW_VALUE)); + + // ── 2) Mark migration done ─────────────────────────────────────────────── + HasMigrationRun::::insert(&migration_name, true); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + log::info!( + target: "runtime", + "Migration '{}' completed - NetworkImmunityPeriod => {}.", + String::from_utf8_lossy(&migration_name), + NEW_VALUE + ); + + weight +} diff --git a/pallets/subtensor/src/migrations/migrate_network_lock_reduction_interval.rs b/pallets/subtensor/src/migrations/migrate_network_lock_reduction_interval.rs new file mode 100644 index 0000000000..99bb5b6e97 --- /dev/null +++ b/pallets/subtensor/src/migrations/migrate_network_lock_reduction_interval.rs @@ -0,0 +1,55 @@ +use super::*; +use frame_support::{traits::Get, weights::Weight}; +use log; +use scale_info::prelude::string::String; + +pub fn migrate_network_lock_reduction_interval() -> Weight { + const FOUR_DAYS: u64 = 28_800; + const EIGHT_DAYS: u64 = 57_600; + const ONE_WEEK_BLOCKS: u64 = 50_400; + + let migration_name = b"migrate_network_lock_reduction_interval".to_vec(); + let mut weight = T::DbWeight::get().reads(1); + + // Skip if already executed + if HasMigrationRun::::get(&migration_name) { + log::info!( + target: "runtime", + "Migration '{}' already run - skipping.", + String::from_utf8_lossy(&migration_name) + ); + return weight; + } + + let current_block = Pallet::::get_current_block_as_u64(); + + // ── 1) Set new values ───────────────────────────────────────────────── + NetworkLockReductionInterval::::put(EIGHT_DAYS); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + NetworkRateLimit::::put(FOUR_DAYS); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + Pallet::::set_network_last_lock(TaoCurrency::from(1_000_000_000_000)); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + // Hold price at 2000 TAO until day 7, then begin linear decay + Pallet::::set_network_last_lock_block(current_block.saturating_add(ONE_WEEK_BLOCKS)); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + // Allow registrations starting at day 7 + NetworkRegistrationStartBlock::::put(current_block.saturating_add(ONE_WEEK_BLOCKS)); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + // ── 2) Mark migration done ─────────────────────────────────────────── + HasMigrationRun::::insert(&migration_name, true); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + log::info!( + target: "runtime", + "Migration '{}' completed.", + String::from_utf8_lossy(&migration_name), + ); + + weight +} diff --git a/pallets/subtensor/src/migrations/migrate_subnet_limit_to_default.rs b/pallets/subtensor/src/migrations/migrate_subnet_limit_to_default.rs new file mode 100644 index 0000000000..3d88337a24 --- /dev/null +++ b/pallets/subtensor/src/migrations/migrate_subnet_limit_to_default.rs @@ -0,0 +1,44 @@ +use super::*; +use frame_support::{traits::Get, weights::Weight}; +use log; +use scale_info::prelude::string::String; + +pub fn migrate_subnet_limit_to_default() -> Weight { + let mig_name: Vec = b"subnet_limit_to_default".to_vec(); + + // 1 read: HasMigrationRun flag + let mut total_weight = T::DbWeight::get().reads(1); + + // Run once guard + if HasMigrationRun::::get(&mig_name) { + log::info!( + "Migration '{}' already executed - skipping", + String::from_utf8_lossy(&mig_name) + ); + return total_weight; + } + log::info!("Running migration '{}'", String::from_utf8_lossy(&mig_name)); + + // Read current and compute target default + let current: u16 = SubnetLimit::::get(); + let target: u16 = DefaultSubnetLimit::::get(); + + if current != target { + total_weight = total_weight.saturating_add(T::DbWeight::get().reads_writes(1, 1)); + SubnetLimit::::put(target); + log::info!("SubnetLimit updated: {current} -> {target}"); + } else { + total_weight = total_weight.saturating_add(T::DbWeight::get().reads(1)); + log::info!("SubnetLimit already equals default ({target}), no update performed."); + } + + // Mark as done + HasMigrationRun::::insert(&mig_name, true); + total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1)); + + log::info!( + "Migration '{}' completed", + String::from_utf8_lossy(&mig_name) + ); + total_weight +} diff --git a/pallets/subtensor/src/migrations/migrate_subnet_locked.rs b/pallets/subtensor/src/migrations/migrate_subnet_locked.rs new file mode 100644 index 0000000000..e72881ea7d --- /dev/null +++ b/pallets/subtensor/src/migrations/migrate_subnet_locked.rs @@ -0,0 +1,118 @@ +use super::*; +use crate::{Config, HasMigrationRun, SubnetLocked, TaoCurrency}; +use frame_support::weights::Weight; +use log; +use scale_info::prelude::string::String; +use subtensor_runtime_common::NetUid; + +pub fn migrate_restore_subnet_locked() -> Weight { + // Track whether we've already run this migration + let migration_name = b"migrate_restore_subnet_locked".to_vec(); + let mut weight = T::DbWeight::get().reads(1); + + if HasMigrationRun::::get(&migration_name) { + log::info!( + target: "runtime", + "Migration '{}' already run - skipping.", + String::from_utf8_lossy(&migration_name) + ); + return weight; + } + + // (netuid, locked_rao) pairs taken from the historical snapshot (block #4_828_623). + const SUBNET_LOCKED: &[(u16, u64)] = &[ + (2, 976_893_069_056), + (3, 2_569_362_397_490), + (4, 1_928_551_593_932), + (5, 1_712_540_082_588), + (6, 1_495_929_556_770), + (7, 1_011_702_451_936), + (8, 337_484_391_024), + (9, 381_240_180_320), + (10, 1_253_515_128_353), + (11, 1_453_924_672_132), + (12, 100_000_000_000), + (13, 100_000_000_000), + (14, 1_489_714_521_808), + (15, 1_784_089_225_496), + (16, 889_176_219_484), + (17, 1_266_310_122_772), + (18, 222_355_058_433), + (19, 100_000_000_000), + (20, 100_000_000_000), + (21, 885_096_322_978), + (22, 100_000_000_000), + (23, 100_000_000_000), + (24, 5_146_073_854_481), + (25, 1_782_920_948_214), + (26, 153_583_865_248), + (27, 201_344_183_084), + (28, 901_455_879_445), + (29, 175_000_001_600), + (30, 1_419_730_660_074), + (31, 319_410_100_502), + (32, 2_016_397_028_246), + (33, 1_626_477_274_174), + (34, 1_455_297_496_345), + (35, 1_191_275_979_639), + (36, 1_097_008_574_216), + (37, 864_664_455_362), + (38, 1_001_936_494_076), + (39, 1_366_096_404_884), + (40, 100_000_000_000), + (41, 535_937_523_200), + (42, 1_215_698_423_344), + (43, 1_641_308_676_800), + (44, 1_514_636_189_434), + (45, 1_605_608_381_438), + (46, 1_095_943_027_350), + (47, 1_499_235_469_986), + (48, 1_308_073_720_362), + (49, 1_222_672_092_068), + (50, 2_628_355_421_561), + (51, 1_520_860_720_561), + (52, 1_794_457_248_725), + (53, 1_721_472_811_492), + (54, 2_048_900_691_868), + (55, 1_278_597_446_119), + (56, 2_016_045_544_480), + (57, 1_920_563_399_676), + (58, 2_246_525_691_504), + (59, 1_776_159_384_888), + (60, 2_173_138_865_414), + (61, 1_435_634_867_728), + (62, 2_061_282_563_888), + (63, 3_008_967_320_998), + (64, 2_099_236_359_026), + ]; + + let mut inserted: u32 = 0; + let mut total_rao: u128 = 0; + + // ── 1) Re-insert the historical values ──────────────────────────────── + for &(netuid_u16, amount_rao_u64) in SUBNET_LOCKED.iter() { + let key: NetUid = NetUid::from(netuid_u16); + let amount: TaoCurrency = TaoCurrency::from(amount_rao_u64); + + SubnetLocked::::insert(key, amount); + + inserted = inserted.saturating_add(1); + total_rao = total_rao.saturating_add(amount_rao_u64 as u128); + + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + } + + // ── 2) Mark migration done ──────────────────────────────────────────── + HasMigrationRun::::insert(&migration_name, true); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + log::info!( + target: "runtime", + "Migration '{}' completed - inserted {} SubnetLocked entries; total≈{} RAO.", + String::from_utf8_lossy(&migration_name), + inserted, + total_rao + ); + + weight +} diff --git a/pallets/subtensor/src/migrations/mod.rs b/pallets/subtensor/src/migrations/mod.rs index b7265cc6d0..e7c50c0080 100644 --- a/pallets/subtensor/src/migrations/mod.rs +++ b/pallets/subtensor/src/migrations/mod.rs @@ -19,6 +19,8 @@ pub mod migrate_fix_root_subnet_tao; pub mod migrate_fix_root_tao_and_alpha_in; pub mod migrate_identities_v2; pub mod migrate_init_total_issuance; +pub mod migrate_network_immunity_period; +pub mod migrate_network_lock_reduction_interval; pub mod migrate_orphaned_storage_items; pub mod migrate_populate_owned_hotkeys; pub mod migrate_rao; @@ -39,6 +41,8 @@ pub mod migrate_set_registration_enable; pub mod migrate_set_subtoken_enabled; pub mod migrate_stake_threshold; pub mod migrate_subnet_identities_to_v3; +pub mod migrate_subnet_limit_to_default; +pub mod migrate_subnet_locked; pub mod migrate_subnet_symbols; pub mod migrate_subnet_volume; pub mod migrate_to_v1_separate_emission; diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index fd9a974645..8a691a9866 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -1,6 +1,7 @@ use subtensor_swap_interface::{OrderType, SwapHandler}; use super::*; +use substrate_fixed::types::U96F32; use subtensor_runtime_common::{AlphaCurrency, Currency, NetUid, TaoCurrency}; impl Pallet { @@ -439,4 +440,174 @@ impl Pallet { Self::do_remove_stake(origin, hotkey, netuid, alpha_unstaked) } } + + pub fn destroy_alpha_in_out_stakes(netuid: NetUid) -> DispatchResult { + // 1) Ensure the subnet exists. + ensure!( + Self::if_subnet_exist(netuid), + Error::::SubNetworkDoesNotExist + ); + + // 2) Owner / lock cost. + let owner_coldkey: T::AccountId = SubnetOwner::::get(netuid); + let lock_cost: TaoCurrency = Self::get_subnet_locked_balance(netuid); + + // 3) Compute owner's received emission in TAO at current price. + // Emission:: is Vec. We: + // - sum emitted α, + // - apply owner fraction to get owner α, + // - price that α using a *simulated* AMM swap. + let total_emitted_alpha_u128: u128 = + Emission::::get(netuid) + .into_iter() + .fold(0u128, |acc, e_alpha| { + let e_u64: u64 = Into::::into(e_alpha); + acc.saturating_add(e_u64 as u128) + }); + + let owner_fraction: U96F32 = Self::get_float_subnet_owner_cut(); + let owner_alpha_u64: u64 = U96F32::from_num(total_emitted_alpha_u128) + .saturating_mul(owner_fraction) + .floor() + .saturating_to_num::(); + + let owner_emission_tao: TaoCurrency = if owner_alpha_u64 > 0 { + match T::SwapInterface::sim_swap(netuid.into(), OrderType::Sell, owner_alpha_u64) { + Ok(sim) => TaoCurrency::from(sim.amount_paid_out), + Err(e) => { + log::debug!( + "destroy_alpha_in_out_stakes: sim_swap owner α→τ failed (netuid={netuid:?}, alpha={owner_alpha_u64}, err={e:?}); falling back to price multiply.", + ); + let cur_price: U96F32 = T::SwapInterface::current_alpha_price(netuid.into()); + let val_u64: u64 = U96F32::from_num(owner_alpha_u64) + .saturating_mul(cur_price) + .floor() + .saturating_to_num::(); + TaoCurrency::from(val_u64) + } + } + } else { + TaoCurrency::ZERO + }; + + // 4) Enumerate all α entries on this subnet to build distribution weights and cleanup lists. + // - collect keys to remove, + // - per (hot,cold) α VALUE (not shares) with fallback to raw share if pool uninitialized, + // - track hotkeys to clear pool totals. + let mut keys_to_remove: Vec<(T::AccountId, T::AccountId)> = Vec::new(); + let mut hotkeys_seen: Vec = Vec::new(); + let mut stakers: Vec<(T::AccountId, T::AccountId, u128)> = Vec::new(); + let mut total_alpha_value_u128: u128 = 0; + + for ((hot, cold, this_netuid), share_u64f64) in Alpha::::iter() { + if this_netuid != netuid { + continue; + } + + keys_to_remove.push((hot.clone(), cold.clone())); + if !hotkeys_seen.contains(&hot) { + hotkeys_seen.push(hot.clone()); + } + + // Primary: actual α value via share pool. + let pool = Self::get_alpha_share_pool(hot.clone(), netuid); + let actual_val_u64 = pool.try_get_value(&cold).unwrap_or(0); + + // Fallback: if pool uninitialized, treat raw Alpha share as value. + let val_u64 = if actual_val_u64 == 0 { + share_u64f64.saturating_to_num::() + } else { + actual_val_u64 + }; + + if val_u64 > 0 { + let val_u128 = val_u64 as u128; + total_alpha_value_u128 = total_alpha_value_u128.saturating_add(val_u128); + stakers.push((hot, cold, val_u128)); + } + } + + // 5) Determine the TAO pot and pre-adjust accounting to avoid double counting. + let pot_tao: TaoCurrency = SubnetTAO::::get(netuid); + let pot_u64: u64 = pot_tao.into(); + + if pot_u64 > 0 { + SubnetTAO::::remove(netuid); + TotalStake::::mutate(|total| *total = total.saturating_sub(pot_tao)); + } + + // 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() { + struct Portion { + _hot: A, + cold: C, + 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 distributed: u128 = 0; + + for (hot, cold, alpha_val) in &stakers { + let prod: u128 = pot_u128.saturating_mul(*alpha_val); + 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: hot.clone(), + cold: cold.clone(), + share: share_u64, + rem, + }); + } + + let leftover: u128 = pot_u128.saturating_sub(distributed); + if leftover > 0 { + portions.sort_by(|a, b| b.rem.cmp(&a.rem)); + let give: usize = core::cmp::min(leftover, portions.len() as u128) as usize; + for p in portions.iter_mut().take(give) { + p.share = p.share.saturating_add(1); + } + } + + // Credit each share directly to coldkey free balance. + for p in portions { + if p.share > 0 { + Self::add_balance_to_coldkey_account(&p.cold, p.share); + } + } + } + + // 7) Destroy all α-in/α-out state for this subnet. + // 7.a) Remove every (hot, cold, netuid) α entry. + for (hot, cold) in keys_to_remove { + Alpha::::remove((hot, cold, netuid)); + } + // 7.b) Clear share‑pool totals for each hotkey on this subnet. + for hot in hotkeys_seen { + TotalHotkeyAlpha::::remove(&hot, netuid); + TotalHotkeyShares::::remove(&hot, netuid); + } + // 7.c) Remove α‑in/α‑out counters (fully destroyed). + SubnetAlphaIn::::remove(netuid); + SubnetAlphaInProvided::::remove(netuid); + SubnetAlphaOut::::remove(netuid); + + // 8) Refund remaining lock to subnet owner: + // refund = max(0, lock_cost(τ) − owner_received_emission_in_τ). + let refund: TaoCurrency = lock_cost.saturating_sub(owner_emission_tao); + + // Clear the locked balance on the subnet. + Self::set_subnet_locked_balance(netuid, TaoCurrency::ZERO); + + if !refund.is_zero() { + Self::add_balance_to_coldkey_account(&owner_coldkey, refund.to_u64()); + } + + Ok(()) + } } diff --git a/pallets/subtensor/src/subnets/subnet.rs b/pallets/subtensor/src/subnets/subnet.rs index 6241c54ef7..bbb9ff9b11 100644 --- a/pallets/subtensor/src/subnets/subnet.rs +++ b/pallets/subtensor/src/subnets/subnet.rs @@ -1,19 +1,9 @@ use super::*; use sp_core::Get; use subtensor_runtime_common::{NetUid, TaoCurrency}; +use subtensor_swap_interface::SwapHandler; impl Pallet { - /// Fetches the total count of subnets. - /// - /// This function retrieves the total number of subnets present on the chain. - /// - /// # Returns: - /// * 'u16': The total number of subnets. - /// - pub fn get_num_subnets() -> u16 { - TotalNetworks::::get() - } - /// Returns true if the subnetwork exists. /// /// This function checks if a subnetwork with the given UID exists. @@ -100,19 +90,25 @@ impl Pallet { /// Facilitates user registration of a new subnetwork. /// - /// # Args: - /// * 'origin': ('T::RuntimeOrigin'): The calling origin. Must be signed. - /// * `identity` (`Option`): Optional identity to be associated with the new subnetwork. - /// - /// # Event: - /// * 'NetworkAdded': Emitted when a new network is successfully added. - /// - /// # Raises: - /// * 'TxRateLimitExceeded': If the rate limit for network registration is exceeded. - /// * 'NotEnoughBalanceToStake': If there isn't enough balance to stake for network registration. - /// * 'BalanceWithdrawalError': If an error occurs during balance withdrawal for network registration. - /// * `SubnetIdentitySet(netuid)`: Emitted when a custom identity is set for a new subnetwork. - /// * `SubnetIdentityRemoved(netuid)`: Emitted when the identity of a removed network is also deleted. + /// ### Args + /// * **`origin`** – `T::RuntimeOrigin`  Must be **signed** by the coldkey. + /// * **`hotkey`** – `&T::AccountId`  First neuron of the new subnet. + /// * **`mechid`** – `u16`  Only the dynamic mechanism (`1`) is currently supported. + /// * **`identity`** – `Option`  Optional metadata for the subnet. + /// + /// ### Events + /// * `NetworkAdded(netuid, mechid)` – always. + /// * `SubnetIdentitySet(netuid)` – when a custom identity is supplied. + /// * `NetworkRemoved(netuid)` – when a subnet is pruned to make room. + /// + /// ### Errors + /// * `NonAssociatedColdKey` – `hotkey` already belongs to another coldkey. + /// * `MechanismDoesNotExist` – unsupported `mechid`. + /// * `NetworkTxRateLimitExceeded` – caller hit the register-network rate limit. + /// * `SubnetLimitReached` – limit hit **and** no eligible subnet to prune. + /// * `CannotAffordLockCost` – caller lacks the lock cost. + /// * `BalanceWithdrawalError` – failed to lock balance. + /// * `InvalidIdentity` – supplied `identity` failed validation. /// pub fn do_register_network( origin: T::RuntimeOrigin, @@ -132,24 +128,43 @@ impl Pallet { // --- 3. Ensure the mechanism is Dynamic. ensure!(mechid == 1, Error::::MechanismDoesNotExist); - // --- 4. Rate limit for network registrations. let current_block = Self::get_current_block_as_u64(); + + ensure!( + current_block >= NetworkRegistrationStartBlock::::get(), + Error::::SubNetRegistrationDisabled + ); + + // --- 4. Rate limit for network registrations. ensure!( TransactionType::RegisterNetwork.passes_rate_limit::(&coldkey), Error::::NetworkTxRateLimitExceeded ); - // --- 5. Calculate and lock the required tokens. + // --- 5. Check if we need to prune a subnet (if at SubnetLimit). + // But do not prune yet; we only do it after all checks pass. + let subnet_limit = Self::get_max_subnets(); + let current_count: u16 = NetworksAdded::::iter() + .filter(|(netuid, added)| *added && *netuid != NetUid::ROOT) + .count() as u16; + + let mut recycle_netuid: Option = None; + if current_count >= subnet_limit { + if let Some(netuid) = Self::get_network_to_prune() { + recycle_netuid = Some(netuid); + } else { + return Err(Error::::SubnetLimitReached.into()); + } + } + + // --- 6. Calculate and lock the required tokens. let lock_amount = Self::get_network_lock_cost(); log::debug!("network lock_amount: {lock_amount:?}"); ensure!( Self::can_remove_balance_from_coldkey_account(&coldkey, lock_amount.into()), - Error::::NotEnoughBalanceToStake + Error::::CannotAffordLockCost ); - // --- 6. Determine the netuid to register. - let netuid_to_register = Self::get_next_netuid(); - // --- 7. Perform the lock operation. let actual_tao_lock_amount = Self::remove_balance_from_coldkey_account(&coldkey, lock_amount.into())?; @@ -157,41 +172,61 @@ impl Pallet { // --- 8. Set the lock amount for use to determine pricing. Self::set_network_last_lock(actual_tao_lock_amount); + Self::set_network_last_lock_block(current_block); - // --- 9. Set initial and custom parameters for the network. + // --- 9. If we identified a subnet to prune, do it now. + if let Some(prune_netuid) = recycle_netuid { + Self::do_dissolve_network(prune_netuid)?; + } + + // --- 10. Determine netuid to register. If we pruned a subnet, reuse that netuid. + let netuid_to_register: NetUid = match recycle_netuid { + Some(prune_netuid) => prune_netuid, + None => Self::get_next_netuid(), + }; + + // --- 11. Set initial and custom parameters for the network. let default_tempo = DefaultTempo::::get(); Self::init_new_network(netuid_to_register, default_tempo); log::debug!("init_new_network: {netuid_to_register:?}"); - // --- 10. Add the caller to the neuron set. + // --- 12. Add the caller to the neuron set. Self::create_account_if_non_existent(&coldkey, hotkey); Self::append_neuron(netuid_to_register, hotkey, current_block); log::debug!("Appended neuron for netuid {netuid_to_register:?}, hotkey: {hotkey:?}"); - // --- 11. Set the mechanism. + // --- 13. Set the mechanism. SubnetMechanism::::insert(netuid_to_register, mechid); log::debug!("SubnetMechanism for netuid {netuid_to_register:?} set to: {mechid:?}"); - // --- 12. Set the creation terms. - Self::set_network_last_lock_block(current_block); + // --- 14. Set the creation terms. NetworkRegisteredAt::::insert(netuid_to_register, current_block); - // --- 13. Set the symbol. + // --- 15. Set the symbol. let symbol = Self::get_next_available_symbol(netuid_to_register); TokenSymbol::::insert(netuid_to_register, symbol); - // --- 14. Init the pool by putting the lock as the initial alpha. - // Put initial TAO from lock into subnet TAO and produce numerically equal amount of Alpha - // The initial TAO is the locked amount, with a minimum of 1 RAO and a cap of 100 TAO. - let pool_initial_tao = Self::get_network_min_lock(); - // FIXME: the result from function is used as a mixed type alpha/tao - let pool_initial_alpha = AlphaCurrency::from(Self::get_network_min_lock().to_u64()); + // The initial TAO is the locked amount + // Put initial TAO from lock into subnet TAO and produce numerically equal amount of Alpha. + let pool_initial_tao: TaoCurrency = Self::get_network_min_lock(); + let pool_initial_alpha: AlphaCurrency = pool_initial_tao.to_u64().into(); let actual_tao_lock_amount_less_pool_tao = actual_tao_lock_amount.saturating_sub(pool_initial_tao); + + // Core pool + ownership SubnetTAO::::insert(netuid_to_register, pool_initial_tao); SubnetAlphaIn::::insert(netuid_to_register, pool_initial_alpha); SubnetOwner::::insert(netuid_to_register, coldkey.clone()); SubnetOwnerHotkey::::insert(netuid_to_register, hotkey.clone()); + SubnetLocked::::insert(netuid_to_register, actual_tao_lock_amount); + SubnetTaoProvided::::insert(netuid_to_register, TaoCurrency::ZERO); + SubnetAlphaInProvided::::insert(netuid_to_register, AlphaCurrency::ZERO); + SubnetAlphaOut::::insert(netuid_to_register, AlphaCurrency::ZERO); + SubnetVolume::::insert(netuid_to_register, 0u128); + RAORecycledForRegistration::::insert( + netuid_to_register, + actual_tao_lock_amount_less_pool_tao, + ); if actual_tao_lock_amount_less_pool_tao > TaoCurrency::ZERO { Self::burn_tokens(actual_tao_lock_amount_less_pool_tao); @@ -202,7 +237,7 @@ impl Pallet { Self::increase_total_stake(pool_initial_tao); } - // --- 15. Add the identity if it exists + // --- 17. Add the identity if it exists if let Some(identity_value) = identity { ensure!( Self::is_valid_subnet_identity(&identity_value), @@ -213,15 +248,12 @@ impl Pallet { Self::deposit_event(Event::SubnetIdentitySet(netuid_to_register)); } - // --- 16. Enable registration for new subnet - NetworkRegistrationAllowed::::set(netuid_to_register, true); - NetworkPowRegistrationAllowed::::set(netuid_to_register, true); - - // --- 17. Emit the NetworkAdded event. + T::SwapInterface::toggle_user_liquidity(netuid_to_register, true); + // --- 18. Emit the NetworkAdded event. log::info!("NetworkAdded( netuid:{netuid_to_register:?}, mechanism:{mechid:?} )"); Self::deposit_event(Event::NetworkAdded(netuid_to_register, mechid)); - // --- 18. Return success. + // --- 19. Return success. Ok(()) } diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index 1dcc911f4b..1e58d2f7ab 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -1704,3 +1704,244 @@ fn test_migrate_remove_network_modality_already_run() { )); }); } + +#[test] +fn test_migrate_subnet_limit_to_default() { + new_test_ext(1).execute_with(|| { + // ------------------------------ + // 0. Constants / helpers + // ------------------------------ + const MIG_NAME: &[u8] = b"subnet_limit_to_default"; + + // Compute a non-default value safely + let default: u16 = DefaultSubnetLimit::::get(); + let not_default: u16 = default.wrapping_add(1); + + // ------------------------------ + // 1. Pre-state: ensure a non-default value is stored + // ------------------------------ + SubnetLimit::::put(not_default); + assert_eq!( + SubnetLimit::::get(), + not_default, + "precondition failed: SubnetLimit should be non-default before migration" + ); + + assert!( + !HasMigrationRun::::get(MIG_NAME.to_vec()), + "migration flag should be false before run" + ); + + // ------------------------------ + // 2. Run migration + // ------------------------------ + let w = crate::migrations::migrate_subnet_limit_to_default::migrate_subnet_limit_to_default::(); + assert!(!w.is_zero(), "weight must be non-zero"); + + // ------------------------------ + // 3. Verify results + // ------------------------------ + assert!( + HasMigrationRun::::get(MIG_NAME.to_vec()), + "migration flag not set" + ); + + assert_eq!( + SubnetLimit::::get(), + default, + "SubnetLimit should be reset to the configured default" + ); + }); +} + +#[test] +fn test_migrate_network_lock_reduction_interval_and_decay() { + new_test_ext(0).execute_with(|| { + const FOUR_DAYS: u64 = 28_800; + const EIGHT_DAYS: u64 = 57_600; + const ONE_WEEK_BLOCKS: u64 = 50_400; + + // ── pre ────────────────────────────────────────────────────────────── + assert!( + !HasMigrationRun::::get(b"migrate_network_lock_reduction_interval".to_vec()), + "HasMigrationRun should be false before migration" + ); + + // ensure current_block > 0 + step_block(1); + let current_block_before = Pallet::::get_current_block_as_u64(); + + // ── run migration ──────────────────────────────────────────────────── + let weight = crate::migrations::migrate_network_lock_reduction_interval::migrate_network_lock_reduction_interval::(); + assert!(!weight.is_zero(), "migration weight should be > 0"); + + // ── params & flags ─────────────────────────────────────────────────── + assert_eq!(NetworkLockReductionInterval::::get(), EIGHT_DAYS); + assert_eq!(NetworkRateLimit::::get(), FOUR_DAYS); + assert_eq!( + Pallet::::get_network_last_lock(), + 1_000_000_000_000u64.into(), // 1000 TAO in rao + "last_lock should be 1_000_000_000_000 rao" + ); + + // last_lock_block should be set one week in the future + let last_lock_block = Pallet::::get_network_last_lock_block(); + let expected_block = current_block_before.saturating_add(ONE_WEEK_BLOCKS); + assert_eq!( + last_lock_block, + expected_block, + "last_lock_block should be current + ONE_WEEK_BLOCKS" + ); + + // registration start block should match the same future block + assert_eq!( + NetworkRegistrationStartBlock::::get(), + expected_block, + "NetworkRegistrationStartBlock should equal last_lock_block" + ); + + // lock cost should be 2000 TAO immediately after migration + let lock_cost_now = Pallet::::get_network_lock_cost(); + assert_eq!( + lock_cost_now, + 2_000_000_000_000u64.into(), + "lock cost should be 2000 TAO right after migration" + ); + + assert!( + HasMigrationRun::::get(b"migrate_network_lock_reduction_interval".to_vec()), + "HasMigrationRun should be true after migration" + ); + }); +} + +#[test] +fn test_migrate_restore_subnet_locked_feb1_2025() { + use sp_runtime::traits::SaturatedConversion; // only for NetUid -> u16 when reading back + use std::collections::BTreeMap; + + use crate::{HasMigrationRun, SubnetLocked, TaoCurrency}; + + // NOTE: Ensure the migration uses `TaoCurrency::from(rao_u64)` and a `&[(u16, u64)]` snapshot. + new_test_ext(0).execute_with(|| { + // ── pre ────────────────────────────────────────────────────────────── + let name = b"migrate_restore_subnet_locked".to_vec(); + assert!( + !HasMigrationRun::::get(name.clone()), + "HasMigrationRun should be false before migration" + ); + + // Snapshot at block #4_828_623 (2025-02-01 00:00:00Z), RAO as u64. + const EXPECTED: &[(u16, u64)] = &[ + (2, 976_893_069_056), + (3, 2_569_362_397_490), + (4, 1_928_551_593_932), + (5, 1_712_540_082_588), + (6, 1_495_929_556_770), + (7, 1_011_702_451_936), + (8, 337_484_391_024), + (9, 381_240_180_320), + (10, 1_253_515_128_353), + (11, 1_453_924_672_132), + (12, 100_000_000_000), + (13, 100_000_000_000), + (14, 1_489_714_521_808), + (15, 1_784_089_225_496), + (16, 889_176_219_484), + (17, 1_266_310_122_772), + (18, 222_355_058_433), + (19, 100_000_000_000), + (20, 100_000_000_000), + (21, 885_096_322_978), + (22, 100_000_000_000), + (23, 100_000_000_000), + (24, 5_146_073_854_481), + (25, 1_782_920_948_214), + (26, 153_583_865_248), + (27, 201_344_183_084), + (28, 901_455_879_445), + (29, 175_000_001_600), + (30, 1_419_730_660_074), + (31, 319_410_100_502), + (32, 2_016_397_028_246), + (33, 1_626_477_274_174), + (34, 1_455_297_496_345), + (35, 1_191_275_979_639), + (36, 1_097_008_574_216), + (37, 864_664_455_362), + (38, 1_001_936_494_076), + (39, 1_366_096_404_884), + (40, 100_000_000_000), + (41, 535_937_523_200), + (42, 1_215_698_423_344), + (43, 1_641_308_676_800), + (44, 1_514_636_189_434), + (45, 1_605_608_381_438), + (46, 1_095_943_027_350), + (47, 1_499_235_469_986), + (48, 1_308_073_720_362), + (49, 1_222_672_092_068), + (50, 2_628_355_421_561), + (51, 1_520_860_720_561), + (52, 1_794_457_248_725), + (53, 1_721_472_811_492), + (54, 2_048_900_691_868), + (55, 1_278_597_446_119), + (56, 2_016_045_544_480), + (57, 1_920_563_399_676), + (58, 2_246_525_691_504), + (59, 1_776_159_384_888), + (60, 2_173_138_865_414), + (61, 1_435_634_867_728), + (62, 2_061_282_563_888), + (63, 3_008_967_320_998), + (64, 2_099_236_359_026), + ]; + + // ── run migration ──────────────────────────────────────────────────── + let weight = + crate::migrations::migrate_subnet_locked::migrate_restore_subnet_locked::(); + assert!(!weight.is_zero(), "migration weight should be > 0"); + + // ── validate: build a (u16 -> u64) map directly from storage iterator ─ + let actual: BTreeMap = SubnetLocked::::iter() + .map(|(k, v)| (k.saturated_into::(), u64::from(v))) + .collect(); + + let expected: BTreeMap = EXPECTED.iter().copied().collect(); + + // 1) exact content match (keys and values) + assert_eq!(actual, expected, "SubnetLocked map mismatch with snapshot"); + + // 2) count and total sum match expectations + let expected_len = expected.len(); + let expected_sum: u128 = expected.values().map(|v| *v as u128).sum(); + + let count_after = actual.len(); + let sum_after: u128 = actual.values().map(|v| *v as u128).sum(); + + assert_eq!(count_after, expected_len, "entry count mismatch"); + assert_eq!(sum_after, expected_sum, "total RAO sum mismatch"); + + // ── migration flag ─────────────────────────────────────────────────── + assert!( + HasMigrationRun::::get(name.clone()), + "HasMigrationRun should be true after migration" + ); + + // ── idempotence: re-running does not change storage ───────────────── + let before = actual; + + let _again = + crate::migrations::migrate_subnet_locked::migrate_restore_subnet_locked::(); + + let after: BTreeMap = SubnetLocked::::iter() + .map(|(k, v)| (k.saturated_into::(), u64::from(v))) + .collect(); + + assert_eq!( + before, after, + "re-running the migration should not change storage" + ); + }); +} diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 3f2ba40eff..1aab19e859 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -202,7 +202,7 @@ parameter_types! { pub const InitialMaxDifficulty: u64 = u64::MAX; pub const InitialRAORecycledForRegistration: u64 = 0; pub const InitialSenateRequiredStakePercentage: u64 = 2; // 2 percent of total stake - pub const InitialNetworkImmunityPeriod: u64 = 7200 * 7; + pub const InitialNetworkImmunityPeriod: u64 = 1_296_000; pub const InitialNetworkMinLockCost: u64 = 100_000_000_000; pub const InitialSubnetOwnerCut: u16 = 0; // 0%. 100% of rewards go to validators + miners. pub const InitialNetworkLockReductionInterval: u64 = 2; // 2 blocks. @@ -461,6 +461,7 @@ impl crate::Config for Test { type LeaseDividendsDistributionInterval = LeaseDividendsDistributionInterval; type GetCommitments = (); type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; + type CommitmentsInterface = CommitmentsI; } // Swap-related parameter types @@ -492,6 +493,11 @@ impl PrivilegeCmp for OriginPrivilegeCmp { } } +pub struct CommitmentsI; +impl CommitmentsInterface for CommitmentsI { + fn purge_netuid(_netuid: NetUid) {} +} + parameter_types! { pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * BlockWeights::get().max_block; diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index 4f0634c64c..732f93d13d 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -1,9 +1,13 @@ use super::mock::*; +use crate::migrations::migrate_network_immunity_period; use crate::*; -use frame_support::assert_ok; +use frame_support::{assert_err, assert_ok}; use frame_system::Config; use sp_core::U256; -use subtensor_runtime_common::TaoCurrency; +use sp_std::collections::btree_map::BTreeMap; +use substrate_fixed::types::{I96F32, U64F64, U96F32}; +use subtensor_runtime_common::{NetUidStorageIndex, TaoCurrency}; +use subtensor_swap_interface::{OrderType, SwapHandler}; #[test] fn test_registration_ok() { @@ -33,15 +37,1257 @@ fn test_registration_ok() { coldkey_account_id )); - assert_ok!(SubtensorModule::user_remove_network( - coldkey_account_id, - netuid - )); + assert_ok!(SubtensorModule::do_dissolve_network(netuid)); assert!(!SubtensorModule::if_subnet_exist(netuid)) }) } +#[test] +fn dissolve_no_stakers_no_alpha_no_emission() { + new_test_ext(0).execute_with(|| { + let cold = U256::from(1); + let hot = U256::from(2); + let net = add_dynamic_network(&hot, &cold); + + SubtensorModule::set_subnet_locked_balance(net, TaoCurrency::from(0)); + SubnetTAO::::insert(net, TaoCurrency::from(0)); + Emission::::insert(net, Vec::::new()); + + let before = SubtensorModule::get_coldkey_balance(&cold); + assert_ok!(SubtensorModule::do_dissolve_network(net)); + let after = SubtensorModule::get_coldkey_balance(&cold); + + // Balance should be unchanged (whatever the network-lock bookkeeping left there) + assert_eq!(after, before); + assert!(!SubtensorModule::if_subnet_exist(net)); + }); +} + +#[test] +fn dissolve_refunds_full_lock_cost_when_no_emission() { + new_test_ext(0).execute_with(|| { + let cold = U256::from(3); + let hot = U256::from(4); + let net = add_dynamic_network(&hot, &cold); + + let lock: TaoCurrency = TaoCurrency::from(1_000_000); + SubtensorModule::set_subnet_locked_balance(net, lock); + SubnetTAO::::insert(net, TaoCurrency::from(0)); + Emission::::insert(net, Vec::::new()); + + let before = SubtensorModule::get_coldkey_balance(&cold); + assert_ok!(SubtensorModule::do_dissolve_network(net)); + let after = SubtensorModule::get_coldkey_balance(&cold); + + assert_eq!(TaoCurrency::from(after), TaoCurrency::from(before) + lock); + }); +} + +#[test] +fn dissolve_single_alpha_out_staker_gets_all_tao() { + new_test_ext(0).execute_with(|| { + // 1. Owner & subnet + let owner_cold = U256::from(10); + let owner_hot = U256::from(20); + let net = add_dynamic_network(&owner_hot, &owner_cold); + + // 2. Single α-out staker + let (s_hot, s_cold) = (U256::from(100), U256::from(200)); + Alpha::::insert((s_hot, s_cold, net), U64F64::from_num(5_000u128)); + + // Entire TAO pot should be paid to staker's cold-key + let pot: u64 = 99_999; + SubnetTAO::::insert(net, TaoCurrency::from(pot)); + SubtensorModule::set_subnet_locked_balance(net, 0.into()); + + // Cold-key balance before + let before = SubtensorModule::get_coldkey_balance(&s_cold); + + // Dissolve + assert_ok!(SubtensorModule::do_dissolve_network(net)); + + // Cold-key received full pot + let after = SubtensorModule::get_coldkey_balance(&s_cold); + assert_eq!(after, before + pot); + + // No α entries left for dissolved subnet + assert!(Alpha::::iter().all(|((_h, _c, n), _)| n != net)); + assert!(!SubnetTAO::::contains_key(net)); + }); +} + +#[allow(clippy::indexing_slicing)] +#[test] +fn dissolve_two_stakers_pro_rata_distribution() { + new_test_ext(0).execute_with(|| { + // Subnet + two stakers + let oc = U256::from(50); + let oh = U256::from(51); + let net = add_dynamic_network(&oh, &oc); + + let (s1_hot, s1_cold, a1) = (U256::from(201), U256::from(301), 300u128); + let (s2_hot, s2_cold, a2) = (U256::from(202), U256::from(302), 700u128); + + Alpha::::insert((s1_hot, s1_cold, net), U64F64::from_num(a1)); + Alpha::::insert((s2_hot, s2_cold, net), U64F64::from_num(a2)); + + let pot: u64 = 10_000; + SubnetTAO::::insert(net, TaoCurrency::from(pot)); + SubtensorModule::set_subnet_locked_balance(net, 5_000.into()); // owner refund path present but emission = 0 + + // Cold-key balances before + let s1_before = SubtensorModule::get_coldkey_balance(&s1_cold); + let s2_before = SubtensorModule::get_coldkey_balance(&s2_cold); + let owner_before = SubtensorModule::get_coldkey_balance(&oc); + + // Expected τ shares with largest remainder + let total = a1 + a2; + let prod1 = a1 * (pot as u128); + let prod2 = a2 * (pot as u128); + let share1 = (prod1 / total) as u64; + let share2 = (prod2 / total) as u64; + let mut distributed = share1 + share2; + let mut rem = [(s1_cold, prod1 % total), (s2_cold, prod2 % total)]; + if distributed < pot { + rem.sort_by_key(|&(_c, r)| core::cmp::Reverse(r)); + let leftover = pot - distributed; + for _ in 0..leftover as usize { + distributed += 1; + } + } + // Recompute exact expected shares using the same logic + let mut expected1 = share1; + let mut expected2 = share2; + if share1 + share2 < pot { + rem.sort_by_key(|&(_c, r)| core::cmp::Reverse(r)); + if rem[0].0 == s1_cold { + expected1 += 1; + } else { + expected2 += 1; + } + } + + // Dissolve + assert_ok!(SubtensorModule::do_dissolve_network(net)); + + // Cold-keys received their τ shares + assert_eq!( + SubtensorModule::get_coldkey_balance(&s1_cold), + s1_before + expected1 + ); + assert_eq!( + SubtensorModule::get_coldkey_balance(&s2_cold), + s2_before + expected2 + ); + + // Owner refunded lock (no emission) + assert_eq!( + SubtensorModule::get_coldkey_balance(&oc), + owner_before + 5_000 + ); + + // α entries for dissolved subnet gone + assert!(Alpha::::iter().all(|((_h, _c, n), _)| n != net)); + }); +} + +#[test] +fn dissolve_owner_cut_refund_logic() { + new_test_ext(0).execute_with(|| { + let oc = U256::from(70); + let oh = U256::from(71); + let net = add_dynamic_network(&oh, &oc); + + // One staker and a TAO pot (not relevant to refund amount). + let sh = U256::from(77); + let sc = U256::from(88); + Alpha::::insert((sh, sc, net), U64F64::from_num(100u128)); + SubnetTAO::::insert(net, TaoCurrency::from(1_000)); + + // Lock & emissions: total emitted α = 800. + let lock: TaoCurrency = TaoCurrency::from(2_000); + SubtensorModule::set_subnet_locked_balance(net, lock); + Emission::::insert( + net, + vec![AlphaCurrency::from(200), AlphaCurrency::from(600)], + ); + + // Owner cut = 11796 / 65535 (about 18%). + SubnetOwnerCut::::put(11_796u16); + + // Compute expected refund with the SAME math as the pallet. + let frac: U96F32 = SubtensorModule::get_float_subnet_owner_cut(); + let total_emitted_alpha: u64 = 800; + let owner_alpha_u64: u64 = U96F32::from_num(total_emitted_alpha) + .saturating_mul(frac) + .floor() + .saturating_to_num::(); + + // Current α→τ price for this subnet. + let price: U96F32 = + ::SwapInterface::current_alpha_price(net.into()); + let owner_emission_tao_u64: u64 = U96F32::from_num(owner_alpha_u64) + .saturating_mul(price) + .floor() + .saturating_to_num::(); + + let expected_refund: TaoCurrency = + lock.saturating_sub(TaoCurrency::from(owner_emission_tao_u64)); + + let before = SubtensorModule::get_coldkey_balance(&oc); + assert_ok!(SubtensorModule::do_dissolve_network(net)); + let after = SubtensorModule::get_coldkey_balance(&oc); + + assert_eq!( + TaoCurrency::from(after), + TaoCurrency::from(before) + expected_refund + ); + }); +} + +#[test] +fn dissolve_zero_refund_when_emission_exceeds_lock() { + new_test_ext(0).execute_with(|| { + let oc = U256::from(1_000); + let oh = U256::from(2_000); + let net = add_dynamic_network(&oh, &oc); + + SubtensorModule::set_subnet_locked_balance(net, TaoCurrency::from(1_000)); + SubnetOwnerCut::::put(u16::MAX); // 100 % + Emission::::insert(net, vec![AlphaCurrency::from(2_000)]); + + let before = SubtensorModule::get_coldkey_balance(&oc); + assert_ok!(SubtensorModule::do_dissolve_network(net)); + let after = SubtensorModule::get_coldkey_balance(&oc); + + assert_eq!(after, before); // no refund + }); +} + +#[test] +fn dissolve_nonexistent_subnet_fails() { + new_test_ext(0).execute_with(|| { + assert_err!( + SubtensorModule::do_dissolve_network(9_999.into()), + Error::::SubNetworkDoesNotExist + ); + }); +} + +#[test] +fn dissolve_clears_all_per_subnet_storages() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(123); + let owner_hot = U256::from(456); + let net = add_dynamic_network(&owner_hot, &owner_cold); + + // ------------------------------------------------------------------ + // Populate each storage item with a minimal value of the CORRECT type + // ------------------------------------------------------------------ + // Core ownership / bookkeeping + SubnetOwner::::insert(net, owner_cold); + SubnetOwnerHotkey::::insert(net, owner_hot); + SubnetworkN::::insert(net, 0u16); + NetworksAdded::::insert(net, true); + NetworkRegisteredAt::::insert(net, 0u64); + + // Consensus vectors + Rank::::insert(net, vec![1u16]); + Trust::::insert(net, vec![1u16]); + Active::::insert(net, vec![true]); + Emission::::insert(net, vec![AlphaCurrency::from(1)]); + Incentive::::insert(NetUidStorageIndex::from(net), vec![1u16]); + Consensus::::insert(net, vec![1u16]); + Dividends::::insert(net, vec![1u16]); + PruningScores::::insert(net, vec![1u16]); + LastUpdate::::insert(NetUidStorageIndex::from(net), vec![0u64]); + ValidatorPermit::::insert(net, vec![true]); + ValidatorTrust::::insert(net, vec![1u16]); + + // Per‑net params + Tempo::::insert(net, 1u16); + Kappa::::insert(net, 1u16); + Difficulty::::insert(net, 1u64); + + MaxAllowedUids::::insert(net, 1u16); + ImmunityPeriod::::insert(net, 1u16); + ActivityCutoff::::insert(net, 1u16); + MaxWeightsLimit::::insert(net, 1u16); + MinAllowedWeights::::insert(net, 1u16); + + RegistrationsThisInterval::::insert(net, 1u16); + POWRegistrationsThisInterval::::insert(net, 1u16); + BurnRegistrationsThisInterval::::insert(net, 1u16); + + // Pool / AMM counters + SubnetTAO::::insert(net, TaoCurrency::from(1)); + SubnetAlphaInEmission::::insert(net, AlphaCurrency::from(1)); + SubnetAlphaOutEmission::::insert(net, AlphaCurrency::from(1)); + SubnetTaoInEmission::::insert(net, TaoCurrency::from(1)); + SubnetVolume::::insert(net, 1u128); + + // Items now REMOVED (not zeroed) by dissolution + SubnetAlphaIn::::insert(net, AlphaCurrency::from(2)); + SubnetAlphaOut::::insert(net, AlphaCurrency::from(3)); + + // Prefix / double-map collections + Keys::::insert(net, 0u16, owner_hot); + Bonds::::insert(NetUidStorageIndex::from(net), 0u16, vec![(0u16, 1u16)]); + Weights::::insert(NetUidStorageIndex::from(net), 0u16, vec![(1u16, 1u16)]); + + // Membership entry for the SAME hotkey as Keys + IsNetworkMember::::insert(owner_hot, net, true); + + // Token / price / provided reserves + TokenSymbol::::insert(net, b"XX".to_vec()); + SubnetMovingPrice::::insert(net, substrate_fixed::types::I96F32::from_num(1)); + SubnetTaoProvided::::insert(net, TaoCurrency::from(1)); + SubnetAlphaInProvided::::insert(net, AlphaCurrency::from(1)); + + // Subnet locks + TransferToggle::::insert(net, true); + SubnetLocked::::insert(net, TaoCurrency::from(1)); + LargestLocked::::insert(net, 1u64); + + // Subnet parameters & pending counters + FirstEmissionBlockNumber::::insert(net, 1u64); + SubnetMechanism::::insert(net, 1u16); + NetworkRegistrationAllowed::::insert(net, true); + NetworkPowRegistrationAllowed::::insert(net, true); + PendingEmission::::insert(net, AlphaCurrency::from(1)); + PendingRootDivs::::insert(net, TaoCurrency::from(1)); + PendingAlphaSwapped::::insert(net, AlphaCurrency::from(1)); + PendingOwnerCut::::insert(net, AlphaCurrency::from(1)); + BlocksSinceLastStep::::insert(net, 1u64); + LastMechansimStepBlock::::insert(net, 1u64); + ServingRateLimit::::insert(net, 1u64); + Rho::::insert(net, 1u16); + AlphaSigmoidSteepness::::insert(net, 1i16); + + // Weights/versioning/targets/limits + WeightsVersionKey::::insert(net, 1u64); + MaxAllowedValidators::::insert(net, 1u16); + AdjustmentInterval::::insert(net, 2u16); + BondsMovingAverage::::insert(net, 1u64); + BondsPenalty::::insert(net, 1u16); + BondsResetOn::::insert(net, true); + WeightsSetRateLimit::::insert(net, 1u64); + ValidatorPruneLen::::insert(net, 1u64); + ScalingLawPower::::insert(net, 1u16); + TargetRegistrationsPerInterval::::insert(net, 1u16); + AdjustmentAlpha::::insert(net, 1u64); + CommitRevealWeightsEnabled::::insert(net, true); + + // Burn/difficulty/adjustment + Burn::::insert(net, TaoCurrency::from(1)); + MinBurn::::insert(net, TaoCurrency::from(1)); + MaxBurn::::insert(net, TaoCurrency::from(2)); + MinDifficulty::::insert(net, 1u64); + MaxDifficulty::::insert(net, 2u64); + RegistrationsThisBlock::::insert(net, 1u16); + EMAPriceHalvingBlocks::::insert(net, 1u64); + RAORecycledForRegistration::::insert(net, TaoCurrency::from(1)); + + // Feature toggles + LiquidAlphaOn::::insert(net, true); + Yuma3On::::insert(net, true); + AlphaValues::::insert(net, (1u16, 2u16)); + SubtokenEnabled::::insert(net, true); + ImmuneOwnerUidsLimit::::insert(net, 1u16); + + // Per‑subnet vectors / indexes + StakeWeight::::insert(net, vec![1u16]); + + // Uid/registration + Uids::::insert(net, owner_hot, 0u16); + BlockAtRegistration::::insert(net, 0u16, 1u64); + + // Per‑subnet dividends + AlphaDividendsPerSubnet::::insert(net, owner_hot, AlphaCurrency::from(1)); + TaoDividendsPerSubnet::::insert(net, owner_hot, TaoCurrency::from(1)); + + // Parent/child topology + takes + ChildkeyTake::::insert(owner_hot, net, 1u16); + PendingChildKeys::::insert(net, owner_cold, (vec![(1u64, owner_hot)], 1u64)); + ChildKeys::::insert(owner_cold, net, vec![(1u64, owner_hot)]); + ParentKeys::::insert(owner_hot, net, vec![(1u64, owner_cold)]); + + // Hotkey swap timestamp for subnet + LastHotkeySwapOnNetuid::::insert(net, owner_cold, 1u64); + + // Axon/prometheus tx key timing (NMap) — ***correct key-tuple insertion*** + TransactionKeyLastBlock::::insert((owner_hot, net, 1u16), 1u64); + + // EVM association indexed by (netuid, uid) + AssociatedEvmAddress::::insert(net, 0u16, (sp_core::H160::zero(), 1u64)); + + // (Optional) subnet -> lease link + SubnetUidToLeaseId::::insert(net, 42u32); + + // ------------------------------------------------------------------ + // Dissolve + // ------------------------------------------------------------------ + assert_ok!(SubtensorModule::do_dissolve_network(net)); + + // ------------------------------------------------------------------ + // Items that must be COMPLETELY REMOVED + // ------------------------------------------------------------------ + assert!(!SubnetOwner::::contains_key(net)); + assert!(!SubnetOwnerHotkey::::contains_key(net)); + assert!(!SubnetworkN::::contains_key(net)); + assert!(!NetworksAdded::::contains_key(net)); + assert!(!NetworkRegisteredAt::::contains_key(net)); + + // Consensus vectors removed + assert!(!Rank::::contains_key(net)); + assert!(!Trust::::contains_key(net)); + assert!(!Active::::contains_key(net)); + assert!(!Emission::::contains_key(net)); + assert!(!Incentive::::contains_key(NetUidStorageIndex::from( + net + ))); + assert!(!Consensus::::contains_key(net)); + assert!(!Dividends::::contains_key(net)); + assert!(!PruningScores::::contains_key(net)); + assert!(!LastUpdate::::contains_key(NetUidStorageIndex::from( + net + ))); + + assert!(!ValidatorPermit::::contains_key(net)); + assert!(!ValidatorTrust::::contains_key(net)); + + // Per‑net params removed + assert!(!Tempo::::contains_key(net)); + assert!(!Kappa::::contains_key(net)); + assert!(!Difficulty::::contains_key(net)); + + assert!(!MaxAllowedUids::::contains_key(net)); + assert!(!ImmunityPeriod::::contains_key(net)); + assert!(!ActivityCutoff::::contains_key(net)); + assert!(!MaxWeightsLimit::::contains_key(net)); + assert!(!MinAllowedWeights::::contains_key(net)); + + assert!(!RegistrationsThisInterval::::contains_key(net)); + assert!(!POWRegistrationsThisInterval::::contains_key(net)); + assert!(!BurnRegistrationsThisInterval::::contains_key(net)); + + // Pool / AMM counters removed + assert!(!SubnetTAO::::contains_key(net)); + assert!(!SubnetAlphaInEmission::::contains_key(net)); + assert!(!SubnetAlphaOutEmission::::contains_key(net)); + assert!(!SubnetTaoInEmission::::contains_key(net)); + assert!(!SubnetVolume::::contains_key(net)); + + // These are now REMOVED + assert!(!SubnetAlphaIn::::contains_key(net)); + assert!(!SubnetAlphaOut::::contains_key(net)); + + // Collections fully cleared + assert!(Keys::::iter_prefix(net).next().is_none()); + assert!( + Bonds::::iter_prefix(NetUidStorageIndex::from(net)) + .next() + .is_none() + ); + assert!( + Weights::::iter_prefix(NetUidStorageIndex::from(net)) + .next() + .is_none() + ); + assert!(!IsNetworkMember::::contains_key(owner_hot, net)); + + // Token / price / provided reserves + assert!(!TokenSymbol::::contains_key(net)); + assert!(!SubnetMovingPrice::::contains_key(net)); + assert!(!SubnetTaoProvided::::contains_key(net)); + assert!(!SubnetAlphaInProvided::::contains_key(net)); + + // Subnet locks + assert!(!TransferToggle::::contains_key(net)); + assert!(!SubnetLocked::::contains_key(net)); + assert!(!LargestLocked::::contains_key(net)); + + // Subnet parameters & pending counters + assert!(!FirstEmissionBlockNumber::::contains_key(net)); + assert!(!SubnetMechanism::::contains_key(net)); + assert!(!NetworkRegistrationAllowed::::contains_key(net)); + assert!(!NetworkPowRegistrationAllowed::::contains_key(net)); + assert!(!PendingEmission::::contains_key(net)); + assert!(!PendingRootDivs::::contains_key(net)); + assert!(!PendingAlphaSwapped::::contains_key(net)); + assert!(!PendingOwnerCut::::contains_key(net)); + assert!(!BlocksSinceLastStep::::contains_key(net)); + assert!(!LastMechansimStepBlock::::contains_key(net)); + assert!(!ServingRateLimit::::contains_key(net)); + assert!(!Rho::::contains_key(net)); + assert!(!AlphaSigmoidSteepness::::contains_key(net)); + + // Weights/versioning/targets/limits + assert!(!WeightsVersionKey::::contains_key(net)); + assert!(!MaxAllowedValidators::::contains_key(net)); + assert!(!AdjustmentInterval::::contains_key(net)); + assert!(!BondsMovingAverage::::contains_key(net)); + assert!(!BondsPenalty::::contains_key(net)); + assert!(!BondsResetOn::::contains_key(net)); + assert!(!WeightsSetRateLimit::::contains_key(net)); + assert!(!ValidatorPruneLen::::contains_key(net)); + assert!(!ScalingLawPower::::contains_key(net)); + assert!(!TargetRegistrationsPerInterval::::contains_key(net)); + assert!(!AdjustmentAlpha::::contains_key(net)); + assert!(!CommitRevealWeightsEnabled::::contains_key(net)); + + // Burn/difficulty/adjustment + assert!(!Burn::::contains_key(net)); + assert!(!MinBurn::::contains_key(net)); + assert!(!MaxBurn::::contains_key(net)); + assert!(!MinDifficulty::::contains_key(net)); + assert!(!MaxDifficulty::::contains_key(net)); + assert!(!RegistrationsThisBlock::::contains_key(net)); + assert!(!EMAPriceHalvingBlocks::::contains_key(net)); + assert!(!RAORecycledForRegistration::::contains_key(net)); + + // Feature toggles + assert!(!LiquidAlphaOn::::contains_key(net)); + assert!(!Yuma3On::::contains_key(net)); + assert!(!AlphaValues::::contains_key(net)); + assert!(!SubtokenEnabled::::contains_key(net)); + assert!(!ImmuneOwnerUidsLimit::::contains_key(net)); + + // Per‑subnet vectors / indexes + assert!(!StakeWeight::::contains_key(net)); + + // Uid/registration + assert!(Uids::::get(net, owner_hot).is_none()); + assert!(!BlockAtRegistration::::contains_key(net, 0u16)); + + // Per‑subnet dividends + assert!(!AlphaDividendsPerSubnet::::contains_key( + net, owner_hot + )); + assert!(!TaoDividendsPerSubnet::::contains_key(net, owner_hot)); + + // Parent/child topology + takes + assert!(!ChildkeyTake::::contains_key(owner_hot, net)); + assert!(!PendingChildKeys::::contains_key(net, owner_cold)); + assert!(!ChildKeys::::contains_key(owner_cold, net)); + assert!(!ParentKeys::::contains_key(owner_hot, net)); + + // Hotkey swap timestamp for subnet + assert!(!LastHotkeySwapOnNetuid::::contains_key( + net, owner_cold + )); + + // Axon/prometheus tx key timing (NMap) — ValueQuery (defaults to 0) + assert_eq!( + TransactionKeyLastBlock::::get((owner_hot, net, 1u16)), + 0u64 + ); + + // EVM association + assert!(AssociatedEvmAddress::::get(net, 0u16).is_none()); + + // Subnet -> lease link + assert!(!SubnetUidToLeaseId::::contains_key(net)); + + // ------------------------------------------------------------------ + // Final subnet removal confirmation + // ------------------------------------------------------------------ + assert!(!SubtensorModule::if_subnet_exist(net)); + }); +} + +#[test] +fn dissolve_alpha_out_but_zero_tao_no_rewards() { + new_test_ext(0).execute_with(|| { + let oc = U256::from(21); + let oh = U256::from(22); + let net = add_dynamic_network(&oh, &oc); + + let sh = U256::from(23); + let sc = U256::from(24); + + Alpha::::insert((sh, sc, net), U64F64::from_num(1_000u64)); + SubnetTAO::::insert(net, TaoCurrency::from(0)); // zero TAO + SubtensorModule::set_subnet_locked_balance(net, TaoCurrency::from(0)); + Emission::::insert(net, Vec::::new()); + + let before = SubtensorModule::get_coldkey_balance(&sc); + assert_ok!(SubtensorModule::do_dissolve_network(net)); + let after = SubtensorModule::get_coldkey_balance(&sc); + + // No reward distributed, α-out cleared. + assert_eq!(after, before); + assert!(Alpha::::iter().next().is_none()); + }); +} + +#[test] +fn dissolve_decrements_total_networks() { + new_test_ext(0).execute_with(|| { + let total_before = TotalNetworks::::get(); + + let cold = U256::from(41); + let hot = U256::from(42); + let net = add_dynamic_network(&hot, &cold); + + // Sanity: adding network increments the counter. + assert_eq!(TotalNetworks::::get(), total_before + 1); + + assert_ok!(SubtensorModule::do_dissolve_network(net)); + assert_eq!(TotalNetworks::::get(), total_before); + }); +} + +#[test] +fn dissolve_rounding_remainder_distribution() { + new_test_ext(0).execute_with(|| { + // 1. Build subnet with two α-out stakers (3 & 2 α) + let oc = U256::from(61); + let oh = U256::from(62); + let net = add_dynamic_network(&oh, &oc); + + let (s1h, s1c) = (U256::from(63), U256::from(64)); + let (s2h, s2c) = (U256::from(65), U256::from(66)); + + Alpha::::insert((s1h, s1c, net), U64F64::from_num(3u128)); + Alpha::::insert((s2h, s2c, net), U64F64::from_num(2u128)); + + SubnetTAO::::insert(net, TaoCurrency::from(1)); // TAO pot = 1 + SubtensorModule::set_subnet_locked_balance(net, TaoCurrency::from(0)); + + // Cold-key balances before + let c1_before = SubtensorModule::get_coldkey_balance(&s1c); + let c2_before = SubtensorModule::get_coldkey_balance(&s2c); + + // 3. Run full dissolve flow + assert_ok!(SubtensorModule::do_dissolve_network(net)); + + // 4. s1 (larger remainder) should get +1 τ on cold-key + let c1_after = SubtensorModule::get_coldkey_balance(&s1c); + let c2_after = SubtensorModule::get_coldkey_balance(&s2c); + + assert_eq!(c1_after, c1_before + 1); + assert_eq!(c2_after, c2_before); + + // α records for subnet gone; TAO key gone + assert!(Alpha::::iter().all(|((_h, _c, n), _)| n != net)); + assert!(!SubnetTAO::::contains_key(net)); + }); +} +#[test] +fn destroy_alpha_out_multiple_stakers_pro_rata() { + new_test_ext(0).execute_with(|| { + // 1. Owner & subnet + let owner_cold = U256::from(10); + let owner_hot = U256::from(20); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + + // 2. Two stakers on that subnet + let (c1, h1) = (U256::from(111), U256::from(211)); + let (c2, h2) = (U256::from(222), U256::from(333)); + register_ok_neuron(netuid, h1, c1, 0); + register_ok_neuron(netuid, h2, c2, 0); + + // 3. Stake 30 : 70 (s1 : s2) in TAO + let min_total = DefaultMinStake::::get(); + let min_total_u64: u64 = min_total.into(); + let s1: u64 = 3u64 * min_total_u64; + let s2: u64 = 7u64 * min_total_u64; + + SubtensorModule::add_balance_to_coldkey_account(&c1, s1 + 50_000); + SubtensorModule::add_balance_to_coldkey_account(&c2, s2 + 50_000); + + assert_ok!(SubtensorModule::do_add_stake( + RuntimeOrigin::signed(c1), + h1, + netuid, + s1.into() + )); + assert_ok!(SubtensorModule::do_add_stake( + RuntimeOrigin::signed(c2), + h2, + netuid, + s2.into() + )); + + // 4. α-out snapshot + let a1: u128 = Alpha::::get((h1, c1, netuid)).saturating_to_num(); + let a2: u128 = Alpha::::get((h2, c2, netuid)).saturating_to_num(); + let atotal = a1 + a2; + + // 5. TAO pot & lock + let tao_pot: u64 = 10_000; + SubnetTAO::::insert(netuid, TaoCurrency::from(tao_pot)); + SubtensorModule::set_subnet_locked_balance(netuid, TaoCurrency::from(5_000)); + + // 6. Balances before + let c1_before = SubtensorModule::get_coldkey_balance(&c1); + let c2_before = SubtensorModule::get_coldkey_balance(&c2); + let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); + + // 7. Run the (now credit-to-coldkey) logic + assert_ok!(SubtensorModule::destroy_alpha_in_out_stakes(netuid)); + + // 8. Expected τ shares via largest remainder + let prod1 = (tao_pot as u128) * a1; + let prod2 = (tao_pot as u128) * a2; + let mut s1_share = (prod1 / atotal) as u64; + let mut s2_share = (prod2 / atotal) as u64; + let distributed = s1_share + s2_share; + if distributed < tao_pot { + // Assign leftover to larger remainder + let r1 = prod1 % atotal; + let r2 = prod2 % atotal; + if r1 >= r2 { + s1_share += 1; + } else { + s2_share += 1; + } + } + + // 9. Cold-key balances must have increased accordingly + assert_eq!( + SubtensorModule::get_coldkey_balance(&c1), + c1_before + s1_share + ); + assert_eq!( + SubtensorModule::get_coldkey_balance(&c2), + c2_before + s2_share + ); + + // 10. Owner refund (5 000 τ) to cold-key (no emission) + assert_eq!( + SubtensorModule::get_coldkey_balance(&owner_cold), + owner_before + 5_000 + ); + + // 11. α entries cleared for the subnet + assert!(!Alpha::::contains_key((h1, c1, netuid))); + assert!(!Alpha::::contains_key((h2, c2, netuid))); + }); +} + +#[allow(clippy::indexing_slicing)] +#[test] +fn destroy_alpha_out_many_stakers_complex_distribution() { + new_test_ext(0).execute_with(|| { + // ── 1) create subnet with 20 stakers ──────────────────────────────── + let owner_cold = U256::from(1_000); + let owner_hot = U256::from(2_000); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + SubtensorModule::set_max_registrations_per_block(netuid, 1_000u16); + SubtensorModule::set_target_registrations_per_interval(netuid, 1_000u16); + + // Runtime-exact min amount = min_stake + fee + let min_amount = { + let min_stake = DefaultMinStake::::get(); + let fee = ::SwapInterface::approx_fee_amount( + netuid.into(), + min_stake.into(), + ); + min_stake.saturating_add(fee.into()) + }; + + const N: usize = 20; + let mut cold = [U256::zero(); N]; + let mut hot = [U256::zero(); N]; + let mut stake = [0u64; N]; + + let min_amount_u64: u64 = min_amount.into(); + for i in 0..N { + cold[i] = U256::from(10_000 + 2 * i as u32); + hot[i] = U256::from(10_001 + 2 * i as u32); + stake[i] = (i as u64 + 1u64) * min_amount_u64; // multiples of min_amount + + register_ok_neuron(netuid, hot[i], cold[i], 0); + SubtensorModule::add_balance_to_coldkey_account(&cold[i], stake[i] + 100_000); + + assert_ok!(SubtensorModule::do_add_stake( + RuntimeOrigin::signed(cold[i]), + hot[i], + netuid, + stake[i].into() + )); + } + + // ── 2) α-out snapshot ─────────────────────────────────────────────── + let mut alpha = [0u128; N]; + let mut alpha_sum: u128 = 0; + for i in 0..N { + alpha[i] = Alpha::::get((hot[i], cold[i], netuid)).saturating_to_num(); + alpha_sum += alpha[i]; + } + + // ── 3) TAO pot & subnet lock ──────────────────────────────────────── + let tao_pot: u64 = 123_456; + let lock: u64 = 30_000; + SubnetTAO::::insert(netuid, TaoCurrency::from(tao_pot)); + SubtensorModule::set_subnet_locked_balance(netuid, TaoCurrency::from(lock)); + + // Owner already earned some emission; owner-cut = 50 % + Emission::::insert( + netuid, + vec![ + AlphaCurrency::from(1_000), + AlphaCurrency::from(2_000), + AlphaCurrency::from(1_500), + ], + ); + SubnetOwnerCut::::put(32_768u16); // ~ 0.5 in fixed-point + + // ── 4) balances before ────────────────────────────────────────────── + let mut bal_before = [0u64; N]; + for i in 0..N { + bal_before[i] = SubtensorModule::get_coldkey_balance(&cold[i]); + } + let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); + + // ── 5) expected τ share per pallet algorithm (incl. remainder) ───── + let mut share = [0u64; N]; + let mut rem = [0u128; N]; + let mut paid: u128 = 0; + + for i in 0..N { + let prod = tao_pot as u128 * alpha[i]; + share[i] = (prod / alpha_sum) as u64; + rem[i] = prod % alpha_sum; + paid += share[i] as u128; + } + let leftover = tao_pot as u128 - paid; + let mut idx: Vec<_> = (0..N).collect(); + idx.sort_by_key(|i| core::cmp::Reverse(rem[*i])); + for i in 0..leftover as usize { + share[idx[i]] += 1; + } + + // ── 5b) expected owner refund with price-aware emission deduction ─── + let frac: U96F32 = SubtensorModule::get_float_subnet_owner_cut(); + let total_emitted_alpha: u64 = 1_000 + 2_000 + 1_500; // 4500 α + let owner_alpha_u64: u64 = U96F32::from_num(total_emitted_alpha) + .saturating_mul(frac) + .floor() + .saturating_to_num::(); + + let owner_emission_tao_u64: u64 = ::SwapInterface::sim_swap( + netuid.into(), + OrderType::Sell, + owner_alpha_u64, + ) + .map(|res| res.amount_paid_out) + .unwrap_or_else(|_| { + // Fallback matches the pallet's fallback + let price: U96F32 = + ::SwapInterface::current_alpha_price(netuid.into()); + U96F32::from_num(owner_alpha_u64) + .saturating_mul(price) + .floor() + .saturating_to_num::() + }); + + let expected_refund: u64 = lock.saturating_sub(owner_emission_tao_u64); + + // ── 6) run distribution (credits τ to coldkeys, wipes α state) ───── + assert_ok!(SubtensorModule::destroy_alpha_in_out_stakes(netuid)); + + // ── 7) post checks ────────────────────────────────────────────────── + for i in 0..N { + // cold-key balances increased by expected τ share + assert_eq!( + SubtensorModule::get_coldkey_balance(&cold[i]), + bal_before[i] + share[i], + "staker {i} cold-key balance changed unexpectedly" + ); + } + + // owner refund + assert_eq!( + SubtensorModule::get_coldkey_balance(&owner_cold), + owner_before + expected_refund + ); + + // α cleared for dissolved subnet & related counters reset + assert!(Alpha::::iter().all(|((_h, _c, n), _)| n != netuid)); + assert_eq!(SubnetAlphaIn::::get(netuid), 0.into()); + assert_eq!(SubnetAlphaOut::::get(netuid), 0.into()); + assert_eq!(SubtensorModule::get_subnet_locked_balance(netuid), 0.into()); + }); +} + +#[test] +fn prune_none_with_no_networks() { + new_test_ext(0).execute_with(|| { + assert_eq!(SubtensorModule::get_network_to_prune(), None); + }); +} + +#[test] +fn prune_none_when_all_networks_immune() { + new_test_ext(0).execute_with(|| { + // two fresh networks → still inside immunity window + let n1 = add_dynamic_network(&U256::from(2), &U256::from(1)); + let _n2 = add_dynamic_network(&U256::from(4), &U256::from(3)); + + // emissions don’t matter while immune + Emission::::insert(n1, vec![AlphaCurrency::from(10)]); + + assert_eq!(SubtensorModule::get_network_to_prune(), None); + }); +} + +#[test] +fn prune_selects_network_with_lowest_price() { + new_test_ext(0).execute_with(|| { + let n1 = add_dynamic_network(&U256::from(20), &U256::from(10)); + let n2 = add_dynamic_network(&U256::from(40), &U256::from(30)); + + // make both networks eligible (past immunity) + let imm = SubtensorModule::get_network_immunity_period(); + System::set_block_number(imm + 10); + + // n1 has lower price → should be pruned + SubnetMovingPrice::::insert(n1, I96F32::from_num(1)); + SubnetMovingPrice::::insert(n2, I96F32::from_num(10)); + + assert_eq!(SubtensorModule::get_network_to_prune(), Some(n1)); + }); +} + +#[test] +fn prune_ignores_immune_network_even_if_lower_price() { + new_test_ext(0).execute_with(|| { + // create mature network n1 first + let n1 = add_dynamic_network(&U256::from(22), &U256::from(11)); + + let imm = SubtensorModule::get_network_immunity_period(); + System::set_block_number(imm + 5); // advance → n1 now mature + + // create second network n2 *inside* immunity + let n2 = add_dynamic_network(&U256::from(44), &U256::from(33)); + + // prices: n2 lower but immune; n1 must be selected + SubnetMovingPrice::::insert(n1, I96F32::from_num(5)); + SubnetMovingPrice::::insert(n2, I96F32::from_num(1)); + + System::set_block_number(imm + 10); // still immune for n2 + assert_eq!(SubtensorModule::get_network_to_prune(), Some(n1)); + }); +} + +#[test] +fn prune_tie_on_price_earlier_registration_wins() { + new_test_ext(0).execute_with(|| { + // n1 registered first + let n1 = add_dynamic_network(&U256::from(66), &U256::from(55)); + + // advance 1 block, then register n2 (later timestamp) + System::set_block_number(1); + let n2 = add_dynamic_network(&U256::from(88), &U256::from(77)); + + // push past immunity for both + let imm = SubtensorModule::get_network_immunity_period(); + System::set_block_number(imm + 20); + + // identical prices → tie; earlier (n1) must be chosen + SubnetMovingPrice::::insert(n1, I96F32::from_num(7)); + SubnetMovingPrice::::insert(n2, I96F32::from_num(7)); + + assert_eq!(SubtensorModule::get_network_to_prune(), Some(n1)); + }); +} + +#[test] +fn prune_selection_complex_state_exhaustive() { + new_test_ext(0).execute_with(|| { + let imm = SubtensorModule::get_network_immunity_period(); + + // --------------------------------------------------------------------- + // Build a rich topology of networks with controlled registration times. + // --------------------------------------------------------------------- + // n1 + n2 in the same block (equal timestamp) to test "tie + same time". + System::set_block_number(0); + let n1 = add_dynamic_network(&U256::from(101), &U256::from(201)); + let n2 = add_dynamic_network(&U256::from(102), &U256::from(202)); // same registered_at as n1 + + // Later registrations (strictly greater timestamp than n1/n2) + System::set_block_number(1); + let n3 = add_dynamic_network(&U256::from(103), &U256::from(203)); + + System::set_block_number(2); + let n4 = add_dynamic_network(&U256::from(104), &U256::from(204)); + + // Create *immune* networks that will remain ineligible initially, + // even if their price is the lowest. + System::set_block_number(imm + 5); + let n5 = add_dynamic_network(&U256::from(105), &U256::from(205)); // immune at first + + System::set_block_number(imm + 6); + let n6 = add_dynamic_network(&U256::from(106), &U256::from(206)); // immune at first + + // (Root is ignored by the selector.) + let root = NetUid::ROOT; + + // --------------------------------------------------------------------- + // Drive pruning via the EMA/moving price used by `get_network_to_prune()`. + // We set the moving prices directly to create deterministic selections. + // + // Intended prices: + // n1: 25, n2: 25, n3: 100, n4: 1, n5: 0 (immune initially), n6: 0 (immune initially) + // --------------------------------------------------------------------- + SubnetMovingPrice::::insert(n1, I96F32::from_num(25)); + SubnetMovingPrice::::insert(n2, I96F32::from_num(25)); + SubnetMovingPrice::::insert(n3, I96F32::from_num(100)); + SubnetMovingPrice::::insert(n4, I96F32::from_num(1)); + SubnetMovingPrice::::insert(n5, I96F32::from_num(0)); + SubnetMovingPrice::::insert(n6, I96F32::from_num(0)); + + // --------------------------------------------------------------------- + // Phase A: Only n1..n4 are mature → lowest price (n4=1) should win. + // --------------------------------------------------------------------- + System::set_block_number(imm + 10); + assert_eq!( + SubtensorModule::get_network_to_prune(), + Some(n4), + "Among mature nets (n1..n4), n4 has price=1 (lowest) and should be chosen." + ); + + // --------------------------------------------------------------------- + // Phase B: Tie on price with *same registration time* (n1 vs n2). + // Raise n4's price to 25 so {n1=25, n2=25, n3=100, n4=25}. + // n1 and n2 share the *same registered_at*. The tie should keep the + // first encountered (stable iteration by key order) → n1. + // --------------------------------------------------------------------- + SubnetMovingPrice::::insert(n4, I96F32::from_num(25)); // n4 now 25 + assert_eq!( + SubtensorModule::get_network_to_prune(), + Some(n1), + "Tie on price with equal timestamps (n1,n2) → first encountered (n1) should persist." + ); + + // --------------------------------------------------------------------- + // Phase C: Tie on price with *different registration times*. + // Make n3 price=25 as well. Now n1,n2,n3,n4 all have price=25. + // Earliest registration among them is n1 (block 0). + // --------------------------------------------------------------------- + SubnetMovingPrice::::insert(n3, I96F32::from_num(25)); + assert_eq!( + SubtensorModule::get_network_to_prune(), + Some(n1), + "Tie on price across multiple nets → earliest registration (n1) wins." + ); + + // --------------------------------------------------------------------- + // Phase D: Immune networks ignored even if strictly cheaper (0). + // n5 and n6 price=0 but still immune at (imm + 10). Ensure they are + // ignored and selection remains n1. + // --------------------------------------------------------------------- + let now = System::block_number(); + assert!( + now < NetworkRegisteredAt::::get(n5) + imm, + "n5 is immune at current block" + ); + assert!( + now < NetworkRegisteredAt::::get(n6) + imm, + "n6 is immune at current block" + ); + assert_eq!( + SubtensorModule::get_network_to_prune(), + Some(n1), + "Immune nets (n5,n6) must be ignored despite lower price." + ); + + // --------------------------------------------------------------------- + // Phase E: If *all* networks are immune → return None. + // Move clock back before any network's immunity expires. + // --------------------------------------------------------------------- + System::set_block_number(0); + assert_eq!( + SubtensorModule::get_network_to_prune(), + None, + "With all networks immune, there is no prunable candidate." + ); + + // --------------------------------------------------------------------- + // Phase F: Advance beyond immunity for n5 & n6. + // Both n5 and n6 now eligible with price=0 (lowest). + // Tie on price; earlier registration between n5 and n6 is n5. + // --------------------------------------------------------------------- + System::set_block_number(2 * imm + 10); + assert!( + System::block_number() >= NetworkRegisteredAt::::get(n5) + imm, + "n5 has matured" + ); + assert!( + System::block_number() >= NetworkRegisteredAt::::get(n6) + imm, + "n6 has matured" + ); + assert_eq!( + SubtensorModule::get_network_to_prune(), + Some(n5), + "After immunity, n5 (price=0) should win; tie with n6 broken by earlier registration." + ); + + // --------------------------------------------------------------------- + // Phase G: Create *sparse* netuids and ensure selection is stable. + // Remove n5; now n6 (price=0) should be selected. + // This validates robustness to holes / non-contiguous netuids. + // --------------------------------------------------------------------- + SubtensorModule::do_dissolve_network(n5).expect("Expected not to panic"); + assert_eq!( + SubtensorModule::get_network_to_prune(), + Some(n6), + "After removing n5, next-lowest (n6=0) should be chosen even with sparse netuids." + ); + + // --------------------------------------------------------------------- + // Phase H: Dynamic price changes. + // Make n6 expensive (price 100); make n3 cheapest (price 1). + // --------------------------------------------------------------------- + SubnetMovingPrice::::insert(n6, I96F32::from_num(100)); + SubnetMovingPrice::::insert(n3, I96F32::from_num(1)); + assert_eq!( + SubtensorModule::get_network_to_prune(), + Some(n3), + "Dynamic changes: n3 set to price=1 (lowest among eligibles) → should be pruned." + ); + + // --------------------------------------------------------------------- + // Phase I: Tie again (n2 vs n3) but earlier registration must win. + // Give n2 the same price as n3; n2 registered at block 0, n3 at block 1. + // n2 should be chosen. + // --------------------------------------------------------------------- + SubnetMovingPrice::::insert(n2, I96F32::from_num(1)); + assert_eq!( + SubtensorModule::get_network_to_prune(), + Some(n2), + "Tie on price across n2 (earlier reg) and n3 → n2 wins by timestamp." + ); + + // --------------------------------------------------------------------- + // (Extra) Mark n2 as 'not added' to assert we honor the `added` flag, + // then restore it to avoid side-effects on subsequent tests. + // --------------------------------------------------------------------- + NetworksAdded::::insert(n2, false); + assert_ne!( + SubtensorModule::get_network_to_prune(), + Some(n2), + "`added=false` must exclude n2 from consideration." + ); + NetworksAdded::::insert(n2, true); + + // Root is always ignored even if cheapest (get_moving_alpha_price returns 1 for ROOT). + assert_ne!( + SubtensorModule::get_network_to_prune(), + Some(root), + "ROOT must never be selected for pruning." + ); + }); +} + +#[test] +fn register_network_prunes_and_recycles_netuid() { + new_test_ext(0).execute_with(|| { + SubnetLimit::::put(2u16); + + let n1_cold = U256::from(21); + let n1_hot = U256::from(22); + let n1 = add_dynamic_network(&n1_hot, &n1_cold); + + let n2_cold = U256::from(23); + let n2_hot = U256::from(24); + let n2 = add_dynamic_network(&n2_hot, &n2_cold); + + let imm = SubtensorModule::get_network_immunity_period(); + System::set_block_number(imm + 100); + + Emission::::insert(n1, vec![AlphaCurrency::from(1)]); + Emission::::insert(n2, vec![AlphaCurrency::from(1_000)]); + + let new_cold = U256::from(30); + let new_hot = U256::from(31); + let needed: u64 = SubtensorModule::get_network_lock_cost().into(); + SubtensorModule::add_balance_to_coldkey_account(&new_cold, needed.saturating_mul(10)); + + assert_ok!(SubtensorModule::do_register_network( + RuntimeOrigin::signed(new_cold), + &new_hot, + 1, + None, + )); + + assert_eq!(TotalNetworks::::get(), 2); + assert_eq!(SubnetOwner::::get(n1), new_cold); + assert_eq!(SubnetOwnerHotkey::::get(n1), new_hot); + assert_eq!(SubnetOwner::::get(n2), n2_cold); + }); +} + +#[test] +fn register_network_fails_before_prune_keeps_existing() { + new_test_ext(0).execute_with(|| { + SubnetLimit::::put(1u16); + + let n_cold = U256::from(41); + let n_hot = U256::from(42); + let net = add_dynamic_network(&n_hot, &n_cold); + + let imm = SubtensorModule::get_network_immunity_period(); + System::set_block_number(imm + 50); + Emission::::insert(net, vec![AlphaCurrency::from(10)]); + + let caller_cold = U256::from(50); + let caller_hot = U256::from(51); + + assert_err!( + SubtensorModule::do_register_network( + RuntimeOrigin::signed(caller_cold), + &caller_hot, + 1, + None, + ), + Error::::CannotAffordLockCost + ); + + assert!(SubtensorModule::if_subnet_exist(net)); + assert_eq!(TotalNetworks::::get(), 1); + }); +} + +#[test] +fn test_migrate_network_immunity_period() { + new_test_ext(0).execute_with(|| { + // -------------------------------------------------------------------- + // ‼️ PRE-CONDITIONS + // -------------------------------------------------------------------- + assert_ne!(NetworkImmunityPeriod::::get(), 864_000); + assert!( + !HasMigrationRun::::get(b"migrate_network_immunity_period".to_vec()), + "HasMigrationRun should be false before migration" + ); + + // -------------------------------------------------------------------- + // ▶️ RUN MIGRATION + // -------------------------------------------------------------------- + let weight = migrate_network_immunity_period::migrate_network_immunity_period::(); + + // -------------------------------------------------------------------- + // ✅ POST-CONDITIONS + // -------------------------------------------------------------------- + assert_eq!( + NetworkImmunityPeriod::::get(), + 864_000, + "NetworkImmunityPeriod should now be 864_000" + ); + + assert!( + HasMigrationRun::::get(b"migrate_network_immunity_period".to_vec()), + "HasMigrationRun should be true after migration" + ); + + assert!(weight != Weight::zero(), "migration weight should be > 0"); + }); +} + // #[test] // fn test_schedule_dissolve_network_execution() { // new_test_ext(1).execute_with(|| { @@ -349,3 +1595,454 @@ fn test_tempo_greater_than_weight_set_rate_limit() { assert!(tempo as u64 >= weights_set_rate_limit); }) } + +#[allow(clippy::indexing_slicing)] +#[test] +fn massive_dissolve_refund_and_reregistration_flow_is_lossless_and_cleans_state() { + new_test_ext(0).execute_with(|| { + // ──────────────────────────────────────────────────────────────────── + // 0) Constants and helpers (distinct hotkeys & coldkeys) + // ──────────────────────────────────────────────────────────────────── + const NUM_NETS: usize = 4; + + // Six LP coldkeys + let cold_lps: [U256; 6] = [ + U256::from(3001), + U256::from(3002), + U256::from(3003), + U256::from(3004), + U256::from(3005), + U256::from(3006), + ]; + + // For each coldkey, define two DISTINCT hotkeys it owns. + let mut cold_to_hots: BTreeMap = BTreeMap::new(); + for &c in cold_lps.iter() { + let h1 = U256::from(c.low_u64().saturating_add(100_000)); + let h2 = U256::from(c.low_u64().saturating_add(200_000)); + cold_to_hots.insert(c, [h1, h2]); + } + + // Distinct τ pot sizes per net. + let pots: [u64; NUM_NETS] = [12_345, 23_456, 34_567, 45_678]; + + let lp_sets_per_net: [&[U256]; NUM_NETS] = [ + &cold_lps[0..4], // net0: A,B,C,D + &cold_lps[2..6], // net1: C,D,E,F + &cold_lps[0..6], // net2: A..F + &cold_lps[1..5], // net3: B,C,D,E + ]; + + // Multiple bands/sizes → many positions per cold across nets, using mixed hotkeys. + let bands: [i32; 3] = [5, 13, 30]; + let liqs: [u64; 3] = [400_000, 700_000, 1_100_000]; + + // Helper: add a V3 position via a (hot, cold) pair. + let add_pos = |net: NetUid, hot: U256, cold: U256, band: i32, liq: u64| { + let ct = pallet_subtensor_swap::CurrentTick::::get(net); + let lo = ct.saturating_sub(band); + let hi = ct.saturating_add(band); + assert_ok!(pallet_subtensor_swap::Pallet::::add_liquidity( + RuntimeOrigin::signed(cold), + hot, + net, + lo, + hi, + liq + )); + }; + + // ──────────────────────────────────────────────────────────────────── + // 1) Create many subnets, enable V3, fix price at tick=0 (sqrt≈1) + // ──────────────────────────────────────────────────────────────────── + let mut nets: Vec = Vec::new(); + for i in 0..NUM_NETS { + let owner_hot = U256::from(10_000 + (i as u64)); + let owner_cold = U256::from(20_000 + (i as u64)); + let net = add_dynamic_network(&owner_hot, &owner_cold); + SubtensorModule::set_max_registrations_per_block(net, 1_000u16); + SubtensorModule::set_target_registrations_per_interval(net, 1_000u16); + Emission::::insert(net, Vec::::new()); + SubtensorModule::set_subnet_locked_balance(net, TaoCurrency::from(0)); + + assert_ok!( + pallet_subtensor_swap::Pallet::::toggle_user_liquidity( + RuntimeOrigin::root(), + net, + true + ) + ); + + // Price/tick pinned so LP math stays stable (sqrt(1)). + let ct0 = pallet_subtensor_swap::tick::TickIndex::new_unchecked(0); + let sqrt1 = ct0.try_to_sqrt_price().expect("sqrt(1) price"); + pallet_subtensor_swap::CurrentTick::::set(net, ct0); + pallet_subtensor_swap::AlphaSqrtPrice::::set(net, sqrt1); + + nets.push(net); + } + + // Map net → index for quick lookups. + let mut net_index: BTreeMap = BTreeMap::new(); + for (i, &n) in nets.iter().enumerate() { + net_index.insert(n, i); + } + + // ──────────────────────────────────────────────────────────────────── + // 2) Pre-create a handful of small (hot, cold) pairs so accounts exist + // ──────────────────────────────────────────────────────────────────── + for id in 0u64..10 { + let cold_acc = U256::from(1_000_000 + id); + let hot_acc = U256::from(2_000_000 + id); + for &net in nets.iter() { + register_ok_neuron(net, hot_acc, cold_acc, 100_000 + id); + } + } + + // ──────────────────────────────────────────────────────────────────── + // 3) LPs per net: register each (hot, cold), massive τ prefund, and stake + // ──────────────────────────────────────────────────────────────────── + for &cold in cold_lps.iter() { + SubtensorModule::add_balance_to_coldkey_account(&cold, u64::MAX); + } + + // τ balances before LP adds (after staking): + let mut tao_before: BTreeMap = BTreeMap::new(); + + // Ordered α snapshot per net at **pair granularity** (pre‑LP): + let mut alpha_pairs_per_net: BTreeMap> = BTreeMap::new(); + + // Register both hotkeys for each participating cold on each net and stake τ→α. + for (ni, &net) in nets.iter().enumerate() { + let participants = lp_sets_per_net[ni]; + for &cold in participants.iter() { + let [hot1, hot2] = cold_to_hots[&cold]; + + // Ensure (hot, cold) neurons exist on this net. + register_ok_neuron( + net, + hot1, + cold, + (ni as u64) * 10_000 + (hot1.low_u64() % 10_000), + ); + register_ok_neuron( + net, + hot2, + cold, + (ni as u64) * 10_000 + (hot2.low_u64() % 10_000) + 1, + ); + + // Stake τ (split across the two hotkeys). + let base: u64 = + 5_000_000 + ((ni as u64) * 1_000_000) + ((cold.low_u64() % 10) * 250_000); + let stake1: u64 = base.saturating_mul(3) / 5; // 60% + let stake2: u64 = base.saturating_sub(stake1); // 40% + + assert_ok!(SubtensorModule::do_add_stake( + RuntimeOrigin::signed(cold), + hot1, + net, + stake1.into() + )); + assert_ok!(SubtensorModule::do_add_stake( + RuntimeOrigin::signed(cold), + hot2, + net, + stake2.into() + )); + } + } + + // Record τ balances now (post‑stake, pre‑LP). + for &cold in cold_lps.iter() { + tao_before.insert(cold, SubtensorModule::get_coldkey_balance(&cold)); + } + + // Capture **pair‑level** α snapshot per net (pre‑LP). + for ((hot, cold, net), amt) in Alpha::::iter() { + if let Some(&ni) = net_index.get(&net) { + if lp_sets_per_net[ni].contains(&cold) { + let a: u128 = amt.saturating_to_num(); + if a > 0 { + alpha_pairs_per_net + .entry(net) + .or_default() + .push(((hot, cold), a)); + } + } + } + } + + // ──────────────────────────────────────────────────────────────────── + // 4) Add many V3 positions per cold across nets, alternating hotkeys + // ──────────────────────────────────────────────────────────────────── + for (ni, &net) in nets.iter().enumerate() { + let participants = lp_sets_per_net[ni]; + for (pi, &cold) in participants.iter().enumerate() { + let [hot1, hot2] = cold_to_hots[&cold]; + let hots = [hot1, hot2]; + for k in 0..3 { + let band = bands[(pi + k) % bands.len()]; + let liq = liqs[(ni + k) % liqs.len()]; + let hot = hots[k % hots.len()]; + add_pos(net, hot, cold, band, liq); + } + } + } + + // Snapshot τ balances AFTER LP adds (to measure actual principal debit). + let mut tao_after_adds: BTreeMap = BTreeMap::new(); + for &cold in cold_lps.iter() { + tao_after_adds.insert(cold, SubtensorModule::get_coldkey_balance(&cold)); + } + + // ──────────────────────────────────────────────────────────────────── + // 5) Compute Hamilton-apportionment BASE shares per cold and total leftover + // from the **pair-level** pre‑LP α snapshot; also count pairs per cold. + // ──────────────────────────────────────────────────────────────────── + let mut base_share_cold: BTreeMap = + cold_lps.iter().copied().map(|c| (c, 0_u64)).collect(); + let mut pair_count_cold: BTreeMap = + cold_lps.iter().copied().map(|c| (c, 0_u32)).collect(); + + let mut leftover_total: u64 = 0; + + for (ni, &net) in nets.iter().enumerate() { + let pot = pots[ni]; + let pairs = alpha_pairs_per_net.get(&net).cloned().unwrap_or_default(); + if pot == 0 || pairs.is_empty() { + continue; + } + let total_alpha: u128 = pairs.iter().map(|(_, a)| *a).sum(); + if total_alpha == 0 { + continue; + } + + let mut base_sum_net: u64 = 0; + for ((_, cold), a) in pairs.iter().copied() { + // quota = a * pot / total_alpha + let prod: u128 = a.saturating_mul(pot as u128); + let base: u64 = (prod / total_alpha) as u64; + base_sum_net = base_sum_net.saturating_add(base); + *base_share_cold.entry(cold).or_default() = + base_share_cold[&cold].saturating_add(base); + *pair_count_cold.entry(cold).or_default() += 1; + } + let leftover_net = pot.saturating_sub(base_sum_net); + leftover_total = leftover_total.saturating_add(leftover_net); + } + + // ──────────────────────────────────────────────────────────────────── + // 6) Seed τ pots and dissolve *all* networks (liquidates LPs + refunds) + // ──────────────────────────────────────────────────────────────────── + for (ni, &net) in nets.iter().enumerate() { + SubnetTAO::::insert(net, TaoCurrency::from(pots[ni])); + } + for &net in nets.iter() { + assert_ok!(SubtensorModule::do_dissolve_network(net)); + } + + // ──────────────────────────────────────────────────────────────────── + // 7) Assertions: τ balances, α gone, nets removed, swap state clean + // (Hamilton invariants enforced at cold-level without relying on tie-break) + // ──────────────────────────────────────────────────────────────────── + // Collect actual pot credits per cold (principal cancels out against adds when comparing before→after). + let mut actual_pot_cold: BTreeMap = + cold_lps.iter().copied().map(|c| (c, 0_u64)).collect(); + for &cold in cold_lps.iter() { + let before = tao_before[&cold]; + let after = SubtensorModule::get_coldkey_balance(&cold); + actual_pot_cold.insert(cold, after.saturating_sub(before)); + } + + // (a) Sum of actual pot credits equals total pots. + let total_actual: u64 = actual_pot_cold.values().copied().sum(); + let total_pots: u64 = pots.iter().copied().sum(); + assert_eq!( + total_actual, total_pots, + "total τ pot credited across colds must equal sum of pots" + ); + + // (b) Each cold’s pot is within Hamilton bounds: base ≤ actual ≤ base + #pairs. + let mut extra_accum: u64 = 0; + for &cold in cold_lps.iter() { + let base = *base_share_cold.get(&cold).unwrap_or(&0); + let pairs = *pair_count_cold.get(&cold).unwrap_or(&0) as u64; + let actual = *actual_pot_cold.get(&cold).unwrap_or(&0); + + assert!( + actual >= base, + "cold {cold:?} actual pot {actual} is below base {base}" + ); + assert!( + actual <= base.saturating_add(pairs), + "cold {cold:?} actual pot {actual} exceeds base + pairs ({base} + {pairs})" + ); + + extra_accum = extra_accum.saturating_add(actual.saturating_sub(base)); + } + + // (c) The total “extra beyond base” equals the computed leftover_total across nets. + assert_eq!( + extra_accum, leftover_total, + "sum of extras beyond base must equal total leftover" + ); + + // (d) τ principal was fully refunded (compare after_adds → after). + for &cold in cold_lps.iter() { + let before = tao_before[&cold]; + let mid = tao_after_adds[&cold]; + let after = SubtensorModule::get_coldkey_balance(&cold); + let principal_actual = before.saturating_sub(mid); + let actual_pot = after.saturating_sub(before); + assert_eq!( + after.saturating_sub(mid), + principal_actual.saturating_add(actual_pot), + "cold {cold:?} τ balance incorrect vs 'after_adds'" + ); + } + + // For each dissolved net, check α ledgers gone, network removed, and swap state clean. + for &net in nets.iter() { + assert!( + Alpha::::iter().all(|((_h, _c, n), _)| n != net), + "alpha ledger not fully cleared for net {net:?}" + ); + assert!( + !SubtensorModule::if_subnet_exist(net), + "subnet {net:?} still exists" + ); + assert!( + pallet_subtensor_swap::Ticks::::iter_prefix(net) + .next() + .is_none(), + "ticks not cleared for net {net:?}" + ); + assert!( + !pallet_subtensor_swap::Positions::::iter() + .any(|((n, _owner, _pid), _)| n == net), + "swap positions not fully cleared for net {net:?}" + ); + assert_eq!( + pallet_subtensor_swap::FeeGlobalTao::::get(net).saturating_to_num::(), + 0, + "FeeGlobalTao nonzero for net {net:?}" + ); + assert_eq!( + pallet_subtensor_swap::FeeGlobalAlpha::::get(net).saturating_to_num::(), + 0, + "FeeGlobalAlpha nonzero for net {net:?}" + ); + assert_eq!( + pallet_subtensor_swap::CurrentLiquidity::::get(net), + 0, + "CurrentLiquidity not zero for net {net:?}" + ); + assert!( + !pallet_subtensor_swap::SwapV3Initialized::::get(net), + "SwapV3Initialized still set" + ); + assert!( + !pallet_subtensor_swap::EnabledUserLiquidity::::get(net), + "EnabledUserLiquidity still set" + ); + assert!( + pallet_subtensor_swap::TickIndexBitmapWords::::iter_prefix((net,)) + .next() + .is_none(), + "TickIndexBitmapWords not cleared for net {net:?}" + ); + } + + // ──────────────────────────────────────────────────────────────────── + // 8) Re-register a fresh subnet and re‑stake using the pallet’s min rule + // Assert αΔ equals the sim-swap result for the exact τ staked. + // ──────────────────────────────────────────────────────────────────── + let new_owner_hot = U256::from(99_000); + let new_owner_cold = U256::from(99_001); + let net_new = add_dynamic_network(&new_owner_hot, &new_owner_cold); + SubtensorModule::set_max_registrations_per_block(net_new, 1_000u16); + SubtensorModule::set_target_registrations_per_interval(net_new, 1_000u16); + Emission::::insert(net_new, Vec::::new()); + SubtensorModule::set_subnet_locked_balance(net_new, TaoCurrency::from(0)); + + assert_ok!( + pallet_subtensor_swap::Pallet::::toggle_user_liquidity( + RuntimeOrigin::root(), + net_new, + true + ) + ); + let ct0 = pallet_subtensor_swap::tick::TickIndex::new_unchecked(0); + let sqrt1 = ct0.try_to_sqrt_price().expect("sqrt(1)"); + pallet_subtensor_swap::CurrentTick::::set(net_new, ct0); + pallet_subtensor_swap::AlphaSqrtPrice::::set(net_new, sqrt1); + + // Compute the exact min stake per the pallet rule: DefaultMinStake + fee(DefaultMinStake). + let min_stake_u64: u64 = DefaultMinStake::::get().into(); + let fee_for_min: u64 = pallet_subtensor_swap::Pallet::::sim_swap( + net_new, + subtensor_swap_interface::OrderType::Buy, + min_stake_u64, + ) + .map(|r| r.fee_paid) + .unwrap_or_else(|_e| { + as subtensor_swap_interface::SwapHandler< + ::AccountId, + >>::approx_fee_amount(net_new, min_stake_u64) + }); + let min_amount_required: u64 = min_stake_u64.saturating_add(fee_for_min); + + // Re‑stake from three coldkeys; choose a specific DISTINCT hotkey per cold. + for &cold in &cold_lps[0..3] { + let [hot1, _hot2] = cold_to_hots[&cold]; + register_ok_neuron(net_new, hot1, cold, 7777); + + let before_tao = SubtensorModule::get_coldkey_balance(&cold); + let a_prev: u64 = Alpha::::get((hot1, cold, net_new)).saturating_to_num(); + + // Expected α for this exact τ, using the same sim path as the pallet. + let expected_alpha_out: u64 = pallet_subtensor_swap::Pallet::::sim_swap( + net_new, + subtensor_swap_interface::OrderType::Buy, + min_amount_required, + ) + .map(|r| r.amount_paid_out) + .expect("sim_swap must succeed for fresh net and min amount"); + + assert_ok!(SubtensorModule::do_add_stake( + RuntimeOrigin::signed(cold), + hot1, + net_new, + min_amount_required.into() + )); + + let after_tao = SubtensorModule::get_coldkey_balance(&cold); + let a_new: u64 = Alpha::::get((hot1, cold, net_new)).saturating_to_num(); + let a_delta = a_new.saturating_sub(a_prev); + + // τ decreased by exactly the amount we sent. + assert_eq!( + after_tao, + before_tao.saturating_sub(min_amount_required), + "τ did not decrease by the min required restake amount for cold {cold:?}" + ); + + // α minted equals the simulated swap’s net out for that same τ. + assert_eq!( + a_delta, expected_alpha_out, + "α minted mismatch for cold {cold:?} (hot {hot1:?}) on new net (αΔ {a_delta}, expected {expected_alpha_out})" + ); + } + + // Ensure V3 still functional on new net: add a small position for the first cold using its hot1 + let who_cold = cold_lps[0]; + let [who_hot, _] = cold_to_hots[&who_cold]; + add_pos(net_new, who_hot, who_cold, 8, 123_456); + assert!( + pallet_subtensor_swap::Positions::::iter() + .any(|((n, owner, _pid), _)| n == net_new && owner == who_cold), + "new position not recorded on the re-registered net" + ); + }); +} diff --git a/pallets/subtensor/src/tests/subnet.rs b/pallets/subtensor/src/tests/subnet.rs index b60f3ffa41..ba5640af3d 100644 --- a/pallets/subtensor/src/tests/subnet.rs +++ b/pallets/subtensor/src/tests/subnet.rs @@ -231,6 +231,7 @@ fn test_register_network_min_burn_at_default() { #[test] fn test_register_network_use_symbol_for_subnet_if_available() { new_test_ext(1).execute_with(|| { + SubtensorModule::set_max_subnets(SYMBOLS.len() as u16); for i in 0..(SYMBOLS.len() - 1) { let coldkey = U256::from(1_000_000 + i); let hotkey = U256::from(2_000_000 + i); @@ -317,6 +318,7 @@ fn test_register_network_use_next_available_symbol_if_symbol_for_subnet_is_taken fn test_register_network_use_default_symbol_if_all_symbols_are_taken() { new_test_ext(1).execute_with(|| { // Register networks until we have exhausted all symbols + SubtensorModule::set_max_subnets(SYMBOLS.len() as u16); for i in 0..(SYMBOLS.len() - 1) { let coldkey = U256::from(1_000_000 + i); let hotkey = U256::from(2_000_000 + i); @@ -725,11 +727,6 @@ fn test_user_liquidity_access_control() { // add network let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); - // Initially should be disabled - assert!(!pallet_subtensor_swap::EnabledUserLiquidity::::get( - NetUid::from(netuid) - )); - // Not owner, not root: should fail assert_noop!( Swap::toggle_user_liquidity(RuntimeOrigin::signed(not_owner), netuid, true), diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index f8f4fe57ef..02752eda72 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -939,4 +939,19 @@ impl Pallet { ImmuneOwnerUidsLimit::::insert(netuid, limit); Ok(()) } + + /// Fetches the max number of subnet + /// + /// # Returns: + /// * 'u16': The max number of subnet + /// + pub fn get_max_subnets() -> u16 { + SubnetLimit::::get() + } + + /// Sets the max number of subnet + pub fn set_max_subnets(limit: u16) { + SubnetLimit::::put(limit); + Self::deposit_event(Event::SubnetLimitSet(limit)); + } } diff --git a/pallets/swap-interface/src/lib.rs b/pallets/swap-interface/src/lib.rs index a0b39e151f..d247b28d35 100644 --- a/pallets/swap-interface/src/lib.rs +++ b/pallets/swap-interface/src/lib.rs @@ -34,6 +34,8 @@ pub trait SwapHandler { alpha_delta: AlphaCurrency, ); fn is_user_liquidity_enabled(netuid: NetUid) -> bool; + fn dissolve_all_liquidity_providers(netuid: NetUid) -> DispatchResult; + fn toggle_user_liquidity(netuid: NetUid, enabled: bool); } #[derive(Debug, PartialEq)] diff --git a/pallets/swap/src/mock.rs b/pallets/swap/src/mock.rs index 7a07cc7007..40aac6d796 100644 --- a/pallets/swap/src/mock.rs +++ b/pallets/swap/src/mock.rs @@ -11,7 +11,7 @@ use frame_support::{ use frame_system::{self as system}; use sp_core::H256; use sp_runtime::{ - BuildStorage, + BuildStorage, Vec, traits::{BlakeTwo256, IdentityLookup}, }; use subtensor_runtime_common::{AlphaCurrency, BalanceOps, NetUid, SubnetInfo, TaoCurrency}; diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index 69bf3eacbb..c0a109bfb5 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -5,7 +5,7 @@ use frame_support::storage::{TransactionOutcome, transactional}; use frame_support::{ensure, pallet_prelude::DispatchError, traits::Get}; use safe_math::*; use sp_arithmetic::helpers_128bit; -use sp_runtime::traits::AccountIdConversion; +use sp_runtime::{DispatchResult, traits::AccountIdConversion}; use substrate_fixed::types::{I64F64, U64F64, U96F32}; use subtensor_runtime_common::{ AlphaCurrency, BalanceOps, Currency, NetUid, SubnetInfo, TaoCurrency, @@ -1212,6 +1212,104 @@ impl Pallet { pub fn protocol_account_id() -> T::AccountId { T::ProtocolId::get().into_account_truncating() } + + /// Dissolve all LPs and clean state. + pub fn do_dissolve_all_liquidity_providers(netuid: NetUid) -> DispatchResult { + if SwapV3Initialized::::get(netuid) { + // 1) Snapshot (owner, position_id). + struct CloseItem { + owner: A, + pos_id: PositionId, + } + let mut to_close: sp_std::vec::Vec> = sp_std::vec::Vec::new(); + for ((owner, pos_id), _pos) in Positions::::iter_prefix((netuid,)) { + to_close.push(CloseItem { owner, pos_id }); + } + + let protocol_account = Self::protocol_account_id(); + + // Non‑protocol first + to_close + .sort_by(|a, b| (a.owner == protocol_account).cmp(&(b.owner == protocol_account))); + + for CloseItem { owner, pos_id } in to_close.into_iter() { + match Self::do_remove_liquidity(netuid, &owner, pos_id) { + Ok(rm) => { + if rm.tao > TaoCurrency::ZERO { + T::BalanceOps::increase_balance(&owner, rm.tao); + } + if owner != protocol_account { + T::BalanceOps::decrease_provided_tao_reserve(netuid, rm.tao); + let alpha_burn = rm.alpha.saturating_add(rm.fee_alpha); + if alpha_burn > AlphaCurrency::ZERO { + T::BalanceOps::decrease_provided_alpha_reserve(netuid, alpha_burn); + } + } + } + Err(e) => { + log::debug!( + "dissolve_all_lp: force-closing failed position: netuid={netuid:?}, owner={owner:?}, pos_id={pos_id:?}, err={e:?}" + ); + continue; + } + } + } + + // 3) Clear active tick index entries, then all swap state. + let active_ticks: sp_std::vec::Vec = + Ticks::::iter_prefix(netuid).map(|(ti, _)| ti).collect(); + for ti in active_ticks { + ActiveTickIndexManager::::remove(netuid, ti); + } + + let _ = Positions::::clear_prefix((netuid,), u32::MAX, None); + let _ = Ticks::::clear_prefix(netuid, u32::MAX, None); + + FeeGlobalTao::::remove(netuid); + FeeGlobalAlpha::::remove(netuid); + CurrentLiquidity::::remove(netuid); + CurrentTick::::remove(netuid); + AlphaSqrtPrice::::remove(netuid); + SwapV3Initialized::::remove(netuid); + + let _ = TickIndexBitmapWords::::clear_prefix((netuid,), u32::MAX, None); + FeeRate::::remove(netuid); + EnabledUserLiquidity::::remove(netuid); + + log::debug!( + "dissolve_all_liquidity_providers: netuid={netuid:?}, mode=V3, positions closed; τ principal refunded; α burned; state cleared" + ); + + return Ok(()); + } + + // V2 / non‑V3: ensure V3 residues are cleared (safe no‑ops). + let _ = Positions::::clear_prefix((netuid,), u32::MAX, None); + let active_ticks: sp_std::vec::Vec = + Ticks::::iter_prefix(netuid).map(|(ti, _)| ti).collect(); + for ti in active_ticks { + ActiveTickIndexManager::::remove(netuid, ti); + } + let _ = Ticks::::clear_prefix(netuid, u32::MAX, None); + + FeeGlobalTao::::remove(netuid); + FeeGlobalAlpha::::remove(netuid); + CurrentLiquidity::::remove(netuid); + CurrentTick::::remove(netuid); + AlphaSqrtPrice::::remove(netuid); + SwapV3Initialized::::remove(netuid); + + let _ = TickIndexBitmapWords::::clear_prefix((netuid,), u32::MAX, None); + + FeeRate::::remove(netuid); + EnabledUserLiquidity::::remove(netuid); + + log::debug!( + "dissolve_all_liquidity_providers: netuid={netuid:?}, mode=V2-or-nonV3, state_cleared" + ); + + Ok(()) + } } impl SwapHandler for Pallet { @@ -1304,6 +1402,12 @@ impl SwapHandler for Pallet { fn is_user_liquidity_enabled(netuid: NetUid) -> bool { EnabledUserLiquidity::::get(netuid) } + fn dissolve_all_liquidity_providers(netuid: NetUid) -> DispatchResult { + Self::do_dissolve_all_liquidity_providers(netuid) + } + fn toggle_user_liquidity(netuid: NetUid, enabled: bool) { + EnabledUserLiquidity::::insert(netuid, enabled) + } } #[derive(Debug, PartialEq)] diff --git a/pallets/swap/src/pallet/tests.rs b/pallets/swap/src/pallet/tests.rs index 396bd656be..153e13a822 100644 --- a/pallets/swap/src/pallet/tests.rs +++ b/pallets/swap/src/pallet/tests.rs @@ -1981,3 +1981,558 @@ fn test_swap_subtoken_disabled() { ); }); } + +/// V3 path: protocol + user positions exist, fees accrued, everything must be removed. +#[test] +fn test_liquidate_v3_removes_positions_ticks_and_state() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + + // Initialize V3 (creates protocol position, ticks, price, liquidity) + assert_ok!(Pallet::::maybe_initialize_v3(netuid)); + assert!(SwapV3Initialized::::get(netuid)); + + // Enable user LP (mock usually enables for 0..=100, but be explicit and consistent) + assert_ok!(Swap::toggle_user_liquidity( + RuntimeOrigin::root(), + netuid.into(), + true + )); + + // Add a user position across the full range to ensure ticks/bitmap are populated. + let min_price = tick_to_price(TickIndex::MIN); + let max_price = tick_to_price(TickIndex::MAX); + let tick_low = price_to_tick(min_price); + let tick_high = price_to_tick(max_price); + let liquidity = 2_000_000_000_u64; + + let (_pos_id, _tao, _alpha) = Pallet::::do_add_liquidity( + netuid, + &OK_COLDKEY_ACCOUNT_ID, + &OK_HOTKEY_ACCOUNT_ID, + tick_low, + tick_high, + liquidity, + ) + .expect("add liquidity"); + + // Accrue some global fees so we can verify fee storage is cleared later. + let sqrt_limit_price = SqrtPrice::from_num(1_000_000.0); + assert_ok!(Pallet::::do_swap( + netuid, + OrderType::Buy, + 1_000_000, + sqrt_limit_price, + false, + false + )); + + // Sanity: protocol & user positions exist, ticks exist, liquidity > 0 + let protocol_id = Pallet::::protocol_account_id(); + let prot_positions = + Positions::::iter_prefix_values((netuid, protocol_id)).collect::>(); + assert!(!prot_positions.is_empty()); + + let user_positions = Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) + .collect::>(); + assert_eq!(user_positions.len(), 1); + + assert!(Ticks::::get(netuid, TickIndex::MIN).is_some()); + assert!(Ticks::::get(netuid, TickIndex::MAX).is_some()); + assert!(CurrentLiquidity::::get(netuid) > 0); + + // There should be some bitmap words (active ticks) after adding a position. + let had_bitmap_words = TickIndexBitmapWords::::iter_prefix((netuid,)) + .next() + .is_some(); + assert!(had_bitmap_words); + + // ACT: Liquidate & reset swap state + assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); + + // ASSERT: positions cleared (both user and protocol) + assert_eq!( + Pallet::::count_positions(netuid, &OK_COLDKEY_ACCOUNT_ID), + 0 + ); + let prot_positions_after = + Positions::::iter_prefix_values((netuid, protocol_id)).collect::>(); + assert!(prot_positions_after.is_empty()); + let user_positions_after = + Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) + .collect::>(); + assert!(user_positions_after.is_empty()); + + // ASSERT: ticks cleared + assert!(Ticks::::iter_prefix(netuid).next().is_none()); + assert!(Ticks::::get(netuid, TickIndex::MIN).is_none()); + assert!(Ticks::::get(netuid, TickIndex::MAX).is_none()); + + // ASSERT: fee globals cleared + assert!(!FeeGlobalTao::::contains_key(netuid)); + assert!(!FeeGlobalAlpha::::contains_key(netuid)); + + // ASSERT: price/tick/liquidity flags cleared + assert!(!AlphaSqrtPrice::::contains_key(netuid)); + assert!(!CurrentTick::::contains_key(netuid)); + assert!(!CurrentLiquidity::::contains_key(netuid)); + assert!(!SwapV3Initialized::::contains_key(netuid)); + + // ASSERT: active tick bitmap cleared + assert!( + TickIndexBitmapWords::::iter_prefix((netuid,)) + .next() + .is_none() + ); + + // ASSERT: knobs removed on dereg + assert!(!FeeRate::::contains_key(netuid)); + assert!(!EnabledUserLiquidity::::contains_key(netuid)); + }); +} + +/// V3 path with user liquidity disabled at teardown: must still remove all positions and clear state. +#[test] +fn test_liquidate_v3_with_user_liquidity_disabled() { + new_test_ext().execute_with(|| { + // Pick a netuid the mock treats as "disabled" by default (per your comment >100), + // then explicitly walk through enable -> add -> disable -> liquidate. + let netuid = NetUid::from(101); + + assert_ok!(Pallet::::maybe_initialize_v3(netuid)); + assert!(SwapV3Initialized::::get(netuid)); + + // Enable temporarily to add a user position + assert_ok!(Swap::toggle_user_liquidity( + RuntimeOrigin::root(), + netuid.into(), + true + )); + + let min_price = tick_to_price(TickIndex::MIN); + let max_price = tick_to_price(TickIndex::MAX); + let tick_low = price_to_tick(min_price); + let tick_high = price_to_tick(max_price); + let liquidity = 1_000_000_000_u64; + + let (_pos_id, _tao, _alpha) = Pallet::::do_add_liquidity( + netuid, + &OK_COLDKEY_ACCOUNT_ID, + &OK_HOTKEY_ACCOUNT_ID, + tick_low, + tick_high, + liquidity, + ) + .expect("add liquidity"); + + // Disable user LP *before* liquidation to validate that removal ignores this flag. + assert_ok!(Swap::toggle_user_liquidity( + RuntimeOrigin::root(), + netuid.into(), + false + )); + + // ACT + assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); + + // ASSERT: positions & ticks gone, state reset + assert_eq!( + Pallet::::count_positions(netuid, &OK_COLDKEY_ACCOUNT_ID), + 0 + ); + assert!( + Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) + .next() + .is_none() + ); + assert!(Ticks::::iter_prefix(netuid).next().is_none()); + assert!( + TickIndexBitmapWords::::iter_prefix((netuid,)) + .next() + .is_none() + ); + assert!(!SwapV3Initialized::::contains_key(netuid)); + assert!(!AlphaSqrtPrice::::contains_key(netuid)); + assert!(!CurrentTick::::contains_key(netuid)); + assert!(!CurrentLiquidity::::contains_key(netuid)); + assert!(!FeeGlobalTao::::contains_key(netuid)); + assert!(!FeeGlobalAlpha::::contains_key(netuid)); + + // `EnabledUserLiquidity` is removed by liquidation. + assert!(!EnabledUserLiquidity::::contains_key(netuid)); + }); +} + +/// Non‑V3 path: V3 not initialized (no positions); function must still clear any residual storages and succeed. +#[test] +fn test_liquidate_non_v3_uninitialized_ok_and_clears() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(202); + + // Sanity: V3 is not initialized + assert!(!SwapV3Initialized::::get(netuid)); + assert!( + Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) + .next() + .is_none() + ); + + // ACT + assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); + + // ASSERT: Defensive clears leave no residues and do not panic + assert!( + Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) + .next() + .is_none() + ); + assert!(Ticks::::iter_prefix(netuid).next().is_none()); + assert!( + TickIndexBitmapWords::::iter_prefix((netuid,)) + .next() + .is_none() + ); + + // All single-key maps should not have the key after liquidation + assert!(!FeeGlobalTao::::contains_key(netuid)); + assert!(!FeeGlobalAlpha::::contains_key(netuid)); + assert!(!CurrentLiquidity::::contains_key(netuid)); + assert!(!CurrentTick::::contains_key(netuid)); + assert!(!AlphaSqrtPrice::::contains_key(netuid)); + assert!(!SwapV3Initialized::::contains_key(netuid)); + assert!(!FeeRate::::contains_key(netuid)); + assert!(!EnabledUserLiquidity::::contains_key(netuid)); + }); +} + +/// Idempotency: calling liquidation twice is safe (both V3 and non‑V3 flavors). +#[test] +fn test_liquidate_idempotent() { + // V3 flavor + new_test_ext().execute_with(|| { + let netuid = NetUid::from(7); + assert_ok!(Pallet::::maybe_initialize_v3(netuid)); + + // Add a small user position + assert_ok!(Swap::toggle_user_liquidity( + RuntimeOrigin::root(), + netuid.into(), + true + )); + let tick_low = price_to_tick(0.2); + let tick_high = price_to_tick(0.3); + assert_ok!(Pallet::::do_add_liquidity( + netuid, + &OK_COLDKEY_ACCOUNT_ID, + &OK_HOTKEY_ACCOUNT_ID, + tick_low, + tick_high, + 123_456_789 + )); + + // 1st liquidation + assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); + // 2nd liquidation (no state left) — must still succeed + assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); + + // State remains empty + assert!( + Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) + .next() + .is_none() + ); + assert!(Ticks::::iter_prefix(netuid).next().is_none()); + assert!( + TickIndexBitmapWords::::iter_prefix((netuid,)) + .next() + .is_none() + ); + assert!(!SwapV3Initialized::::contains_key(netuid)); + }); + + // Non‑V3 flavor + new_test_ext().execute_with(|| { + let netuid = NetUid::from(8); + + // Never initialize V3 + assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); + assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); + + assert!( + Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) + .next() + .is_none() + ); + assert!(Ticks::::iter_prefix(netuid).next().is_none()); + assert!( + TickIndexBitmapWords::::iter_prefix((netuid,)) + .next() + .is_none() + ); + assert!(!SwapV3Initialized::::contains_key(netuid)); + }); +} + +#[test] +fn liquidate_v3_refunds_user_funds_and_clears_state() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + + // Enable V3 path & initialize price/ticks (also creates a protocol position). + assert_ok!(Pallet::::toggle_user_liquidity( + RuntimeOrigin::root(), + netuid, + true + )); + assert_ok!(Pallet::::maybe_initialize_v3(netuid)); + + // Use distinct cold/hot to demonstrate alpha refund goes to (owner, owner). + let cold = OK_COLDKEY_ACCOUNT_ID; + let hot = OK_HOTKEY_ACCOUNT_ID; + + // Tight in‑range band around current tick. + let ct = CurrentTick::::get(netuid); + let tick_low = ct.saturating_sub(10); + let tick_high = ct.saturating_add(10); + let liquidity: u64 = 1_000_000; + + // Snapshot balances BEFORE. + let tao_before = ::BalanceOps::tao_balance(&cold); + let alpha_before_hot = + ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot); + let alpha_before_owner = + ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); + let alpha_before_total = alpha_before_hot + alpha_before_owner; + + // Create the user position (storage & v3 state only; no balances moved yet). + let (_pos_id, need_tao, need_alpha) = + Pallet::::do_add_liquidity(netuid, &cold, &hot, tick_low, tick_high, liquidity) + .expect("add liquidity"); + + // Mirror extrinsic bookkeeping: withdraw funds & bump provided‑reserve counters. + let tao_taken = ::BalanceOps::decrease_balance(&cold, need_tao.into()) + .expect("decrease TAO"); + let alpha_taken = ::BalanceOps::decrease_stake( + &cold, + &hot, + netuid.into(), + need_alpha.into(), + ) + .expect("decrease ALPHA"); + ::BalanceOps::increase_provided_tao_reserve(netuid.into(), tao_taken); + ::BalanceOps::increase_provided_alpha_reserve(netuid.into(), alpha_taken); + + // Liquidate everything on the subnet. + assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); + + // Expect balances restored to BEFORE snapshots (no swaps ran -> zero fees). + // TAO: we withdrew 'need_tao' above and liquidation refunded it, so we should be back to 'tao_before'. + let tao_after = ::BalanceOps::tao_balance(&cold); + assert_eq!(tao_after, tao_before, "TAO principal must be refunded"); + + // ALPHA: refund is credited to (coldkey=cold, hotkey=cold). Compare totals across both ledgers. + let alpha_after_hot = + ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot); + let alpha_after_owner = + ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); + let alpha_after_total = alpha_after_hot + alpha_after_owner; + assert_eq!( + alpha_after_total, alpha_before_total, + "ALPHA principal must be refunded to the account (may be credited to (owner, owner))" + ); + + // User position(s) are gone and all V3 state cleared. + assert_eq!(Pallet::::count_positions(netuid, &cold), 0); + assert!(Ticks::::iter_prefix(netuid).next().is_none()); + assert!(!SwapV3Initialized::::contains_key(netuid)); + }); +} + +#[test] +fn refund_alpha_single_provider_exact() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(11); + let cold = OK_COLDKEY_ACCOUNT_ID; + let hot = OK_HOTKEY_ACCOUNT_ID; + + assert_ok!(Pallet::::maybe_initialize_v3(netuid)); + + // --- Create an alpha‑only position (range entirely above current tick → TAO = 0, ALPHA > 0). + let ct = CurrentTick::::get(netuid); + let tick_low = ct.next().expect("current tick should not be MAX in tests"); + let tick_high = TickIndex::MAX; + + let liquidity = 1_000_000_u64; + let (_pos_id, tao_needed, alpha_needed) = + Pallet::::do_add_liquidity(netuid, &cold, &hot, tick_low, tick_high, liquidity) + .expect("add alpha-only liquidity"); + assert_eq!(tao_needed, 0, "alpha-only position must not require TAO"); + assert!(alpha_needed > 0, "alpha-only position must require ALPHA"); + + // --- Snapshot BEFORE we withdraw funds (baseline for conservation). + let alpha_before_hot = + ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot); + let alpha_before_owner = + ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); + let alpha_before_total = alpha_before_hot + alpha_before_owner; + + // --- Mimic extrinsic bookkeeping: withdraw α and record provided reserve. + let alpha_taken = ::BalanceOps::decrease_stake( + &cold, + &hot, + netuid.into(), + alpha_needed.into(), + ) + .expect("decrease ALPHA"); + ::BalanceOps::increase_provided_alpha_reserve(netuid.into(), alpha_taken); + + // --- Act: dissolve (calls refund_alpha inside). + assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); + + // --- Assert: refunded back to the owner (may credit to (cold,cold)). + let alpha_after_hot = + ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot); + let alpha_after_owner = + ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); + let alpha_after_total = alpha_after_hot + alpha_after_owner; + assert_eq!( + alpha_after_total, alpha_before_total, + "ALPHA principal must be conserved to the owner" + ); + + // --- State is cleared. + assert!(Ticks::::iter_prefix(netuid).next().is_none()); + assert_eq!(Pallet::::count_positions(netuid, &cold), 0); + assert!(!SwapV3Initialized::::contains_key(netuid)); + }); +} + +#[test] +fn refund_alpha_multiple_providers_proportional_to_principal() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(12); + let c1 = OK_COLDKEY_ACCOUNT_ID; + let h1 = OK_HOTKEY_ACCOUNT_ID; + let c2 = OK_COLDKEY_ACCOUNT_ID_2; + let h2 = OK_HOTKEY_ACCOUNT_ID_2; + + assert_ok!(Pallet::::maybe_initialize_v3(netuid)); + + // Use the same "above current tick" trick for alpha‑only positions. + let ct = CurrentTick::::get(netuid); + let tick_low = ct.next().expect("current tick should not be MAX in tests"); + let tick_high = TickIndex::MAX; + + // Provider #1 (smaller α) + let liq1 = 700_000_u64; + let (_p1, t1, a1) = + Pallet::::do_add_liquidity(netuid, &c1, &h1, tick_low, tick_high, liq1) + .expect("add alpha-only liquidity #1"); + assert_eq!(t1, 0); + assert!(a1 > 0); + + // Provider #2 (larger α) + let liq2 = 2_100_000_u64; + let (_p2, t2, a2) = + Pallet::::do_add_liquidity(netuid, &c2, &h2, tick_low, tick_high, liq2) + .expect("add alpha-only liquidity #2"); + assert_eq!(t2, 0); + assert!(a2 > 0); + + // Baselines BEFORE withdrawing + let a1_before_hot = ::BalanceOps::alpha_balance(netuid.into(), &c1, &h1); + let a1_before_owner = ::BalanceOps::alpha_balance(netuid.into(), &c1, &c1); + let a1_before = a1_before_hot + a1_before_owner; + + let a2_before_hot = ::BalanceOps::alpha_balance(netuid.into(), &c2, &h2); + let a2_before_owner = ::BalanceOps::alpha_balance(netuid.into(), &c2, &c2); + let a2_before = a2_before_hot + a2_before_owner; + + // Withdraw α and account reserves for each provider. + let a1_taken = + ::BalanceOps::decrease_stake(&c1, &h1, netuid.into(), a1.into()) + .expect("decrease α #1"); + ::BalanceOps::increase_provided_alpha_reserve(netuid.into(), a1_taken); + + let a2_taken = + ::BalanceOps::decrease_stake(&c2, &h2, netuid.into(), a2.into()) + .expect("decrease α #2"); + ::BalanceOps::increase_provided_alpha_reserve(netuid.into(), a2_taken); + + // Act + assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); + + // Each owner is restored to their exact baseline. + let a1_after_hot = ::BalanceOps::alpha_balance(netuid.into(), &c1, &h1); + let a1_after_owner = ::BalanceOps::alpha_balance(netuid.into(), &c1, &c1); + let a1_after = a1_after_hot + a1_after_owner; + assert_eq!( + a1_after, a1_before, + "owner #1 must receive their α principal back" + ); + + let a2_after_hot = ::BalanceOps::alpha_balance(netuid.into(), &c2, &h2); + let a2_after_owner = ::BalanceOps::alpha_balance(netuid.into(), &c2, &c2); + let a2_after = a2_after_hot + a2_after_owner; + assert_eq!( + a2_after, a2_before, + "owner #2 must receive their α principal back" + ); + }); +} + +#[test] +fn refund_alpha_same_cold_multiple_hotkeys_conserved_to_owner() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(13); + let cold = OK_COLDKEY_ACCOUNT_ID; + let hot1 = OK_HOTKEY_ACCOUNT_ID; + let hot2 = OK_HOTKEY_ACCOUNT_ID_2; + + assert_ok!(Pallet::::maybe_initialize_v3(netuid)); + + // Two alpha‑only positions on different hotkeys of the same owner. + let ct = CurrentTick::::get(netuid); + let tick_low = ct.next().expect("current tick should not be MAX in tests"); + let tick_high = TickIndex::MAX; + + let (_p1, _t1, a1) = + Pallet::::do_add_liquidity(netuid, &cold, &hot1, tick_low, tick_high, 900_000) + .expect("add alpha-only pos (hot1)"); + let (_p2, _t2, a2) = + Pallet::::do_add_liquidity(netuid, &cold, &hot2, tick_low, tick_high, 1_500_000) + .expect("add alpha-only pos (hot2)"); + assert!(a1 > 0 && a2 > 0); + + // Baseline BEFORE: sum over (cold,hot1) + (cold,hot2) + (cold,cold). + let before_hot1 = ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot1); + let before_hot2 = ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot2); + let before_owner = ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); + let before_total = before_hot1 + before_hot2 + before_owner; + + // Withdraw α from both hotkeys; track provided‑reserve. + let t1 = + ::BalanceOps::decrease_stake(&cold, &hot1, netuid.into(), a1.into()) + .expect("decr α #hot1"); + ::BalanceOps::increase_provided_alpha_reserve(netuid.into(), t1); + + let t2 = + ::BalanceOps::decrease_stake(&cold, &hot2, netuid.into(), a2.into()) + .expect("decr α #hot2"); + ::BalanceOps::increase_provided_alpha_reserve(netuid.into(), t2); + + // Act + assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); + + // The total α "owned" by the coldkey is conserved (credit may land on (cold,cold)). + let after_hot1 = ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot1); + let after_hot2 = ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot2); + let after_owner = ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); + let after_total = after_hot1 + after_hot2 + after_owner; + + assert_eq!( + after_total, before_total, + "owner’s α must be conserved across hot ledgers + (owner,owner)" + ); + }); +} diff --git a/pallets/transaction-fee/src/tests/mock.rs b/pallets/transaction-fee/src/tests/mock.rs index 8c6a064ef6..9651f0ea2d 100644 --- a/pallets/transaction-fee/src/tests/mock.rs +++ b/pallets/transaction-fee/src/tests/mock.rs @@ -295,6 +295,7 @@ impl pallet_subtensor::Config for Test { type LeaseDividendsDistributionInterval = LeaseDividendsDistributionInterval; type GetCommitments = (); type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; + type CommitmentsInterface = CommitmentsI; } parameter_types! { @@ -421,6 +422,11 @@ impl PrivilegeCmp for OriginPrivilegeCmp { } } +pub struct CommitmentsI; +impl pallet_subtensor::CommitmentsInterface for CommitmentsI { + fn purge_netuid(_netuid: NetUid) {} +} + parameter_types! { pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * BlockWeights::get().max_block; diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 9eb2f8dbe9..ecadb7c289 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -490,7 +490,9 @@ impl CanVote for CanVoteToTriumvirate { } } -use pallet_subtensor::{CollectiveInterface, MemberManagement, ProxyInterface}; +use pallet_subtensor::{ + CollectiveInterface, CommitmentsInterface, MemberManagement, ProxyInterface, +}; pub struct ManageSenateMembers; impl MemberManagement for ManageSenateMembers { fn add_member(account: &AccountId) -> DispatchResultWithPostInfo { @@ -911,6 +913,13 @@ impl ProxyInterface for Proxier { } } +pub struct CommitmentsI; +impl CommitmentsInterface for CommitmentsI { + fn purge_netuid(netuid: NetUid) { + pallet_commitments::Pallet::::purge_netuid(netuid); + } +} + parameter_types! { pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * BlockWeights::get().max_block; @@ -1152,7 +1161,7 @@ parameter_types! { pub const SubtensorInitialTxChildKeyTakeRateLimit: u64 = INITIAL_CHILDKEY_TAKE_RATELIMIT; pub const SubtensorInitialRAORecycledForRegistration: u64 = 0; // 0 rao pub const SubtensorInitialSenateRequiredStakePercentage: u64 = 1; // 1 percent of total stake - pub const SubtensorInitialNetworkImmunity: u64 = 7 * 7200; + pub const SubtensorInitialNetworkImmunity: u64 = 1_296_000; pub const SubtensorInitialMinAllowedUids: u16 = 64; pub const SubtensorInitialMinLockCost: u64 = 1_000_000_000_000; // 1000 TAO pub const SubtensorInitialSubnetOwnerCut: u16 = 11_796; // 18 percent @@ -1254,6 +1263,7 @@ impl pallet_subtensor::Config for Runtime { type LeaseDividendsDistributionInterval = LeaseDividendsDistributionInterval; type GetCommitments = GetCommitmentsStruct; type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; + type CommitmentsInterface = CommitmentsI; } parameter_types! { @@ -2357,6 +2367,9 @@ impl_runtime_apis! { fn get_selective_metagraph(netuid: NetUid, metagraph_indexes: Vec) -> Option> { SubtensorModule::get_selective_metagraph(netuid, metagraph_indexes) } + fn get_subnet_to_prune() -> Option { + pallet_subtensor::Pallet::::get_network_to_prune() + } fn get_selective_submetagraph(netuid: NetUid, subid: SubId, metagraph_indexes: Vec) -> Option> { SubtensorModule::get_selective_submetagraph(netuid, subid, metagraph_indexes) diff --git a/scripts/benchmark_action.sh b/scripts/benchmark_action.sh index 105cbe9bf5..a475211163 100755 --- a/scripts/benchmark_action.sh +++ b/scripts/benchmark_action.sh @@ -11,7 +11,7 @@ declare -A DISPATCH_PATHS=( [swap]="../pallets/swap/src/pallet/mod.rs" ) -THRESHOLD=20 +THRESHOLD=40 MAX_RETRIES=3 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"