diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index f789d81918..064bb5c6e4 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1887,6 +1887,8 @@ where *origin_netuid, *destination_netuid, *alpha_amount, + *alpha_amount, + None, )) } Some(Call::transfer_stake { @@ -1905,6 +1907,8 @@ where *origin_netuid, *destination_netuid, *alpha_amount, + *alpha_amount, + None, )) } Some(Call::swap_stake { @@ -1922,6 +1926,36 @@ where *origin_netuid, *destination_netuid, *alpha_amount, + *alpha_amount, + None, + )) + } + 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, + who, + hotkey, + hotkey, + *origin_netuid, + *destination_netuid, + *alpha_amount, + 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 6b8592db8e..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: @@ -1811,5 +1811,52 @@ mod dispatches { allow_partial, ) } + + /// Swaps a specified amount of stake from one subnet to another, while keeping the same coldkey and hotkey. + /// + /// # 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. + /// + /// # 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 swap_stake_limit( + origin: T::RuntimeOrigin, + hotkey: T::AccountId, + origin_netuid: u16, + destination_netuid: u16, + alpha_amount: u64, + limit_price: u64, + allow_partial: bool, + ) -> DispatchResult { + Self::do_swap_stake_limit( + origin, + hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + limit_price, + allow_partial, + ) + } } } 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/move_stake.rs b/pallets/subtensor/src/staking/move_stake.rs index 75f9331356..a60191de88 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::U64F64; impl Pallet { /// Moves stake from one hotkey to another across subnets. @@ -44,6 +45,8 @@ impl Pallet { origin_netuid, destination_netuid, alpha_amount, + None, + None, )?; // Log the event. @@ -113,6 +116,8 @@ impl Pallet { origin_netuid, destination_netuid, alpha_amount, + None, + None, )?; // 9. Emit an event for logging/monitoring. @@ -180,6 +185,8 @@ impl Pallet { origin_netuid, destination_netuid, alpha_amount, + None, + None, )?; // Emit an event for logging. @@ -203,6 +210,79 @@ impl Pallet { Ok(()) } + /// 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 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. + /// * `limit_price` - The limit price. + /// * `allow_partial` - Allow partial execution + /// + /// # Returns + /// * `DispatchResult` - Indicates success or failure. + /// + /// # Errors + /// 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 `StakeSwapped` event upon successful completion. + 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)?; + + // Validate input and move stake + let tao_moved = Self::transition_stake_internal( + &coldkey, + &coldkey, + &hotkey, + &hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + Some(limit_price), + Some(allow_partial), + )?; + + // Emit an event for logging. + log::info!( + "StakeSwapped(coldkey: {:?}, hotkey: {:?}, origin_netuid: {:?}, destination_netuid: {:?}, amount: {:?})", + coldkey, + hotkey, + origin_netuid, + destination_netuid, + tao_moved + ); + Self::deposit_event(Event::StakeSwapped( + coldkey, + hotkey, + origin_netuid, + destination_netuid, + tao_moved, + )); + + // 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( 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,126 @@ 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 { + 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. + // 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) + { + if limit_price > tao.saturating_to_num::() { + return 0; + } else { + 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) + { + 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(U64F64::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 + // 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: 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); + let alpha_in_2 = SubnetAlphaIn::::get(destination_netuid); + if (alpha_in_1 == 0) || (alpha_in_2 == 0) { + return 0; + } + 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: 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 { + 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: 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); + + alpha_in_2_float + .saturating_mul(t1_over_sum) + .safe_div(limit_price_float) + .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..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 @@ -358,26 +356,33 @@ impl Pallet { return u64::MAX; } - // 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) { + // Corner case: limit_price >= current_price (price cannot increase with unstaking) + // 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/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index 147490dd8f..b4b650804a 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(()) } } 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..5282c9941c 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 @@ -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 @@ -2535,44 +2534,113 @@ 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 - ); - - // 1.5 price => max is 0 because of non-zero slippage - assert_eq!( - SubtensorModule::get_max_amount_add(netuid, 1_500_000_000), - 0 - ); - - // 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, - ); - - // Precision test: - // 1.50000..100 price => max > 0 - assert!(SubtensorModule::get_max_amount_add(netuid, 1_500_000_100) > 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,=B8*D8-A8 + // + // tao_in, alpha_in, limit_price, expected_max_swappable + [ + // Zero handling (no panics) + (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, 0), + (1, 1, 2, 0), + (1, 1, 50_000_000_000, 49), + // Basic math + (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, + 15_000_000_000_000, + ), + // Normal range values with edge cases + (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, + 3_000_000_000, + 150_000_000_000, + ), + // Miscellaneous overflows and underflows + (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, + 9_999_999_000_000, + ), + ( + 1_000_000, + 1_000_000_000_000_000_000_u64, + 100_000, + 99_999_999_000_000, + ), + ( + 1_000_000, + 1_000_000_000_000_000_000_u64, + 1_000_000, + 999_999_999_000_000, + ), + ( + 1_000_000, + 1_000_000_000_000_000_000_u64, + 1_000_000_000, + 999_999_999_999_000_000, + ), + ( + 21_000_000_000_000_000, + 10_000_000, + 4_200_000_000_000_000_000, + 21_000_000_000_000_000, + ), + ( + 21_000_000_000_000_000, + 1_000_000_000_000_000_000_u64, + u64::MAX, + u64::MAX, + ), + ( + 21_000_000_000_000_000, + 1_000_000_000_000_000_000_u64, + 42_000_000, + 21_000_000_000_000_000, + ), + ] + .iter() + .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); + } - // 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); + assert_eq!( + SubtensorModule::get_max_amount_add(netuid, limit_price), + expected_max_swappable, + ); + }); }); } @@ -2651,53 +2719,546 @@ fn test_max_amount_remove_dynamic() { let subnet_owner_hotkey = U256::from(1002); let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + // 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,=A2/D2-B2 + // + // tao_in, alpha_in, limit_price, expected_max_swappable + [ + // Zero handling (no panics) + (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, 999_999_999), + (1, 1, 2, 499_999_999), + (1, 1, 250_000_000, 3), + // Basic math + (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, + 15_000_000_000_000, + ), + // Normal range values with edge cases + (200_000_000_000, 100_000_000_000, 0, u64::MAX), + ( + 200_000_000_000, + 100_000_000_000, + 1_000_000_000, + 100_000_000_000, + ), + ( + 200_000_000_000, + 100_000_000_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), + (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, + 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, + ), + ( + 21_000_000_000_000_000, + 1_000_000_000_000_000_000_u64, + 20_000_000, + 50_000_000_000_000_000, + ), + ] + .iter() + .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_eq!( + SubtensorModule::get_max_amount_remove(netuid, limit_price), + expected_max_swappable, + ); + }); + }); +} + +// 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 + ); + + // 2x price => max is 1x TAO + assert_abs_diff_eq!( + SubtensorModule::get_max_amount_move(stable_netuid, dynamic_netuid, 1_000_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(|| { + // 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 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)); + 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)); + // The tests below just mimic the remove_stake_limit tests + // 0 price => max is u64::MAX - assert_eq!(SubtensorModule::get_max_amount_remove(netuid, 0), u64::MAX); + assert_eq!( + SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, 0), + u64::MAX + ); // 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); + 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); // 1.5000...1 price => max is 0 assert_eq!( - SubtensorModule::get_max_amount_remove(netuid, 1_500_000_001), + SubtensorModule::get_max_amount_move(dynamic_netuid, stable_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), + SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, 1_500_000_000), 0, 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_remove(netuid, 375_000_000), + SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, 750_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); + 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!(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 + 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_remove(netuid, u64::MAX / 2) < 21_000_000_000_000_000 + SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, u64::MAX / 2) + < 21_000_000_000_000_000 + ); + }); +} + +// 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(|| { + // 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); + + // 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 + ); + }, ); }); } @@ -2707,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 @@ -2725,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( @@ -2740,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, @@ -2751,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); @@ -2771,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); @@ -2788,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 @@ -2806,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 )); @@ -2850,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(