diff --git a/pallets/subtensor/src/benchmarks.rs b/pallets/subtensor/src/benchmarks.rs index c62d1dcecc..2b75da19d5 100644 --- a/pallets/subtensor/src/benchmarks.rs +++ b/pallets/subtensor/src/benchmarks.rs @@ -1355,4 +1355,58 @@ mod pallet_benchmarks { #[extrinsic_call] _(RawOrigin::Signed(coldkey), hotkey); } + + #[benchmark] + fn remove_stake_full_limit() { + let netuid = NetUid::from(1); + let tempo: u16 = 1; + let seed: u32 = 1; + + // Set our total stake to 1000 TAO + Subtensor::::increase_total_stake(1_000_000_000_000); + + Subtensor::::init_new_network(netuid, tempo); + Subtensor::::set_network_registration_allowed(netuid, true); + SubtokenEnabled::::insert(netuid, true); + + Subtensor::::set_max_allowed_uids(netuid, 4096); + assert_eq!(Subtensor::::get_max_allowed_uids(netuid), 4096); + + let coldkey: T::AccountId = account("Test", 0, seed); + let hotkey: T::AccountId = account("Alice", 0, seed); + Subtensor::::set_burn(netuid, 1); + + let limit: u64 = 1_000_000_000; + let tao_reserve = 150_000_000_000_u64; + let alpha_in = 100_000_000_000_u64; + SubnetTAO::::insert(netuid, tao_reserve); + SubnetAlphaIn::::insert(netuid, alpha_in); + + let wallet_bal = 1000000u32.into(); + Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), wallet_bal); + + assert_ok!(Subtensor::::do_burned_registration( + RawOrigin::Signed(coldkey.clone()).into(), + netuid, + hotkey.clone() + )); + + let u64_staked_amt = 100_000_000_000; + Subtensor::::add_balance_to_coldkey_account(&coldkey.clone(), u64_staked_amt); + + assert_ok!(Subtensor::::add_stake( + RawOrigin::Signed(coldkey.clone()).into(), + hotkey.clone(), + netuid, + u64_staked_amt + )); + + #[extrinsic_call] + _( + RawOrigin::Signed(coldkey.clone()), + hotkey.clone(), + netuid, + Some(limit), + ); + } } diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 8ceccc5fa2..838aa1c715 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -2069,5 +2069,22 @@ mod dispatches { PendingChildKeyCooldown::::put(cooldown); Ok(()) } + + /// Removes all stake from a hotkey on a subnet with a price limit. + /// This extrinsic allows to specify the limit price for alpha token + /// at which or better (higher) the staking should execute. + /// Without limit_price it remove all the stake similar to `remove_stake` extrinsic + #[pallet::call_index(103)] + #[pallet::weight((Weight::from_parts(398_000_000, 10142) + .saturating_add(T::DbWeight::get().reads(29_u64)) + .saturating_add(T::DbWeight::get().writes(14_u64)), DispatchClass::Normal, Pays::No))] + pub fn remove_stake_full_limit( + origin: T::RuntimeOrigin, + hotkey: T::AccountId, + netuid: NetUid, + limit_price: Option, + ) -> DispatchResult { + Self::do_remove_stake_full_limit(origin, hotkey, netuid, limit_price) + } } } diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index 5f54a7449b..02453cf96c 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -403,4 +403,22 @@ impl Pallet { Err(Error::ZeroMaxStakeAmount) } } + + pub fn do_remove_stake_full_limit( + origin: T::RuntimeOrigin, + hotkey: T::AccountId, + netuid: NetUid, + limit_price: Option, + ) -> DispatchResult { + let coldkey = ensure_signed(origin.clone())?; + + let alpha_unstaked = + Self::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + + if let Some(limit_price) = limit_price { + Self::do_remove_stake_limit(origin, hotkey, netuid, alpha_unstaked, limit_price, false) + } else { + Self::do_remove_stake(origin, hotkey, netuid, alpha_unstaked) + } + } } diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index bca880b01c..21fe4a4869 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -5055,6 +5055,137 @@ fn test_increase_stake_for_hotkey_and_coldkey_on_subnet_adds_to_staking_hotkeys_ }); } +#[test] +fn test_remove_stake_full_limit_ok() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(1); + let coldkey_account_id = U256::from(2); + let stake_amount = 10_000_000_000; + + // add network + let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + + // Give the neuron some stake to remove + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + stake_amount, + ); + + let tao_reserve: U96F32 = U96F32::from_num(100_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 limit_price = 90_000_000; + + // Remove stake with slippage safety + assert_ok!(SubtensorModule::remove_stake_full_limit( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + Some(limit_price), + )); + + // Check if stake has decreased to zero + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid + ), + 0 + ); + + let new_balance = SubtensorModule::get_coldkey_balance(&coldkey_account_id); + assert_abs_diff_eq!(new_balance, 9_066_000_000, epsilon = 1_000_000); + }); +} + +#[test] +fn test_remove_stake_full_limit_fails_slippage_too_high() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(1); + let coldkey_account_id = U256::from(2); + let stake_amount = 10_000_000_000; + + // add network + let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + + // Give the neuron some stake to remove + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + stake_amount, + ); + + let tao_reserve: U96F32 = U96F32::from_num(100_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 invalid_limit_price = 910_000_000; + + // Remove stake with slippage safety + assert_err!( + SubtensorModule::remove_stake_full_limit( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + Some(invalid_limit_price), + ), + Error::::SlippageTooHigh + ); + }); +} + +#[test] +fn test_remove_stake_full_limit_ok_with_no_limit_price() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(1); + let coldkey_account_id = U256::from(2); + let stake_amount = 10_000_000_000; + + // add network + let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + + // Give the neuron some stake to remove + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid, + stake_amount, + ); + + let tao_reserve: U96F32 = U96F32::from_num(100_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::()); + + // Remove stake with slippage safety + assert_ok!(SubtensorModule::remove_stake_full_limit( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + None, + )); + + // Check if stake has decreased to zero + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey_account_id, + &coldkey_account_id, + netuid + ), + 0 + ); + + let new_balance = SubtensorModule::get_coldkey_balance(&coldkey_account_id); + assert_abs_diff_eq!(new_balance, 9_066_000_000, epsilon = 1_000_000); + }); +} /// This test verifies that minimum stake amount is sufficient to move price and apply /// non-zero staking fees #[test] diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 8adf4b6c55..4089522ee4 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -217,7 +217,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: 279, + spec_version: 280, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -708,18 +708,9 @@ impl InstanceFilter for ProxyType { | RuntimeCall::SubtensorModule( pallet_subtensor::Call::remove_stake_limit { .. } ) - // | RuntimeCall::SubtensorModule( - // pallet_subtensor::Call::add_stake_aggregate { .. } - // ) - // | RuntimeCall::SubtensorModule( - // pallet_subtensor::Call::add_stake_limit_aggregate { .. } - // ) - // | RuntimeCall::SubtensorModule( - // pallet_subtensor::Call::remove_stake_aggregate { .. } - // ) - // | RuntimeCall::SubtensorModule( - // pallet_subtensor::Call::remove_stake_limit_aggregate { .. } - // ) + | RuntimeCall::SubtensorModule( + pallet_subtensor::Call::remove_stake_full_limit { .. } + ) | RuntimeCall::SubtensorModule(pallet_subtensor::Call::unstake_all { .. }) | RuntimeCall::SubtensorModule( pallet_subtensor::Call::unstake_all_alpha { .. } @@ -800,18 +791,10 @@ impl InstanceFilter for ProxyType { | RuntimeCall::SubtensorModule(pallet_subtensor::Call::add_stake_limit { .. }) | RuntimeCall::SubtensorModule( pallet_subtensor::Call::remove_stake_limit { .. } - ) // | RuntimeCall::SubtensorModule( - // pallet_subtensor::Call::add_stake_aggregate { .. } - // ) - // | RuntimeCall::SubtensorModule( - // pallet_subtensor::Call::add_stake_limit_aggregate { .. } - // ) - // | RuntimeCall::SubtensorModule( - // pallet_subtensor::Call::remove_stake_aggregate { .. } - // ) - // | RuntimeCall::SubtensorModule( - // pallet_subtensor::Call::remove_stake_limit_aggregate { .. } - // ) + ) + | RuntimeCall::SubtensorModule( + pallet_subtensor::Call::remove_stake_full_limit { .. } + ) ), ProxyType::Registration => matches!( c,