diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index d42862054a..5133046669 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -764,6 +764,27 @@ pub mod pallet { T::Subtensor::set_weights_min_stake(min_stake); Ok(()) } + + /// Sets the minimum stake required for nominators, and clears small nominations + /// that are below the minimum required stake. + #[pallet::call_index(43)] + #[pallet::weight((0, DispatchClass::Operational, Pays::No))] + pub fn sudo_set_nominator_min_required_stake( + origin: OriginFor, + // The minimum stake required for nominators. + min_stake: u64, + ) -> DispatchResult { + ensure_root(origin)?; + let prev_min_stake = T::Subtensor::get_nominator_min_required_stake(); + log::trace!("Setting minimum stake to: {}", min_stake); + T::Subtensor::set_nominator_min_required_stake(min_stake); + if min_stake > prev_min_stake { + log::trace!("Clearing small nominations"); + T::Subtensor::clear_small_nominations(); + log::trace!("Small nominations cleared"); + } + Ok(()) + } } } @@ -852,4 +873,7 @@ pub trait SubtensorInterface { fn set_weights_set_rate_limit(netuid: u16, weights_set_rate_limit: u64); fn init_new_network(netuid: u16, tempo: u16); fn set_weights_min_stake(min_stake: u64); + fn get_nominator_min_required_stake() -> u64; + fn set_nominator_min_required_stake(min_stake: u64); + fn clear_small_nominations(); } diff --git a/pallets/admin-utils/tests/mock.rs b/pallets/admin-utils/tests/mock.rs index ddc7509760..de5efce03e 100644 --- a/pallets/admin-utils/tests/mock.rs +++ b/pallets/admin-utils/tests/mock.rs @@ -1,5 +1,5 @@ use frame_support::{ - parameter_types, + assert_ok, parameter_types, traits::{Everything, Hooks}, weights, }; @@ -429,6 +429,18 @@ impl pallet_admin_utils::SubtensorInterface f fn set_weights_min_stake(min_stake: u64) { SubtensorModule::set_weights_min_stake(min_stake); } + + fn set_nominator_min_required_stake(min_stake: u64) { + SubtensorModule::set_nominator_min_required_stake(min_stake); + } + + fn get_nominator_min_required_stake() -> u64 { + SubtensorModule::get_nominator_min_required_stake() + } + + fn clear_small_nominations() { + SubtensorModule::clear_small_nominations(); + } } impl pallet_admin_utils::Config for Test { @@ -462,3 +474,42 @@ pub(crate) fn run_to_block(n: u64) { SubtensorModule::on_initialize(System::block_number()); } } + +#[allow(dead_code)] +pub fn register_ok_neuron( + netuid: u16, + hotkey_account_id: U256, + coldkey_account_id: U256, + start_nonce: u64, +) { + let block_number: u64 = SubtensorModule::get_current_block_as_u64(); + let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( + netuid, + block_number, + start_nonce, + &hotkey_account_id, + ); + let result = SubtensorModule::register( + <::RuntimeOrigin>::signed(hotkey_account_id), + netuid, + block_number, + nonce, + work, + hotkey_account_id, + coldkey_account_id, + ); + assert_ok!(result); + log::info!( + "Register ok neuron: netuid: {:?}, coldkey: {:?}, hotkey: {:?}", + netuid, + hotkey_account_id, + coldkey_account_id + ); +} + +#[allow(dead_code)] +pub fn add_network(netuid: u16, tempo: u16) { + SubtensorModule::init_new_network(netuid, tempo); + SubtensorModule::set_network_registration_allowed(netuid, true); + SubtensorModule::set_network_pow_registration_allowed(netuid, true); +} diff --git a/pallets/admin-utils/tests/tests.rs b/pallets/admin-utils/tests/tests.rs index a18f3b093d..7ef34cbef1 100644 --- a/pallets/admin-utils/tests/tests.rs +++ b/pallets/admin-utils/tests/tests.rs @@ -8,12 +8,6 @@ use sp_core::U256; mod mock; use mock::*; -pub fn add_network(netuid: u16, tempo: u16) { - SubtensorModule::init_new_network(netuid, tempo); - SubtensorModule::set_network_registration_allowed(netuid, true); - SubtensorModule::set_network_pow_registration_allowed(netuid, true); -} - #[test] fn test_sudo_set_default_take() { new_test_ext().execute_with(|| { @@ -880,3 +874,189 @@ fn test_sudo_set_network_pow_registration_allowed() { ); }); } + +mod sudo_set_nominator_min_required_stake { + use super::*; + + #[test] + fn can_only_be_called_by_admin() { + new_test_ext().execute_with(|| { + let to_be_set: u64 = SubtensorModule::get_nominator_min_required_stake() + 5 as u64; + assert_eq!( + AdminUtils::sudo_set_nominator_min_required_stake( + <::RuntimeOrigin>::signed(U256::from(0)), + to_be_set + ), + Err(DispatchError::BadOrigin.into()) + ); + }); + } + + #[test] + fn sets_a_lower_value() { + new_test_ext().execute_with(|| { + assert_ok!(AdminUtils::sudo_set_nominator_min_required_stake( + <::RuntimeOrigin>::root(), + 10u64 + )); + assert_eq!(SubtensorModule::get_nominator_min_required_stake(), 10u64); + + assert_ok!(AdminUtils::sudo_set_nominator_min_required_stake( + <::RuntimeOrigin>::root(), + 5u64 + )); + assert_eq!(SubtensorModule::get_nominator_min_required_stake(), 5u64); + }); + } + + #[test] + fn sets_a_higher_value() { + new_test_ext().execute_with(|| { + let to_be_set: u64 = SubtensorModule::get_nominator_min_required_stake() + 5 as u64; + assert_ok!(AdminUtils::sudo_set_nominator_min_required_stake( + <::RuntimeOrigin>::root(), + to_be_set + )); + assert_eq!( + SubtensorModule::get_nominator_min_required_stake(), + to_be_set + ); + }); + } + + #[test] + fn clears_staker_nominations_below_min() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + + // Create accounts. + let netuid = 1; + let hot1 = U256::from(1); + let hot2 = U256::from(2); + let cold1 = U256::from(3); + let cold2 = U256::from(4); + + SubtensorModule::set_target_stakes_per_interval(10); + // Register network. + add_network(netuid, 0); + + // Register hot1. + register_ok_neuron(netuid, hot1, cold1, 0); + assert_ok!(SubtensorModule::do_become_delegate( + <::RuntimeOrigin>::signed(cold1), + hot1, + 0 + )); + assert_eq!(SubtensorModule::get_owning_coldkey_for_hotkey(&hot1), cold1); + + // Register hot2. + register_ok_neuron(netuid, hot2, cold2, 0); + assert_ok!(SubtensorModule::do_become_delegate( + <::RuntimeOrigin>::signed(cold2), + hot2, + 0 + )); + assert_eq!(SubtensorModule::get_owning_coldkey_for_hotkey(&hot2), cold2); + + // Add stake cold1 --> hot1 (non delegation.) + SubtensorModule::add_balance_to_coldkey_account(&cold1, 5); + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(cold1), + hot1, + 1 + )); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(&cold1, &hot1), + 1 + ); + assert_eq!(Balances::free_balance(cold1), 4); + + // Add stake cold2 --> hot1 (is delegation.) + SubtensorModule::add_balance_to_coldkey_account(&cold2, 5); + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(cold2), + hot1, + 1 + )); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(&cold2, &hot1), + 1 + ); + assert_eq!(Balances::free_balance(cold2), 4); + + // Add stake cold1 --> hot2 (non delegation.) + SubtensorModule::add_balance_to_coldkey_account(&cold1, 5); + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(cold1), + hot2, + 1 + )); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(&cold1, &hot2), + 1 + ); + assert_eq!(Balances::free_balance(cold1), 8); + + // Add stake cold2 --> hot2 (is delegation.) + SubtensorModule::add_balance_to_coldkey_account(&cold2, 5); + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(cold2), + hot2, + 1 + )); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(&cold2, &hot2), + 1 + ); + assert_eq!(Balances::free_balance(cold2), 8); + + // Set min stake to 0 (noop) + assert_ok!(AdminUtils::sudo_set_nominator_min_required_stake( + <::RuntimeOrigin>::root(), + 0u64 + )); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(&cold1, &hot1), + 1 + ); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(&cold1, &hot2), + 1 + ); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(&cold2, &hot1), + 1 + ); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(&cold2, &hot2), + 1 + ); + + // Set min nomination to 10: should clear (cold2, hot1) and (cold1, hot2). + assert_ok!(AdminUtils::sudo_set_nominator_min_required_stake( + <::RuntimeOrigin>::root(), + 10u64 + )); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(&cold1, &hot1), + 1 + ); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(&cold1, &hot2), + 0 + ); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(&cold2, &hot1), + 0 + ); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(&cold2, &hot2), + 1 + ); + + // Balances have been added back into accounts. + assert_eq!(Balances::free_balance(cold1), 9); + assert_eq!(Balances::free_balance(cold2), 9); + }); + } +} diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 11cb912779..0b9b7f3800 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -262,10 +262,17 @@ pub mod pallet { pub type TotalColdkeyStake = StorageMap<_, Identity, T::AccountId, u64, ValueQuery, DefaultAccountTake>; #[pallet::storage] - // --- MAP (hot) --> stake | Returns a tuple (u64: stakes, u64: block_number) - pub type TotalHotkeyStakesThisInterval = - StorageMap<_, Identity, T::AccountId, (u64, u64), ValueQuery, DefaultStakesPerInterval>; - + // --- MAP (hot, cold) --> stake | Returns a tuple (u64: stakes, u64: block_number) + pub type TotalHotkeyColdkeyStakesThisInterval = StorageDoubleMap< + _, + Identity, + T::AccountId, + Identity, + T::AccountId, + (u64, u64), + ValueQuery, + DefaultStakesPerInterval, + >; #[pallet::storage] // --- MAP ( hot ) --> cold | Returns the controlling coldkey for a hotkey. pub type Owner = StorageMap<_, Blake2_128Concat, T::AccountId, T::AccountId, ValueQuery, DefaultAccount>; @@ -397,6 +404,10 @@ pub mod pallet { 0 } #[pallet::type_value] + pub fn DefaultNominatorMinRequiredStake() -> u64 { + 0 + } + #[pallet::type_value] pub fn DefaultNetworkMinAllowedUids() -> u16 { T::InitialNetworkMinAllowedUids::get() } @@ -477,6 +488,9 @@ pub mod pallet { pub type SubnetOwnerCut = StorageValue<_, u16, ValueQuery, DefaultSubnetOwnerCut>; #[pallet::storage] // ITEM( network_rate_limit ) pub type NetworkRateLimit = StorageValue<_, u64, ValueQuery, DefaultNetworkRateLimit>; + #[pallet::storage] // ITEM( nominator_min_required_stake ) + pub type NominatorMinRequiredStake = + StorageValue<_, u64, ValueQuery, DefaultNominatorMinRequiredStake>; // ============================== // ==== Subnetwork Features ===== @@ -968,6 +982,8 @@ pub mod pallet { StakeTooLowForRoot, // --- Thrown when a hotkey attempts to join the root subnet with too little stake AllNetworksInImmunity, // --- Thrown when all subnets are in the immunity period NotEnoughBalance, + /// Thrown a stake would be below the minimum threshold for nominator validations + NomStakeBelowMinimumThreshold, } // ================== @@ -1704,7 +1720,7 @@ pub mod pallet { pub fn get_priority_set_weights(hotkey: &T::AccountId, netuid: u16) -> u64 { if Uids::::contains_key(netuid, hotkey) { let uid = Self::get_uid_for_net_and_hotkey(netuid, &hotkey.clone()).unwrap(); - let _stake = Self::get_total_stake_for_hotkey(hotkey); + let _stake = Self::get_total_stake_for_hotkey(&hotkey); let current_block_number: u64 = Self::get_current_block_as_u64(); let default_priority: u64 = current_block_number - Self::get_last_update_for_uid(netuid, uid); @@ -1847,32 +1863,14 @@ where Err(InvalidTransaction::Call.into()) } } - Some(Call::add_stake { hotkey, .. }) => { - let stakes_this_interval = Pallet::::get_stakes_this_interval_for_hotkey(hotkey); - let max_stakes_per_interval = Pallet::::get_target_stakes_per_interval(); - - if stakes_this_interval >= max_stakes_per_interval { - return InvalidTransaction::ExhaustsResources.into(); - } - - Ok(ValidTransaction { - priority: Self::get_priority_vanilla(), - ..Default::default() - }) - } - Some(Call::remove_stake { hotkey, .. }) => { - let stakes_this_interval = Pallet::::get_stakes_this_interval_for_hotkey(hotkey); - let max_stakes_per_interval = Pallet::::get_target_stakes_per_interval(); - - if stakes_this_interval >= max_stakes_per_interval { - return InvalidTransaction::ExhaustsResources.into(); - } - - Ok(ValidTransaction { - priority: Self::get_priority_vanilla(), - ..Default::default() - }) - } + Some(Call::add_stake { .. }) => Ok(ValidTransaction { + priority: Self::get_priority_vanilla(), + ..Default::default() + }), + Some(Call::remove_stake { .. }) => Ok(ValidTransaction { + priority: Self::get_priority_vanilla(), + ..Default::default() + }), Some(Call::register { netuid, .. } | Call::burned_register { netuid, .. }) => { let registrations_this_interval = Pallet::::get_registrations_this_interval(*netuid); diff --git a/pallets/subtensor/src/migration.rs b/pallets/subtensor/src/migration.rs index a516041d36..354b199d88 100644 --- a/pallets/subtensor/src/migration.rs +++ b/pallets/subtensor/src/migration.rs @@ -1,4 +1,5 @@ use super::*; +use frame_support::traits::DefensiveResult; use frame_support::{ pallet_prelude::{Identity, OptionQuery}, sp_std::vec::Vec, @@ -22,41 +23,53 @@ pub mod deprecated_loaded_emission_format { StorageMap, Identity, u16, Vec<(AccountIdOf, u64)>, OptionQuery>; } - /// Performs migration to update the total issuance based on the sum of stakes and total balances. /// This migration is applicable only if the current storage version is 5, after which it updates the storage version to 6. /// /// # Returns /// Weight of the migration process. -pub fn migration5_total_issuance( test: bool ) -> Weight { +pub fn migration5_total_issuance(test: bool) -> Weight { let mut weight = T::DbWeight::get().reads(1); // Initialize migration weight // Execute migration if the current storage version is 5 if Pallet::::on_chain_storage_version() == StorageVersion::new(5) || test { // Calculate the sum of all stake values - let stake_sum: u64 = Stake::::iter() - .fold(0, |accumulator, (_, _, stake_value)| accumulator.saturating_add(stake_value)); - weight = weight.saturating_add(T::DbWeight::get().reads_writes(Stake::::iter().count() as u64, 0)); + let stake_sum: u64 = Stake::::iter().fold(0, |accumulator, (_, _, stake_value)| { + accumulator.saturating_add(stake_value) + }); + weight = weight + .saturating_add(T::DbWeight::get().reads_writes(Stake::::iter().count() as u64, 0)); // Calculate the sum of all stake values let locked_sum: u64 = SubnetLocked::::iter() - .fold(0, |accumulator, (_, locked_value)| accumulator.saturating_add(locked_value)); - weight = weight.saturating_add(T::DbWeight::get().reads_writes(SubnetLocked::::iter().count() as u64, 0)); + .fold(0, |accumulator, (_, locked_value)| { + accumulator.saturating_add(locked_value) + }); + weight = weight.saturating_add( + T::DbWeight::get().reads_writes(SubnetLocked::::iter().count() as u64, 0), + ); // Retrieve the total balance sum let total_balance = T::Currency::total_issuance(); - weight = weight.saturating_add(T::DbWeight::get().reads(1)); + match TryInto::::try_into(total_balance) { + Ok(total_balance_sum) => { + weight = weight.saturating_add(T::DbWeight::get().reads(1)); - // Compute the total issuance value - let total_issuance_value: u64 = stake_sum + total_balance + locked_sum; + // Compute the total issuance value + let total_issuance_value: u64 = stake_sum + total_balance_sum + locked_sum; - // Update the total issuance in storage - TotalIssuance::::put(total_issuance_value); - } + // Update the total issuance in storage + TotalIssuance::::put(total_issuance_value); - // Update the storage version to 6 - StorageVersion::new(6).put::>(); - weight = weight.saturating_add(T::DbWeight::get().writes(1)); + // Update the storage version to 6 + StorageVersion::new(6).put::>(); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + } + Err(_) => { + log::error!("Failed to convert total balance to u64, bailing"); + } + } + } weight // Return the computed weight of the migration process } @@ -152,8 +165,8 @@ pub fn migrate_create_root_network() -> Weight { // Empty senate members entirely, they will be filled by by registrations // on the subnet. for hotkey_i in T::SenateMembers::members().iter() { - T::TriumvirateInterface::remove_votes(hotkey_i).unwrap(); - T::SenateMembers::remove_member(hotkey_i).unwrap(); + T::TriumvirateInterface::remove_votes(&hotkey_i).defensive_ok(); + T::SenateMembers::remove_member(&hotkey_i).defensive_ok(); weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); } diff --git a/pallets/subtensor/src/registration.rs b/pallets/subtensor/src/registration.rs index c5d6b2e300..5d91ec03c5 100644 --- a/pallets/subtensor/src/registration.rs +++ b/pallets/subtensor/src/registration.rs @@ -1,5 +1,5 @@ use super::*; -use frame_support::pallet_prelude::DispatchResultWithPostInfo; +use frame_support::pallet_prelude::{DispatchResult, DispatchResultWithPostInfo}; use frame_support::storage::IterableStorageDoubleMap; use sp_core::{Get, H256, U256}; use sp_io::hashing::{keccak_256, sha2_256}; @@ -394,7 +394,7 @@ impl Pallet { // --- 5. Add Balance via faucet. let balance_to_add: u64 = 100_000_000_000; - Self::coinbase( 100_000_000_000 ); // We are creating tokens here from the coinbase. + Self::coinbase(100_000_000_000); // We are creating tokens here from the coinbase. let balance_to_be_added_as_balance = Self::u64_to_balance(balance_to_add); Self::add_balance_to_coldkey_account(&coldkey, balance_to_be_added_as_balance.unwrap()); diff --git a/pallets/subtensor/src/staking.rs b/pallets/subtensor/src/staking.rs index 02c4d3c0d9..34f1f3c156 100644 --- a/pallets/subtensor/src/staking.rs +++ b/pallets/subtensor/src/staking.rs @@ -166,31 +166,42 @@ impl Pallet { Error::::NonAssociatedColdKey ); - // --- 6. Ensure we don't exceed tx rate limit - let block: u64 = Self::get_current_block_as_u64(); - ensure!( - !Self::exceeds_tx_rate_limit(Self::get_last_tx_block(&coldkey), block), - Error::::TxRateLimitExceeded - ); - - // --- 7. Ensure we don't exceed stake rate limit - let stakes_this_interval = Self::get_stakes_this_interval_for_hotkey(&hotkey); + // --- 6. Ensure we don't exceed stake rate limit + let stakes_this_interval = + Self::get_stakes_this_interval_for_coldkey_hotkey(&coldkey, &hotkey); ensure!( stakes_this_interval < Self::get_target_stakes_per_interval(), Error::::StakeRateLimitExceeded ); + // --- 7. If this is a nomination stake, check if total stake after adding will be above + // the minimum required stake. + + // If coldkey is not owner of the hotkey, it's a nomination stake. + if !Self::coldkey_owns_hotkey(&coldkey, &hotkey) { + let total_stake_after_add = + Stake::::get(&hotkey, &coldkey).saturating_add(stake_to_be_added); + + ensure!( + total_stake_after_add >= NominatorMinRequiredStake::::get(), + Error::::NomStakeBelowMinimumThreshold + ); + } + // --- 8. Ensure the remove operation from the coldkey is a success. - let actual_amount_to_stake = Self::remove_balance_from_coldkey_account(&coldkey, stake_as_balance.unwrap())?; + let actual_amount_to_stake = + Self::remove_balance_from_coldkey_account(&coldkey, stake_as_balance.unwrap())?; // --- 9. If we reach here, add the balance to the hotkey. Self::increase_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, actual_amount_to_stake); // Set last block for rate limiting + let block: u64 = Self::get_current_block_as_u64(); Self::set_last_tx_block(&coldkey, block); // --- 10. Emit the staking event. - Self::set_stakes_this_interval_for_hotkey( + Self::set_stakes_this_interval_for_coldkey_hotkey( + &coldkey, &hotkey, stakes_this_interval + 1, block, @@ -284,20 +295,28 @@ impl Pallet { Error::::CouldNotConvertToBalance ); - // --- 6. Ensure we don't exceed tx rate limit - let block: u64 = Self::get_current_block_as_u64(); - ensure!( - !Self::exceeds_tx_rate_limit(Self::get_last_tx_block(&coldkey), block), - Error::::TxRateLimitExceeded - ); - - // --- 7. Ensure we don't exceed stake rate limit - let unstakes_this_interval = Self::get_stakes_this_interval_for_hotkey(&hotkey); + // --- 6. Ensure we don't exceed stake rate limit + let unstakes_this_interval = + Self::get_stakes_this_interval_for_coldkey_hotkey(&coldkey, &hotkey); ensure!( unstakes_this_interval < Self::get_target_stakes_per_interval(), Error::::UnstakeRateLimitExceeded ); + // --- 7. If this is a nomination stake, check if total stake after removing will be above + // the minimum required stake. + + // If coldkey is not owner of the hotkey, it's a nomination stake. + if !Self::coldkey_owns_hotkey(&coldkey, &hotkey) { + let total_stake_after_remove = + Stake::::get(&hotkey, &coldkey).saturating_sub(stake_to_be_removed); + + ensure!( + total_stake_after_remove >= NominatorMinRequiredStake::::get(), + Error::::NomStakeBelowMinimumThreshold + ); + } + // --- 8. We remove the balance from the hotkey. Self::decrease_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, stake_to_be_removed); @@ -305,10 +324,12 @@ impl Pallet { Self::add_balance_to_coldkey_account(&coldkey, stake_to_be_added_as_currency.unwrap()); // Set last block for rate limiting + let block: u64 = Self::get_current_block_as_u64(); Self::set_last_tx_block(&coldkey, block); // --- 10. Emit the unstaking event. - Self::set_stakes_this_interval_for_hotkey( + Self::set_stakes_this_interval_for_coldkey_hotkey( + &coldkey, &hotkey, unstakes_this_interval + 1, block, @@ -373,7 +394,10 @@ impl Pallet { } // Retrieves the total stakes for a given hotkey (account ID) for the current staking interval. - pub fn get_stakes_this_interval_for_hotkey(hotkey: &T::AccountId) -> u64 { + pub fn get_stakes_this_interval_for_coldkey_hotkey( + coldkey: &T::AccountId, + hotkey: &T::AccountId, + ) -> u64 { // Retrieve the configured stake interval duration from storage. let stake_interval = StakeInterval::::get(); @@ -381,7 +405,8 @@ impl Pallet { let current_block = Self::get_current_block_as_u64(); // Fetch the total stakes and the last block number when stakes were made for the hotkey. - let (stakes, block_last_staked_at) = TotalHotkeyStakesThisInterval::::get(hotkey); + let (stakes, block_last_staked_at) = + TotalHotkeyColdkeyStakesThisInterval::::get(coldkey, hotkey); // Calculate the block number after which the stakes for the hotkey should be reset. let block_to_reset_after = block_last_staked_at + stake_interval; @@ -390,7 +415,12 @@ impl Pallet { // it indicates the end of the staking interval for the hotkey. if block_to_reset_after <= current_block { // Reset the stakes for this hotkey for the current interval. - Self::set_stakes_this_interval_for_hotkey(hotkey, 0, block_last_staked_at); + Self::set_stakes_this_interval_for_coldkey_hotkey( + coldkey, + hotkey, + 0, + block_last_staked_at, + ); // Return 0 as the stake amount since we've just reset the stakes. return 0; } @@ -504,11 +534,61 @@ impl Pallet { TotalStake::::put(TotalStake::::get().saturating_sub(decrement)); } + /// Empties the stake associated with a given coldkey-hotkey account pairing. + /// This function retrieves the current stake for the specified coldkey-hotkey pairing, + /// then subtracts this stake amount from both the TotalColdkeyStake and TotalHotkeyStake. + /// It also removes the stake entry for the hotkey-coldkey pairing and adjusts the TotalStake + /// and TotalIssuance by subtracting the removed stake amount. + /// + /// # Arguments + /// + /// * `coldkey` - A reference to the AccountId of the coldkey involved in the staking. + /// * `hotkey` - A reference to the AccountId of the hotkey associated with the coldkey. + pub fn empty_stake_on_coldkey_hotkey_account(coldkey: &T::AccountId, hotkey: &T::AccountId) { + let current_stake: u64 = Stake::::get(hotkey, coldkey); + TotalColdkeyStake::::mutate(coldkey, |old| *old = old.saturating_sub(current_stake)); + TotalHotkeyStake::::mutate(hotkey, |stake| *stake = stake.saturating_sub(current_stake)); + Stake::::remove(hotkey, coldkey); + TotalStake::::mutate(|stake| *stake = stake.saturating_sub(current_stake)); + TotalIssuance::::mutate(|issuance| *issuance = issuance.saturating_sub(current_stake)); + } + + /// Clears the nomination for an account, if it is a nominator account and the stake is below the minimum required threshold. + pub fn clear_small_nomination_if_required( + hotkey: &T::AccountId, + coldkey: &T::AccountId, + stake: u64, + ) { + // Verify if the account is a nominator account by checking ownership of the hotkey by the coldkey. + if !Self::coldkey_owns_hotkey(&coldkey, &hotkey) { + // If the stake is below the minimum required, it's considered a small nomination and needs to be cleared. + if stake < Self::get_nominator_min_required_stake() { + // Remove the stake from the nominator account. (this is a more forceful unstake operation which ) + // Actually deletes the staking account. + Self::empty_stake_on_coldkey_hotkey_account(&coldkey, &hotkey); + // Convert the removed stake back to balance and add it to the coldkey account. + let stake_as_balance = Self::u64_to_balance(stake); + Self::add_balance_to_coldkey_account(&coldkey, stake_as_balance.unwrap()); + } + } + } + + /// Clears small nominations for all accounts. + /// + /// WARN: This is an O(N) operation, where N is the number of staking accounts. It should be + /// used with caution. + pub fn clear_small_nominations() { + // Loop through all staking accounts to identify and clear nominations below the minimum stake. + for (hotkey, coldkey, stake) in Stake::::iter() { + Self::clear_small_nomination_if_required(&hotkey, &coldkey, stake); + } + } + pub fn u64_to_balance( input: u64, ) -> Option< <::Currency as fungible::Inspect<::AccountId>>::Balance, - > { + >{ input.try_into().ok() } @@ -537,24 +617,17 @@ impl Pallet { } // This bit is currently untested. @todo - - T::Currency::can_withdraw( - coldkey, - amount, - ) - .into_result(false) - .is_ok() + + T::Currency::can_withdraw(coldkey, amount) + .into_result(false) + .is_ok() } pub fn get_coldkey_balance( coldkey: &T::AccountId, ) -> <::Currency as fungible::Inspect<::AccountId>>::Balance { - T::Currency::reducible_balance( - coldkey, - Preservation::Expendable, - Fortitude::Polite, - ) + T::Currency::reducible_balance(coldkey, Preservation::Expendable, Fortitude::Polite) } #[must_use = "Balance must be used to preserve total issuance of token"] @@ -562,23 +635,27 @@ impl Pallet { coldkey: &T::AccountId, amount: <::Currency as fungible::Inspect<::AccountId>>::Balance, ) -> Result { - let amount_u64: u64 = amount.try_into().map_err(|_| Error::::CouldNotConvertToU64)?; + let amount_u64: u64 = amount + .try_into() + .map_err(|_| Error::::CouldNotConvertToU64)?; if amount_u64 == 0 { return Ok(0); } let credit = T::Currency::withdraw( - coldkey, - amount, - Precision::BestEffort, - Preservation::Preserve, - Fortitude::Polite, - ) - .map_err(|_| Error::::BalanceWithdrawalError)? - .peek(); + coldkey, + amount, + Precision::BestEffort, + Preservation::Preserve, + Fortitude::Polite, + ) + .map_err(|_| Error::::BalanceWithdrawalError)? + .peek(); - let credit_u64: u64 = credit.try_into().map_err(|_| Error::::CouldNotConvertToU64)?; + let credit_u64: u64 = credit + .try_into() + .map_err(|_| Error::::CouldNotConvertToU64)?; if credit_u64 == 0 { return Err(Error::::BalanceWithdrawalError.into()); diff --git a/pallets/subtensor/src/utils.rs b/pallets/subtensor/src/utils.rs index 3051adb66d..a39589176b 100644 --- a/pallets/subtensor/src/utils.rs +++ b/pallets/subtensor/src/utils.rs @@ -138,8 +138,8 @@ impl Pallet { pub fn set_target_stakes_per_interval(target_stakes_per_interval: u64) { TargetStakesPerInterval::::set(target_stakes_per_interval) } - pub fn set_stakes_this_interval_for_hotkey(hotkey: &T::AccountId, stakes_this_interval: u64, last_staked_block_number: u64) { - TotalHotkeyStakesThisInterval::::insert(hotkey, (stakes_this_interval, last_staked_block_number)); + pub fn set_stakes_this_interval_for_coldkey_hotkey(coldkey: &T::AccountId, hotkey: &T::AccountId, stakes_this_interval: u64, last_staked_block_number: u64) { + TotalHotkeyColdkeyStakesThisInterval::::insert(coldkey, hotkey, (stakes_this_interval, last_staked_block_number)); } pub fn set_stake_interval(block: u64) { StakeInterval::::set(block); @@ -604,4 +604,12 @@ impl Pallet { pub fn is_subnet_owner(address: &T::AccountId) -> bool { SubnetOwner::::iter_values().any(|owner| *address == owner) } + + pub fn get_nominator_min_required_stake() -> u64 { + NominatorMinRequiredStake::::get() + } + + pub fn set_nominator_min_required_stake(min_stake: u64) { + NominatorMinRequiredStake::::put(min_stake); + } } diff --git a/pallets/subtensor/tests/staking.rs b/pallets/subtensor/tests/staking.rs index 1547102706..b5ba056d1f 100644 --- a/pallets/subtensor/tests/staking.rs +++ b/pallets/subtensor/tests/staking.rs @@ -2,11 +2,10 @@ use frame_support::{assert_err, assert_noop, assert_ok, traits::Currency}; use frame_system::Config; mod mock; use frame_support::dispatch::{DispatchClass, DispatchInfo, GetDispatchInfo, Pays}; -use frame_support::sp_runtime::{transaction_validity::InvalidTransaction, DispatchError}; +use frame_support::sp_runtime::DispatchError; use mock::*; -use pallet_subtensor::{Error, SubtensorSignedExtension}; +use pallet_subtensor::*; use sp_core::{H256, U256}; -use sp_runtime::traits::{DispatchInfoOf, SignedExtension}; /*********************************************************** staking::add_stake() tests @@ -337,28 +336,29 @@ fn test_add_stake_total_issuance_no_change() { #[test] fn test_reset_stakes_per_interval() { new_test_ext(0).execute_with(|| { + let coldkey = U256::from(561330); let hotkey = U256::from(561337); SubtensorModule::set_stake_interval(7); - SubtensorModule::set_stakes_this_interval_for_hotkey(&hotkey, 5, 1); + SubtensorModule::set_stakes_this_interval_for_coldkey_hotkey(&coldkey, &hotkey, 5, 1); step_block(1); assert_eq!( - SubtensorModule::get_stakes_this_interval_for_hotkey(&hotkey), + SubtensorModule::get_stakes_this_interval_for_coldkey_hotkey(&coldkey, &hotkey), 5 ); // block: 7 interval not yet passed step_block(6); assert_eq!( - SubtensorModule::get_stakes_this_interval_for_hotkey(&hotkey), + SubtensorModule::get_stakes_this_interval_for_coldkey_hotkey(&coldkey, &hotkey), 5 ); // block 8: interval passed step_block(1); assert_eq!( - SubtensorModule::get_stakes_this_interval_for_hotkey(&hotkey), + SubtensorModule::get_stakes_this_interval_for_coldkey_hotkey(&coldkey, &hotkey), 0 ); }); @@ -376,18 +376,6 @@ fn test_add_stake_under_limit() { let max_stakes = 2; SubtensorModule::set_target_stakes_per_interval(max_stakes); - - let call: pallet_subtensor::Call = pallet_subtensor::Call::add_stake { - hotkey: hotkey_account_id, - amount_staked: 1, - }; - let info: DispatchInfo = - DispatchInfoOf::<::RuntimeCall>::default(); - let extension = SubtensorSignedExtension::::new(); - let result = extension.validate(&who, &call.into(), &info, 10); - - assert_ok!(result); - add_network(netuid, tempo, 0); register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, start_nonce); SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, 60000); @@ -402,8 +390,10 @@ fn test_add_stake_under_limit() { 1, )); - let current_stakes = - SubtensorModule::get_stakes_this_interval_for_hotkey(&hotkey_account_id); + let current_stakes = SubtensorModule::get_stakes_this_interval_for_coldkey_hotkey( + &coldkey_account_id, + &hotkey_account_id, + ); assert!(current_stakes <= max_stakes); }); } @@ -421,23 +411,13 @@ fn test_add_stake_rate_limit_exceeded() { let block_number = 1; SubtensorModule::set_target_stakes_per_interval(max_stakes); - SubtensorModule::set_stakes_this_interval_for_hotkey( + SubtensorModule::set_stakes_this_interval_for_coldkey_hotkey( + &coldkey_account_id, &hotkey_account_id, max_stakes, block_number, ); - let call: pallet_subtensor::Call = pallet_subtensor::Call::add_stake { - hotkey: hotkey_account_id, - amount_staked: 1, - }; - let info: DispatchInfo = - DispatchInfoOf::<::RuntimeCall>::default(); - let extension = SubtensorSignedExtension::::new(); - let result = extension.validate(&who, &call.into(), &info, 10); - - assert_err!(result, InvalidTransaction::ExhaustsResources); - add_network(netuid, tempo, 0); register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, start_nonce); SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, 60000); @@ -450,8 +430,10 @@ fn test_add_stake_rate_limit_exceeded() { Error::::StakeRateLimitExceeded ); - let current_stakes = - SubtensorModule::get_stakes_this_interval_for_hotkey(&hotkey_account_id); + let current_stakes = SubtensorModule::get_stakes_this_interval_for_coldkey_hotkey( + &coldkey_account_id, + &hotkey_account_id, + ); assert_eq!(current_stakes, max_stakes); }); } @@ -471,18 +453,6 @@ fn test_remove_stake_under_limit() { let max_unstakes = 2; SubtensorModule::set_target_stakes_per_interval(max_unstakes); - - let call = pallet_subtensor::Call::remove_stake { - hotkey: hotkey_account_id, - amount_unstaked: 1, - }; - let info: DispatchInfo = - DispatchInfoOf::<::RuntimeCall>::default(); - let extension = SubtensorSignedExtension::::new(); - let result = extension.validate(&who, &call.into(), &info, 10); - - assert_ok!(result); - add_network(netuid, tempo, 0); register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, start_nonce); SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, 60000); @@ -499,8 +469,10 @@ fn test_remove_stake_under_limit() { 1, )); - let current_unstakes = - SubtensorModule::get_stakes_this_interval_for_hotkey(&hotkey_account_id); + let current_unstakes = SubtensorModule::get_stakes_this_interval_for_coldkey_hotkey( + &coldkey_account_id, + &hotkey_account_id, + ); assert!(current_unstakes <= max_unstakes); }); } @@ -518,23 +490,13 @@ fn test_remove_stake_rate_limit_exceeded() { let block_number = 1; SubtensorModule::set_target_stakes_per_interval(max_unstakes); - SubtensorModule::set_stakes_this_interval_for_hotkey( + SubtensorModule::set_stakes_this_interval_for_coldkey_hotkey( + &coldkey_account_id, &hotkey_account_id, max_unstakes, block_number, ); - let call = pallet_subtensor::Call::remove_stake { - hotkey: hotkey_account_id, - amount_unstaked: 1, - }; - let info: DispatchInfo = - DispatchInfoOf::<::RuntimeCall>::default(); - let extension = SubtensorSignedExtension::::new(); - let result = extension.validate(&who, &call.into(), &info, 10); - - assert_err!(result, InvalidTransaction::ExhaustsResources); - add_network(netuid, tempo, 0); register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, start_nonce); SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, 60000); @@ -548,8 +510,10 @@ fn test_remove_stake_rate_limit_exceeded() { Error::::UnstakeRateLimitExceeded ); - let current_unstakes = - SubtensorModule::get_stakes_this_interval_for_hotkey(&hotkey_account_id); + let current_unstakes = SubtensorModule::get_stakes_this_interval_for_coldkey_hotkey( + &coldkey_account_id, + &hotkey_account_id, + ); assert_eq!(current_unstakes, max_unstakes); }); } @@ -2469,3 +2433,274 @@ fn test_faucet_ok() { )); }); } + +/// This test ensures that the clear_small_nominations function works as expected. +/// It creates a network with two hotkeys and two coldkeys, and then registers a nominator account for each hotkey. +/// When we call set_nominator_min_required_stake, it should clear all small nominations that are below the minimum required stake. +/// Run this test using: cargo test --package pallet-subtensor --test staking test_clear_small_nominations +#[test] +fn test_clear_small_nominations() { + new_test_ext(0).execute_with(|| { + System::set_block_number(1); + + // Create accounts. + let netuid = 1; + let hot1 = U256::from(1); + let hot2 = U256::from(2); + let cold1 = U256::from(3); + let cold2 = U256::from(4); + + SubtensorModule::set_target_stakes_per_interval(10); + // Register hot1 and hot2 . + add_network(netuid, 0, 0); + + // Register hot1. + register_ok_neuron(netuid, hot1, cold1, 0); + assert_ok!(SubtensorModule::do_become_delegate( + <::RuntimeOrigin>::signed(cold1), + hot1, + 0 + )); + assert_eq!(SubtensorModule::get_owning_coldkey_for_hotkey(&hot1), cold1); + + // Register hot2. + register_ok_neuron(netuid, hot2, cold2, 0); + assert_ok!(SubtensorModule::do_become_delegate( + <::RuntimeOrigin>::signed(cold2), + hot2, + 0 + )); + assert_eq!(SubtensorModule::get_owning_coldkey_for_hotkey(&hot2), cold2); + + // Add stake cold1 --> hot1 (non delegation.) + SubtensorModule::add_balance_to_coldkey_account(&cold1, 5); + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(cold1), + hot1, + 1 + )); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(&cold1, &hot1), + 1 + ); + assert_eq!(Balances::free_balance(cold1), 4); + + // Add stake cold2 --> hot1 (is delegation.) + SubtensorModule::add_balance_to_coldkey_account(&cold2, 5); + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(cold2), + hot1, + 1 + )); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(&cold2, &hot1), + 1 + ); + assert_eq!(Balances::free_balance(cold2), 4); + + // Add stake cold1 --> hot2 (non delegation.) + SubtensorModule::add_balance_to_coldkey_account(&cold1, 5); + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(cold1), + hot2, + 1 + )); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(&cold1, &hot2), + 1 + ); + assert_eq!(Balances::free_balance(cold1), 8); + + // Add stake cold2 --> hot2 (is delegation.) + SubtensorModule::add_balance_to_coldkey_account(&cold2, 5); + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(cold2), + hot2, + 1 + )); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(&cold2, &hot2), + 1 + ); + assert_eq!(Balances::free_balance(cold2), 8); + + // Run clear all small nominations when min stake is zero (noop) + SubtensorModule::set_nominator_min_required_stake(0); + SubtensorModule::clear_small_nominations(); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(&cold1, &hot1), + 1 + ); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(&cold1, &hot2), + 1 + ); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(&cold2, &hot1), + 1 + ); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(&cold2, &hot2), + 1 + ); + + // Set min nomination to 10 + let total_cold1_stake_before = TotalColdkeyStake::::get(cold1); + let total_cold2_stake_before = TotalColdkeyStake::::get(cold2); + let total_hot1_stake_before = TotalHotkeyStake::::get(hot1); + let total_hot2_stake_before = TotalHotkeyStake::::get(hot2); + let _ = Stake::::try_get(&hot2, &cold1).unwrap(); // ensure exists before + let _ = Stake::::try_get(&hot1, &cold2).unwrap(); // ensure exists before + let total_stake_before = TotalStake::::get(); + SubtensorModule::set_nominator_min_required_stake(10); + + // Run clear all small nominations (removes delegations under 10) + SubtensorModule::clear_small_nominations(); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(&cold1, &hot1), + 1 + ); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(&cold1, &hot2), + 0 + ); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(&cold2, &hot1), + 0 + ); + assert_eq!( + SubtensorModule::get_stake_for_coldkey_and_hotkey(&cold2, &hot2), + 1 + ); + + // Balances have been added back into accounts. + assert_eq!(Balances::free_balance(cold1), 9); + assert_eq!(Balances::free_balance(cold2), 9); + + // Internal storage is updated + assert_eq!( + TotalColdkeyStake::::get(cold2), + total_cold2_stake_before - 1 + ); + assert_eq!( + TotalHotkeyStake::::get(hot2), + total_hot2_stake_before - 1 + ); + Stake::::try_get(&hot2, &cold1).unwrap_err(); + Stake::::try_get(&hot1, &cold2).unwrap_err(); + assert_eq!( + TotalColdkeyStake::::get(cold1), + total_cold1_stake_before - 1 + ); + assert_eq!( + TotalHotkeyStake::::get(hot1), + total_hot1_stake_before - 1 + ); + Stake::::try_get(&hot2, &cold1).unwrap_err(); + assert_eq!(TotalStake::::get(), total_stake_before - 2); + }); +} + +/// Test that the nominator minimum staking threshold is enforced when stake is added. +#[test] +fn test_add_stake_below_minimum_threshold() { + new_test_ext(0).execute_with(|| { + let netuid: u16 = 1; + let coldkey1 = U256::from(0); + let hotkey1 = U256::from(1); + let coldkey2 = U256::from(2); + let minimum_threshold = 10_000_000; + let amount_below = 50_000; + + // Add balances. + SubtensorModule::add_balance_to_coldkey_account(&coldkey1, 100_000); + SubtensorModule::add_balance_to_coldkey_account(&coldkey2, 100_000); + SubtensorModule::set_nominator_min_required_stake(minimum_threshold); + SubtensorModule::set_target_stakes_per_interval(10); + + // Create network + add_network(netuid, 0, 0); + + // Register the neuron to a new network. + register_ok_neuron(netuid, hotkey1, coldkey1, 0); + assert_ok!(SubtensorModule::become_delegate( + <::RuntimeOrigin>::signed(coldkey1), + hotkey1 + )); + + // Coldkey staking on its own hotkey can stake below min threshold. + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(coldkey1), + hotkey1, + amount_below + )); + + // Nomination stake cannot stake below min threshold. + assert_noop!( + SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(coldkey2), + hotkey1, + amount_below + ), + pallet_subtensor::Error::::NomStakeBelowMinimumThreshold + ); + }); +} + +/// Test that the nominator minimum staking threshold is enforced when stake is removed. +#[test] +fn test_remove_stake_below_minimum_threshold() { + new_test_ext(0).execute_with(|| { + let netuid: u16 = 1; + let coldkey1 = U256::from(0); + let hotkey1 = U256::from(1); + let coldkey2 = U256::from(2); + let initial_balance = 200_000_000; + let initial_stake = 100_000; + let minimum_threshold = 50_000; + let stake_amount_to_remove = 51_000; + + // Add balances. + SubtensorModule::add_balance_to_coldkey_account(&coldkey1, initial_balance); + SubtensorModule::add_balance_to_coldkey_account(&coldkey2, initial_balance); + SubtensorModule::set_nominator_min_required_stake(minimum_threshold); + SubtensorModule::set_target_stakes_per_interval(10); + + // Create network + add_network(netuid, 0, 0); + + // Register the neuron to a new network. + register_ok_neuron(netuid, hotkey1, coldkey1, 0); + assert_ok!(SubtensorModule::become_delegate( + <::RuntimeOrigin>::signed(coldkey1), + hotkey1 + )); + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(coldkey1), + hotkey1, + initial_stake + )); + assert_ok!(SubtensorModule::add_stake( + <::RuntimeOrigin>::signed(coldkey2), + hotkey1, + initial_stake + )); + + // Coldkey staking on its own hotkey can unstake below min threshold. + assert_ok!(SubtensorModule::remove_stake( + <::RuntimeOrigin>::signed(coldkey1), + hotkey1, + stake_amount_to_remove + )); + + // Nomination stake cannot stake below min threshold. + assert_noop!( + SubtensorModule::remove_stake( + <::RuntimeOrigin>::signed(coldkey2), + hotkey1, + stake_amount_to_remove + ), + Error::::NomStakeBelowMinimumThreshold + ); + }) +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 3a507e1aef..c2ea64da1e 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1091,6 +1091,18 @@ impl fn set_weights_min_stake(min_stake: u64) { SubtensorModule::set_weights_min_stake(min_stake); } + + fn clear_small_nominations() { + SubtensorModule::clear_small_nominations(); + } + + fn set_nominator_min_required_stake(min_stake: u64) { + SubtensorModule::set_nominator_min_required_stake(min_stake); + } + + fn get_nominator_min_required_stake() -> u64 { + SubtensorModule::get_nominator_min_required_stake() + } } impl pallet_admin_utils::Config for Runtime {