From fbc3a3ec90e7eb948da56c3d87a1786e3a141120 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Thu, 30 Jan 2025 13:50:54 -0500 Subject: [PATCH 1/6] Implement slippage safe move_stake_limit --- pallets/subtensor/src/lib.rs | 28 +++ pallets/subtensor/src/macros/dispatches.rs | 63 ++++++ pallets/subtensor/src/staking/move_stake.rs | 199 +++++++++++++++++++ pallets/subtensor/src/staking/stake_utils.rs | 10 + 4 files changed, 300 insertions(+) diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index d346b9241a..31d4e43cb1 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1842,6 +1842,30 @@ where *origin_netuid, *destination_netuid, *alpha_amount, + *alpha_amount, + None + )) + } + Some(Call::move_stake_limit { + origin_hotkey, + destination_hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + limit_price, + allow_partial, + }) => { + // Fully validate the user input + Self::result_to_validity(Pallet::::validate_stake_transition( + who, + who, + origin_hotkey, + destination_hotkey, + *origin_netuid, + *destination_netuid, + *alpha_amount, + *limit_price, + Some(*allow_partial), )) } Some(Call::transfer_stake { @@ -1860,6 +1884,8 @@ where *origin_netuid, *destination_netuid, *alpha_amount, + *alpha_amount, + None )) } Some(Call::swap_stake { @@ -1877,6 +1903,8 @@ where *origin_netuid, *destination_netuid, *alpha_amount, + *alpha_amount, + None )) } Some(Call::register { netuid, .. } | Call::burned_register { netuid, .. }) => { diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 6b8592db8e..d2747f0f02 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -1811,5 +1811,68 @@ mod dispatches { allow_partial, ) } + + /// Moves specified amount of stake from a hotkey to another across subnets with Alpha<>Beta + /// price limit. + /// + /// The limit price is expressed is expressed in rao units of origin_netuid Alpha per one tao + /// unit of destination_netuid Alpha. + /// + /// Example 1: Exchanging Alpha for Beta. 1_000_000_000 limit_price value for would mean that + /// limit price euqals 1.0 Alpha per 1 Beta. Exchanging 100 Alpha will result in receiving at + /// least 100 Beta. + /// + /// Example 2: Exchanging Alpha for Beta. 500_000_000 for would mean that limit price euqals 0.5 + /// Alpha per 1 Beta. Exchanging 100 Alpha will result in receiving at least 50 Beta. + /// + /// # Args: + /// * `origin` - (::Origin): + /// - The signature of the caller's coldkey. + /// + /// * `origin_hotkey` (T::AccountId): + /// - The hotkey account to move stake from. + /// + /// * `destination_hotkey` (T::AccountId): + /// - The hotkey account to move stake to. + /// + /// * `origin_netuid` (T::AccountId): + /// - The subnet ID to move stake from. + /// + /// * `destination_netuid` (T::AccountId): + /// - The subnet ID to move stake to. + /// + /// * `alpha_amount` (T::AccountId): + /// - The alpha stake amount to move. + /// + /// * 'limit_price' (u64): + /// - The limit price + /// + /// * 'allow_partial' (bool): + /// - Allows partial execution of the amount. If set to false, this becomes + /// fill or kill type or order. + /// + #[pallet::call_index(90)] + #[pallet::weight((Weight::from_parts(3_000_000, 0).saturating_add(T::DbWeight::get().writes(1)), DispatchClass::Operational, Pays::No))] + pub fn move_stake_limit( + origin: T::RuntimeOrigin, + origin_hotkey: T::AccountId, + destination_hotkey: T::AccountId, + origin_netuid: u16, + destination_netuid: u16, + alpha_amount: u64, + limit_price: u64, + allow_partial: bool, + ) -> DispatchResult { + Self::do_move_stake_limit( + origin, + origin_hotkey, + destination_hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + limit_price, + allow_partial, + ) + } } } diff --git a/pallets/subtensor/src/staking/move_stake.rs b/pallets/subtensor/src/staking/move_stake.rs index 75f9331356..9cd6ec3cef 100644 --- a/pallets/subtensor/src/staking/move_stake.rs +++ b/pallets/subtensor/src/staking/move_stake.rs @@ -1,6 +1,7 @@ use super::*; use safe_math::*; use sp_core::Get; +use substrate_fixed::types::U96F32; impl Pallet { /// Moves stake from one hotkey to another across subnets. @@ -44,6 +45,79 @@ impl Pallet { origin_netuid, destination_netuid, alpha_amount, + None, + None, + )?; + + // Log the event. + log::info!( + "StakeMoved( coldkey:{:?}, origin_hotkey:{:?}, origin_netuid:{:?}, destination_hotkey:{:?}, destination_netuid:{:?} )", + coldkey.clone(), + origin_hotkey.clone(), + origin_netuid, + destination_hotkey.clone(), + destination_netuid + ); + Self::deposit_event(Event::StakeMoved( + coldkey, + origin_hotkey, + origin_netuid, + destination_hotkey, + destination_netuid, + tao_moved, + )); + + // Ok and return. + Ok(()) + } + + /// Moves stake from one hotkey to another across subnets. + /// + /// # Arguments + /// * `origin` - The origin of the transaction, which must be signed by the `origin_hotkey`. + /// * `origin_hotkey` - The account ID of the hotkey from which the stake is being moved. + /// * `destination_hotkey` - The account ID of the hotkey to which the stake is being moved. + /// * `origin_netuid` - The network ID of the origin subnet. + /// * `destination_netuid` - The network ID of the destination subnet. + /// * `alpha_amount` - The alpha stake amount to move. + /// * 'limit_price' - The limit price. + /// + /// # Returns + /// * `DispatchResult` - Indicates the success or failure of the operation. + /// + /// # Errors + /// This function will return an error if: + /// * The origin is not signed by the `origin_hotkey`. + /// * Either the origin or destination subnet does not exist. + /// * The `origin_hotkey` or `destination_hotkey` does not exist. + /// * There are locked funds that cannot be moved across subnets. + /// + /// # Events + /// Emits a `StakeMoved` event upon successful completion of the stake movement. + pub fn do_move_stake_limit( + origin: T::RuntimeOrigin, + origin_hotkey: T::AccountId, + destination_hotkey: T::AccountId, + origin_netuid: u16, + destination_netuid: u16, + alpha_amount: u64, + limit_price: u64, + allow_partial: bool, + ) -> dispatch::DispatchResult { + // Check that the origin is signed by the origin_hotkey. + let coldkey = ensure_signed(origin)?; + + // Validate input and move stake + let tao_moved = Self::transition_stake_internal( + &coldkey, + &coldkey, + &origin_hotkey, + &destination_hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + Some(limit_price), + Some(allow_partial), )?; // Log the event. @@ -113,6 +187,8 @@ impl Pallet { origin_netuid, destination_netuid, alpha_amount, + None, + None, )?; // 9. Emit an event for logging/monitoring. @@ -180,6 +256,8 @@ impl Pallet { origin_netuid, destination_netuid, alpha_amount, + None, + None, )?; // Emit an event for logging. @@ -203,6 +281,8 @@ impl Pallet { Ok(()) } + // If limit_price is None, this is a regular operation, otherwise, it is slippage-protected + // by setting limit price between origin_netuid and destination_netuid token fn transition_stake_internal( origin_coldkey: &T::AccountId, destination_coldkey: &T::AccountId, @@ -211,7 +291,16 @@ impl Pallet { origin_netuid: u16, destination_netuid: u16, alpha_amount: u64, + maybe_limit_price: Option, + maybe_allow_partial: Option, ) -> Result> { + // Calculate the maximum amount that can be executed + let max_amount = if let Some(limit_price) = maybe_limit_price { + Self::get_max_amount_move(origin_netuid, destination_netuid, limit_price) + } else { + alpha_amount + }; + // Validate user input Self::validate_stake_transition( origin_coldkey, @@ -221,6 +310,8 @@ impl Pallet { origin_netuid, destination_netuid, alpha_amount, + max_amount, + maybe_allow_partial, )?; // Unstake from the origin subnet, returning TAO (or a 1:1 equivalent). @@ -248,4 +339,112 @@ impl Pallet { Ok(tao_unstaked.saturating_sub(fee)) } + + /// Returns the maximum amount of origin netuid Alpha that can be executed before we cross + /// limit_price. + /// + /// ```ignore + /// The TAO we get from unstaking is + /// unstaked_tao = subnet_tao(1) - alpha_in(1) * subnet_tao(1) / (alpha_in(1) + unstaked_alpha) + /// + /// The Alpha we get from staking is + /// moved_alpha = alpha_in(2) - alpha_in(2) * subnet_tao(2) / (subnet_tao(2) + unstaked_tao) + /// + /// The resulting swap price that shall be compared to limit_price is moved_alpha / unstaked_alpha + /// + /// With a known limit_price parameter x = unstaked_alpha can be found using the formula: + /// + /// alpha_in(2) * subnet_tao(1) - limit_price * alpha_in(1) * subnet_tao(2) + /// x = ----------------------------------------------------------------------- + /// limit_price * (subnet_tao(1) + subnet_tao(2)) + /// ``` + /// + /// In the corner case when SubnetTAO(2) == SubnetTAO(1), no slippage is going to occur. + /// + pub fn get_max_amount_move( + origin_netuid: u16, + destination_netuid: u16, + limit_price: u64, + ) -> u64 { + // Corner case: both subnet IDs are root or stao + // There's no slippage for root or stable subnets, so slippage is always 0. + if ((origin_netuid == Self::get_root_netuid()) + || (SubnetMechanism::::get(origin_netuid)) == 0) + && ((destination_netuid == Self::get_root_netuid()) + || (SubnetMechanism::::get(destination_netuid)) == 0) + { + return u64::MAX; + } + + // Corner case: Origin is root or stable, destination is dynamic + // Same as adding stake with limit price + if ((origin_netuid == Self::get_root_netuid()) + || (SubnetMechanism::::get(origin_netuid)) == 0) + && ((SubnetMechanism::::get(destination_netuid)) == 1) + { + return Self::get_max_amount_add(destination_netuid, limit_price); + } + + // Corner case: Origin is dynamic, destination is root or stable + // Same as removing stake with limit price + if ((destination_netuid == Self::get_root_netuid()) + || (SubnetMechanism::::get(destination_netuid)) == 0) + && ((SubnetMechanism::::get(origin_netuid)) == 1) + { + return Self::get_max_amount_remove(origin_netuid, limit_price); + } + + // Corner case: SubnetTAO for any of two subnets is zero + let subnet_tao_1 = SubnetTAO::::get(origin_netuid); + let subnet_tao_2 = SubnetTAO::::get(destination_netuid); + if (subnet_tao_1 == 0) || (subnet_tao_2 == 0) { + return 0; + } + let subnet_tao_1_float: U96F32 = U96F32::saturating_from_num(subnet_tao_1); + let subnet_tao_2_float: U96F32 = U96F32::saturating_from_num(subnet_tao_2); + + // Corner case: SubnetAlphaIn for any of two subnets is zero + let alpha_in_1 = SubnetAlphaIn::::get(origin_netuid); + let alpha_in_2 = SubnetAlphaIn::::get(destination_netuid); + if (alpha_in_1 == 0) || (alpha_in_2 == 0) { + return 0; + } + let alpha_in_1_float: U96F32 = U96F32::saturating_from_num(alpha_in_1); + let alpha_in_2_float: U96F32 = U96F32::saturating_from_num(alpha_in_2); + + // Corner case: limit_price > current_price (price of origin (as a base) relative + // to destination (as a quote) cannot increase with moving) + // The alpha price is never zero at this point because of the checks above. + // Excluding this corner case guarantees that main case nominator is non-negative + let limit_price_float: U96F32 = U96F32::saturating_from_num(limit_price) + .checked_div(U96F32::saturating_from_num(1_000_000_000)) + .unwrap_or(U96F32::saturating_from_num(0)); + let current_price = Self::get_alpha_price(origin_netuid) + .safe_div(Self::get_alpha_price(destination_netuid)); + if limit_price_float > current_price { + return 0; + } + + // Corner case: limit_price is zero + if limit_price == 0 { + return u64::MAX; + } + + // Main case + // Nominator is positive + // Denominator is positive + // Perform calculation in a non-overflowing order + let tao_sum: U96F32 = + U96F32::saturating_from_num(subnet_tao_2_float.saturating_add(subnet_tao_1_float)); + let a1_over_sum: U96F32 = alpha_in_1_float.safe_div(tao_sum); + let a2_over_sum: U96F32 = alpha_in_2_float.safe_div(tao_sum); + let t1_over_sum: U96F32 = subnet_tao_1_float.safe_div(tao_sum); + let t2_over_sum: U96F32 = subnet_tao_2_float.safe_div(tao_sum); + + a2_over_sum + .saturating_mul(t1_over_sum) + .safe_div(limit_price_float) + .saturating_sub(a1_over_sum.saturating_mul(t2_over_sum)) + .saturating_to_num::() + } } diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index 4c446947f5..4f42c959b8 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -823,6 +823,8 @@ impl Pallet { origin_netuid: u16, destination_netuid: u16, alpha_amount: u64, + max_amount: u64, + maybe_allow_partial: Option, ) -> Result<(), Error> { // Ensure that both subnets exist. ensure!( @@ -869,6 +871,14 @@ impl Pallet { return Err(Error::::InsufficientLiquidity); } + // Ensure that if partial execution is not allowed, the amount will not cause + // slippage over desired + if let Some(allow_partial) = maybe_allow_partial { + if !allow_partial { + ensure!(alpha_amount <= max_amount, Error::::SlippageTooHigh); + } + } + Ok(()) } } From 2e7c08bf1c61675a9c93ef9ce3863c3aba79fd0f Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Thu, 30 Jan 2025 17:41:03 -0500 Subject: [PATCH 2/6] Add swap_stake_limit --- pallets/subtensor/src/lib.rs | 34 ++--- pallets/subtensor/src/macros/dispatches.rs | 74 +++++------ pallets/subtensor/src/staking/move_stake.rs | 130 ++++++++++---------- 3 files changed, 112 insertions(+), 126 deletions(-) diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 31d4e43cb1..aebad4698e 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1846,30 +1846,27 @@ where None )) } - Some(Call::move_stake_limit { - origin_hotkey, - destination_hotkey, + Some(Call::transfer_stake { + destination_coldkey, + hotkey, origin_netuid, destination_netuid, alpha_amount, - limit_price, - allow_partial, }) => { // Fully validate the user input Self::result_to_validity(Pallet::::validate_stake_transition( who, - who, - origin_hotkey, - destination_hotkey, + destination_coldkey, + hotkey, + hotkey, *origin_netuid, *destination_netuid, *alpha_amount, - *limit_price, - Some(*allow_partial), + *alpha_amount, + None )) } - Some(Call::transfer_stake { - destination_coldkey, + Some(Call::swap_stake { hotkey, origin_netuid, destination_netuid, @@ -1878,7 +1875,7 @@ where // Fully validate the user input Self::result_to_validity(Pallet::::validate_stake_transition( who, - destination_coldkey, + who, hotkey, hotkey, *origin_netuid, @@ -1888,12 +1885,17 @@ where None )) } - Some(Call::swap_stake { + Some(Call::swap_stake_limit { hotkey, origin_netuid, destination_netuid, alpha_amount, + limit_price, + allow_partial, }) => { + // Get the max amount possible to exchange + let max_amount = Pallet::::get_max_amount_move(*origin_netuid, *destination_netuid, *limit_price); + // Fully validate the user input Self::result_to_validity(Pallet::::validate_stake_transition( who, @@ -1903,8 +1905,8 @@ where *origin_netuid, *destination_netuid, *alpha_amount, - *alpha_amount, - None + max_amount, + Some(*allow_partial), )) } Some(Call::register { netuid, .. } | Call::burned_register { netuid, .. }) => { diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index d2747f0f02..2735b6bb79 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -1769,10 +1769,10 @@ mod dispatches { /// - 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. + /// - 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 + /// - Allows partial execution of the amount. If set to false, this becomes /// fill or kill type or order. /// /// # Event: @@ -1812,61 +1812,45 @@ mod dispatches { ) } - /// Moves specified amount of stake from a hotkey to another across subnets with Alpha<>Beta - /// price limit. - /// - /// The limit price is expressed is expressed in rao units of origin_netuid Alpha per one tao - /// unit of destination_netuid Alpha. - /// - /// Example 1: Exchanging Alpha for Beta. 1_000_000_000 limit_price value for would mean that - /// limit price euqals 1.0 Alpha per 1 Beta. Exchanging 100 Alpha will result in receiving at - /// least 100 Beta. - /// - /// Example 2: Exchanging Alpha for Beta. 500_000_000 for would mean that limit price euqals 0.5 - /// Alpha per 1 Beta. Exchanging 100 Alpha will result in receiving at least 50 Beta. - /// - /// # Args: - /// * `origin` - (::Origin): - /// - The signature of the caller's coldkey. - /// - /// * `origin_hotkey` (T::AccountId): - /// - The hotkey account to move stake from. - /// - /// * `destination_hotkey` (T::AccountId): - /// - The hotkey account to move stake to. - /// - /// * `origin_netuid` (T::AccountId): - /// - The subnet ID to move stake from. - /// - /// * `destination_netuid` (T::AccountId): - /// - The subnet ID to move stake to. - /// - /// * `alpha_amount` (T::AccountId): - /// - The alpha stake amount to move. + /// Swaps a specified amount of stake from one subnet to another, while keeping the same coldkey and hotkey. /// - /// * 'limit_price' (u64): - /// - The limit price + /// # Arguments + /// * `origin` - The origin of the transaction, which must be signed by the coldkey that owns the `hotkey`. + /// * `hotkey` - The hotkey whose stake is being swapped. + /// * `origin_netuid` - The network/subnet ID from which stake is removed. + /// * `destination_netuid` - The network/subnet ID to which stake is added. + /// * `alpha_amount` - The amount of stake to swap. + /// * `limit_price` - The limit price expressed in units of RAO per one Alpha. + /// * `allow_partial` - Allows partial execution of the amount. If set to false, this becomes fill or kill type or order. /// - /// * 'allow_partial' (bool): - /// - Allows partial execution of the amount. If set to false, this becomes - /// fill or kill type or order. + /// # Errors + /// Returns an error if: + /// * The transaction is not signed by the correct coldkey (i.e., `coldkey_owns_hotkey` fails). + /// * Either `origin_netuid` or `destination_netuid` does not exist. + /// * The hotkey does not exist. + /// * There is insufficient stake on `(coldkey, hotkey, origin_netuid)`. + /// * The swap amount is below the minimum stake requirement. /// + /// # Events + /// May emit a `StakeSwapped` event on success. #[pallet::call_index(90)] - #[pallet::weight((Weight::from_parts(3_000_000, 0).saturating_add(T::DbWeight::get().writes(1)), DispatchClass::Operational, Pays::No))] - pub fn move_stake_limit( + #[pallet::weight(( + Weight::from_parts(3_000_000, 0).saturating_add(T::DbWeight::get().writes(1)), + DispatchClass::Operational, + Pays::No + ))] + pub fn swap_stake_limit( origin: T::RuntimeOrigin, - origin_hotkey: T::AccountId, - destination_hotkey: T::AccountId, + hotkey: T::AccountId, origin_netuid: u16, destination_netuid: u16, alpha_amount: u64, limit_price: u64, allow_partial: bool, ) -> DispatchResult { - Self::do_move_stake_limit( + Self::do_swap_stake_limit( origin, - origin_hotkey, - destination_hotkey, + hotkey, origin_netuid, destination_netuid, alpha_amount, diff --git a/pallets/subtensor/src/staking/move_stake.rs b/pallets/subtensor/src/staking/move_stake.rs index 9cd6ec3cef..c3a1f28a58 100644 --- a/pallets/subtensor/src/staking/move_stake.rs +++ b/pallets/subtensor/src/staking/move_stake.rs @@ -71,117 +71,115 @@ impl Pallet { Ok(()) } - /// Moves stake from one hotkey to another across subnets. + /// Transfers stake from one coldkey to another, optionally moving from one subnet to another, + /// while keeping the same hotkey. /// /// # Arguments - /// * `origin` - The origin of the transaction, which must be signed by the `origin_hotkey`. - /// * `origin_hotkey` - The account ID of the hotkey from which the stake is being moved. - /// * `destination_hotkey` - The account ID of the hotkey to which the stake is being moved. - /// * `origin_netuid` - The network ID of the origin subnet. - /// * `destination_netuid` - The network ID of the destination subnet. - /// * `alpha_amount` - The alpha stake amount to move. - /// * 'limit_price' - The limit price. + /// * `origin` - The origin of the transaction, which must be signed by the `origin_coldkey`. + /// * `destination_coldkey` - The account ID of the coldkey to which the stake is being transferred. + /// * `hotkey` - The account ID of the hotkey associated with this stake. + /// * `origin_netuid` - The network ID (subnet) from which the stake is being transferred. + /// * `destination_netuid` - The network ID (subnet) to which the stake is being transferred. + /// * `alpha_amount` - The amount of stake to transfer. /// /// # Returns - /// * `DispatchResult` - Indicates the success or failure of the operation. + /// * `DispatchResult` - Indicates success or failure. /// /// # Errors /// This function will return an error if: - /// * The origin is not signed by the `origin_hotkey`. - /// * Either the origin or destination subnet does not exist. - /// * The `origin_hotkey` or `destination_hotkey` does not exist. - /// * There are locked funds that cannot be moved across subnets. + /// * The transaction is not signed by the `origin_coldkey`. + /// * The subnet (`origin_netuid` or `destination_netuid`) does not exist. + /// * The `hotkey` does not exist. + /// * The `(origin_coldkey, hotkey, origin_netuid)` does not have enough stake for `alpha_amount`. + /// * The amount to be transferred is below the minimum stake requirement. + /// * There is a failure in staking or unstaking logic. /// /// # Events - /// Emits a `StakeMoved` event upon successful completion of the stake movement. - pub fn do_move_stake_limit( + /// Emits a `StakeTransferred` event upon successful completion of the transfer. + pub fn do_transfer_stake( origin: T::RuntimeOrigin, - origin_hotkey: T::AccountId, - destination_hotkey: T::AccountId, + destination_coldkey: T::AccountId, + hotkey: T::AccountId, origin_netuid: u16, destination_netuid: u16, alpha_amount: u64, - limit_price: u64, - allow_partial: bool, ) -> dispatch::DispatchResult { - // Check that the origin is signed by the origin_hotkey. + // Ensure the extrinsic is signed by the origin_coldkey. let coldkey = ensure_signed(origin)?; // Validate input and move stake let tao_moved = Self::transition_stake_internal( &coldkey, - &coldkey, - &origin_hotkey, - &destination_hotkey, + &destination_coldkey, + &hotkey, + &hotkey, origin_netuid, destination_netuid, alpha_amount, - Some(limit_price), - Some(allow_partial), + None, + None, )?; - // Log the event. + // 9. Emit an event for logging/monitoring. log::info!( - "StakeMoved( coldkey:{:?}, origin_hotkey:{:?}, origin_netuid:{:?}, destination_hotkey:{:?}, destination_netuid:{:?} )", - coldkey.clone(), - origin_hotkey.clone(), + "StakeTransferred(origin_coldkey: {:?}, destination_coldkey: {:?}, hotkey: {:?}, origin_netuid: {:?}, destination_netuid: {:?}, amount: {:?})", + coldkey, + destination_coldkey, + hotkey, origin_netuid, - destination_hotkey.clone(), - destination_netuid + destination_netuid, + tao_moved ); - Self::deposit_event(Event::StakeMoved( + Self::deposit_event(Event::StakeTransferred( coldkey, - origin_hotkey, + destination_coldkey, + hotkey, origin_netuid, - destination_hotkey, destination_netuid, tao_moved, )); - // Ok and return. + // 10. Return success. Ok(()) } - /// Transfers stake from one coldkey to another, optionally moving from one subnet to another, - /// while keeping the same hotkey. + /// Swaps a specified amount of stake for the same `(coldkey, hotkey)` pair from one subnet + /// (`origin_netuid`) to another (`destination_netuid`). /// /// # Arguments - /// * `origin` - The origin of the transaction, which must be signed by the `origin_coldkey`. - /// * `destination_coldkey` - The account ID of the coldkey to which the stake is being transferred. - /// * `hotkey` - The account ID of the hotkey associated with this stake. - /// * `origin_netuid` - The network ID (subnet) from which the stake is being transferred. - /// * `destination_netuid` - The network ID (subnet) to which the stake is being transferred. - /// * `alpha_amount` - The amount of stake to transfer. + /// * `origin` - The origin of the transaction, which must be signed by the coldkey that owns the hotkey. + /// * `hotkey` - The hotkey whose stake is being swapped. + /// * `origin_netuid` - The subnet ID from which stake is removed. + /// * `destination_netuid` - The subnet ID to which stake is added. + /// * `alpha_amount` - The amount of stake to swap. /// /// # Returns /// * `DispatchResult` - Indicates success or failure. /// /// # Errors - /// This function will return an error if: - /// * The transaction is not signed by the `origin_coldkey`. - /// * The subnet (`origin_netuid` or `destination_netuid`) does not exist. - /// * The `hotkey` does not exist. - /// * The `(origin_coldkey, hotkey, origin_netuid)` does not have enough stake for `alpha_amount`. - /// * The amount to be transferred is below the minimum stake requirement. - /// * There is a failure in staking or unstaking logic. + /// This function returns an error if: + /// * The origin is not signed by the correct coldkey (i.e., not associated with `hotkey`). + /// * Either the `origin_netuid` or the `destination_netuid` does not exist. + /// * The specified `hotkey` does not exist. + /// * The `(coldkey, hotkey, origin_netuid)` does not have enough stake (`alpha_amount`). + /// * The unstaked amount is below `DefaultMinStake`. /// /// # Events - /// Emits a `StakeTransferred` event upon successful completion of the transfer. - pub fn do_transfer_stake( + /// Emits a `StakeSwapped` event upon successful completion. + pub fn do_swap_stake( origin: T::RuntimeOrigin, - destination_coldkey: T::AccountId, hotkey: T::AccountId, origin_netuid: u16, destination_netuid: u16, alpha_amount: u64, ) -> dispatch::DispatchResult { - // Ensure the extrinsic is signed by the origin_coldkey. + // Ensure the extrinsic is signed by the coldkey. let coldkey = ensure_signed(origin)?; // Validate input and move stake let tao_moved = Self::transition_stake_internal( &coldkey, - &destination_coldkey, + &coldkey, &hotkey, &hotkey, origin_netuid, @@ -191,26 +189,24 @@ impl Pallet { None, )?; - // 9. Emit an event for logging/monitoring. + // Emit an event for logging. log::info!( - "StakeTransferred(origin_coldkey: {:?}, destination_coldkey: {:?}, hotkey: {:?}, origin_netuid: {:?}, destination_netuid: {:?}, amount: {:?})", + "StakeSwapped(coldkey: {:?}, hotkey: {:?}, origin_netuid: {:?}, destination_netuid: {:?}, amount: {:?})", coldkey, - destination_coldkey, hotkey, origin_netuid, destination_netuid, tao_moved ); - Self::deposit_event(Event::StakeTransferred( + Self::deposit_event(Event::StakeSwapped( coldkey, - destination_coldkey, hotkey, origin_netuid, destination_netuid, tao_moved, )); - // 10. Return success. + // 6. Return success. Ok(()) } @@ -223,6 +219,8 @@ impl Pallet { /// * `origin_netuid` - The subnet ID from which stake is removed. /// * `destination_netuid` - The subnet ID to which stake is added. /// * `alpha_amount` - The amount of stake to swap. + /// * `limit_price` - The limit price. + /// * `allow_partial` - Allow partial execution /// /// # Returns /// * `DispatchResult` - Indicates success or failure. @@ -237,12 +235,14 @@ impl Pallet { /// /// # Events /// Emits a `StakeSwapped` event upon successful completion. - pub fn do_swap_stake( + pub fn do_swap_stake_limit( origin: T::RuntimeOrigin, hotkey: T::AccountId, origin_netuid: u16, destination_netuid: u16, alpha_amount: u64, + limit_price: u64, + allow_partial: bool, ) -> dispatch::DispatchResult { // Ensure the extrinsic is signed by the coldkey. let coldkey = ensure_signed(origin)?; @@ -256,8 +256,8 @@ impl Pallet { origin_netuid, destination_netuid, alpha_amount, - None, - None, + Some(limit_price), + Some(allow_partial), )?; // Emit an event for logging. @@ -280,7 +280,7 @@ impl Pallet { // 6. Return success. Ok(()) } - + // If limit_price is None, this is a regular operation, otherwise, it is slippage-protected // by setting limit price between origin_netuid and destination_netuid token fn transition_stake_internal( From 49f4e0ad132574cff083331e5854dc2873001142 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Thu, 30 Jan 2025 18:41:08 -0500 Subject: [PATCH 3/6] Add tests, fix corner case bugs --- pallets/subtensor/src/lib.rs | 12 +- pallets/subtensor/src/staking/move_stake.rs | 22 +- pallets/subtensor/src/staking/stake_utils.rs | 2 +- pallets/subtensor/src/tests/move_stake.rs | 54 ++++ pallets/subtensor/src/tests/staking.rs | 294 +++++++++++++++++++ 5 files changed, 376 insertions(+), 8 deletions(-) diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 314a99dd37..064bb5c6e4 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1888,7 +1888,7 @@ where *destination_netuid, *alpha_amount, *alpha_amount, - None + None, )) } Some(Call::transfer_stake { @@ -1908,7 +1908,7 @@ where *destination_netuid, *alpha_amount, *alpha_amount, - None + None, )) } Some(Call::swap_stake { @@ -1927,7 +1927,7 @@ where *destination_netuid, *alpha_amount, *alpha_amount, - None + None, )) } Some(Call::swap_stake_limit { @@ -1939,7 +1939,11 @@ where allow_partial, }) => { // Get the max amount possible to exchange - let max_amount = Pallet::::get_max_amount_move(*origin_netuid, *destination_netuid, *limit_price); + let max_amount = Pallet::::get_max_amount_move( + *origin_netuid, + *destination_netuid, + *limit_price, + ); // Fully validate the user input Self::result_to_validity(Pallet::::validate_stake_transition( diff --git a/pallets/subtensor/src/staking/move_stake.rs b/pallets/subtensor/src/staking/move_stake.rs index c3a1f28a58..e311589bed 100644 --- a/pallets/subtensor/src/staking/move_stake.rs +++ b/pallets/subtensor/src/staking/move_stake.rs @@ -280,7 +280,7 @@ impl Pallet { // 6. Return success. Ok(()) } - + // If limit_price is None, this is a regular operation, otherwise, it is slippage-protected // by setting limit price between origin_netuid and destination_netuid token fn transition_stake_internal( @@ -366,14 +366,21 @@ impl Pallet { destination_netuid: u16, limit_price: u64, ) -> u64 { + let tao: U96F32 = U96F32::saturating_from_num(1_000_000_000); + // Corner case: both subnet IDs are root or stao // There's no slippage for root or stable subnets, so slippage is always 0. + // The price always stays at 1.0, return 0 if price is expected to raise. if ((origin_netuid == Self::get_root_netuid()) || (SubnetMechanism::::get(origin_netuid)) == 0) && ((destination_netuid == Self::get_root_netuid()) || (SubnetMechanism::::get(destination_netuid)) == 0) { - return u64::MAX; + if limit_price > tao.saturating_to_num::() { + return 0; + } else { + return u64::MAX; + } } // Corner case: Origin is root or stable, destination is dynamic @@ -382,7 +389,16 @@ impl Pallet { || (SubnetMechanism::::get(origin_netuid)) == 0) && ((SubnetMechanism::::get(destination_netuid)) == 1) { - return Self::get_max_amount_add(destination_netuid, limit_price); + if limit_price == 0 { + return u64::MAX; + } else { + // The destination price is reverted because the limit_price is origin_price / destination_price + let destination_subnet_price = tao + .safe_div(U96F32::saturating_from_num(limit_price)) + .saturating_mul(tao) + .saturating_to_num::(); + return Self::get_max_amount_add(destination_netuid, destination_subnet_price); + } } // Corner case: Origin is dynamic, destination is root or stable diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index b13a087195..b4b650804a 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -878,7 +878,7 @@ impl Pallet { ensure!(alpha_amount <= max_amount, Error::::SlippageTooHigh); } } - + Ok(()) } } diff --git a/pallets/subtensor/src/tests/move_stake.rs b/pallets/subtensor/src/tests/move_stake.rs index 7a60d47367..f0a35f91ae 100644 --- a/pallets/subtensor/src/tests/move_stake.rs +++ b/pallets/subtensor/src/tests/move_stake.rs @@ -1485,3 +1485,57 @@ fn test_do_swap_multiple_times() { assert_eq!(final_stake_netuid2, 0); }); } + +// cargo test --package pallet-subtensor --lib -- tests::move_stake::test_swap_stake_limit_validate --exact --show-output +#[test] +fn test_swap_stake_limit_validate() { + // Testing the signed extension validate function + // correctly filters the `add_stake` transaction. + + new_test_ext(0).execute_with(|| { + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let origin_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + let destination_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let stake_amount = 100_000_000_000; + + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + let unstake_amount = + SubtensorModule::stake_into_subnet(&hotkey, &coldkey, origin_netuid, stake_amount, 0); + + // Setup limit price so that it doesn't allow much slippage at all + let limit_price = ((SubtensorModule::get_alpha_price(origin_netuid) + / SubtensorModule::get_alpha_price(destination_netuid)) + * I96F32::from_num(1_000_000_000)) + .to_num::() + - 1_u64; + + // Swap stake limit call + let call = RuntimeCall::SubtensorModule(SubtensorCall::swap_stake_limit { + hotkey, + origin_netuid, + destination_netuid, + alpha_amount: unstake_amount, + limit_price, + allow_partial: false, + }); + + let info: crate::DispatchInfo = + crate::DispatchInfoOf::<::RuntimeCall>::default(); + + let extension = crate::SubtensorSignedExtension::::new(); + // Submit to the signed extension validate function + let result_no_stake = extension.validate(&coldkey, &call.clone(), &info, 10); + + // Should fail due to slippage + assert_err!( + result_no_stake, + crate::TransactionValidityError::Invalid(crate::InvalidTransaction::Custom( + CustomTransactionError::SlippageTooHigh.into() + )) + ); + }); +} diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index 3d21726923..c36d6db544 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -2702,6 +2702,300 @@ fn test_max_amount_remove_dynamic() { }); } +// cargo test --package pallet-subtensor --lib -- tests::staking::test_max_amount_move_root_root --exact --show-output +#[test] +fn test_max_amount_move_root_root() { + new_test_ext(0).execute_with(|| { + // 0 price on (root, root) exchange => max is u64::MAX + assert_eq!(SubtensorModule::get_max_amount_move(0, 0, 0), u64::MAX); + + // 0.5 price on (root, root) => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_move(0, 0, 500_000_000), + u64::MAX + ); + + // 0.999999... price on (root, root) => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_move(0, 0, 999_999_999), + u64::MAX + ); + + // 1.0 price on (root, root) => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_move(0, 0, 1_000_000_000), + u64::MAX + ); + + // 1.000...001 price on (root, root) => max is 0 + assert_eq!(SubtensorModule::get_max_amount_move(0, 0, 1_000_000_001), 0); + + // 2.0 price on (root, root) => max is 0 + assert_eq!(SubtensorModule::get_max_amount_move(0, 0, 2_000_000_000), 0); + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::staking::test_max_amount_move_root_stable --exact --show-output +#[test] +fn test_max_amount_move_root_stable() { + new_test_ext(0).execute_with(|| { + let netuid: u16 = 1; + add_network(netuid, 1, 0); + + // 0 price on (root, stable) exchange => max is u64::MAX + assert_eq!(SubtensorModule::get_max_amount_move(0, netuid, 0), u64::MAX); + + // 0.5 price on (root, stable) => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_move(0, netuid, 500_000_000), + u64::MAX + ); + + // 0.999999... price on (root, stable) => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_move(0, netuid, 999_999_999), + u64::MAX + ); + + // 1.0 price on (root, stable) => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_move(0, netuid, 1_000_000_000), + u64::MAX + ); + + // 1.000...001 price on (root, stable) => max is 0 + assert_eq!( + SubtensorModule::get_max_amount_move(0, netuid, 1_000_000_001), + 0 + ); + + // 2.0 price on (root, stable) => max is 0 + assert_eq!( + SubtensorModule::get_max_amount_move(0, netuid, 2_000_000_000), + 0 + ); + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::staking::test_max_amount_move_stable_dynamic --exact --show-output +#[test] +fn test_max_amount_move_stable_dynamic() { + new_test_ext(0).execute_with(|| { + // Add stable subnet + let stable_netuid: u16 = 1; + add_network(stable_netuid, 1, 0); + + // Add dynamic subnet + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let dynamic_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + // Forse-set alpha in and tao reserve to make price equal 0.5 + let tao_reserve: U96F32 = U96F32::from_num(50_000_000_000_u64); + let alpha_in: U96F32 = U96F32::from_num(100_000_000_000_u64); + SubnetTAO::::insert(dynamic_netuid, tao_reserve.to_num::()); + SubnetAlphaIn::::insert(dynamic_netuid, alpha_in.to_num::()); + let current_price: U96F32 = + U96F32::from_num(SubtensorModule::get_alpha_price(dynamic_netuid)); + assert_eq!(current_price, U96F32::from_num(0.5)); + + // The tests below just mimic the add_stake_limit tests for reverted price + + // 0 price => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 0), + u64::MAX + ); + + // 2.0 price => max is 0 + assert_eq!( + SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 2_000_000_000), + 0 + ); + + // 3.0 price => max is 0 + assert_eq!( + SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 3_000_000_000), + 0 + ); + + // 0.5x price => max is 1x TAO + assert_abs_diff_eq!( + SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 500_000_000), + 50_000_000_000, + epsilon = 10_000, + ); + + // Precision test: + // 1.99999..9000 price => max > 0 + assert!( + SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 1_999_999_000) > 0 + ); + + // Max price doesn't panic and returns something meaningful + assert_eq!( + SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, u64::MAX), + 0 + ); + assert_eq!( + SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, u64::MAX - 1), + 0 + ); + assert_eq!( + SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, u64::MAX / 2), + 0 + ); + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::staking::test_max_amount_move_dynamic_stable --exact --show-output +#[test] +fn test_max_amount_move_dynamic_stable() { + new_test_ext(0).execute_with(|| { + + // TODO + + // // Add stable subnet + // let stable_netuid: u16 = 1; + // add_network(stable_netuid, 1, 0); + + // // Add dynamic subnet + // let subnet_owner_coldkey = U256::from(1001); + // let subnet_owner_hotkey = U256::from(1002); + // let dynamic_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + // // Forse-set alpha in and tao reserve to make price equal 0.5 + // let tao_reserve: U96F32 = U96F32::from_num(50_000_000_000_u64); + // let alpha_in: U96F32 = U96F32::from_num(100_000_000_000_u64); + // SubnetTAO::::insert(dynamic_netuid, tao_reserve.to_num::()); + // SubnetAlphaIn::::insert(dynamic_netuid, alpha_in.to_num::()); + // let current_price: U96F32 = + // U96F32::from_num(SubtensorModule::get_alpha_price(dynamic_netuid)); + // assert_eq!(current_price, U96F32::from_num(0.5)); + + // // The tests below just mimic the add_stake_limit tests for reverted price + + // // 0 price => max is u64::MAX + // assert_eq!( + // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 0), + // u64::MAX + // ); + + // // 2.0 price => max is 0 + // assert_eq!( + // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 2_000_000_000), + // 0 + // ); + + // // 3.0 price => max is 0 + // assert_eq!( + // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 3_000_000_000), + // 0 + // ); + + // // 0.5x price => max is 1x TAO + // assert_abs_diff_eq!( + // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 500_000_000), + // 50_000_000_000, + // epsilon = 10_000, + // ); + + // // Precision test: + // // 1.99999..9000 price => max > 0 + // assert!( + // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 1_999_999_000) > 0 + // ); + + // // Max price doesn't panic and returns something meaningful + // assert_eq!( + // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, u64::MAX), + // 0 + // ); + // assert_eq!( + // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, u64::MAX - 1), + // 0 + // ); + // assert_eq!( + // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, u64::MAX / 2), + // 0 + // ); + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::staking::test_max_amount_move_dynamic_dynamic --exact --show-output +#[test] +fn test_max_amount_move_dynamic_dynamic() { + new_test_ext(0).execute_with(|| { + + // TODO + + // // Add stable subnet + // let stable_netuid: u16 = 1; + // add_network(stable_netuid, 1, 0); + + // // Add dynamic subnet + // let subnet_owner_coldkey = U256::from(1001); + // let subnet_owner_hotkey = U256::from(1002); + // let dynamic_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + // // Forse-set alpha in and tao reserve to make price equal 0.5 + // let tao_reserve: U96F32 = U96F32::from_num(50_000_000_000_u64); + // let alpha_in: U96F32 = U96F32::from_num(100_000_000_000_u64); + // SubnetTAO::::insert(dynamic_netuid, tao_reserve.to_num::()); + // SubnetAlphaIn::::insert(dynamic_netuid, alpha_in.to_num::()); + // let current_price: U96F32 = + // U96F32::from_num(SubtensorModule::get_alpha_price(dynamic_netuid)); + // assert_eq!(current_price, U96F32::from_num(0.5)); + + // // The tests below just mimic the add_stake_limit tests for reverted price + + // // 0 price => max is u64::MAX + // assert_eq!( + // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 0), + // u64::MAX + // ); + + // // 2.0 price => max is 0 + // assert_eq!( + // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 2_000_000_000), + // 0 + // ); + + // // 3.0 price => max is 0 + // assert_eq!( + // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 3_000_000_000), + // 0 + // ); + + // // 0.5x price => max is 1x TAO + // assert_abs_diff_eq!( + // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 500_000_000), + // 50_000_000_000, + // epsilon = 10_000, + // ); + + // // Precision test: + // // 1.99999..9000 price => max > 0 + // assert!( + // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 1_999_999_000) > 0 + // ); + + // // Max price doesn't panic and returns something meaningful + // assert_eq!( + // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, u64::MAX), + // 0 + // ); + // assert_eq!( + // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, u64::MAX - 1), + // 0 + // ); + // assert_eq!( + // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, u64::MAX / 2), + // 0 + // ); + }); +} + #[test] fn test_add_stake_limit_ok() { new_test_ext(1).execute_with(|| { From dd50902ca9e31d4cce0402858c72359153d30268 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 31 Jan 2025 14:56:50 -0500 Subject: [PATCH 4/6] Add tests, fix bugs --- pallets/subtensor/src/staking/move_stake.rs | 34 +- pallets/subtensor/src/staking/remove_stake.rs | 4 +- pallets/subtensor/src/tests/staking.rs | 758 +++++++++++++----- 3 files changed, 574 insertions(+), 222 deletions(-) diff --git a/pallets/subtensor/src/staking/move_stake.rs b/pallets/subtensor/src/staking/move_stake.rs index e311589bed..a60191de88 100644 --- a/pallets/subtensor/src/staking/move_stake.rs +++ b/pallets/subtensor/src/staking/move_stake.rs @@ -1,7 +1,7 @@ use super::*; use safe_math::*; use sp_core::Get; -use substrate_fixed::types::U96F32; +use substrate_fixed::types::U64F64; impl Pallet { /// Moves stake from one hotkey to another across subnets. @@ -366,7 +366,7 @@ impl Pallet { destination_netuid: u16, limit_price: u64, ) -> u64 { - let tao: U96F32 = U96F32::saturating_from_num(1_000_000_000); + let tao: U64F64 = U64F64::saturating_from_num(1_000_000_000); // Corner case: both subnet IDs are root or stao // There's no slippage for root or stable subnets, so slippage is always 0. @@ -394,7 +394,7 @@ impl Pallet { } else { // The destination price is reverted because the limit_price is origin_price / destination_price let destination_subnet_price = tao - .safe_div(U96F32::saturating_from_num(limit_price)) + .safe_div(U64F64::saturating_from_num(limit_price)) .saturating_mul(tao) .saturating_to_num::(); return Self::get_max_amount_add(destination_netuid, destination_subnet_price); @@ -416,8 +416,8 @@ impl Pallet { if (subnet_tao_1 == 0) || (subnet_tao_2 == 0) { return 0; } - let subnet_tao_1_float: U96F32 = U96F32::saturating_from_num(subnet_tao_1); - let subnet_tao_2_float: U96F32 = U96F32::saturating_from_num(subnet_tao_2); + let subnet_tao_1_float: U64F64 = U64F64::saturating_from_num(subnet_tao_1); + let subnet_tao_2_float: U64F64 = U64F64::saturating_from_num(subnet_tao_2); // Corner case: SubnetAlphaIn for any of two subnets is zero let alpha_in_1 = SubnetAlphaIn::::get(origin_netuid); @@ -425,16 +425,16 @@ impl Pallet { if (alpha_in_1 == 0) || (alpha_in_2 == 0) { return 0; } - let alpha_in_1_float: U96F32 = U96F32::saturating_from_num(alpha_in_1); - let alpha_in_2_float: U96F32 = U96F32::saturating_from_num(alpha_in_2); + let alpha_in_1_float: U64F64 = U64F64::saturating_from_num(alpha_in_1); + let alpha_in_2_float: U64F64 = U64F64::saturating_from_num(alpha_in_2); // Corner case: limit_price > current_price (price of origin (as a base) relative // to destination (as a quote) cannot increase with moving) // The alpha price is never zero at this point because of the checks above. // Excluding this corner case guarantees that main case nominator is non-negative - let limit_price_float: U96F32 = U96F32::saturating_from_num(limit_price) - .checked_div(U96F32::saturating_from_num(1_000_000_000)) - .unwrap_or(U96F32::saturating_from_num(0)); + let limit_price_float: U64F64 = U64F64::saturating_from_num(limit_price) + .checked_div(U64F64::saturating_from_num(1_000_000_000)) + .unwrap_or(U64F64::saturating_from_num(0)); let current_price = Self::get_alpha_price(origin_netuid) .safe_div(Self::get_alpha_price(destination_netuid)); if limit_price_float > current_price { @@ -450,17 +450,15 @@ impl Pallet { // Nominator is positive // Denominator is positive // Perform calculation in a non-overflowing order - let tao_sum: U96F32 = - U96F32::saturating_from_num(subnet_tao_2_float.saturating_add(subnet_tao_1_float)); - let a1_over_sum: U96F32 = alpha_in_1_float.safe_div(tao_sum); - let a2_over_sum: U96F32 = alpha_in_2_float.safe_div(tao_sum); - let t1_over_sum: U96F32 = subnet_tao_1_float.safe_div(tao_sum); - let t2_over_sum: U96F32 = subnet_tao_2_float.safe_div(tao_sum); + let tao_sum: U64F64 = + U64F64::saturating_from_num(subnet_tao_2_float.saturating_add(subnet_tao_1_float)); + let t1_over_sum: U64F64 = subnet_tao_1_float.safe_div(tao_sum); + let t2_over_sum: U64F64 = subnet_tao_2_float.safe_div(tao_sum); - a2_over_sum + alpha_in_2_float .saturating_mul(t1_over_sum) .safe_div(limit_price_float) - .saturating_sub(a1_over_sum.saturating_mul(t2_over_sum)) + .saturating_sub(alpha_in_1_float.saturating_mul(t2_over_sum)) .saturating_to_num::() } } diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index 92d0c2e83c..1a7bf4ae4c 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -358,11 +358,11 @@ impl Pallet { return u64::MAX; } - // Corner case: limit_price > current_price (price cannot increase with unstaking) + // Corner case: limit_price >= current_price (price cannot increase with unstaking) let limit_price_float: U96F32 = U96F32::saturating_from_num(limit_price) .checked_div(U96F32::saturating_from_num(1_000_000_000)) .unwrap_or(U96F32::saturating_from_num(0)); - if limit_price_float > Self::get_alpha_price(netuid) { + if limit_price_float >= Self::get_alpha_price(netuid) { return 0; } diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index c36d6db544..c96903d4f3 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -10,7 +10,7 @@ use approx::assert_abs_diff_eq; use frame_support::dispatch::{DispatchClass, DispatchInfo, GetDispatchInfo, Pays}; use frame_support::sp_runtime::DispatchError; use sp_core::{Get, H256, U256}; -use substrate_fixed::types::U96F32; +use substrate_fixed::types::{I96F32, U96F32}; /*********************************************************** staking::add_stake() tests @@ -2535,44 +2535,141 @@ fn test_max_amount_add_dynamic() { let subnet_owner_hotkey = U256::from(1002); let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - // 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)); - - // 0 price => max is 0 - assert_eq!(SubtensorModule::get_max_amount_add(netuid, 0), 0); - - // 1.499999... price => max is 0 - assert_eq!( - SubtensorModule::get_max_amount_add(netuid, 1_499_999_999), - 0 - ); + // Test cases are generated with help with this limit-staking calculator: + // https://docs.google.com/spreadsheets/d/1pfU-PVycd3I4DbJIc0GjtPohy4CbhdV6CWqgiy__jKE + // This is for reference only; verify before use. + // + // CSV backup for this spreadhsheet: + // + // SubnetTAO,AlphaIn,initial price,limit price,max swappable + // 100,100,=A2/B2,4,=SQRT(D2*A2*B2)-A2 + // + // tao_in, alpha_in, limit_price, expected_max_swappable, precision + [ + // Zero handling (no panics) + (0, 1_000_000_000, 100, 0, 1), + (1_000_000_000, 0, 100, 0, 1), + (1_000_000_000, 1_000_000_000, 0, 0, 1), + // Low bounds + (1, 1, 0, 0, 1), + (1, 1, 1, 0, 1), + (1, 1, 2, 0, 1), + (1, 1, 50_000_000_000, 6, 1), + // Basic math + (1_000, 1_000, 4_000_000_000, 1_000, 1), + (1_000, 1_000, 16_000_000_000, 3_000, 5), + ( + 1_000_000_000_000, + 1_000_000_000_000, + 16_000_000_000, + 3_000_000_000_000, + 1_000_000, + ), + // Normal range values with edge cases + (150_000_000_000, 100_000_000_000, 0, 0, 1), + (150_000_000_000, 100_000_000_000, 100_000_000, 0, 1), + (150_000_000_000, 100_000_000_000, 500_000_000, 0, 1), + (150_000_000_000, 100_000_000_000, 1_499_999_999, 0, 1), + (150_000_000_000, 100_000_000_000, 1_500_000_000, 0, 1), + (150_000_000_000, 100_000_000_000, 1_500_000_100, 5000, 1000), + ( + 150_000_000_000, + 100_000_000_000, + 6_000_000_000, + 150_000_000_000, + 10_000, + ), + // Miscellaneous overflows and underflows + ( + 150_000_000_000, + 100_000_000_000, + u64::MAX, + 16_634_186_809_913_500, + 1_000_000_000, + ), + ( + 150_000_000_000, + 100_000_000_000, + u64::MAX / 2, + 11_762_102_358_830_800, + 1_000_000_000, + ), + ( + 1_000_000, + 1_000_000_000_000_000_000_u64, + 3, + 1_731_051, + 100_000, + ), + ( + 1_000_000, + 1_000_000_000_000_000_000_u64, + 10_000, + 3_100_000_000, + 50_000_000, + ), + ( + 1_000_000, + 1_000_000_000_000_000_000_u64, + 100_000, + 9_999_000_000, + 50_000_000, + ), + ( + 1_000_000, + 1_000_000_000_000_000_000_u64, + 1_000_000, + 31_621_776_602, + 50_000_000, + ), + ( + 1_000_000, + 1_000_000_000_000_000_000_u64, + 1_000_000_000, + 999_999_000_000, + 50_000_000, + ), + ( + 21_000_000_000_000_000, + 10_000_000, + 8_400_000_000_000_000_000, + 21_000_000_000_000_000, + 1_000_000_000_000, + ), + ( + 21_000_000_000_000_000, + 1_000_000_000_000_000_000_u64, + u64::MAX, + u64::MAX, + 1_000_000_000, + ), + ( + 21_000_000_000_000_000, + 1_000_000_000_000_000_000_u64, + 84_000_000, + 21_000_000_000_000_000, + 1_000_000_000, + ), + ] + .iter() + .for_each( + |&(tao_in, alpha_in, limit_price, expected_max_swappable, precision)| { + // Forse-set alpha in and tao reserve to achieve relative price of subnets + SubnetTAO::::insert(netuid, tao_in); + SubnetAlphaIn::::insert(netuid, alpha_in); - // 1.5 price => max is 0 because of non-zero slippage - assert_eq!( - SubtensorModule::get_max_amount_add(netuid, 1_500_000_000), - 0 - ); + if alpha_in != 0 { + let expected_price = I96F32::from_num(tao_in) / I96F32::from_num(alpha_in); + assert_eq!(SubtensorModule::get_alpha_price(netuid), expected_price); + } - // 4x price => max is 1x TAO - assert_abs_diff_eq!( - SubtensorModule::get_max_amount_add(netuid, 6_000_000_000), - 150_000_000_000, - epsilon = 10_000, + assert_abs_diff_eq!( + SubtensorModule::get_max_amount_add(netuid, limit_price), + expected_max_swappable, + epsilon = precision + ); + }, ); - - // Precision test: - // 1.50000..100 price => max > 0 - assert!(SubtensorModule::get_max_amount_add(netuid, 1_500_000_100) > 0); - - // Max price doesn't panic and returns something meaningful - assert!(SubtensorModule::get_max_amount_add(netuid, u64::MAX) < 21_000_000_000_000_000); - assert!(SubtensorModule::get_max_amount_add(netuid, u64::MAX - 1) < 21_000_000_000_000_000); - assert!(SubtensorModule::get_max_amount_add(netuid, u64::MAX / 2) < 21_000_000_000_000_000); }); } @@ -2647,57 +2744,158 @@ fn test_max_amount_remove_stable() { #[test] fn test_max_amount_remove_dynamic() { new_test_ext(0).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + // // Low price values don't blow things up + // assert!(SubtensorModule::get_max_amount_remove(netuid, 1) > 0); + // assert!(SubtensorModule::get_max_amount_remove(netuid, 2) > 0); + // assert!(SubtensorModule::get_max_amount_remove(netuid, 3) > 0); - // 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)); + // // 1.5000...1 price => max is 0 + // assert_eq!( + // SubtensorModule::get_max_amount_remove(netuid, 1_500_000_001), + // 0 + // ); - // 0 price => max is u64::MAX - assert_eq!(SubtensorModule::get_max_amount_remove(netuid, 0), u64::MAX); + // // 1.5 price => max is 0 because of non-zero slippage + // assert_abs_diff_eq!( + // SubtensorModule::get_max_amount_remove(netuid, 1_500_000_000), + // 0, + // epsilon = 10_000 + // ); - // Low price values don't blow things up - assert!(SubtensorModule::get_max_amount_remove(netuid, 1) > 0); - assert!(SubtensorModule::get_max_amount_remove(netuid, 2) > 0); - assert!(SubtensorModule::get_max_amount_remove(netuid, 3) > 0); + // // 1/4 price => max is 2x Alpha + // assert_abs_diff_eq!( + // SubtensorModule::get_max_amount_remove(netuid, 375_000_000), + // 100_000_000_000, + // epsilon = 10_000, + // ); - // 1.5000...1 price => max is 0 - assert_eq!( - SubtensorModule::get_max_amount_remove(netuid, 1_500_000_001), - 0 - ); + // // Precision test: + // // 1.499999.. price => max > 0 + // assert!(SubtensorModule::get_max_amount_remove(netuid, 1_499_999_999) > 0); - // 1.5 price => max is 0 because of non-zero slippage - assert_abs_diff_eq!( - SubtensorModule::get_max_amount_remove(netuid, 1_500_000_000), - 0, - epsilon = 10_000 - ); + // // Max price doesn't panic and returns something meaningful + // assert!(SubtensorModule::get_max_amount_remove(netuid, u64::MAX) < 21_000_000_000_000_000); + // assert!( + // SubtensorModule::get_max_amount_remove(netuid, u64::MAX - 1) < 21_000_000_000_000_000 + // ); + // assert!( + // SubtensorModule::get_max_amount_remove(netuid, u64::MAX / 2) < 21_000_000_000_000_000 + // ); - // 1/4 price => max is 2x Alpha - assert_abs_diff_eq!( - SubtensorModule::get_max_amount_remove(netuid, 375_000_000), - 100_000_000_000, - epsilon = 10_000, - ); + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - // Precision test: - // 1.499999.. price => max > 0 - assert!(SubtensorModule::get_max_amount_remove(netuid, 1_499_999_999) > 0); + // Test cases are generated with help with this limit-staking calculator: + // https://docs.google.com/spreadsheets/d/1pfU-PVycd3I4DbJIc0GjtPohy4CbhdV6CWqgiy__jKE + // This is for reference only; verify before use. + // + // CSV backup for this spreadhsheet: + // + // SubnetTAO,AlphaIn,initial price,limit price,max swappable + // 100,100,=A2/B2,4,=SQRT(D2*A2*B2)-A2 + // + // tao_in, alpha_in, limit_price, expected_max_swappable, precision + [ + // Zero handling (no panics) + (0, 1_000_000_000, 100, 0, 1), + (1_000_000_000, 0, 100, 0, 1), + (1_000_000_000, 1_000_000_000, 0, u64::MAX, 1), + // Low bounds + (1, 1, 0, u64::MAX, 1), + (1, 1, 1, 31_622, 1_000), + (1, 1, 2, 22_360, 1_000), + (1, 1, 250_000_000, 1, 1), + // Basic math + (1_000, 1_000, 250_000_000, 1_000, 1), + (1_000, 1_000, 62_500_000, 3_000, 5), + ( + 1_000_000_000_000, + 1_000_000_000_000, + 62_500_000, + 3_000_000_000_000, + 1_000_000, + ), + // Normal range values with edge cases + (200_000_000_000, 100_000_000_000, 0, u64::MAX, 1), + ( + 200_000_000_000, + 100_000_000_000, + 1_000_000_000, + 41_421_356_237, + 1_000_000, + ), + ( + 200_000_000_000, + 100_000_000_000, + 500_000_000, + 100_000_000_000, + 1_000_000, + ), + (200_000_000_000, 100_000_000_000, 2_000_000_000, 0, 1), + (200_000_000_000, 100_000_000_000, 2_000_000_001, 0, 1), + ( + 200_000_000_000, + 100_000_000_000, + 1_999_999_000, + 25_000, + 10_000, + ), + ( + 200_000_000_000, + 100_000_000_000, + 1_999_000_000, + 25_009_379, + 10_000, + ), + // Miscellaneous overflows and underflows + (2_000_000_000_000, 100_000_000_000, u64::MAX, 0, 1), + (200_000_000_000, 100_000_000_000, u64::MAX / 2, 0, 1), + (1_000_000, 1_000_000_000_000_000_000_u64, 1, 0, 1), + (1_000_000, 1_000_000_000_000_000_000_u64, 10, 0, 1), + (1_000_000, 1_000_000_000_000_000_000_u64, 100, 0, 1), + (1_000_000, 1_000_000_000_000_000_000_u64, 1_000, 0, 1), + (1_000_000, 1_000_000_000_000_000_000_u64, u64::MAX, 0, 1), + ( + 21_000_000_000_000_000, + 1_000_000_000, + 5_250_000_000_000_000, + 1_000_000_000, + 10_000_000, + ), + ( + 21_000_000_000_000_000, + 1_000_000_000_000_000_000_u64, + u64::MAX, + 0, + 1, + ), + ( + 21_000_000_000_000_000, + 1_000_000_000_000_000_000_u64, + 5_250_000, + 1_000_000_000_000_000_000_u64, + 100_000_000_000, + ), + ] + .iter() + .for_each( + |&(tao_in, alpha_in, limit_price, expected_max_swappable, precision)| { + // Forse-set alpha in and tao reserve to achieve relative price of subnets + SubnetTAO::::insert(netuid, tao_in); + SubnetAlphaIn::::insert(netuid, alpha_in); - // Max price doesn't panic and returns something meaningful - assert!(SubtensorModule::get_max_amount_remove(netuid, u64::MAX) < 21_000_000_000_000_000); - assert!( - SubtensorModule::get_max_amount_remove(netuid, u64::MAX - 1) < 21_000_000_000_000_000 - ); - assert!( - SubtensorModule::get_max_amount_remove(netuid, u64::MAX / 2) < 21_000_000_000_000_000 + if alpha_in != 0 { + let expected_price = I96F32::from_num(tao_in) / I96F32::from_num(alpha_in); + assert_eq!(SubtensorModule::get_alpha_price(netuid), expected_price); + } + + assert_abs_diff_eq!( + SubtensorModule::get_max_amount_remove(netuid, limit_price), + expected_max_swappable, + epsilon = precision + ); + }, ); }); } @@ -2852,73 +3050,76 @@ fn test_max_amount_move_stable_dynamic() { #[test] fn test_max_amount_move_dynamic_stable() { new_test_ext(0).execute_with(|| { + // Add stable subnet + let stable_netuid: u16 = 1; + add_network(stable_netuid, 1, 0); - // TODO - - // // Add stable subnet - // let stable_netuid: u16 = 1; - // add_network(stable_netuid, 1, 0); + // Add dynamic subnet + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let dynamic_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - // // Add dynamic subnet - // let subnet_owner_coldkey = U256::from(1001); - // let subnet_owner_hotkey = U256::from(1002); - // let dynamic_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + // 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(dynamic_netuid, tao_reserve.to_num::()); + SubnetAlphaIn::::insert(dynamic_netuid, alpha_in.to_num::()); + let current_price: U96F32 = + U96F32::from_num(SubtensorModule::get_alpha_price(dynamic_netuid)); + assert_eq!(current_price, U96F32::from_num(1.5)); - // // Forse-set alpha in and tao reserve to make price equal 0.5 - // let tao_reserve: U96F32 = U96F32::from_num(50_000_000_000_u64); - // let alpha_in: U96F32 = U96F32::from_num(100_000_000_000_u64); - // SubnetTAO::::insert(dynamic_netuid, tao_reserve.to_num::()); - // SubnetAlphaIn::::insert(dynamic_netuid, alpha_in.to_num::()); - // let current_price: U96F32 = - // U96F32::from_num(SubtensorModule::get_alpha_price(dynamic_netuid)); - // assert_eq!(current_price, U96F32::from_num(0.5)); + // The tests below just mimic the remove_stake_limit tests - // // The tests below just mimic the add_stake_limit tests for reverted price + // 0 price => max is u64::MAX + assert_eq!( + SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, 0), + u64::MAX + ); - // // 0 price => max is u64::MAX - // assert_eq!( - // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 0), - // u64::MAX - // ); + // Low price values don't blow things up + assert!(SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, 1) > 0); + assert!(SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, 2) > 0); + assert!(SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, 3) > 0); - // // 2.0 price => max is 0 - // assert_eq!( - // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 2_000_000_000), - // 0 - // ); + // 1.5000...1 price => max is 0 + assert_eq!( + SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, 1_500_000_001), + 0 + ); - // // 3.0 price => max is 0 - // assert_eq!( - // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 3_000_000_000), - // 0 - // ); + // 1.5 price => max is 0 because of non-zero slippage + assert_abs_diff_eq!( + SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, 1_500_000_000), + 0, + epsilon = 10_000 + ); - // // 0.5x price => max is 1x TAO - // assert_abs_diff_eq!( - // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 500_000_000), - // 50_000_000_000, - // epsilon = 10_000, - // ); + // 1/4 price => max is 2x Alpha + assert_abs_diff_eq!( + SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, 375_000_000), + 100_000_000_000, + epsilon = 10_000, + ); - // // Precision test: - // // 1.99999..9000 price => max > 0 - // assert!( - // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 1_999_999_000) > 0 - // ); + // Precision test: + // 1.499999.. price => max > 0 + assert!( + SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, 1_499_999_999) > 0 + ); - // // Max price doesn't panic and returns something meaningful - // assert_eq!( - // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, u64::MAX), - // 0 - // ); - // assert_eq!( - // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, u64::MAX - 1), - // 0 - // ); - // assert_eq!( - // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, u64::MAX / 2), - // 0 - // ); + // Max price doesn't panic and returns something meaningful + assert!( + SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, u64::MAX) + < 21_000_000_000_000_000 + ); + assert!( + SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, u64::MAX - 1) + < 21_000_000_000_000_000 + ); + assert!( + SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, u64::MAX / 2) + < 21_000_000_000_000_000 + ); }); } @@ -2926,73 +3127,226 @@ fn test_max_amount_move_dynamic_stable() { #[test] fn test_max_amount_move_dynamic_dynamic() { new_test_ext(0).execute_with(|| { + // Add two dynamic subnets + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let origin_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + let destination_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - // TODO - - // // Add stable subnet - // let stable_netuid: u16 = 1; - // add_network(stable_netuid, 1, 0); - - // // Add dynamic subnet - // let subnet_owner_coldkey = U256::from(1001); - // let subnet_owner_hotkey = U256::from(1002); - // let dynamic_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - // // Forse-set alpha in and tao reserve to make price equal 0.5 - // let tao_reserve: U96F32 = U96F32::from_num(50_000_000_000_u64); - // let alpha_in: U96F32 = U96F32::from_num(100_000_000_000_u64); - // SubnetTAO::::insert(dynamic_netuid, tao_reserve.to_num::()); - // SubnetAlphaIn::::insert(dynamic_netuid, alpha_in.to_num::()); - // let current_price: U96F32 = - // U96F32::from_num(SubtensorModule::get_alpha_price(dynamic_netuid)); - // assert_eq!(current_price, U96F32::from_num(0.5)); - - // // The tests below just mimic the add_stake_limit tests for reverted price - - // // 0 price => max is u64::MAX - // assert_eq!( - // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 0), - // u64::MAX - // ); - - // // 2.0 price => max is 0 - // assert_eq!( - // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 2_000_000_000), - // 0 - // ); - - // // 3.0 price => max is 0 - // assert_eq!( - // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 3_000_000_000), - // 0 - // ); - - // // 0.5x price => max is 1x TAO - // assert_abs_diff_eq!( - // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 500_000_000), - // 50_000_000_000, - // epsilon = 10_000, - // ); - - // // Precision test: - // // 1.99999..9000 price => max > 0 - // assert!( - // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 1_999_999_000) > 0 - // ); - - // // Max price doesn't panic and returns something meaningful - // assert_eq!( - // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, u64::MAX), - // 0 - // ); - // assert_eq!( - // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, u64::MAX - 1), - // 0 - // ); - // assert_eq!( - // SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, u64::MAX / 2), - // 0 - // ); + // Test cases are generated with help with this limit-staking calculator: + // https://docs.google.com/spreadsheets/d/1pfU-PVycd3I4DbJIc0GjtPohy4CbhdV6CWqgiy__jKE + // This is for reference only; verify before use. + // + // CSV backup for this spreadhsheet: + // + // SubnetTAO 1,AlphaIn 1,SubnetTAO 2,AlphaIn 2,,initial price,limit price,max swappable + // 150,100,100,100,,=(A2/B2)/(C2/D2),0.1,=(D2*A2-B2*C2*G2)/(G2*(A2+C2)) + // + // tao_in_1, alpha_in_1, tao_in_2, alpha_in_2, limit_price, expected_max_swappable, precision + [ + // Zero handling (no panics) + (0, 1_000_000_000, 1_000_000_000, 1_000_000_000, 100, 0, 1), + (1_000_000_000, 0, 1_000_000_000, 1_000_000_000, 100, 0, 1), + (1_000_000_000, 1_000_000_000, 0, 1_000_000_000, 100, 0, 1), + (1_000_000_000, 1_000_000_000, 1_000_000_000, 0, 100, 0, 1), + // Low bounds + (1, 1, 1, 1, 0, u64::MAX, 1), + (1, 1, 1, 1, 1, 500_000_000, 1), + (1, 1, 1, 1, 2, 250_000_000, 1), + (1, 1, 1, 1, 3, 166_666_666, 1), + (1, 1, 1, 1, 4, 125_000_000, 1), + (1, 1, 1, 1, 1_000, 500_000, 1), + // Basic math + (1_000, 1_000, 1_000, 1_000, 500_000_000, 500, 1), + (1_000, 1_000, 1_000, 1_000, 100_000_000, 4_500, 1), + // Normal range values edge cases + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000, + 560_000_000_000, + 1_000_000, + ), + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000_000, + 500_000_000, + 80_000_000_000, + 1_000_000, + ), + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000_000, + 750_000_000, + 40_000_000_000, + 1_000_000, + ), + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000_000, + 1_000_000_000, + 20_000_000_000, + 1_000, + ), + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000_000, + 1_250_000_000, + 8_000_000_000, + 1_000, + ), + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000_000, + 1_499_999_999, + 27, + 1, + ), + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000_000, + 1_500_000_000, + 0, + 1, + ), + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000_000, + 1_500_000_001, + 0, + 1, + ), + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000_000, + 1_500_001_000, + 0, + 1, + ), + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000_000, + 2_000_000_000, + 0, + 1, + ), + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000_000, + u64::MAX, + 0, + 1, + ), + ( + 100_000_000_000, + 200_000_000_000, + 300_000_000_000, + 400_000_000_000, + 500_000_000, + 50_000_000_000, + 1_000, + ), + // Miscellaneous overflows + ( + 1_000_000_000, + 1_000_000_000, + 1_000_000_000, + 1_000_000_000, + 1, + 499_999_999_500_000_000, + 100_000_000, + ), + ( + 1_000_000, + 1_000_000, + 21_000_000_000_000_000, + 1_000_000_000_000_000_000_u64, + 1, + 48_000_000_000_000_000, + 1_000_000_000_000_000, + ), + ( + 150_000_000_000, + 100_000_000_000, + 100_000_000_000, + 100_000_000_000, + u64::MAX, + 0, + 1, + ), + ( + 1_000_000, + 1_000_000, + 21_000_000_000_000_000, + 1_000_000_000_000_000_000_u64, + u64::MAX, + 0, + 1, + ), + ] + .iter() + .for_each( + |&( + tao_in_1, + alpha_in_1, + tao_in_2, + alpha_in_2, + limit_price, + expected_max_swappable, + precision, + )| { + // Forse-set alpha in and tao reserve to achieve relative price of subnets + SubnetTAO::::insert(origin_netuid, tao_in_1); + SubnetAlphaIn::::insert(origin_netuid, alpha_in_1); + SubnetTAO::::insert(destination_netuid, tao_in_2); + SubnetAlphaIn::::insert(destination_netuid, alpha_in_2); + + if (alpha_in_1 != 0) && (alpha_in_2 != 0) { + let origin_price = I96F32::from_num(tao_in_1) / I96F32::from_num(alpha_in_1); + let dest_price = I96F32::from_num(tao_in_2) / I96F32::from_num(alpha_in_2); + if dest_price != 0 { + let expected_price = origin_price / dest_price; + assert_eq!( + SubtensorModule::get_alpha_price(origin_netuid) + / SubtensorModule::get_alpha_price(destination_netuid), + expected_price + ); + } + } + + assert_abs_diff_eq!( + SubtensorModule::get_max_amount_move( + origin_netuid, + destination_netuid, + limit_price + ), + expected_max_swappable, + epsilon = precision + ); + }, + ); }); } From c4b1bf51631e036f4bcb05bcf73b86390655014d Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 31 Jan 2025 15:41:28 -0500 Subject: [PATCH 5/6] Cleanup --- pallets/subtensor/src/tests/staking.rs | 38 -------------------------- 1 file changed, 38 deletions(-) diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index c96903d4f3..da3e6a05d1 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -2744,44 +2744,6 @@ fn test_max_amount_remove_stable() { #[test] fn test_max_amount_remove_dynamic() { new_test_ext(0).execute_with(|| { - // // Low price values don't blow things up - // assert!(SubtensorModule::get_max_amount_remove(netuid, 1) > 0); - // assert!(SubtensorModule::get_max_amount_remove(netuid, 2) > 0); - // assert!(SubtensorModule::get_max_amount_remove(netuid, 3) > 0); - - // // 1.5000...1 price => max is 0 - // assert_eq!( - // SubtensorModule::get_max_amount_remove(netuid, 1_500_000_001), - // 0 - // ); - - // // 1.5 price => max is 0 because of non-zero slippage - // assert_abs_diff_eq!( - // SubtensorModule::get_max_amount_remove(netuid, 1_500_000_000), - // 0, - // epsilon = 10_000 - // ); - - // // 1/4 price => max is 2x Alpha - // assert_abs_diff_eq!( - // SubtensorModule::get_max_amount_remove(netuid, 375_000_000), - // 100_000_000_000, - // epsilon = 10_000, - // ); - - // // Precision test: - // // 1.499999.. price => max > 0 - // assert!(SubtensorModule::get_max_amount_remove(netuid, 1_499_999_999) > 0); - - // // Max price doesn't panic and returns something meaningful - // assert!(SubtensorModule::get_max_amount_remove(netuid, u64::MAX) < 21_000_000_000_000_000); - // assert!( - // SubtensorModule::get_max_amount_remove(netuid, u64::MAX - 1) < 21_000_000_000_000_000 - // ); - // assert!( - // SubtensorModule::get_max_amount_remove(netuid, u64::MAX / 2) < 21_000_000_000_000_000 - // ); - let subnet_owner_coldkey = U256::from(1001); let subnet_owner_hotkey = U256::from(1002); let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); From fa5a388d49496a7698a2d3be5bc934223734effd Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Sat, 1 Feb 2025 12:03:40 -0500 Subject: [PATCH 6/6] Use resulting average price formula for add-remove limit stake --- pallets/subtensor/src/staking/add_stake.rs | 43 +-- pallets/subtensor/src/staking/remove_stake.rs | 47 +-- pallets/subtensor/src/tests/staking.rs | 274 ++++++++---------- 3 files changed, 162 insertions(+), 202 deletions(-) diff --git a/pallets/subtensor/src/staking/add_stake.rs b/pallets/subtensor/src/staking/add_stake.rs index ded1ae18a6..2291de748e 100644 --- a/pallets/subtensor/src/staking/add_stake.rs +++ b/pallets/subtensor/src/staking/add_stake.rs @@ -1,7 +1,5 @@ use super::*; -use safe_math::*; use sp_core::Get; -use substrate_fixed::types::U96F32; impl Pallet { /// ---- The implementation for the extrinsic add_stake: Adds stake to a hotkey account. @@ -174,34 +172,39 @@ impl Pallet { if alpha_in == 0 { return 0; } - let alpha_in_float: U96F32 = U96F32::saturating_from_num(alpha_in); + let alpha_in_u128 = alpha_in as u128; // Corner case: SubnetTAO is zero. Staking can't happen, so max amount is zero. let tao_reserve = SubnetTAO::::get(netuid); if tao_reserve == 0 { return 0; } - let tao_reserve_float: U96F32 = U96F32::saturating_from_num(tao_reserve); + let tao_reserve_u128 = tao_reserve as u128; // Corner case: limit_price < current_price (price cannot decrease with staking) - let limit_price_float: U96F32 = U96F32::saturating_from_num(limit_price) - .checked_div(U96F32::saturating_from_num(1_000_000_000)) - .unwrap_or(U96F32::saturating_from_num(0)); - if limit_price_float < Self::get_alpha_price(netuid) { + let tao = 1_000_000_000_u128; + let limit_price_u128 = limit_price as u128; + if (limit_price_u128 + < Self::get_alpha_price(netuid) + .saturating_to_num::() + .saturating_mul(tao)) + || (limit_price == 0u64) + { return 0; } - // Main case: return SQRT(limit_price * SubnetTAO * SubnetAlphaIn) - SubnetTAO - // This is the positive solution of quare equation for finding additional TAO from - // limit_price. - let zero: U96F32 = U96F32::saturating_from_num(0.0); - let epsilon: U96F32 = U96F32::saturating_from_num(0.1); - let sqrt: U96F32 = - checked_sqrt(limit_price_float.saturating_mul(tao_reserve_float), epsilon) - .unwrap_or(zero) - .saturating_mul(checked_sqrt(alpha_in_float, epsilon).unwrap_or(zero)); - - sqrt.saturating_sub(U96F32::saturating_from_num(tao_reserve_float)) - .saturating_to_num::() + // Main case: return limit_price * SubnetAlphaIn - SubnetTAO + // Non overflowing calculation: limit_price * alpha_in <= u64::MAX * u64::MAX <= u128::MAX + // May overflow result, then it will be capped at u64::MAX, which is OK because that matches balance u64 size. + let result = limit_price_u128 + .saturating_mul(alpha_in_u128) + .checked_div(tao) + .unwrap_or(0) + .saturating_sub(tao_reserve_u128); + if result < u64::MAX as u128 { + result as u64 + } else { + u64::MAX + } } } diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index 1a7bf4ae4c..c1db7012c3 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -1,7 +1,5 @@ use super::*; -use safe_math::*; use sp_core::Get; -use substrate_fixed::types::U96F32; impl Pallet { /// ---- The implementation for the extrinsic remove_stake: Removes stake from a hotkey account and adds it onto a coldkey. @@ -343,14 +341,14 @@ impl Pallet { if alpha_in == 0 { return 0; } - let alpha_in_float: U96F32 = U96F32::saturating_from_num(alpha_in); + let alpha_in_u128 = alpha_in as u128; // Corner case: SubnetTAO is zero. Staking can't happen, so max amount is zero. let tao_reserve = SubnetTAO::::get(netuid); if tao_reserve == 0 { return 0; } - let tao_reserve_float: U96F32 = U96F32::saturating_from_num(tao_reserve); + let tao_reserve_u128 = tao_reserve as u128; // Corner case: limit_price == 0 (because there's division by limit price) // => can sell all @@ -359,25 +357,32 @@ impl Pallet { } // Corner case: limit_price >= current_price (price cannot increase with unstaking) - let limit_price_float: U96F32 = U96F32::saturating_from_num(limit_price) - .checked_div(U96F32::saturating_from_num(1_000_000_000)) - .unwrap_or(U96F32::saturating_from_num(0)); - if limit_price_float >= Self::get_alpha_price(netuid) { + // No overflows: alpha_price * tao <= u64::MAX * u64::MAX + // Alpha price is U96F32 size, but it is calculated as u64/u64, so it never uses all 96 bits. + let limit_price_u128 = limit_price as u128; + let tao = 1_000_000_000_u128; + if limit_price_u128 + >= tao_reserve_u128 + .saturating_mul(tao) + .checked_div(alpha_in_u128) + .unwrap_or(0) + { return 0; } - // Main case: return SQRT(SubnetTAO * SubnetAlphaIn / limit_price) - SubnetAlphaIn - // This is the positive solution of quare equation for finding Alpha amount from - // limit_price. - let zero: U96F32 = U96F32::saturating_from_num(0.0); - let epsilon: U96F32 = U96F32::saturating_from_num(0.1); - let sqrt: U96F32 = checked_sqrt(tao_reserve_float, epsilon) - .unwrap_or(zero) - .saturating_mul( - checked_sqrt(alpha_in_float.safe_div(limit_price_float), epsilon).unwrap_or(zero), - ); - - sqrt.saturating_sub(U96F32::saturating_from_num(alpha_in_float)) - .saturating_to_num::() + // Main case: SubnetTAO / limit_price - SubnetAlphaIn + // Non overflowing calculation: tao_reserve * tao <= u64::MAX * u64::MAX <= u128::MAX + // May overflow result, then it will be capped at u64::MAX, which is OK because that matches Alpha u64 size. + let result = tao_reserve_u128 + .saturating_mul(tao) + .checked_div(limit_price_u128) + .unwrap_or(0) + .saturating_sub(alpha_in_u128); + + if result < u64::MAX as u128 { + result as u64 + } else { + u64::MAX + } } } diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index da3e6a05d1..5282c9941c 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -2215,7 +2215,7 @@ fn test_add_stake_limit_validate() { new_test_ext(0).execute_with(|| { let hotkey = U256::from(533453); let coldkey = U256::from(55453); - let amount = 300_000_000_000; + let amount = 900_000_000_000; // add network let netuid: u16 = add_dynamic_network(&hotkey, &coldkey); @@ -2232,8 +2232,7 @@ fn test_add_stake_limit_validate() { SubtensorModule::add_balance_to_coldkey_account(&coldkey, 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 150 TAO only - // Alpha produced will be equal to 50 = 100 - 150*100/300 + // The amount that can be executed at this price is 450 TAO only let limit_price = 6_000_000_000; // Add stake limit call @@ -2542,134 +2541,106 @@ fn test_max_amount_add_dynamic() { // CSV backup for this spreadhsheet: // // SubnetTAO,AlphaIn,initial price,limit price,max swappable - // 100,100,=A2/B2,4,=SQRT(D2*A2*B2)-A2 + // 100,100,=A2/B2,4,=B8*D8-A8 // - // tao_in, alpha_in, limit_price, expected_max_swappable, precision + // tao_in, alpha_in, limit_price, expected_max_swappable [ // Zero handling (no panics) - (0, 1_000_000_000, 100, 0, 1), - (1_000_000_000, 0, 100, 0, 1), - (1_000_000_000, 1_000_000_000, 0, 0, 1), + (0, 1_000_000_000, 100, 0), + (1_000_000_000, 0, 100, 0), + (1_000_000_000, 1_000_000_000, 0, 0), // Low bounds - (1, 1, 0, 0, 1), - (1, 1, 1, 0, 1), - (1, 1, 2, 0, 1), - (1, 1, 50_000_000_000, 6, 1), + (1, 1, 0, 0), + (1, 1, 1, 0), + (1, 1, 2, 0), + (1, 1, 50_000_000_000, 49), // Basic math - (1_000, 1_000, 4_000_000_000, 1_000, 1), - (1_000, 1_000, 16_000_000_000, 3_000, 5), + (1_000, 1_000, 2_000_000_000, 1_000), + (1_000, 1_000, 4_000_000_000, 3_000), + (1_000, 1_000, 16_000_000_000, 15_000), ( 1_000_000_000_000, 1_000_000_000_000, 16_000_000_000, - 3_000_000_000_000, - 1_000_000, + 15_000_000_000_000, ), // Normal range values with edge cases - (150_000_000_000, 100_000_000_000, 0, 0, 1), - (150_000_000_000, 100_000_000_000, 100_000_000, 0, 1), - (150_000_000_000, 100_000_000_000, 500_000_000, 0, 1), - (150_000_000_000, 100_000_000_000, 1_499_999_999, 0, 1), - (150_000_000_000, 100_000_000_000, 1_500_000_000, 0, 1), - (150_000_000_000, 100_000_000_000, 1_500_000_100, 5000, 1000), + (150_000_000_000, 100_000_000_000, 0, 0), + (150_000_000_000, 100_000_000_000, 100_000_000, 0), + (150_000_000_000, 100_000_000_000, 500_000_000, 0), + (150_000_000_000, 100_000_000_000, 1_499_999_999, 0), + (150_000_000_000, 100_000_000_000, 1_500_000_000, 0), + (150_000_000_000, 100_000_000_000, 1_500_000_001, 100), ( 150_000_000_000, 100_000_000_000, - 6_000_000_000, + 3_000_000_000, 150_000_000_000, - 10_000, ), // Miscellaneous overflows and underflows - ( - 150_000_000_000, - 100_000_000_000, - u64::MAX, - 16_634_186_809_913_500, - 1_000_000_000, - ), - ( - 150_000_000_000, - 100_000_000_000, - u64::MAX / 2, - 11_762_102_358_830_800, - 1_000_000_000, - ), - ( - 1_000_000, - 1_000_000_000_000_000_000_u64, - 3, - 1_731_051, - 100_000, - ), + (150_000_000_000, 100_000_000_000, u64::MAX, u64::MAX), + (150_000_000_000, 100_000_000_000, u64::MAX / 2, u64::MAX), + (1_000_000, 1_000_000_000_000_000_000_u64, 1, 999_000_000), + (1_000_000, 1_000_000_000_000_000_000_u64, 2, 1_999_000_000), ( 1_000_000, 1_000_000_000_000_000_000_u64, 10_000, - 3_100_000_000, - 50_000_000, + 9_999_999_000_000, ), ( 1_000_000, 1_000_000_000_000_000_000_u64, 100_000, - 9_999_000_000, - 50_000_000, + 99_999_999_000_000, ), ( 1_000_000, 1_000_000_000_000_000_000_u64, 1_000_000, - 31_621_776_602, - 50_000_000, + 999_999_999_000_000, ), ( 1_000_000, 1_000_000_000_000_000_000_u64, 1_000_000_000, - 999_999_000_000, - 50_000_000, + 999_999_999_999_000_000, ), ( 21_000_000_000_000_000, 10_000_000, - 8_400_000_000_000_000_000, + 4_200_000_000_000_000_000, 21_000_000_000_000_000, - 1_000_000_000_000, ), ( 21_000_000_000_000_000, 1_000_000_000_000_000_000_u64, u64::MAX, u64::MAX, - 1_000_000_000, ), ( 21_000_000_000_000_000, 1_000_000_000_000_000_000_u64, - 84_000_000, + 42_000_000, 21_000_000_000_000_000, - 1_000_000_000, ), ] .iter() - .for_each( - |&(tao_in, alpha_in, limit_price, expected_max_swappable, precision)| { - // Forse-set alpha in and tao reserve to achieve relative price of subnets - SubnetTAO::::insert(netuid, tao_in); - SubnetAlphaIn::::insert(netuid, alpha_in); - - if alpha_in != 0 { - let expected_price = I96F32::from_num(tao_in) / I96F32::from_num(alpha_in); - assert_eq!(SubtensorModule::get_alpha_price(netuid), expected_price); - } + .for_each(|&(tao_in, alpha_in, limit_price, expected_max_swappable)| { + // Forse-set alpha in and tao reserve to achieve relative price of subnets + SubnetTAO::::insert(netuid, tao_in); + SubnetAlphaIn::::insert(netuid, alpha_in); + + if alpha_in != 0 { + let expected_price = I96F32::from_num(tao_in) / I96F32::from_num(alpha_in); + assert_eq!(SubtensorModule::get_alpha_price(netuid), expected_price); + } - assert_abs_diff_eq!( - SubtensorModule::get_max_amount_add(netuid, limit_price), - expected_max_swappable, - epsilon = precision - ); - }, - ); + assert_eq!( + SubtensorModule::get_max_amount_add(netuid, limit_price), + expected_max_swappable, + ); + }); }); } @@ -2755,110 +2726,90 @@ fn test_max_amount_remove_dynamic() { // CSV backup for this spreadhsheet: // // SubnetTAO,AlphaIn,initial price,limit price,max swappable - // 100,100,=A2/B2,4,=SQRT(D2*A2*B2)-A2 + // 100,100,=A2/B2,4,=A2/D2-B2 // - // tao_in, alpha_in, limit_price, expected_max_swappable, precision + // tao_in, alpha_in, limit_price, expected_max_swappable [ // Zero handling (no panics) - (0, 1_000_000_000, 100, 0, 1), - (1_000_000_000, 0, 100, 0, 1), - (1_000_000_000, 1_000_000_000, 0, u64::MAX, 1), + (0, 1_000_000_000, 100, 0), + (1_000_000_000, 0, 100, 0), + (1_000_000_000, 1_000_000_000, 0, u64::MAX), // Low bounds - (1, 1, 0, u64::MAX, 1), - (1, 1, 1, 31_622, 1_000), - (1, 1, 2, 22_360, 1_000), - (1, 1, 250_000_000, 1, 1), + (1, 1, 0, u64::MAX), + (1, 1, 1, 999_999_999), + (1, 1, 2, 499_999_999), + (1, 1, 250_000_000, 3), // Basic math - (1_000, 1_000, 250_000_000, 1_000, 1), - (1_000, 1_000, 62_500_000, 3_000, 5), + (1_000, 1_000, 250_000_000, 3_000), + (1_000, 1_000, 62_500_000, 15_000), ( 1_000_000_000_000, 1_000_000_000_000, 62_500_000, - 3_000_000_000_000, - 1_000_000, + 15_000_000_000_000, ), // Normal range values with edge cases - (200_000_000_000, 100_000_000_000, 0, u64::MAX, 1), + (200_000_000_000, 100_000_000_000, 0, u64::MAX), ( 200_000_000_000, 100_000_000_000, 1_000_000_000, - 41_421_356_237, - 1_000_000, - ), - ( - 200_000_000_000, - 100_000_000_000, - 500_000_000, 100_000_000_000, - 1_000_000, ), - (200_000_000_000, 100_000_000_000, 2_000_000_000, 0, 1), - (200_000_000_000, 100_000_000_000, 2_000_000_001, 0, 1), ( 200_000_000_000, 100_000_000_000, - 1_999_999_000, - 25_000, - 10_000, - ), - ( - 200_000_000_000, - 100_000_000_000, - 1_999_000_000, - 25_009_379, - 10_000, + 500_000_000, + 300_000_000_000, ), + (200_000_000_000, 100_000_000_000, 2_000_000_000, 0), + (200_000_000_000, 100_000_000_000, 2_000_000_001, 0), + (200_000_000_000, 100_000_000_000, 1_999_999_999, 50), + (200_000_000_000, 100_000_000_000, 1_999_999_990, 500), // Miscellaneous overflows and underflows - (2_000_000_000_000, 100_000_000_000, u64::MAX, 0, 1), - (200_000_000_000, 100_000_000_000, u64::MAX / 2, 0, 1), - (1_000_000, 1_000_000_000_000_000_000_u64, 1, 0, 1), - (1_000_000, 1_000_000_000_000_000_000_u64, 10, 0, 1), - (1_000_000, 1_000_000_000_000_000_000_u64, 100, 0, 1), - (1_000_000, 1_000_000_000_000_000_000_u64, 1_000, 0, 1), - (1_000_000, 1_000_000_000_000_000_000_u64, u64::MAX, 0, 1), + (2_000_000_000_000, 100_000_000_000, u64::MAX, 0), + (200_000_000_000, 100_000_000_000, u64::MAX / 2, 0), + (1_000_000, 1_000_000_000_000_000_000_u64, 1, 0), + (1_000_000, 1_000_000_000_000_000_000_u64, 10, 0), + (1_000_000, 1_000_000_000_000_000_000_u64, 100, 0), + (1_000_000, 1_000_000_000_000_000_000_u64, 1_000, 0), + (1_000_000, 1_000_000_000_000_000_000_u64, u64::MAX, 0), ( 21_000_000_000_000_000, - 1_000_000_000, - 5_250_000_000_000_000, - 1_000_000_000, - 10_000_000, + 1_000_000, + 21_000_000_000_000_000, + 999_000_000, ), + (21_000_000_000_000_000, 1_000_000, u64::MAX, 138_412), ( 21_000_000_000_000_000, 1_000_000_000_000_000_000_u64, u64::MAX, 0, - 1, ), ( 21_000_000_000_000_000, 1_000_000_000_000_000_000_u64, - 5_250_000, - 1_000_000_000_000_000_000_u64, - 100_000_000_000, + 20_000_000, + 50_000_000_000_000_000, ), ] .iter() - .for_each( - |&(tao_in, alpha_in, limit_price, expected_max_swappable, precision)| { - // Forse-set alpha in and tao reserve to achieve relative price of subnets - SubnetTAO::::insert(netuid, tao_in); - SubnetAlphaIn::::insert(netuid, alpha_in); - - if alpha_in != 0 { - let expected_price = I96F32::from_num(tao_in) / I96F32::from_num(alpha_in); - assert_eq!(SubtensorModule::get_alpha_price(netuid), expected_price); - } + .for_each(|&(tao_in, alpha_in, limit_price, expected_max_swappable)| { + // Forse-set alpha in and tao reserve to achieve relative price of subnets + SubnetTAO::::insert(netuid, tao_in); + SubnetAlphaIn::::insert(netuid, alpha_in); + + if alpha_in != 0 { + let expected_price = I96F32::from_num(tao_in) / I96F32::from_num(alpha_in); + assert_eq!(SubtensorModule::get_alpha_price(netuid), expected_price); + } - assert_abs_diff_eq!( - SubtensorModule::get_max_amount_remove(netuid, limit_price), - expected_max_swappable, - epsilon = precision - ); - }, - ); + assert_eq!( + SubtensorModule::get_max_amount_remove(netuid, limit_price), + expected_max_swappable, + ); + }); }); } @@ -2979,9 +2930,9 @@ fn test_max_amount_move_stable_dynamic() { 0 ); - // 0.5x price => max is 1x TAO + // 2x price => max is 1x TAO assert_abs_diff_eq!( - SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 500_000_000), + SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 1_000_000_000), 50_000_000_000, epsilon = 10_000, ); @@ -3056,9 +3007,9 @@ fn test_max_amount_move_dynamic_stable() { epsilon = 10_000 ); - // 1/4 price => max is 2x Alpha + // 1/2 price => max is 1x Alpha assert_abs_diff_eq!( - SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, 375_000_000), + SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, 750_000_000), 100_000_000_000, epsilon = 10_000, ); @@ -3317,7 +3268,7 @@ 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 = 300_000_000_000; + let amount = 900_000_000_000; // over the maximum let fee = DefaultStakingFee::::get(); // add network @@ -3335,10 +3286,10 @@ fn test_add_stake_limit_ok() { 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 150 TAO only - // Alpha produced will be equal to 50 = 100 - 150*100/300 + // 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 = 50_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( @@ -3350,7 +3301,7 @@ fn test_add_stake_limit_ok() { true )); - // Check if stake has increased only by 50 Alpha + // 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, @@ -3361,15 +3312,15 @@ fn test_add_stake_limit_ok() { epsilon = expected_executed_stake / 1000, ); - // Check that 150 TAO balance still remains free on coldkey + // Check that 450 TAO balance still remains free on coldkey assert_abs_diff_eq!( SubtensorModule::get_coldkey_balance(&coldkey_account_id), - 150_000_000_000, + 450_000_000_000, epsilon = 10_000 ); - // Check that price has updated to ~6 - let exp_price = U96F32::from_num(6.0); + // 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!(exp_price.saturating_sub(current_price) < 0.0001); assert!(current_price.saturating_sub(exp_price) < 0.0001); @@ -3381,7 +3332,7 @@ fn test_add_stake_limit_fill_or_kill() { new_test_ext(1).execute_with(|| { let hotkey_account_id = U256::from(533453); let coldkey_account_id = U256::from(55453); - let amount = 300_000_000_000; + let amount = 900_000_000_000; // over the maximum // add network let netuid: u16 = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); @@ -3398,8 +3349,8 @@ fn test_add_stake_limit_fill_or_kill() { 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 150 TAO only - // Alpha produced will be equal to 50 = 100 - 150*100/300 + // The amount that can be executed at this price is 450 TAO only + // Alpha produced will be equal to 25 = 100 - 450*100/(150+450) let limit_price = 6_000_000_000; // Add stake with slippage safety and check if it fails @@ -3416,11 +3367,12 @@ fn test_add_stake_limit_fill_or_kill() { ); // Lower the amount and it should succeed now + let amount_ok = 450_000_000_000; // fits the maximum assert_ok!(SubtensorModule::add_stake_limit( RuntimeOrigin::signed(coldkey_account_id), hotkey_account_id, netuid, - amount / 100, + amount_ok, limit_price, false )); @@ -3460,11 +3412,11 @@ fn test_remove_stake_limit_ok() { 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 that it doesn't drop by more than 10% from current price + // 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 = sqrt(150 * 100 / 1.35) - 100 ~ 5.409 - let expected_alpha_reduction = 5_409_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(