diff --git a/pallets/subtensor/src/benchmarks.rs b/pallets/subtensor/src/benchmarks.rs index 9dbf4480a5..7433176398 100644 --- a/pallets/subtensor/src/benchmarks.rs +++ b/pallets/subtensor/src/benchmarks.rs @@ -135,48 +135,138 @@ benchmarks! { assert_ok!(Subtensor::::do_burned_registration(RawOrigin::Signed(coldkey.clone()).into(), netuid, hotkey.clone())); }: add_stake(RawOrigin::Signed( coldkey.clone() ), hotkey, netuid, amount) - benchmark_remove_stake{ - let caller: T::AccountId = whitelisted_caller::(); + benchmark_add_stake_aggregate { + let caller: T::AccountId = whitelisted_caller::>(); + let caller_origin = ::RuntimeOrigin::from(RawOrigin::Signed(caller.clone())); + let netuid: u16 = 1; + let version_key: u64 = 1; + let tempo: u16 = 1; + let modality: u16 = 0; + let seed : u32 = 1; + + Subtensor::::init_new_network(netuid, tempo); + + Subtensor::::set_burn(netuid, 1); + Subtensor::::set_network_registration_allowed( netuid, true ); + + Subtensor::::set_max_allowed_uids( netuid, 4096 ); + assert_eq!(Subtensor::::get_max_allowed_uids(netuid), 4096); + + let coldkey: T::AccountId = account("Test", 0, seed); + let hotkey: T::AccountId = account("Alice", 0, seed); + + let amount: u64 = 600000; + let amount_to_be_staked = 1000000000u64; + Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), amount_to_be_staked); + + assert_ok!(Subtensor::::do_burned_registration(RawOrigin::Signed(coldkey.clone()).into(), netuid, hotkey.clone())); + }: add_stake_aggregate(RawOrigin::Signed( coldkey.clone() ), hotkey, netuid, amount) + + benchmark_remove_stake_limit_aggregate{ + let caller: T::AccountId = whitelisted_caller::>(); + let caller_origin = ::RuntimeOrigin::from(RawOrigin::Signed(caller.clone())); let netuid: u16 = 1; let tempo: u16 = 1; + let modality: u16 = 0; let seed : u32 = 1; + // Set our total stake to 1000 TAO Subtensor::::increase_total_stake(1_000_000_000_000); + Subtensor::::init_new_network(netuid, tempo); - SubtokenEnabled::::insert(netuid, true); - Subtensor::::set_network_registration_allowed(netuid, true); - Subtensor::::set_max_allowed_uids(netuid, 4096); + Subtensor::::set_network_registration_allowed( netuid, true ); + + Subtensor::::set_max_allowed_uids( netuid, 4096 ); + assert_eq!(Subtensor::::get_max_allowed_uids(netuid), 4096); + + let coldkey: T::AccountId = account("Test", 0, seed); + let hotkey: T::AccountId = account("Alice", 0, seed); + Subtensor::::set_burn(netuid, 1); + + let limit: u64 = 1_000_000_000; + let tao_reserve = 150_000_000_000_u64; + let alpha_in = 100_000_000_000_u64; + SubnetTAO::::insert(netuid, tao_reserve); + SubnetAlphaIn::::insert(netuid, alpha_in); + + let wallet_bal = 1000000u32.into(); + Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), wallet_bal); + + assert_ok!(Subtensor::::do_burned_registration(RawOrigin::Signed(coldkey.clone()).into(), netuid, hotkey.clone())); + + let u64_staked_amt = 100_000_000_000; + Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), u64_staked_amt); + + assert_ok!(Subtensor::::add_stake(RawOrigin::Signed( coldkey.clone() ).into() , hotkey.clone(), netuid, u64_staked_amt)); + + let amount_unstaked: u64 = 30_000_000_000; + }: remove_stake_limit_aggregate(RawOrigin::Signed( coldkey.clone() ), hotkey.clone(), netuid, amount_unstaked, limit, false) + + benchmark_remove_stake_aggregate{ + let caller: T::AccountId = whitelisted_caller::>(); + let caller_origin = ::RuntimeOrigin::from(RawOrigin::Signed(caller.clone())); + let netuid: u16 = 1; + let version_key: u64 = 1; + let tempo: u16 = 1; + let modality: u16 = 0; + let seed : u32 = 1; + + // Set our total stake to 1000 TAO + Subtensor::::increase_total_stake(1_000_000_000_000); + + Subtensor::::init_new_network(netuid, tempo); + Subtensor::::set_network_registration_allowed( netuid, true ); + + Subtensor::::set_max_allowed_uids( netuid, 4096 ); assert_eq!(Subtensor::::get_max_allowed_uids(netuid), 4096); let coldkey: T::AccountId = account("Test", 0, seed); let hotkey: T::AccountId = account("Alice", 0, seed); + Subtensor::::set_burn(netuid, 1); + + let wallet_bal = 1000000u32.into(); + Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), wallet_bal); + + assert_ok!(Subtensor::::do_burned_registration(RawOrigin::Signed(coldkey.clone()).into(), netuid, hotkey.clone())); + // Stake 10% of our current total staked TAO + let u64_staked_amt = 100_000_000_000; + Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), u64_staked_amt); + + assert_ok!( Subtensor::::add_stake(RawOrigin::Signed( coldkey.clone() ).into() , hotkey.clone(), netuid, u64_staked_amt)); + + let amount_unstaked: u64 = 600000; + }: remove_stake_aggregate(RawOrigin::Signed( coldkey.clone() ), hotkey.clone(), netuid, amount_unstaked) + + benchmark_add_stake_limit_aggregate { + let caller: T::AccountId = whitelisted_caller::>(); + let caller_origin = ::RuntimeOrigin::from(RawOrigin::Signed(caller.clone())); + let netuid: u16 = 1; + let tempo: u16 = 1; + let modality: u16 = 0; + let seed : u32 = 1; + + Subtensor::::init_new_network(netuid, tempo); Subtensor::::set_burn(netuid, 1); - let wallet_bal = 9_999_999_999_999u64.into(); - Subtensor::::add_balance_to_coldkey_account(&coldkey, wallet_bal); + Subtensor::::set_network_registration_allowed( netuid, true ); + Subtensor::::set_max_allowed_uids( netuid, 4096 ); - assert_ok!(Subtensor::::do_burned_registration( - RawOrigin::Signed(coldkey.clone()).into(), - netuid, - hotkey.clone() - )); + let coldkey: T::AccountId = account("Test", 0, seed); + let hotkey: T::AccountId = account("Alice", 0, seed); - let tao_to_stake = 100_000_000_000u64; - Subtensor::::add_balance_to_coldkey_account(&coldkey, tao_to_stake); - assert_ok!( Subtensor::::add_stake( - RawOrigin::Signed(coldkey.clone()).into(), - hotkey.clone(), - netuid, - tao_to_stake - )); + let amount = 900_000_000_000; + let limit: u64 = 6_000_000_000; + let amount_to_be_staked = 440_000_000_000; + Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), amount); - let actual_alpha_minted: u64 = Subtensor::::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); - assert!(actual_alpha_minted > 0, "No alpha minted after add_stake"); + let tao_reserve = 150_000_000_000_u64; + let alpha_in = 100_000_000_000_u64; + SubnetTAO::::insert(netuid, tao_reserve); + SubnetAlphaIn::::insert(netuid, alpha_in); - SubnetAlphaOut::::insert(netuid, actual_alpha_minted * 2); + assert_ok!(Subtensor::::do_burned_registration(RawOrigin::Signed(coldkey.clone()).into(), netuid, hotkey.clone())); + }: add_stake_limit_aggregate(RawOrigin::Signed( coldkey.clone() ), hotkey, netuid, amount_to_be_staked, limit, false) - let amount_unstaked: u64 = actual_alpha_minted / 2; - }: remove_stake(RawOrigin::Signed( coldkey.clone() ), hotkey.clone(), netuid, amount_unstaked) benchmark_serve_axon{ let caller: T::AccountId = whitelisted_caller::>(); diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 38ed1d955b..2905375d9b 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -268,6 +268,80 @@ pub mod pallet { /// Additional information about the subnet pub additional: Vec, } + + /// Data structure for stake related jobs. + #[derive(Encode, Decode, TypeInfo, Clone, PartialEq, Eq, Debug)] + pub enum StakeJob { + /// Represents a job for "add_stake" operation + AddStake { + /// Hotkey account + hotkey: AccountId, + /// Coldkey account + coldkey: AccountId, + /// Subnet ID + netuid: u16, + /// The amount of stake to be added to the hotkey staking account. + stake_to_be_added: u64, + }, + /// Represents a job for "remove_stake" operation + RemoveStake { + /// Hotkey account + hotkey: AccountId, + /// Coldkey account + coldkey: AccountId, + /// Subnet ID + netuid: u16, + /// Alpha value + alpha_unstaked: u64, + }, + /// Represents a job for "add_stake_limit" operation + AddStakeLimit { + /// Coldkey account + coldkey: AccountId, + /// Hotkey account + hotkey: AccountId, + /// Subnet ID + netuid: u16, + /// The amount of stake to be added to the hotkey staking account. + stake_to_be_added: u64, + /// The limit price expressed in units of RAO per one Alpha. + limit_price: u64, + /// Allows partial execution of the amount. If set to false, this becomes + /// fill or kill type or order. + allow_partial: bool, + }, + /// Represents a job for "remove_stake_limit" operation + RemoveStakeLimit { + /// Coldkey account + coldkey: AccountId, + /// Hotkey account + hotkey: AccountId, + /// Subnet ID + netuid: u16, + /// The amount of stake to be added to the hotkey staking account. + alpha_unstaked: u64, + /// The limit price + limit_price: u64, + /// Allows partial execution of the amount. If set to false, this becomes + /// fill or kill type or order. + allow_partial: bool, + }, + /// Represents a job for "unstake_all" operation + UnstakeAll { + /// Coldkey account + coldkey: AccountId, + /// Hotkey account + hotkey: AccountId, + }, + /// Represents a job for "unstake_all_alpha" operation + UnstakeAllAlpha { + /// Coldkey account + coldkey: AccountId, + /// Hotkey account + hotkey: AccountId, + }, + } + /// ============================ /// ==== Staking + Accounts ==== /// ============================ @@ -816,6 +890,21 @@ pub mod pallet { pub type SenateRequiredStakePercentage = StorageValue<_, u64, ValueQuery, DefaultSenateRequiredStakePercentage>; + #[pallet::storage] + pub type StakeJobs = StorageDoubleMap< + _, + Blake2_128Concat, + BlockNumberFor, // first key: current block number + Twox64Concat, + u64, // second key: unique job ID + StakeJob, + OptionQuery, + >; + + #[pallet::storage] + /// Ensures unique IDs for StakeJobs storage map + pub type NextStakeJobId = StorageValue<_, u64, ValueQuery, DefaultZeroU64>; + /// ============================ /// ==== Staking Variables ==== /// ============================ @@ -2057,6 +2146,101 @@ where Self::get_priority_staking(who, hotkey, *amount_unstaked), ) } + Some(Call::add_stake_aggregate { + hotkey, + netuid, + amount_staked, + }) => { + if ColdkeySwapScheduled::::contains_key(who) { + return InvalidTransaction::Custom( + CustomTransactionError::ColdkeyInSwapSchedule.into(), + ) + .into(); + } + // Fully validate the user input + Self::result_to_validity( + Pallet::::validate_add_stake( + who, + hotkey, + *netuid, + *amount_staked, + *amount_staked, + false, + ), + Self::get_priority_staking(who, hotkey, *amount_staked), + ) + } + Some(Call::add_stake_limit_aggregate { + hotkey, + netuid, + amount_staked, + limit_price, + allow_partial, + }) => { + if ColdkeySwapScheduled::::contains_key(who) { + return InvalidTransaction::Custom( + CustomTransactionError::ColdkeyInSwapSchedule.into(), + ) + .into(); + } + + //Calculate the maximum amount that can be executed with price limit + let max_amount = Pallet::::get_max_amount_add(*netuid, *limit_price); + + // Fully validate the user input + Self::result_to_validity( + Pallet::::validate_add_stake( + who, + hotkey, + *netuid, + *amount_staked, + max_amount, + *allow_partial, + ), + Self::get_priority_staking(who, hotkey, *amount_staked), + ) + } + Some(Call::remove_stake_aggregate { + hotkey, + netuid, + amount_unstaked, + }) => { + // Fully validate the user input + Self::result_to_validity( + Pallet::::validate_remove_stake( + who, + hotkey, + *netuid, + *amount_unstaked, + *amount_unstaked, + false, + ), + Self::get_priority_staking(who, hotkey, *amount_unstaked), + ) + } + Some(Call::remove_stake_limit_aggregate { + hotkey, + netuid, + amount_unstaked, + limit_price, + allow_partial, + }) => { + // Calculate the maximum amount that can be executed with price limit + let max_amount = Pallet::::get_max_amount_remove(*netuid, *limit_price); + + // Fully validate the user input + Self::result_to_validity( + Pallet::::validate_remove_stake( + who, + hotkey, + *netuid, + *amount_unstaked, + max_amount, + *allow_partial, + ), + Self::get_priority_staking(who, hotkey, *amount_unstaked), + ) + } Some(Call::move_stake { origin_hotkey, destination_hotkey, diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 4ea03c957b..30041e037f 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -560,6 +560,9 @@ mod dispatches { /// * 'hotkey' (T::AccountId): /// - The associated hotkey account. /// + /// * 'netuid' (u16): + /// - Subnetwork UID + /// /// * 'amount_staked' (u64): /// - The amount of stake to be added to the hotkey staking account. /// @@ -601,6 +604,9 @@ mod dispatches { /// * 'hotkey' (T::AccountId): /// - The associated hotkey account. /// + /// * 'netuid' (u16): + /// - Subnetwork UID + /// /// * 'amount_unstaked' (u64): /// - The amount of stake to be added to the hotkey staking account. /// @@ -1731,6 +1737,9 @@ mod dispatches { /// * 'hotkey' (T::AccountId): /// - The associated hotkey account. /// + /// * 'netuid' (u16): + /// - Subnetwork UID + /// /// * 'amount_staked' (u64): /// - The amount of stake to be added to the hotkey staking account. /// @@ -1792,6 +1801,9 @@ mod dispatches { /// * 'hotkey' (T::AccountId): /// - The associated hotkey account. /// + /// * 'netuid' (u16): + /// - Subnetwork UID + /// /// * 'amount_unstaked' (u64): /// - The amount of stake to be added to the hotkey staking account. /// @@ -2023,5 +2035,289 @@ mod dispatches { ) -> DispatchResult { Self::do_burn_alpha(origin, hotkey, amount, netuid) } + + /// --- Adds stake to a hotkey on a subnet with a price limit. + /// This extrinsic allows to specify the limit price for alpha token + /// at which or better (lower) the staking should execute. + /// + /// In case if slippage occurs and the price shall move beyond the limit + /// price, the staking order may execute only partially or not execute + /// at all. + /// + /// The operation will be delayed. + /// + /// # Args: + /// * 'origin': (Origin): + /// - The signature of the caller's coldkey. + /// + /// * 'hotkey' (T::AccountId): + /// - The associated hotkey account. + /// + /// * 'netuid' (u16): + /// - Subnetwork UID + /// + /// * 'amount_staked' (u64): + /// - The amount of stake to be added to the hotkey staking account. + /// + /// # Event: + /// * StakeAdded; + /// - On the successfully adding stake to a global account. + /// + /// # Raises: + /// * 'NotEnoughBalanceToStake': + /// - Not enough balance on the coldkey to add onto the global account. + /// + /// * 'NonAssociatedColdKey': + /// - The calling coldkey is not associated with this hotkey. + /// + /// * 'BalanceWithdrawalError': + /// - Errors stemming from transaction pallet. + /// + #[pallet::call_index(103)] + #[pallet::weight((Weight::from_parts(99_000_000, 5127) + .saturating_add(T::DbWeight::get().reads(14_u64)) + .saturating_add(T::DbWeight::get().writes(12_u64)), DispatchClass::Normal, Pays::No))] + pub fn add_stake_aggregate( + origin: OriginFor, + hotkey: T::AccountId, + netuid: u16, + amount_staked: u64, + ) -> DispatchResult { + Self::do_add_stake_aggregate(origin, hotkey, netuid, amount_staked) + } + + /// --- Removes stake from a hotkey on a subnet with a price limit. + /// This extrinsic allows to specify the limit price for alpha token + /// at which or better (higher) the staking should execute. + /// + /// In case if slippage occurs and the price shall move beyond the limit + /// price, the staking order may execute only partially or not execute + /// at all. + /// + /// The operation will be delayed. + /// + /// # Args: + /// * 'origin': (Origin): + /// - The signature of the caller's coldkey. + /// + /// * 'hotkey' (T::AccountId): + /// - The associated hotkey account. + /// + /// * 'netuid' (u16): + /// - Subnetwork UID + /// + /// * 'amount_unstaked' (u64): + /// - The amount of stake to be added to the hotkey staking account. + /// + /// # Event: + /// * StakeRemoved; + /// - On the successfully removing stake from the hotkey account. + /// + /// # Raises: + /// * 'NotRegistered': + /// - Thrown if the account we are attempting to unstake from is non existent. + /// + /// * 'NonAssociatedColdKey': + /// - Thrown if the coldkey does not own the hotkey we are unstaking from. + /// + /// * 'NotEnoughStakeToWithdraw': + /// - Thrown if there is not enough stake on the hotkey to withdwraw this amount. + /// + #[pallet::call_index(104)] + #[pallet::weight((Weight::from_parts(129_000_000, 10163) + .saturating_add(T::DbWeight::get().reads(19_u64)) + .saturating_add(T::DbWeight::get().writes(12_u64)), DispatchClass::Normal, Pays::No))] + pub fn remove_stake_aggregate( + origin: OriginFor, + hotkey: T::AccountId, + netuid: u16, + amount_unstaked: u64, + ) -> DispatchResult { + Self::do_remove_stake_aggregate(origin, hotkey, netuid, amount_unstaked) + } + + /// --- Adds stake to a hotkey on a subnet with a price limit. + /// This extrinsic allows to specify the limit price for alpha token + /// at which or better (lower) the staking should execute. + /// + /// In case if slippage occurs and the price shall move beyond the limit + /// price, the staking order may execute only partially or not execute + /// at all. + /// + /// The operation will be delayed. + /// + /// # Args: + /// * 'origin': (Origin): + /// - The signature of the caller's coldkey. + /// + /// * 'hotkey' (T::AccountId): + /// - The associated hotkey account. + /// + /// * 'netuid' (u16): + /// - Subnetwork UID + /// + /// * 'amount_staked' (u64): + /// - The amount of stake to be added to the hotkey staking account. + /// + /// * 'limit_price' (u64): + /// - The limit price expressed in units of RAO per one Alpha. + /// + /// * 'allow_partial' (bool): + /// - Allows partial execution of the amount. If set to false, this becomes + /// fill or kill type or order. + /// + /// # Event: + /// * StakeAdded; + /// - On the successfully adding stake to a global account. + /// + /// # Raises: + /// * 'NotEnoughBalanceToStake': + /// - Not enough balance on the coldkey to add onto the global account. + /// + /// * 'NonAssociatedColdKey': + /// - The calling coldkey is not associated with this hotkey. + /// + /// * 'BalanceWithdrawalError': + /// - Errors stemming from transaction pallet. + /// + #[pallet::call_index(105)] + #[pallet::weight((Weight::from_parts(99_000_000, 5127) + .saturating_add(T::DbWeight::get().reads(14_u64)) + .saturating_add(T::DbWeight::get().writes(12_u64)), DispatchClass::Normal, Pays::No))] + pub fn add_stake_limit_aggregate( + origin: OriginFor, + hotkey: T::AccountId, + netuid: u16, + amount_staked: u64, + limit_price: u64, + allow_partial: bool, + ) -> DispatchResult { + Self::do_add_stake_limit_aggregate( + origin, + hotkey, + netuid, + amount_staked, + limit_price, + allow_partial, + ) + } + + /// --- Removes stake from a hotkey on a subnet with a price limit. + /// This extrinsic allows to specify the limit price for alpha token + /// at which or better (higher) the staking should execute. + /// + /// In case if slippage occurs and the price shall move beyond the limit + /// price, the staking order may execute only partially or not execute + /// at all. + /// + /// The operation will be delayed. + /// + /// # Args: + /// * 'origin': (Origin): + /// - The signature of the caller's coldkey. + /// + /// * 'hotkey' (T::AccountId): + /// - The associated hotkey account. + /// + /// * 'netuid' (u16): + /// - Subnetwork UID + /// + /// * 'amount_unstaked' (u64): + /// - The amount of stake to be added to the hotkey staking account. + /// + /// * 'limit_price' (u64): + /// - The limit price expressed in units of RAO per one Alpha. + /// + /// * 'allow_partial' (bool): + /// - Allows partial execution of the amount. If set to false, this becomes + /// fill or kill type or order. + /// + /// # Event: + /// * StakeRemoved; + /// - On the successfully removing stake from the hotkey account. + /// + /// # Raises: + /// * 'NotRegistered': + /// - Thrown if the account we are attempting to unstake from is non existent. + /// + /// * 'NonAssociatedColdKey': + /// - Thrown if the coldkey does not own the hotkey we are unstaking from. + /// + /// * 'NotEnoughStakeToWithdraw': + /// - Thrown if there is not enough stake on the hotkey to withdwraw this amount. + /// + #[pallet::call_index(106)] + #[pallet::weight((Weight::from_parts(129_000_000, 10163) + .saturating_add(T::DbWeight::get().reads(19_u64)) + .saturating_add(T::DbWeight::get().writes(12_u64)), DispatchClass::Normal, Pays::No))] + pub fn remove_stake_limit_aggregate( + origin: OriginFor, + hotkey: T::AccountId, + netuid: u16, + amount_unstaked: u64, + limit_price: u64, + allow_partial: bool, + ) -> DispatchResult { + Self::do_remove_stake_limit_aggregate( + origin, + hotkey, + netuid, + amount_unstaked, + limit_price, + allow_partial, + ) + } + + /// ---- The implementation for the extrinsic unstake_all_aggregate: Removes all stake from a hotkey account across all subnets and adds it onto a coldkey. + /// + /// The operation will be delayed. + /// + /// # Args: + /// * `origin` - (::Origin): + /// - The signature of the caller's coldkey. + /// + /// * `hotkey` (T::AccountId): + /// - The associated hotkey account. + /// + /// # Event: + /// * StakeRemoved; + /// - On the successfully removing stake from the hotkey account. + /// + /// # Raises: + /// * `NotRegistered`: + /// - Thrown if the account we are attempting to unstake from is non existent. + /// + /// * `NonAssociatedColdKey`: + /// - Thrown if the coldkey does not own the hotkey we are unstaking from. + /// + /// * `NotEnoughStakeToWithdraw`: + /// - Thrown if there is not enough stake on the hotkey to withdraw this amount. + /// + /// * `TxRateLimitExceeded`: + /// - Thrown if key has hit transaction rate limit + #[pallet::call_index(107)] + #[pallet::weight((Weight::from_parts(3_000_000, 0).saturating_add(T::DbWeight::get().writes(1)), DispatchClass::Operational, Pays::No))] + pub fn unstake_all_aggregate(origin: OriginFor, hotkey: T::AccountId) -> DispatchResult { + Self::do_unstake_all_aggregate(origin, hotkey) + } + + /// ---- The implementation for the extrinsic unstake_all_alpha_aggregate: Removes all stake from a hotkey account across all subnets and adds it onto a coldkey. + /// + /// The operation will be delayed. + /// + /// # Args: + /// * `origin` - (::Origin): + /// - The signature of the caller's coldkey. + /// + /// * `hotkey` (T::AccountId): + /// - The associated hotkey account. + #[pallet::call_index(108)] + #[pallet::weight((Weight::from_parts(3_000_000, 0).saturating_add(T::DbWeight::get().writes(1)), DispatchClass::Operational, Pays::No))] + pub fn unstake_all_alpha_aggregate( + origin: OriginFor, + hotkey: T::AccountId, + ) -> DispatchResult { + Self::do_unstake_all_alpha_aggregate(origin, hotkey) + } } } diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index 8c2e863d0e..ccbfed9eff 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -17,6 +17,30 @@ mod events { StakeAdded(T::AccountId, T::AccountId, u64, u64, u16, u64), /// stake has been removed from the hotkey staking account onto the coldkey account. StakeRemoved(T::AccountId, T::AccountId, u64, u64, u16, u64), + /// stake has been transferred from the coldkey account onto the hotkey staking account (at the end of the block) + AggregatedStakeAdded(T::AccountId, T::AccountId, u16, u64), + /// adding aggregated stake has failed + FailedToAddAggregatedStake(T::AccountId, T::AccountId, u16, u64), + /// limited stake has been transferred from the coldkey account onto the hotkey staking account (at the end of the block) + AggregatedLimitedStakeAdded(T::AccountId, T::AccountId, u16, u64, u64, bool), + /// adding limited aggregated stake has failed + FailedToAddAggregatedLimitedStake(T::AccountId, T::AccountId, u16, u64, u64, bool), + /// stake has been removed from the hotkey staking account into the coldkey account (at the end of the block). + AggregatedStakeRemoved(T::AccountId, T::AccountId, u16, u64), + /// removing aggregated stake has failed + FailedToRemoveAggregatedStake(T::AccountId, T::AccountId, u16, u64), + /// aggregated limited stake has been removed from the hotkey staking account into the coldkey account (at the end of the block). + AggregatedLimitedStakeRemoved(T::AccountId, T::AccountId, u16, u64, u64, bool), + /// removing limited aggregated stake has failed + FailedToRemoveAggregatedLimitedStake(T::AccountId, T::AccountId, u16, u64, u64, bool), + /// aggregated unstake_all operation has succeeded + AggregatedUnstakeAllSucceeded(T::AccountId, T::AccountId), + /// aggregated unstake_all operation has failed + AggregatedUnstakeAllFailed(T::AccountId, T::AccountId), + /// aggregated unstake_all_alpha operation has succeeded + AggregatedUnstakeAllAlphaSucceeded(T::AccountId, T::AccountId), + /// aggregated unstake_all_alpha operation has failed + AggregatedUnstakeAllAlphaFailed(T::AccountId, T::AccountId), /// stake has been moved from origin (hotkey, subnet ID) to destination (hotkey, subnet ID) of this amount (in TAO). StakeMoved(T::AccountId, T::AccountId, u16, T::AccountId, u16, u64), /// a caller successfully sets their weights on a subnetwork. diff --git a/pallets/subtensor/src/macros/hooks.rs b/pallets/subtensor/src/macros/hooks.rs index 7b2d1c5ea3..a89885e389 100644 --- a/pallets/subtensor/src/macros/hooks.rs +++ b/pallets/subtensor/src/macros/hooks.rs @@ -34,6 +34,15 @@ mod hooks { } } + // ---- Called on the finalization of this pallet. The code weight must be taken into account prior to the execution of this macro. + // + // # Args: + // * 'n': (BlockNumberFor): + // - The number of the block we are finalizing. + fn on_finalize(block_number: BlockNumberFor) { + Self::do_on_finalize(block_number); + } + fn on_runtime_upgrade() -> frame_support::weights::Weight { // --- Migrate storage let mut weight = frame_support::weights::Weight::from_parts(0, 0); diff --git a/pallets/subtensor/src/staking/add_stake.rs b/pallets/subtensor/src/staking/add_stake.rs index 6ecb8eec46..ad8c356d50 100644 --- a/pallets/subtensor/src/staking/add_stake.rs +++ b/pallets/subtensor/src/staking/add_stake.rs @@ -11,6 +11,9 @@ impl Pallet { /// * 'hotkey' (T::AccountId): /// - The associated hotkey account. /// + /// * 'netuid' (u16): + /// - Subnetwork UID + /// /// * 'stake_to_be_added' (u64): /// - The amount of stake to be added to the hotkey staking account. /// @@ -78,6 +81,154 @@ impl Pallet { Ok(()) } + /// ---- The implementation for the extrinsic add_stake_aggregate: Adds stake to a hotkey account. + /// The operation will be delayed until the end of the block. + /// # Args: + /// * 'origin': (RuntimeOrigin): + /// - The signature of the caller's coldkey. + /// + /// * 'hotkey' (T::AccountId): + /// - The associated hotkey account. + /// + /// * 'netuid' (u16): + /// - Subnetwork UID + /// + /// * 'stake_to_be_added' (u64): + /// - The amount of stake to be added to the hotkey staking account. + /// + /// # Event: + /// * StakeAdded; + /// - On the successfully adding stake to a global account. + /// + /// # Raises: + /// * 'NotEnoughBalanceToStake': + /// - Not enough balance on the coldkey to add onto the global account. + /// + /// * 'NonAssociatedColdKey': + /// - The calling coldkey is not associated with this hotkey. + /// + /// * 'BalanceWithdrawalError': + /// - Errors stemming from transaction pallet. + /// + /// * 'TxRateLimitExceeded': + /// - Thrown if key has hit transaction rate limit + /// + pub fn do_add_stake_aggregate( + origin: T::RuntimeOrigin, + hotkey: T::AccountId, + netuid: u16, + stake_to_be_added: u64, + ) -> dispatch::DispatchResult { + // We check that the transaction is signed by the caller and retrieve the T::AccountId coldkey information. + let coldkey = ensure_signed(origin)?; + + // Consider the weight from on_finalize + if cfg!(feature = "runtime-benchmarks") && !cfg!(test) { + Self::do_add_stake( + crate::dispatch::RawOrigin::Signed(coldkey.clone()).into(), + hotkey.clone(), + netuid, + stake_to_be_added, + )?; + } + + // Save the staking job for the on_finalize + let stake_job = StakeJob::AddStake { + hotkey, + coldkey, + netuid, + stake_to_be_added, + }; + + let stake_job_id = NextStakeJobId::::get(); + let current_blocknumber = >::block_number(); + + StakeJobs::::insert(current_blocknumber, stake_job_id, stake_job); + NextStakeJobId::::set(stake_job_id.saturating_add(1)); + + Ok(()) + } + + /// ---- The implementation for the extrinsic add_stake_limit_aggregate: Adds stake to a hotkey + /// account on a subnet with price limit. The operation will be delayed until the end of the + /// block. + /// + /// # Args: + /// * 'origin': (RuntimeOrigin): + /// - The signature of the caller's coldkey. + /// + /// * 'hotkey' (T::AccountId): + /// - The associated hotkey account. + /// + /// * 'netuid' (u16): + /// - Subnetwork UID + /// + /// * 'stake_to_be_added' (u64): + /// - The amount of stake to be added to the hotkey staking account. + /// + /// * 'limit_price' (u64): + /// - The limit price expressed in units of RAO per one Alpha. + /// + /// * 'allow_partial' (bool): + /// - Allows partial execution of the amount. If set to false, this becomes + /// fill or kill type or order. + /// + /// # Event: + /// * StakeAdded; + /// - On the successfully adding stake to a global account. + /// + /// # Raises: + /// * 'NotEnoughBalanceToStake': + /// - Not enough balance on the coldkey to add onto the global account. + /// + /// * 'NonAssociatedColdKey': + /// - The calling coldkey is not associated with this hotkey. + /// + /// * 'BalanceWithdrawalError': + /// - Errors stemming from transaction pallet. + /// + /// * 'TxRateLimitExceeded': + /// - Thrown if key has hit transaction rate limit + /// + pub fn do_add_stake_limit_aggregate( + origin: T::RuntimeOrigin, + hotkey: T::AccountId, + netuid: u16, + stake_to_be_added: u64, + limit_price: u64, + allow_partial: bool, + ) -> dispatch::DispatchResult { + let coldkey = ensure_signed(origin)?; + + if cfg!(feature = "runtime-benchmarks") && !cfg!(test) { + Self::do_add_stake_limit( + crate::dispatch::RawOrigin::Signed(coldkey.clone()).into(), + hotkey.clone(), + netuid, + stake_to_be_added, + limit_price, + allow_partial, + )?; + } + + let stake_job = StakeJob::AddStakeLimit { + hotkey, + coldkey, + netuid, + stake_to_be_added, + limit_price, + allow_partial, + }; + + let stake_job_id = NextStakeJobId::::get(); + let current_blocknumber = >::block_number(); + + StakeJobs::::insert(current_blocknumber, stake_job_id, stake_job); + NextStakeJobId::::set(stake_job_id.saturating_add(1)); + + Ok(()) + } + /// ---- The implementation for the extrinsic add_stake_limit: Adds stake to a hotkey /// account on a subnet with price limit. /// @@ -88,6 +239,9 @@ impl Pallet { /// * 'hotkey' (T::AccountId): /// - The associated hotkey account. /// + /// * 'netuid' (u16): + /// - Subnetwork UID + /// /// * 'stake_to_be_added' (u64): /// - The amount of stake to be added to the hotkey staking account. /// @@ -133,7 +287,7 @@ impl Pallet { stake_to_be_added ); - // 2. Calcaulate the maximum amount that can be executed with price limit + // 2. Calculate the maximum amount that can be executed with price limit let max_amount = Self::get_max_amount_add(netuid, limit_price); let mut possible_stake = stake_to_be_added; if possible_stake > max_amount { diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index 4b080b395a..3930a923a8 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -11,6 +11,9 @@ impl Pallet { /// * 'hotkey' (T::AccountId): /// - The associated hotkey account. /// + /// * 'netuid' (u16): + /// - Subnetwork UID + /// /// * 'stake_to_be_added' (u64): /// - The amount of stake to be added to the hotkey staking account. /// @@ -87,6 +90,74 @@ impl Pallet { Ok(()) } + /// ---- The implementation for the extrinsic remove_stake_aggregate: Removes stake from a hotkey account and adds it onto a coldkey. + /// The operation will be delayed until the end of the block. + /// # Args: + /// * 'origin': (RuntimeOrigin): + /// - The signature of the caller's coldkey. + /// + /// * 'hotkey' (T::AccountId): + /// - The associated hotkey account. + /// + /// * 'netuid' (u16): + /// - Subnetwork UID + /// + /// * 'stake_to_be_added' (u64): + /// - The amount of stake to be added to the hotkey staking account. + /// + /// # Event: + /// * StakeRemoved; + /// - On the successfully removing stake from the hotkey account. + /// + /// # Raises: + /// * 'NotRegistered': + /// - Thrown if the account we are attempting to unstake from is non existent. + /// + /// * 'NonAssociatedColdKey': + /// - Thrown if the coldkey does not own the hotkey we are unstaking from. + /// + /// * 'NotEnoughStakeToWithdraw': + /// - Thrown if there is not enough stake on the hotkey to withdwraw this amount. + /// + /// * 'TxRateLimitExceeded': + /// - Thrown if key has hit transaction rate limit + /// + pub fn do_remove_stake_aggregate( + origin: T::RuntimeOrigin, + hotkey: T::AccountId, + netuid: u16, + alpha_unstaked: u64, + ) -> dispatch::DispatchResult { + // We check the transaction is signed by the caller and retrieve the T::AccountId coldkey information. + let coldkey = ensure_signed(origin)?; + + // Consider the weight from on_finalize + if cfg!(feature = "runtime-benchmarks") && !cfg!(test) { + Self::do_remove_stake( + crate::dispatch::RawOrigin::Signed(coldkey.clone()).into(), + hotkey.clone(), + netuid, + alpha_unstaked, + )?; + } + + // Save the staking job for the on_finalize + let stake_job = StakeJob::RemoveStake { + hotkey, + coldkey, + netuid, + alpha_unstaked, + }; + + let stake_job_id = NextStakeJobId::::get(); + let current_blocknumber = >::block_number(); + + StakeJobs::::insert(current_blocknumber, stake_job_id, stake_job); + NextStakeJobId::::set(stake_job_id.saturating_add(1)); + + Ok(()) + } + /// ---- The implementation for the extrinsic unstake_all: Removes all stake from a hotkey account across all subnets and adds it onto a coldkey. /// /// # Args: @@ -179,6 +250,41 @@ impl Pallet { Ok(()) } + /// ---- The implementation for the extrinsic unstake_all_aggregate: Removes all stake from a hotkey account across all subnets and adds it onto a coldkey. + /// + /// # Args: + /// * 'origin': (RuntimeOrigin): + /// - The signature of the caller's coldkey. + /// + /// * 'hotkey' (T::AccountId): + /// - The associated hotkey account. + pub fn do_unstake_all_aggregate( + origin: T::RuntimeOrigin, + hotkey: T::AccountId, + ) -> dispatch::DispatchResult { + // We check the transaction is signed by the caller and retrieve the T::AccountId coldkey information. + let coldkey = ensure_signed(origin)?; + + // Consider the weight from on_finalize + if cfg!(feature = "runtime-benchmarks") && !cfg!(test) { + Self::do_unstake_all( + crate::dispatch::RawOrigin::Signed(coldkey.clone()).into(), + hotkey.clone(), + )?; + } + + // Save the unstake_all job for the on_finalize + let stake_job = StakeJob::UnstakeAll { hotkey, coldkey }; + + let stake_job_id = NextStakeJobId::::get(); + let current_blocknumber = >::block_number(); + + StakeJobs::::insert(current_blocknumber, stake_job_id, stake_job); + NextStakeJobId::::set(stake_job_id.saturating_add(1)); + + Ok(()) + } + /// ---- The implementation for the extrinsic unstake_all: Removes all stake from a hotkey account across all subnets and adds it onto a coldkey. /// /// # Args: @@ -284,6 +390,41 @@ impl Pallet { Ok(()) } + /// ---- The implementation for the extrinsic unstake_all_alpha_aggregate: Removes all stake from a hotkey account across all subnets and adds it onto a coldkey. + /// + /// # Args: + /// * 'origin': (RuntimeOrigin): + /// - The signature of the caller's coldkey. + /// + /// * 'hotkey' (T::AccountId): + /// - The associated hotkey account. + pub fn do_unstake_all_alpha_aggregate( + origin: T::RuntimeOrigin, + hotkey: T::AccountId, + ) -> dispatch::DispatchResult { + // We check the transaction is signed by the caller and retrieve the T::AccountId coldkey information. + let coldkey = ensure_signed(origin)?; + + // Consider the weight from on_finalize + if cfg!(feature = "runtime-benchmarks") && !cfg!(test) { + Self::do_unstake_all_alpha( + crate::dispatch::RawOrigin::Signed(coldkey.clone()).into(), + hotkey.clone(), + )?; + } + + // Save the unstake_all_alpha job for the on_finalize + let stake_job = StakeJob::UnstakeAllAlpha { hotkey, coldkey }; + + let stake_job_id = NextStakeJobId::::get(); + let current_blocknumber = >::block_number(); + + StakeJobs::::insert(current_blocknumber, stake_job_id, stake_job); + NextStakeJobId::::set(stake_job_id.saturating_add(1)); + + Ok(()) + } + /// ---- The implementation for the extrinsic remove_stake_limit: Removes stake from /// a hotkey on a subnet with a price limit. /// @@ -298,6 +439,9 @@ impl Pallet { /// * 'hotkey' (T::AccountId): /// - The associated hotkey account. /// + /// * 'netuid' (u16): + /// - Subnetwork UID + /// /// * 'amount_unstaked' (u64): /// - The amount of stake to be added to the hotkey staking account. /// @@ -385,6 +529,90 @@ impl Pallet { Ok(()) } + /// ---- The implementation for the extrinsic remove_stake_limit_aggregate: Removes stake from + /// a hotkey on a subnet with a price limit. + /// + /// In case if slippage occurs and the price shall move beyond the limit + /// price, the staking order may execute only partially or not execute + /// at all. + /// + /// The operation will be delayed until the end of the block. + /// + /// # Args: + /// * 'origin': (Origin): + /// - The signature of the caller's coldkey. + /// + /// * 'hotkey' (T::AccountId): + /// - The associated hotkey account. + /// + /// * 'netuid' (u16): + /// - Subnetwork UID + /// + /// * 'amount_unstaked' (u64): + /// - The amount of stake to be added to the hotkey staking account. + /// + /// * 'limit_price' (u64): + /// - The limit price expressed in units of RAO per one Alpha. + /// + /// * 'allow_partial' (bool): + /// - Allows partial execution of the amount. If set to false, this becomes + /// fill or kill type or order. + /// + /// # Event: + /// * StakeRemoved; + /// - On the successfully removing stake from the hotkey account. + /// + /// # Raises: + /// * 'NotRegistered': + /// - Thrown if the account we are attempting to unstake from is non existent. + /// + /// * 'NonAssociatedColdKey': + /// - Thrown if the coldkey does not own the hotkey we are unstaking from. + /// + /// * 'NotEnoughStakeToWithdraw': + /// - Thrown if there is not enough stake on the hotkey to withdwraw this amount. + /// + pub fn do_remove_stake_limit_aggregate( + origin: T::RuntimeOrigin, + hotkey: T::AccountId, + netuid: u16, + alpha_unstaked: u64, + limit_price: u64, + allow_partial: bool, + ) -> dispatch::DispatchResult { + let coldkey = ensure_signed(origin)?; + + // Consider the weight from on_finalize + if cfg!(feature = "runtime-benchmarks") && !cfg!(test) { + Self::do_remove_stake_limit( + crate::dispatch::RawOrigin::Signed(coldkey.clone()).into(), + hotkey.clone(), + netuid, + alpha_unstaked, + limit_price, + allow_partial, + )?; + } + + let stake_job = StakeJob::RemoveStakeLimit { + hotkey, + coldkey, + netuid, + alpha_unstaked, + limit_price, + allow_partial, + }; + + let stake_job_id = NextStakeJobId::::get(); + let current_blocknumber = >::block_number(); + + StakeJobs::::insert(current_blocknumber, stake_job_id, stake_job); + NextStakeJobId::::set(stake_job_id.saturating_add(1)); + + // Done and ok. + Ok(()) + } + // Returns the maximum amount of RAO that can be executed with price limit pub fn get_max_amount_remove(netuid: u16, limit_price: u64) -> u64 { // Corner case: root and stao diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index 40c65c9bc3..11e040db1d 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -1,6 +1,8 @@ use super::*; +use frame_system::pallet_prelude::BlockNumberFor; use safe_math::*; use share_pool::{SharePool, SharePoolDataOperations}; +use sp_runtime::Saturating; use sp_std::ops::Neg; use substrate_fixed::types::{I64F64, I96F32, U64F64, U96F32, U110F18}; @@ -1156,6 +1158,368 @@ impl Pallet { None => DefaultStakingFee::::get(), } } + + // Process staking job for on_finalize() hook. + pub(crate) fn do_on_finalize(current_block_number: BlockNumberFor) { + // We delay job execution + const DELAY_IN_BLOCKS: u32 = 1u32; + let actual_block_with_delay = current_block_number.saturating_sub(DELAY_IN_BLOCKS.into()); + + let stake_jobs = StakeJobs::::drain_prefix(actual_block_with_delay).collect::>(); + + // Sort jobs by job type + let mut add_stake = vec![]; + let mut remove_stake = vec![]; + let mut add_stake_limit = vec![]; + let mut remove_stake_limit = vec![]; + let mut unstake_all = vec![]; + let mut unstake_all_aplha = vec![]; + + for (_, job) in stake_jobs.into_iter() { + match &job { + StakeJob::AddStake { .. } => add_stake.push(job), + StakeJob::RemoveStake { .. } => remove_stake.push(job), + StakeJob::AddStakeLimit { .. } => add_stake_limit.push(job), + StakeJob::RemoveStakeLimit { .. } => remove_stake_limit.push(job), + StakeJob::UnstakeAll { .. } => unstake_all.push(job), + StakeJob::UnstakeAllAlpha { .. } => unstake_all_aplha.push(job), + } + } + // Reorder jobs based on the previous block hash + let previous_block_hash = >::parent_hash(); + let hash_bytes = previous_block_hash.as_ref(); + let first_byte = hash_bytes.first().expect("hash operation is infallible"); + // Extract the first bit + let altered_order = (first_byte & 0b10000000) != 0; + + // Ascending sort by coldkey + remove_stake_limit.sort_by(|a, b| match (a, b) { + ( + StakeJob::RemoveStakeLimit { coldkey: a_key, .. }, + StakeJob::RemoveStakeLimit { coldkey: b_key, .. }, + ) => { + let direct_order = a_key.cmp(b_key); // ascending + + if altered_order { + direct_order.reverse() + } else { + direct_order + } + } + _ => sp_std::cmp::Ordering::Equal, // unreachable + }); + + remove_stake.sort_by(|a, b| match (a, b) { + ( + StakeJob::RemoveStake { coldkey: a_key, .. }, + StakeJob::RemoveStake { coldkey: b_key, .. }, + ) => { + let direct_order = a_key.cmp(b_key); // ascending + + if altered_order { + direct_order.reverse() + } else { + direct_order + } + } + _ => sp_std::cmp::Ordering::Equal, // unreachable + }); + + unstake_all.sort_by(|a, b| match (a, b) { + ( + StakeJob::UnstakeAll { coldkey: a_key, .. }, + StakeJob::UnstakeAll { coldkey: b_key, .. }, + ) => { + let direct_order = a_key.cmp(b_key); // ascending + + if altered_order { + direct_order.reverse() + } else { + direct_order + } + } + _ => sp_std::cmp::Ordering::Equal, // unreachable + }); + + unstake_all_aplha.sort_by(|a, b| match (a, b) { + ( + StakeJob::UnstakeAllAlpha { coldkey: a_key, .. }, + StakeJob::UnstakeAllAlpha { coldkey: b_key, .. }, + ) => { + let direct_order = a_key.cmp(b_key); // ascending + + if altered_order { + direct_order.reverse() + } else { + direct_order + } + } + _ => sp_std::cmp::Ordering::Equal, // unreachable + }); + + // Descending sort by coldkey + add_stake_limit.sort_by(|a, b| match (a, b) { + ( + StakeJob::AddStakeLimit { coldkey: a_key, .. }, + StakeJob::AddStakeLimit { coldkey: b_key, .. }, + ) => { + let direct_order = b_key.cmp(a_key); // descending + + if altered_order { + direct_order.reverse() + } else { + direct_order + } + } + _ => sp_std::cmp::Ordering::Equal, // unreachable + }); + + add_stake.sort_by(|a, b| match (a, b) { + ( + StakeJob::AddStake { coldkey: a_key, .. }, + StakeJob::AddStake { coldkey: b_key, .. }, + ) => { + let direct_order = b_key.cmp(a_key); // descending + + if altered_order { + direct_order.reverse() + } else { + direct_order + } + } + _ => sp_std::cmp::Ordering::Equal, // unreachable + }); + + // direct job order + let mut job_batches = vec![ + remove_stake_limit, + remove_stake, + unstake_all, + unstake_all_aplha, + add_stake_limit, + add_stake, + ]; + if altered_order { + job_batches.reverse(); + } + + for jobs in job_batches.into_iter() { + for job in jobs.into_iter() { + match job { + StakeJob::RemoveStakeLimit { + hotkey, + coldkey, + netuid, + alpha_unstaked, + limit_price, + allow_partial, + } => { + let result = Self::do_remove_stake_limit( + dispatch::RawOrigin::Signed(coldkey.clone()).into(), + hotkey.clone(), + netuid, + alpha_unstaked, + limit_price, + allow_partial, + ); + + if let Err(err) = result { + log::debug!( + "Failed to remove aggregated limited stake: {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}", + coldkey, + hotkey, + netuid, + alpha_unstaked, + limit_price, + allow_partial, + err + ); + Self::deposit_event(Event::FailedToRemoveAggregatedLimitedStake( + coldkey, + hotkey, + netuid, + alpha_unstaked, + limit_price, + allow_partial, + )); + } else { + Self::deposit_event(Event::AggregatedLimitedStakeRemoved( + coldkey, + hotkey, + netuid, + alpha_unstaked, + limit_price, + allow_partial, + )); + } + } + StakeJob::RemoveStake { + coldkey, + hotkey, + netuid, + alpha_unstaked, + } => { + let result = Self::do_remove_stake( + dispatch::RawOrigin::Signed(coldkey.clone()).into(), + hotkey.clone(), + netuid, + alpha_unstaked, + ); + + if let Err(err) = result { + log::debug!( + "Failed to remove aggregated stake: {:?}, {:?}, {:?}, {:?}, {:?}", + coldkey, + hotkey, + netuid, + alpha_unstaked, + err + ); + Self::deposit_event(Event::FailedToRemoveAggregatedStake( + coldkey, + hotkey, + netuid, + alpha_unstaked, + )); + } else { + Self::deposit_event(Event::AggregatedStakeRemoved( + coldkey, + hotkey, + netuid, + alpha_unstaked, + )); + } + } + StakeJob::UnstakeAll { hotkey, coldkey } => { + let result = Self::do_unstake_all( + dispatch::RawOrigin::Signed(coldkey.clone()).into(), + hotkey.clone(), + ); + + if let Err(err) = result { + log::debug!( + "Failed to unstake all: {:?}, {:?}, {:?}", + coldkey, + hotkey, + err + ); + Self::deposit_event(Event::AggregatedUnstakeAllFailed(coldkey, hotkey)); + } else { + Self::deposit_event(Event::AggregatedUnstakeAllSucceeded( + coldkey, hotkey, + )); + } + } + StakeJob::UnstakeAllAlpha { hotkey, coldkey } => { + let result = Self::do_unstake_all_alpha( + dispatch::RawOrigin::Signed(coldkey.clone()).into(), + hotkey.clone(), + ); + + if let Err(err) = result { + log::debug!( + "Failed to unstake all alpha: {:?}, {:?}, {:?}", + coldkey, + hotkey, + err + ); + Self::deposit_event(Event::AggregatedUnstakeAllAlphaFailed( + coldkey, hotkey, + )); + } else { + Self::deposit_event(Event::AggregatedUnstakeAllAlphaSucceeded( + coldkey, hotkey, + )); + } + } + StakeJob::AddStakeLimit { + hotkey, + coldkey, + netuid, + stake_to_be_added, + limit_price, + allow_partial, + } => { + let result = Self::do_add_stake_limit( + dispatch::RawOrigin::Signed(coldkey.clone()).into(), + hotkey.clone(), + netuid, + stake_to_be_added, + limit_price, + allow_partial, + ); + + if let Err(err) = result { + log::debug!( + "Failed to add aggregated limited stake: {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}", + coldkey, + hotkey, + netuid, + stake_to_be_added, + limit_price, + allow_partial, + err + ); + Self::deposit_event(Event::FailedToAddAggregatedLimitedStake( + coldkey, + hotkey, + netuid, + stake_to_be_added, + limit_price, + allow_partial, + )); + } else { + Self::deposit_event(Event::AggregatedLimitedStakeAdded( + coldkey, + hotkey, + netuid, + stake_to_be_added, + limit_price, + allow_partial, + )); + } + } + StakeJob::AddStake { + hotkey, + coldkey, + netuid, + stake_to_be_added, + } => { + let result = Self::do_add_stake( + dispatch::RawOrigin::Signed(coldkey.clone()).into(), + hotkey.clone(), + netuid, + stake_to_be_added, + ); + + if let Err(err) = result { + log::debug!( + "Failed to add aggregated stake: {:?}, {:?}, {:?}, {:?}, {:?}", + coldkey, + hotkey, + netuid, + stake_to_be_added, + err + ); + Self::deposit_event(Event::FailedToAddAggregatedStake( + coldkey, + hotkey, + netuid, + stake_to_be_added, + )); + } else { + Self::deposit_event(Event::AggregatedStakeAdded( + coldkey, + hotkey, + netuid, + stake_to_be_added, + )); + } + } + } + } + } + } } /////////////////////////////////////////// diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index e9c20a672c..a8ab96be8c 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -579,16 +579,23 @@ pub(crate) fn step_block(n: u16) { #[allow(dead_code)] pub(crate) fn run_to_block(n: u64) { + run_to_block_ext(n, false) +} + +#[allow(dead_code)] +pub(crate) fn run_to_block_ext(n: u64, enable_events: bool) { while System::block_number() < n { Scheduler::on_finalize(System::block_number()); SubtensorModule::on_finalize(System::block_number()); System::on_finalize(System::block_number()); System::set_block_number(System::block_number() + 1); System::on_initialize(System::block_number()); - System::events().iter().for_each(|event| { - log::info!("Event: {:?}", event.event); - }); - System::reset_events(); + if !enable_events { + System::events().iter().for_each(|event| { + log::info!("Event: {:?}", event.event); + }); + System::reset_events(); + } SubtensorModule::on_initialize(System::block_number()); Scheduler::on_initialize(System::block_number()); } diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index a9fa11ba3a..9ab34c608f 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -13,7 +13,6 @@ use frame_support::dispatch::{DispatchClass, DispatchInfo, GetDispatchInfo, Pays use frame_support::sp_runtime::DispatchError; use sp_core::{Get, H256, U256}; use substrate_fixed::types::{I96F32, I110F18, U64F64, U96F32}; - /*********************************************************** staking::add_stake() tests ************************************************************/ @@ -90,6 +89,866 @@ fn test_add_stake_ok_no_emission() { ); }); } +#[test] +fn test_add_stake_aggregate_ok_no_emission() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(533453); + let coldkey_account_id = U256::from(55453); + let amount = DefaultMinStake::::get() * 10; + let fee = DefaultStakingFee::::get(); + + //add network + let netuid: u16 = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + + // Give it some $$$ in his coldkey balance + SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, amount); + + // Check we have zero staked before transfer + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), + 0 + ); + + // Also total stake should be equal to the network initial lock + assert_eq!( + SubtensorModule::get_total_stake(), + SubtensorModule::get_network_min_lock() + ); + + // Transfer to hotkey account, and check if the result is ok + assert_ok!(SubtensorModule::add_stake_aggregate( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount + )); + + // Ensure that extrinsic call doesn't change the stake. + assert_eq!( + SubtensorModule::get_total_stake(), + SubtensorModule::get_network_min_lock() + ); + + // Check for the block delay + run_to_block_ext(2, true); + + // Check that event was not emitted. + assert!(System::events().iter().all(|e| { + !matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedStakeAdded(..)) + ) + })); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + + // Check if stake has increased + assert_abs_diff_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), + amount - fee, + epsilon = amount / 1000, + ); + + // Check if balance has decreased + assert_eq!(SubtensorModule::get_coldkey_balance(&coldkey_account_id), 1); + + // Check if total stake has increased accordingly. + assert_eq!( + SubtensorModule::get_total_stake(), + amount + SubtensorModule::get_network_min_lock() + ); + + // Check that event was emitted. + assert!(System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::StakeAdded(..)) + ) + })); + + // Check that event was emitted. + assert!(System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedStakeAdded(..)) + ) + })); + }); +} + +#[test] +fn test_add_stake_aggregate_failed() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(533453); + let coldkey_account_id = U256::from(55453); + let amount = DefaultMinStake::::get() * 100; + //add network + let netuid: u16 = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + + // Transfer to hotkey account, and check if the result is ok + assert_ok!(SubtensorModule::add_stake_aggregate( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount + )); + + // Check for the block delay + run_to_block_ext(2, true); + + // Check that event was not emitted. + assert!(System::events().iter().all(|e| { + !matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::FailedToAddAggregatedStake(..)) + ) + })); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + + // Check that event was emitted. + assert!(System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::FailedToAddAggregatedStake(..)) + ) + })); + }); +} + +#[test] +fn test_verify_aggregated_stake_order() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(533453); + let coldkey_account_id = U256::from(55453); + let amount = 900_000_000_000; // over the maximum + + // add network + let netuid1: u16 = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + let netuid2: u16 = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + let netuid3: u16 = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + let netuid4: u16 = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + let netuid5: u16 = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + let netuid6: u16 = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + + let tao_reserve: U96F32 = U96F32::from_num(150_000_000_000_u64); + let alpha_in: U96F32 = U96F32::from_num(100_000_000_000_u64); + + for netuid in [netuid1, netuid3, netuid3, netuid4, netuid5, netuid6] { + SubnetTAO::::insert(netuid, tao_reserve.to_num::()); + SubnetAlphaIn::::insert(netuid, alpha_in.to_num::()); + } + + // Give it some $$$ in his coldkey balance + SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, 6 * amount); + // Give the neuron some stake to remove + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid3, + amount, + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid4, + amount, + ); + + let limit_price = 6_000_000_000u64; + + // Add stake with slippage safety and check if the result is ok + assert_ok!(SubtensorModule::remove_stake_aggregate( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid3, + amount + )); + + assert_ok!(SubtensorModule::remove_stake_limit_aggregate( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid4, + amount, + limit_price, + true + )); + + assert_ok!(SubtensorModule::add_stake_aggregate( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid1, + amount, + )); + + assert_ok!(SubtensorModule::add_stake_limit_aggregate( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid2, + amount, + limit_price, + true + )); + + assert_ok!(SubtensorModule::unstake_all_aggregate( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + )); + + assert_ok!(SubtensorModule::unstake_all_alpha_aggregate( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + )); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + + let add_stake_position = System::events() + .iter() + .position(|e| { + if let RuntimeEvent::SubtensorModule(Event::AggregatedStakeAdded(.., netuid, _)) = + e.event + { + netuid == netuid1 + } else { + false + } + }) + .expect("Stake event must be present in the event log."); + + let add_stake_limit_position = System::events() + .iter() + .position(|e| { + if let RuntimeEvent::SubtensorModule(Event::AggregatedLimitedStakeAdded( + _, + _, + netuid, + _, + _, + _, + )) = e.event + { + netuid == netuid2 + } else { + false + } + }) + .expect("Stake event must be present in the event log."); + + let remove_stake_position = System::events() + .iter() + .position(|e| { + if let RuntimeEvent::SubtensorModule(Event::AggregatedStakeRemoved(.., netuid, _)) = + e.event + { + netuid == netuid3 + } else { + false + } + }) + .expect("Stake event must be present in the event log."); + + let remove_stake_limit_position = System::events() + .iter() + .position(|e| { + if let RuntimeEvent::SubtensorModule(Event::AggregatedLimitedStakeRemoved( + .., + netuid, + _, + _, + _, + )) = e.event + { + netuid == netuid4 + } else { + false + } + }) + .expect("Stake event must be present in the event log."); + + let unstake_all_position = System::events() + .iter() + .position(|e| { + matches!( + e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedUnstakeAllSucceeded(..)) + ) + }) + .expect("Stake event must be present in the event log."); + + let unstake_all_alpha_position = System::events() + .iter() + .position(|e| { + matches!( + e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedUnstakeAllAlphaSucceeded(..)) + ) + }) + .expect("Stake event must be present in the event log."); + + // Check events order + assert!(remove_stake_limit_position < remove_stake_position); + assert!(remove_stake_position < unstake_all_position); + assert!(unstake_all_position < unstake_all_alpha_position); + assert!(add_stake_position > unstake_all_alpha_position); + assert!(add_stake_limit_position < add_stake_position); + }); +} + +#[test] +#[allow(clippy::indexing_slicing)] +fn test_verify_aggregated_stake_order_reversed() { + new_test_ext(1).execute_with(|| { + let amount = 900_000_000_000; // over the maximum + let limit_price = 6_000_000_000u64; + + // Coldkeys and hotkeys + let coldkeys = vec![ + U256::from(100), // add_stake + U256::from(200), // add_stake_limit + U256::from(300), // remove_stake + U256::from(400), // remove_stake_limit + U256::from(500), // unstake_all + U256::from(600), // unstake_all_alpha + ]; + + let hotkeys = (1..=6).map(U256::from).collect::>(); + + let netuids: Vec<_> = hotkeys + .iter() + .zip(coldkeys.iter()) + .map(|(h, c)| add_dynamic_network(h, c)) + .collect(); + + let tao_reserve = U96F32::from_num(150_000_000_000u64); + let alpha_in = U96F32::from_num(100_000_000_000u64); + + for netuid in &netuids { + SubnetTAO::::insert(*netuid, tao_reserve.to_num::()); + SubnetAlphaIn::::insert(*netuid, alpha_in.to_num::()); + } + + for coldkey in &coldkeys { + SubtensorModule::add_balance_to_coldkey_account(coldkey, amount); + } + + for ((hotkey, coldkey), netuid) in hotkeys.iter().zip(coldkeys.iter()).zip(netuids.iter()) { + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + hotkey, coldkey, *netuid, amount, + ); + } + + // Add stake with slippage safety and check if the result is ok + assert_ok!(SubtensorModule::remove_stake_aggregate( + RuntimeOrigin::signed(coldkeys[2]), + hotkeys[2], + netuids[2], + amount + )); + + assert_ok!(SubtensorModule::remove_stake_limit_aggregate( + RuntimeOrigin::signed(coldkeys[3]), + hotkeys[3], + netuids[3], + amount, + limit_price, + true + )); + + assert_ok!(SubtensorModule::add_stake_aggregate( + RuntimeOrigin::signed(coldkeys[0]), + hotkeys[0], + netuids[0], + amount, + )); + + assert_ok!(SubtensorModule::add_stake_limit_aggregate( + RuntimeOrigin::signed(coldkeys[1]), + hotkeys[1], + netuids[1], + amount, + limit_price, + true + )); + + assert_ok!(SubtensorModule::unstake_all_aggregate( + RuntimeOrigin::signed(coldkeys[4]), + hotkeys[4], + )); + + assert_ok!(SubtensorModule::unstake_all_alpha_aggregate( + RuntimeOrigin::signed(coldkeys[5]), + hotkeys[5], + )); + + // Enable on_finalize code to run + run_to_block_ext(2, false); + // Reorder jobs based on the previous block hash + let mut parent_hash = >::parent_hash(); + parent_hash.as_mut()[0] = 0b10000000; + >::set_parent_hash(parent_hash); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + + let add_stake_position = System::events() + .iter() + .position(|e| { + if let RuntimeEvent::SubtensorModule(Event::AggregatedStakeAdded(.., netuid, _)) = + e.event + { + netuid == netuids[0] + } else { + false + } + }) + .expect("Stake event must be present in the event log."); + + let add_stake_limit_position = System::events() + .iter() + .position(|e| { + if let RuntimeEvent::SubtensorModule(Event::AggregatedLimitedStakeAdded( + _, + _, + netuid, + _, + _, + _, + )) = e.event + { + netuid == netuids[1] + } else { + false + } + }) + .expect("Stake event must be present in the event log."); + + let remove_stake_position = System::events() + .iter() + .position(|e| { + if let RuntimeEvent::SubtensorModule(Event::AggregatedStakeRemoved(.., netuid, _)) = + e.event + { + netuid == netuids[2] + } else { + false + } + }) + .expect("Stake event must be present in the event log."); + + let remove_stake_limit_position = System::events() + .iter() + .position(|e| { + if let RuntimeEvent::SubtensorModule(Event::AggregatedLimitedStakeRemoved( + .., + netuid, + _, + _, + _, + )) = e.event + { + netuid == netuids[3] + } else { + false + } + }) + .expect("Stake event must be present in the event log."); + + let unstake_all_position = System::events() + .iter() + .position(|e| { + matches!( + e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedUnstakeAllSucceeded(..)) + ) + }) + .expect("Stake event must be present in the event log."); + + let unstake_all_alpha_position = System::events() + .iter() + .position(|e| { + matches!( + e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedUnstakeAllAlphaSucceeded(..)) + ) + }) + .expect("Stake event must be present in the event log."); + + // Check events order + assert!(add_stake_limit_position > add_stake_position); + assert!(add_stake_position < unstake_all_alpha_position); + assert!(unstake_all_position > unstake_all_alpha_position); + assert!(remove_stake_position > unstake_all_position); + assert!(remove_stake_limit_position > remove_stake_position); + }); +} + +#[test] +#[allow(clippy::indexing_slicing)] +fn test_verify_all_job_type_sort_by_coldkey() { + new_test_ext(1).execute_with(|| { + let amount = 1_000_000_000_000; + let limit_price = 6_000_000_000u64; + + // Coldkeys and hotkeys + let coldkeys = vec![ + U256::from(100), // add_stake + U256::from(200), // add_stake + U256::from(300), // add_stake_limit + U256::from(400), // add_stake_limit + U256::from(500), // remove_stake + U256::from(600), // remove_stake + U256::from(700), // remove_stake_limit + U256::from(800), // remove_stake_limit + U256::from(900), // unstake_all + U256::from(1000), // unstake_all + U256::from(1100), // unstake_all_alpha + U256::from(1200), // unstake_all_alpha + ]; + + let hotkeys = (1..=12).map(U256::from).collect::>(); + + let netuids: Vec<_> = hotkeys + .iter() + .zip(coldkeys.iter()) + .map(|(h, c)| add_dynamic_network(h, c)) + .collect(); + + let tao_reserve = U96F32::from_num(150_000_000_000u64); + let alpha_in = U96F32::from_num(100_000_000_000u64); + + for netuid in &netuids { + SubnetTAO::::insert(*netuid, tao_reserve.to_num::()); + SubnetAlphaIn::::insert(*netuid, alpha_in.to_num::()); + } + + for coldkey in &coldkeys { + SubtensorModule::add_balance_to_coldkey_account(coldkey, amount); + } + + for ((hotkey, coldkey), netuid) in hotkeys.iter().zip(coldkeys.iter()).zip(netuids.iter()) { + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + hotkey, coldkey, *netuid, amount, + ); + } + + // === Submit all job types === + + assert_ok!(SubtensorModule::add_stake_aggregate( + RuntimeOrigin::signed(coldkeys[0]), + hotkeys[0], + netuids[0], + amount + )); + assert_ok!(SubtensorModule::add_stake_aggregate( + RuntimeOrigin::signed(coldkeys[1]), + hotkeys[1], + netuids[1], + amount + )); + + assert_ok!(SubtensorModule::add_stake_limit_aggregate( + RuntimeOrigin::signed(coldkeys[2]), + hotkeys[2], + netuids[2], + amount, + limit_price, + true + )); + assert_ok!(SubtensorModule::add_stake_limit_aggregate( + RuntimeOrigin::signed(coldkeys[3]), + hotkeys[3], + netuids[3], + amount, + limit_price, + true + )); + + assert_ok!(SubtensorModule::remove_stake_aggregate( + RuntimeOrigin::signed(coldkeys[4]), + hotkeys[4], + netuids[4], + amount + )); + assert_ok!(SubtensorModule::remove_stake_aggregate( + RuntimeOrigin::signed(coldkeys[5]), + hotkeys[5], + netuids[5], + amount + )); + + assert_ok!(SubtensorModule::remove_stake_limit_aggregate( + RuntimeOrigin::signed(coldkeys[6]), + hotkeys[6], + netuids[6], + amount, + limit_price, + true + )); + assert_ok!(SubtensorModule::remove_stake_limit_aggregate( + RuntimeOrigin::signed(coldkeys[7]), + hotkeys[7], + netuids[7], + amount, + limit_price, + true + )); + + assert_ok!(SubtensorModule::unstake_all_aggregate( + RuntimeOrigin::signed(coldkeys[8]), + hotkeys[8], + )); + assert_ok!(SubtensorModule::unstake_all_aggregate( + RuntimeOrigin::signed(coldkeys[9]), + hotkeys[9], + )); + + assert_ok!(SubtensorModule::unstake_all_alpha_aggregate( + RuntimeOrigin::signed(coldkeys[10]), + hotkeys[10], + )); + assert_ok!(SubtensorModule::unstake_all_alpha_aggregate( + RuntimeOrigin::signed(coldkeys[11]), + hotkeys[11], + )); + + // Finalize block + run_to_block_ext(3, true); + + // === Collect coldkeys by event type === + let mut add_coldkeys = vec![]; + let mut add_limit_coldkeys = vec![]; + let mut remove_coldkeys = vec![]; + let mut remove_limit_coldkeys = vec![]; + let mut unstake_all_coldkeys = vec![]; + let mut unstake_all_alpha_coldkeys = vec![]; + + for event in System::events().iter().map(|e| &e.event) { + match event { + RuntimeEvent::SubtensorModule(Event::AggregatedStakeAdded(coldkey, ..)) => { + add_coldkeys.push(*coldkey); + } + RuntimeEvent::SubtensorModule(Event::AggregatedLimitedStakeAdded(coldkey, ..)) => { + add_limit_coldkeys.push(*coldkey); + } + RuntimeEvent::SubtensorModule(Event::AggregatedStakeRemoved(coldkey, ..)) => { + remove_coldkeys.push(*coldkey); + } + RuntimeEvent::SubtensorModule(Event::AggregatedLimitedStakeRemoved( + coldkey, + .., + )) => { + remove_limit_coldkeys.push(*coldkey); + } + RuntimeEvent::SubtensorModule(Event::AggregatedUnstakeAllSucceeded(coldkey, _)) => { + unstake_all_coldkeys.push(*coldkey); + } + RuntimeEvent::SubtensorModule(Event::AggregatedUnstakeAllAlphaSucceeded( + coldkey, + _, + )) => { + unstake_all_alpha_coldkeys.push(*coldkey); + } + _ => {} + } + } + + // === Assertions === + assert_eq!(add_coldkeys, vec![coldkeys[1], coldkeys[0]]); // descending + assert_eq!(add_limit_coldkeys, vec![coldkeys[3], coldkeys[2]]); // descending + assert_eq!(remove_coldkeys, vec![coldkeys[4], coldkeys[5]]); // ascending + assert_eq!(remove_limit_coldkeys, vec![coldkeys[6], coldkeys[7]]); // ascending + assert_eq!(unstake_all_coldkeys, vec![coldkeys[8], coldkeys[9]]); // ascending + assert_eq!(unstake_all_alpha_coldkeys, vec![coldkeys[10], coldkeys[11]]); // ascending + }); +} + +#[test] +#[allow(clippy::indexing_slicing)] +fn test_verify_all_job_type_sort_by_coldkey_reverse_order() { + new_test_ext(1).execute_with(|| { + let amount = 1_000_000_000_000; + let limit_price = 6_000_000_000u64; + + // Coldkeys and hotkeys + let coldkeys = vec![ + U256::from(100), // add_stake + U256::from(200), // add_stake + U256::from(300), // add_stake_limit + U256::from(400), // add_stake_limit + U256::from(500), // remove_stake + U256::from(600), // remove_stake + U256::from(700), // remove_stake_limit + U256::from(800), // remove_stake_limit + U256::from(900), // unstake_all + U256::from(1000), // unstake_all + U256::from(1100), // unstake_all_alpha + U256::from(1200), // unstake_all_alpha + ]; + + let hotkeys = (1..=12).map(U256::from).collect::>(); + + let netuids: Vec<_> = hotkeys + .iter() + .zip(coldkeys.iter()) + .map(|(h, c)| add_dynamic_network(h, c)) + .collect(); + + let tao_reserve = U96F32::from_num(150_000_000_000u64); + let alpha_in = U96F32::from_num(100_000_000_000u64); + + for netuid in &netuids { + SubnetTAO::::insert(*netuid, tao_reserve.to_num::()); + SubnetAlphaIn::::insert(*netuid, alpha_in.to_num::()); + } + + for coldkey in &coldkeys { + SubtensorModule::add_balance_to_coldkey_account(coldkey, amount); + } + + for ((hotkey, coldkey), netuid) in hotkeys.iter().zip(coldkeys.iter()).zip(netuids.iter()) { + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + hotkey, coldkey, *netuid, amount, + ); + } + + // === Submit all job types === + + assert_ok!(SubtensorModule::add_stake_aggregate( + RuntimeOrigin::signed(coldkeys[0]), + hotkeys[0], + netuids[0], + amount + )); + assert_ok!(SubtensorModule::add_stake_aggregate( + RuntimeOrigin::signed(coldkeys[1]), + hotkeys[1], + netuids[1], + amount + )); + + assert_ok!(SubtensorModule::add_stake_limit_aggregate( + RuntimeOrigin::signed(coldkeys[2]), + hotkeys[2], + netuids[2], + amount, + limit_price, + true + )); + assert_ok!(SubtensorModule::add_stake_limit_aggregate( + RuntimeOrigin::signed(coldkeys[3]), + hotkeys[3], + netuids[3], + amount, + limit_price, + true + )); + + assert_ok!(SubtensorModule::remove_stake_aggregate( + RuntimeOrigin::signed(coldkeys[4]), + hotkeys[4], + netuids[4], + amount + )); + assert_ok!(SubtensorModule::remove_stake_aggregate( + RuntimeOrigin::signed(coldkeys[5]), + hotkeys[5], + netuids[5], + amount + )); + + assert_ok!(SubtensorModule::remove_stake_limit_aggregate( + RuntimeOrigin::signed(coldkeys[6]), + hotkeys[6], + netuids[6], + amount, + limit_price, + true + )); + assert_ok!(SubtensorModule::remove_stake_limit_aggregate( + RuntimeOrigin::signed(coldkeys[7]), + hotkeys[7], + netuids[7], + amount, + limit_price, + true + )); + + assert_ok!(SubtensorModule::unstake_all_aggregate( + RuntimeOrigin::signed(coldkeys[8]), + hotkeys[8], + )); + assert_ok!(SubtensorModule::unstake_all_aggregate( + RuntimeOrigin::signed(coldkeys[9]), + hotkeys[9], + )); + + assert_ok!(SubtensorModule::unstake_all_alpha_aggregate( + RuntimeOrigin::signed(coldkeys[10]), + hotkeys[10], + )); + assert_ok!(SubtensorModule::unstake_all_alpha_aggregate( + RuntimeOrigin::signed(coldkeys[11]), + hotkeys[11], + )); + + // Reorder jobs based on the previous block hash + let mut parent_hash = >::parent_hash(); + parent_hash.as_mut()[0] = 0b10000000; + >::set_parent_hash(parent_hash); + + // Finalize block + run_to_block_ext(3, true); + + // === Collect coldkeys by event type === + let mut add_coldkeys = vec![]; + let mut add_limit_coldkeys = vec![]; + let mut remove_coldkeys = vec![]; + let mut remove_limit_coldkeys = vec![]; + let mut unstake_all_coldkeys = vec![]; + let mut unstake_all_alpha_coldkeys = vec![]; + + for event in System::events().iter().map(|e| &e.event) { + match event { + RuntimeEvent::SubtensorModule(Event::AggregatedStakeAdded(coldkey, ..)) => { + add_coldkeys.push(*coldkey); + } + RuntimeEvent::SubtensorModule(Event::AggregatedLimitedStakeAdded(coldkey, ..)) => { + add_limit_coldkeys.push(*coldkey); + } + RuntimeEvent::SubtensorModule(Event::AggregatedStakeRemoved(coldkey, ..)) => { + remove_coldkeys.push(*coldkey); + } + RuntimeEvent::SubtensorModule(Event::AggregatedLimitedStakeRemoved( + coldkey, + .., + )) => { + remove_limit_coldkeys.push(*coldkey); + } + RuntimeEvent::SubtensorModule(Event::AggregatedUnstakeAllSucceeded(coldkey, _)) => { + unstake_all_coldkeys.push(*coldkey); + } + RuntimeEvent::SubtensorModule(Event::AggregatedUnstakeAllAlphaSucceeded( + coldkey, + _, + )) => { + unstake_all_alpha_coldkeys.push(*coldkey); + } + _ => {} + } + } + + // === Assertions === + assert_eq!(add_coldkeys, vec![coldkeys[0], coldkeys[1]]); // ascending (reversed) + assert_eq!(add_limit_coldkeys, vec![coldkeys[2], coldkeys[3]]); // ascending (reversed) + assert_eq!(remove_coldkeys, vec![coldkeys[5], coldkeys[4]]); // descending (reversed) + assert_eq!(remove_limit_coldkeys, vec![coldkeys[7], coldkeys[6]]); // descending (reversed) + assert_eq!(unstake_all_coldkeys, vec![coldkeys[9], coldkeys[8]]); // descending (reversed) + assert_eq!(unstake_all_alpha_coldkeys, vec![coldkeys[11], coldkeys[10]]); // descending (reversed) + }); +} #[test] fn test_dividends_with_run_to_block() { @@ -422,6 +1281,145 @@ fn test_remove_stake_ok_no_emission() { }); } +#[test] +fn test_remove_stake_aggregate_ok_no_emission() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1); + let subnet_owner_hotkey = U256::from(2); + let coldkey_account_id = U256::from(4343); + let hotkey_account_id = U256::from(4968585); + let amount = DefaultMinStake::::get() * 10; + let netuid: u16 = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); + + // Some basic assertions + assert_eq!( + SubtensorModule::get_total_stake(), + SubtensorModule::get_network_min_lock() + ); + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), + 0 + ); + assert_eq!(SubtensorModule::get_coldkey_balance(&coldkey_account_id), 0); + + // Give the neuron some stake to remove + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + amount, + ); + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), + amount + ); + + // Add subnet TAO for the equivalent amount added at price + let amount_tao = + U96F32::saturating_from_num(amount) * SubtensorModule::get_alpha_price(netuid); + SubnetTAO::::mutate(netuid, |v| *v += amount_tao.saturating_to_num::()); + TotalStake::::mutate(|v| *v += amount_tao.saturating_to_num::()); + + // Do the magic + assert_ok!(SubtensorModule::remove_stake_aggregate( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount + )); + + // Check for the block delay + run_to_block_ext(2, true); + + // Check that event was not emitted. + assert!(System::events().iter().all(|e| { + !matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedStakeRemoved(..)) + ) + })); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + + let fee = SubtensorModule::calculate_staking_fee( + Some((&hotkey_account_id, netuid)), + &coldkey_account_id, + None, + &coldkey_account_id, + U96F32::saturating_from_num(amount), + ); + + // we do not expect the exact amount due to slippage + assert!(SubtensorModule::get_coldkey_balance(&coldkey_account_id) > amount / 10 * 9 - fee); + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey_account_id), + 0 + ); + assert_eq!( + SubtensorModule::get_total_stake(), + SubtensorModule::get_network_min_lock() + fee + ); + + // Check that event was emitted. + assert!(System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::StakeRemoved(..)) + ) + })); + // Check that event was emitted. + assert!(System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedStakeRemoved(..)) + ) + })); + }); +} +#[test] +fn test_remove_stake_aggregate_fail() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1); + let subnet_owner_hotkey = U256::from(2); + let coldkey_account_id = U256::from(4343); + let hotkey_account_id = U256::from(4968585); + let amount = DefaultMinStake::::get() * 10; + let netuid: u16 = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); + + assert_ok!(SubtensorModule::remove_stake_aggregate( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount + )); + + // Check for the block delay + run_to_block_ext(2, true); + + // Check that event was not emitted. + assert!(System::events().iter().all(|e| { + !matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::FailedToRemoveAggregatedStake(..)) + ) + })); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + + // Check that event was emitted. + assert!(System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::FailedToRemoveAggregatedStake(..)) + ) + })); + }); +} + #[test] fn test_remove_stake_amount_too_low() { new_test_ext(1).execute_with(|| { @@ -3635,7 +4633,74 @@ fn test_max_amount_move_dynamic_dynamic() { } #[test] -fn test_add_stake_limit_ok() { +fn test_add_stake_limit_ok() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(533453); + let coldkey_account_id = U256::from(55453); + let amount = 900_000_000_000; // over the maximum + let fee = DefaultStakingFee::::get(); + + // add network + let netuid: u16 = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + + // Forse-set alpha in and tao reserve to make price equal 1.5 + let tao_reserve: U96F32 = U96F32::from_num(150_000_000_000_u64); + let alpha_in: U96F32 = U96F32::from_num(100_000_000_000_u64); + SubnetTAO::::insert(netuid, tao_reserve.to_num::()); + SubnetAlphaIn::::insert(netuid, alpha_in.to_num::()); + let current_price: U96F32 = U96F32::from_num(SubtensorModule::get_alpha_price(netuid)); + assert_eq!(current_price, U96F32::from_num(1.5)); + + // Give it some $$$ in his coldkey balance + SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, amount); + + // Setup limit price so that it doesn't peak above 4x of current price + // The amount that can be executed at this price is 450 TAO only + // Alpha produced will be equal to 75 = 450*100/(450+150) + let limit_price = 6_000_000_000; + let expected_executed_stake = 75_000_000_000; + + // Add stake with slippage safety and check if the result is ok + assert_ok!(SubtensorModule::add_stake_limit( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount, + limit_price, + true + )); + + // Check if stake has increased only by 75 Alpha + assert_abs_diff_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid + ), + expected_executed_stake - fee, + epsilon = expected_executed_stake / 1000, + ); + + // Check that 450 TAO balance still remains free on coldkey + assert_abs_diff_eq!( + SubtensorModule::get_coldkey_balance(&coldkey_account_id), + 450_000_000_000, + epsilon = 10_000 + ); + + // Check that price has updated to ~24 = (150+450) / (100 - 75) + let exp_price = U96F32::from_num(24.0); + let current_price: U96F32 = U96F32::from_num(SubtensorModule::get_alpha_price(netuid)); + assert_abs_diff_eq!( + exp_price.to_num::(), + current_price.to_num::(), + epsilon = 0.0001, + ); + }); +} + +#[test] +fn test_add_stake_limit_aggregate_ok() { new_test_ext(1).execute_with(|| { let hotkey_account_id = U256::from(533453); let coldkey_account_id = U256::from(55453); @@ -3663,7 +4728,7 @@ fn test_add_stake_limit_ok() { let expected_executed_stake = 75_000_000_000; // Add stake with slippage safety and check if the result is ok - assert_ok!(SubtensorModule::add_stake_limit( + assert_ok!(SubtensorModule::add_stake_limit_aggregate( RuntimeOrigin::signed(coldkey_account_id), hotkey_account_id, netuid, @@ -3672,6 +4737,20 @@ fn test_add_stake_limit_ok() { true )); + // Check for the block delay + run_to_block_ext(2, true); + + // Check that event was not emitted. + assert!(System::events().iter().all(|e| { + !matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedLimitedStakeAdded(..)) + ) + })); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + // Check if stake has increased only by 75 Alpha assert_abs_diff_eq!( SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -3698,6 +4777,64 @@ fn test_add_stake_limit_ok() { current_price.to_num::(), epsilon = 0.0001, ); + + // Check that event was emitted. + assert!(System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::StakeAdded(..)) + ) + })); + // Check that event was emitted. + assert!(System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedLimitedStakeAdded(..)) + ) + })); + }); +} + +#[test] +fn test_add_stake_limit_aggregate_fail() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(533453); + let coldkey_account_id = U256::from(55453); + let amount = 900_000_000_000; + let limit_price = 6_000_000_000; + // add network + let netuid: u16 = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + + assert_ok!(SubtensorModule::add_stake_limit_aggregate( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount, + limit_price, + true + )); + + // Check for the block delay + run_to_block_ext(2, true); + + // Check that event was not emitted. + assert!(System::events().iter().all(|e| { + !matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::FailedToAddAggregatedLimitedStake(..)) + ) + })); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + + // Check that event was emitted. + assert!(System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::FailedToAddAggregatedLimitedStake(..)) + ) + })); }); } @@ -3815,6 +4952,149 @@ fn test_remove_stake_limit_ok() { }); } +#[test] +fn test_remove_stake_limit_aggregate_ok() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(533453); + let coldkey_account_id = U256::from(55453); + let stake_amount = 300_000_000_000; + let unstake_amount = 150_000_000_000; + let fee = DefaultStakingFee::::get(); + + // add network + let netuid: u16 = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + + // Give the neuron some stake to remove + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + stake_amount, + ); + let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + ); + + // Forse-set alpha in and tao reserve to make price equal 1.5 + let tao_reserve: U96F32 = U96F32::from_num(150_000_000_000_u64); + let alpha_in: U96F32 = U96F32::from_num(100_000_000_000_u64); + SubnetTAO::::insert(netuid, tao_reserve.to_num::()); + SubnetAlphaIn::::insert(netuid, alpha_in.to_num::()); + let current_price: U96F32 = U96F32::from_num(SubtensorModule::get_alpha_price(netuid)); + assert_eq!(current_price, U96F32::from_num(1.5)); + + // Setup limit price so resulting average price doesn't drop by more than 10% from current price + let limit_price = 1_350_000_000; + + // Alpha unstaked = 150 / 1.35 - 100 ~ 11.1 + let expected_alpha_reduction = 11_111_111_111; + + // Remove stake with slippage safety + assert_ok!(SubtensorModule::remove_stake_limit_aggregate( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + unstake_amount, + limit_price, + true + )); + + // Check for the block delay + run_to_block_ext(2, true); + + // Check that event was not emitted. + assert!(System::events().iter().all(|e| { + !matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedLimitedStakeRemoved(..)) + ) + })); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + + // Check if stake has decreased only by + assert_abs_diff_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid + ), + alpha_before - expected_alpha_reduction - fee, + epsilon = expected_alpha_reduction / 1_000, + ); + + // Check that event was emitted. + assert!(System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::StakeRemoved(..)) + ) + })); + // Check that event was emitted. + assert!(System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedLimitedStakeRemoved(..)) + ) + })); + }); +} + +#[test] +fn test_remove_stake_limit_aggregate_fail() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(533453); + let coldkey_account_id = U256::from(55453); + let stake_amount = 300_000_000; + let unstake_amount = 150_000_000_000; + let limit_price = 1_350_000_000; + // add network + let netuid: u16 = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + + // Give the neuron some stake to remove + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + stake_amount, + ); + + assert_ok!(SubtensorModule::remove_stake_limit_aggregate( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + unstake_amount, + limit_price, + true + )); + + // Check for the block delay + run_to_block_ext(2, true); + + // Check that event was not emitted. + assert!(System::events().iter().all(|e| { + !matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::FailedToRemoveAggregatedLimitedStake(..)) + ) + })); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + + // Check that event was emitted. + assert!(System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::FailedToRemoveAggregatedLimitedStake(..)) + ) + })); + }); +} + #[test] fn test_remove_stake_limit_fill_or_kill() { new_test_ext(1).execute_with(|| { @@ -4384,6 +5664,110 @@ fn test_unstake_all_alpha_works() { assert!(new_root > 100_000); }); } +#[test] +fn test_unstake_all_alpha_aggregate_works() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let stake_amount = 190_000_000_000; // 190 Alpha + + let netuid: u16 = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, hotkey, coldkey, 192213123); + // Give the neuron some stake to remove + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + stake_amount, + ); + + // Setup the Alpha pool so that removing all the Alpha will keep liq above min + let remaining_tao: I96F32 = + DefaultMinimumPoolLiquidity::::get().saturating_add(I96F32::from(10_000_000)); + let alpha_reserves: I110F18 = I110F18::from(stake_amount + 10_000_000); + let alpha = stake_amount; + + let k: I110F18 = I110F18::from_fixed(remaining_tao) + .saturating_mul(alpha_reserves.saturating_add(I110F18::from(alpha))); + let tao_reserves: I110F18 = k.safe_div(alpha_reserves); + + SubnetTAO::::insert(netuid, tao_reserves.to_num::()); + SubnetAlphaIn::::insert(netuid, alpha_reserves.to_num::()); + + // Unstake all alpha to root + assert_ok!(SubtensorModule::unstake_all_alpha_aggregate( + RuntimeOrigin::signed(coldkey), + hotkey, + )); + + // Check for the block delay + run_to_block_ext(2, true); + + // Check that event was not emitted. + assert!(System::events().iter().all(|e| { + !matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedUnstakeAllAlphaSucceeded(..)) + ) + })); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + + let new_alpha = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + assert_abs_diff_eq!(new_alpha, 0, epsilon = 1_000,); + let new_root = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, 0); + assert!(new_root > 100_000); + + // Check that event was emitted. + assert!(System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedUnstakeAllAlphaSucceeded(..)) + ) + })); + }); +} + +#[test] +fn test_unstake_all_alpha_aggregate_fails() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + assert_ok!(SubtensorModule::unstake_all_alpha_aggregate( + RuntimeOrigin::signed(coldkey), + hotkey, + )); + + // Check for the block delay + run_to_block_ext(2, true); + + // Check that event was not emitted. + assert!(System::events().iter().all(|e| { + !matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedUnstakeAllAlphaFailed(..)) + ) + })); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + + // Check that event was emitted. + assert!(System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedUnstakeAllAlphaFailed(..)) + ) + })); + }); +} #[test] fn test_unstake_all_works() { @@ -4431,3 +5815,108 @@ fn test_unstake_all_works() { assert!(new_balance > 100_000); }); } + +#[test] +fn test_unstake_all_aggregate_works() { + new_test_ext(1).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let stake_amount = 190_000_000_000; // 190 Alpha + + let netuid: u16 = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + register_ok_neuron(netuid, hotkey, coldkey, 192213123); + // Give the neuron some stake to remove + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid, + stake_amount, + ); + + // Setup the Alpha pool so that removing all the Alpha will keep liq above min + let remaining_tao: I96F32 = + DefaultMinimumPoolLiquidity::::get().saturating_add(I96F32::from(10_000_000)); + let alpha_reserves: I110F18 = I110F18::from(stake_amount + 10_000_000); + let alpha = stake_amount; + + let k: I110F18 = I110F18::from_fixed(remaining_tao) + .saturating_mul(alpha_reserves.saturating_add(I110F18::from(alpha))); + let tao_reserves: I110F18 = k.safe_div(alpha_reserves); + + SubnetTAO::::insert(netuid, tao_reserves.to_num::()); + SubnetAlphaIn::::insert(netuid, alpha_reserves.to_num::()); + + // Unstake all alpha to root + assert_ok!(SubtensorModule::unstake_all_aggregate( + RuntimeOrigin::signed(coldkey), + hotkey, + )); + + // Check for the block delay + run_to_block_ext(2, true); + + // Check that event was not emitted. + assert!(System::events().iter().all(|e| { + !matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedUnstakeAllSucceeded(..)) + ) + })); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + + let new_alpha = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + assert_abs_diff_eq!(new_alpha, 0, epsilon = 1_000,); + let new_balance = SubtensorModule::get_coldkey_balance(&coldkey); + assert!(new_balance > 100_000); + + // Check that event was emitted. + assert!(System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedUnstakeAllSucceeded(..)) + ) + })); + }); +} + +#[test] +fn test_unstake_all_aggregate_fails() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + // Unstake all alpha to root + assert_ok!(SubtensorModule::unstake_all_aggregate( + RuntimeOrigin::signed(coldkey), + hotkey, + )); + + // Check for the block delay + run_to_block_ext(2, true); + + // Check that event was not emitted. + assert!(System::events().iter().all(|e| { + !matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedUnstakeAllFailed(..)) + ) + })); + + // Enable on_finalize code to run + run_to_block_ext(3, true); + + // Check that event was emitted. + assert!(System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AggregatedUnstakeAllFailed(..)) + ) + })); + }); +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 94f7aadceb..86b0ef7235 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -207,7 +207,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 262, + spec_version: 263, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -673,6 +673,18 @@ impl InstanceFilter for ProxyType { | RuntimeCall::SubtensorModule( pallet_subtensor::Call::remove_stake_limit { .. } ) + | RuntimeCall::SubtensorModule( + pallet_subtensor::Call::add_stake_aggregate { .. } + ) + | RuntimeCall::SubtensorModule( + pallet_subtensor::Call::add_stake_limit_aggregate { .. } + ) + | RuntimeCall::SubtensorModule( + pallet_subtensor::Call::remove_stake_aggregate { .. } + ) + | RuntimeCall::SubtensorModule( + pallet_subtensor::Call::remove_stake_limit_aggregate { .. } + ) | RuntimeCall::SubtensorModule(pallet_subtensor::Call::unstake_all { .. }) | RuntimeCall::SubtensorModule( pallet_subtensor::Call::unstake_all_alpha { .. } @@ -746,6 +758,18 @@ impl InstanceFilter for ProxyType { | RuntimeCall::SubtensorModule( pallet_subtensor::Call::remove_stake_limit { .. } ) + | RuntimeCall::SubtensorModule( + pallet_subtensor::Call::add_stake_aggregate { .. } + ) + | RuntimeCall::SubtensorModule( + pallet_subtensor::Call::add_stake_limit_aggregate { .. } + ) + | RuntimeCall::SubtensorModule( + pallet_subtensor::Call::remove_stake_aggregate { .. } + ) + | RuntimeCall::SubtensorModule( + pallet_subtensor::Call::remove_stake_limit_aggregate { .. } + ) ), ProxyType::Registration => matches!( c,