diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 7f9cd7120d..d346b9241a 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1569,6 +1569,7 @@ pub enum CustomTransactionError { NotEnoughStakeToWithdraw, RateLimitExceeded, InsufficientLiquidity, + SlippageTooHigh, BadRequest, } @@ -1583,6 +1584,7 @@ impl From for u8 { CustomTransactionError::NotEnoughStakeToWithdraw => 5, CustomTransactionError::RateLimitExceeded => 6, CustomTransactionError::InsufficientLiquidity => 7, + CustomTransactionError::SlippageTooHigh => 8, CustomTransactionError::BadRequest => 255, } } @@ -1654,6 +1656,10 @@ where CustomTransactionError::InsufficientLiquidity.into(), ) .into()), + Error::::SlippageTooHigh => Err(InvalidTransaction::Custom( + CustomTransactionError::SlippageTooHigh.into(), + ) + .into()), _ => Err( InvalidTransaction::Custom(CustomTransactionError::BadRequest.into()).into(), ), @@ -1801,6 +1807,8 @@ where hotkey, *netuid, *amount_staked, + *amount_staked, + false, )) } Some(Call::remove_stake { @@ -1814,6 +1822,8 @@ where hotkey, *netuid, *amount_unstaked, + *amount_unstaked, + false, )) } Some(Call::move_stake { diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 2a1165b1f2..6b8592db8e 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -1710,6 +1710,10 @@ mod dispatches { /// * 'limit_price' (u64): /// - The limit price expressed in units of RAO per one Alpha. /// + /// * 'allow_partial' (bool): + /// - Allows partial execution of the amount. If set to false, this becomes + /// fill or kill type or order. + /// /// # Event: /// * StakeAdded; /// - On the successfully adding stake to a global account. @@ -1734,8 +1738,16 @@ mod dispatches { netuid: u16, amount_staked: u64, limit_price: u64, + allow_partial: bool, ) -> DispatchResult { - Self::do_add_stake_limit(origin, hotkey, netuid, amount_staked, limit_price) + Self::do_add_stake_limit( + origin, + hotkey, + netuid, + amount_staked, + limit_price, + allow_partial, + ) } /// --- Removes stake from a hotkey on a subnet with a price limit. @@ -1759,6 +1771,10 @@ mod dispatches { /// * 'limit_price' (u64): /// - The limit price expressed in units of RAO per one Alpha. /// + /// * 'allow_partial' (bool): + /// - Allows partial execution of the amount. If set to false, this becomes + /// fill or kill type or order. + /// /// # Event: /// * StakeRemoved; /// - On the successfully removing stake from the hotkey account. @@ -1784,8 +1800,16 @@ mod dispatches { netuid: u16, amount_unstaked: u64, limit_price: u64, + allow_partial: bool, ) -> DispatchResult { - Self::do_remove_stake_limit(origin, hotkey, netuid, amount_unstaked, limit_price) + Self::do_remove_stake_limit( + origin, + hotkey, + netuid, + amount_unstaked, + limit_price, + allow_partial, + ) } } } diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index f1eea3b9c6..926aead580 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -187,5 +187,7 @@ mod errors { AmountTooLow, /// Not enough liquidity. InsufficientLiquidity, + /// Slippage is too high for the transaction. + SlippageTooHigh, } } diff --git a/pallets/subtensor/src/staking/add_stake.rs b/pallets/subtensor/src/staking/add_stake.rs index fd4ec630a6..ded1ae18a6 100644 --- a/pallets/subtensor/src/staking/add_stake.rs +++ b/pallets/subtensor/src/staking/add_stake.rs @@ -50,7 +50,14 @@ impl Pallet { ); // 2. Validate user input - Self::validate_add_stake(&coldkey, &hotkey, netuid, stake_to_be_added)?; + Self::validate_add_stake( + &coldkey, + &hotkey, + netuid, + stake_to_be_added, + stake_to_be_added, + false, + )?; // 3. Ensure the remove operation from the coldkey is a success. let tao_staked: u64 = @@ -81,6 +88,10 @@ impl Pallet { /// * 'limit_price' (u64): /// - The limit price expressed in units of RAO per one Alpha. /// + /// * 'allow_partial' (bool): + /// - Allows partial execution of the amount. If set to false, this becomes + /// fill or kill type or order. + /// /// # Event: /// * StakeAdded; /// - On the successfully adding stake to a global account. @@ -104,6 +115,7 @@ impl Pallet { netuid: u16, stake_to_be_added: u64, limit_price: u64, + allow_partial: bool, ) -> dispatch::DispatchResult { // 1. We check that the transaction is signed by the caller and retrieve the T::AccountId coldkey information. let coldkey = ensure_signed(origin)?; @@ -115,16 +127,23 @@ impl Pallet { stake_to_be_added ); - // 2. Validate user input - Self::validate_add_stake(&coldkey, &hotkey, netuid, stake_to_be_added)?; - - // 3. Calcaulate the maximum amount that can be executed with price limit + // 2. Calcaulate the maximum amount that can be executed with price limit let max_amount = Self::get_max_amount_add(netuid, limit_price); let mut possible_stake = stake_to_be_added; if possible_stake > max_amount { possible_stake = max_amount; } + // 3. Validate user input + Self::validate_add_stake( + &coldkey, + &hotkey, + netuid, + stake_to_be_added, + max_amount, + allow_partial, + )?; + // 4. Ensure the remove operation from the coldkey is a success. let tao_staked: u64 = Self::remove_balance_from_coldkey_account(&coldkey, possible_stake)?; diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index 564a62f78b..92d0c2e83c 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -50,7 +50,14 @@ impl Pallet { ); // 2. Validate the user input - Self::validate_remove_stake(&coldkey, &hotkey, netuid, alpha_unstaked)?; + Self::validate_remove_stake( + &coldkey, + &hotkey, + netuid, + alpha_unstaked, + alpha_unstaked, + false, + )?; // 3. Swap the alpba to tao and update counters for this subnet. let fee = DefaultStakingFee::::get(); @@ -223,12 +230,51 @@ impl Pallet { Ok(()) } + /// ---- The implementation for the extrinsic remove_stake_limit: Removes stake from + /// a hotkey on a subnet with a price limit. + /// + /// In case if slippage occurs and the price shall move beyond the limit + /// price, the staking order may execute only partially or not execute + /// at all. + /// + /// # Args: + /// * 'origin': (Origin): + /// - The signature of the caller's coldkey. + /// + /// * 'hotkey' (T::AccountId): + /// - The associated hotkey account. + /// + /// * 'amount_unstaked' (u64): + /// - The amount of stake to be added to the hotkey staking account. + /// + /// * 'limit_price' (u64): + /// - The limit price expressed in units of RAO per one Alpha. + /// + /// * 'allow_partial' (bool): + /// - Allows partial execution of the amount. If set to false, this becomes + /// fill or kill type or order. + /// + /// # Event: + /// * StakeRemoved; + /// - On the successfully removing stake from the hotkey account. + /// + /// # Raises: + /// * 'NotRegistered': + /// - Thrown if the account we are attempting to unstake from is non existent. + /// + /// * 'NonAssociatedColdKey': + /// - Thrown if the coldkey does not own the hotkey we are unstaking from. + /// + /// * 'NotEnoughStakeToWithdraw': + /// - Thrown if there is not enough stake on the hotkey to withdwraw this amount. + /// pub fn do_remove_stake_limit( origin: T::RuntimeOrigin, hotkey: T::AccountId, netuid: u16, alpha_unstaked: u64, limit_price: u64, + allow_partial: bool, ) -> dispatch::DispatchResult { // 1. We check the transaction is signed by the caller and retrieve the T::AccountId coldkey information. let coldkey = ensure_signed(origin)?; @@ -240,17 +286,24 @@ impl Pallet { alpha_unstaked ); - // 2. Validate the user input - Self::validate_remove_stake(&coldkey, &hotkey, netuid, alpha_unstaked)?; - - // 3. Calcaulate the maximum amount that can be executed with price limit + // 2. Calcaulate the maximum amount that can be executed with price limit let max_amount = Self::get_max_amount_remove(netuid, limit_price); let mut possible_alpha = alpha_unstaked; if possible_alpha > max_amount { possible_alpha = max_amount; } - // 4. Swap the alpba to tao and update counters for this subnet. + // 3. Validate the user input + Self::validate_remove_stake( + &coldkey, + &hotkey, + netuid, + alpha_unstaked, + max_amount, + allow_partial, + )?; + + // 4. Swap the alpha to tao and update counters for this subnet. let fee = DefaultStakingFee::::get(); let tao_unstaked: u64 = Self::unstake_from_subnet(&hotkey, &coldkey, netuid, possible_alpha, fee); diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index e247f7f4f5..4c446947f5 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -729,6 +729,8 @@ impl Pallet { hotkey: &T::AccountId, netuid: u16, stake_to_be_added: u64, + max_amount: u64, + allow_partial: bool, ) -> Result<(), Error> { // Ensure that the subnet exists. ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); @@ -739,6 +741,12 @@ impl Pallet { // Ensure that the stake_to_be_added is at least the min_amount ensure!(stake_to_be_added >= min_amount, Error::::AmountTooLow); + // Ensure that if partial execution is not allowed, the amount will not cause + // slippage over desired + if !allow_partial { + ensure!(stake_to_be_added <= max_amount, Error::::SlippageTooHigh); + } + // Ensure the callers coldkey has enough stake to perform the transaction. ensure!( Self::can_remove_balance_from_coldkey_account(coldkey, stake_to_be_added), @@ -767,6 +775,8 @@ impl Pallet { hotkey: &T::AccountId, netuid: u16, alpha_unstaked: u64, + max_amount: u64, + allow_partial: bool, ) -> Result<(), Error> { // Ensure that the subnet exists. ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); @@ -781,6 +791,12 @@ impl Pallet { return Err(Error::::InsufficientLiquidity); }; + // Ensure that if partial execution is not allowed, the amount will not cause + // slippage over desired + if !allow_partial { + ensure!(alpha_unstaked <= max_amount, Error::::SlippageTooHigh); + } + // Ensure that the hotkey account exists this is only possible through registration. ensure!( Self::hotkey_account_exists(hotkey), diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index 51887a17dd..6a61008a3f 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -2620,7 +2620,8 @@ fn test_add_stake_limit_ok() { hotkey_account_id, netuid, amount, - limit_price + limit_price, + true )); // Check if stake has increased only by 50 Alpha @@ -2649,6 +2650,57 @@ fn test_add_stake_limit_ok() { }); } +#[test] +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; + + // add network + let netuid: u16 = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + + // Force-set alpha in and tao reserve to make price equal 1.5 + let tao_reserve: U96F32 = U96F32::from_num(150_000_000_000_u64); + let alpha_in: U96F32 = U96F32::from_num(100_000_000_000_u64); + SubnetTAO::::insert(netuid, tao_reserve.to_num::()); + SubnetAlphaIn::::insert(netuid, alpha_in.to_num::()); + let current_price: U96F32 = U96F32::from_num(SubtensorModule::get_alpha_price(netuid)); + assert_eq!(current_price, U96F32::from_num(1.5)); + + // Give it some $$$ in his coldkey balance + SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, amount); + + // Setup limit price so that it doesn't peak above 4x of current price + // The amount that can be executed at this price is 150 TAO only + // Alpha produced will be equal to 50 = 100 - 150*100/300 + let limit_price = 6_000_000_000; + + // Add stake with slippage safety and check if it fails + assert_noop!( + SubtensorModule::add_stake_limit( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount, + limit_price, + false + ), + Error::::SlippageTooHigh + ); + + // Lower the amount and it should succeed now + assert_ok!(SubtensorModule::add_stake_limit( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount / 100, + limit_price, + false + )); + }); +} + #[test] fn test_remove_stake_limit_ok() { new_test_ext(1).execute_with(|| { @@ -2694,7 +2746,8 @@ fn test_remove_stake_limit_ok() { hotkey_account_id, netuid, unstake_amount, - limit_price + limit_price, + true )); // Check if stake has decreased only by @@ -2709,3 +2762,58 @@ fn test_remove_stake_limit_ok() { ); }); } + +#[test] +fn test_remove_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 stake_amount = 300_000_000_000; + let unstake_amount = 150_000_000_000; + + // add network + let netuid: u16 = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + + // Give the neuron some stake to remove + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + stake_amount, + ); + + // Forse-set alpha in and tao reserve to make price equal 1.5 + let tao_reserve: U96F32 = U96F32::from_num(150_000_000_000_u64); + let alpha_in: U96F32 = U96F32::from_num(100_000_000_000_u64); + SubnetTAO::::insert(netuid, tao_reserve.to_num::()); + SubnetAlphaIn::::insert(netuid, alpha_in.to_num::()); + let current_price: U96F32 = U96F32::from_num(SubtensorModule::get_alpha_price(netuid)); + assert_eq!(current_price, U96F32::from_num(1.5)); + + // Setup limit price so that it doesn't drop by more than 10% from current price + let limit_price = 1_350_000_000; + + // Remove stake with slippage safety - fails + assert_noop!( + SubtensorModule::remove_stake_limit( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + unstake_amount, + limit_price, + false + ), + Error::::SlippageTooHigh + ); + + // Lower the amount: Should succeed + assert_ok!(SubtensorModule::remove_stake_limit( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + unstake_amount / 100, + limit_price, + false + ),); + }); +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 372c9a9815..2a2d2fac47 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -220,7 +220,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 224, + spec_version: 225, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1,