diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index ef965bf169..3404b36d8d 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -201,5 +201,7 @@ mod errors { NeedWaitingMoreBlocksToStarCall, /// Not enough AlphaOut on the subnet to recycle NotEnoughAlphaOutToRecycle, + /// Cannot burn or recycle TAO from root subnet + CannotBurnOrRecycleOnRootSubnet, } } diff --git a/pallets/subtensor/src/staking/recycle_alpha.rs b/pallets/subtensor/src/staking/recycle_alpha.rs index cb5e740e84..b5e6762e6a 100644 --- a/pallets/subtensor/src/staking/recycle_alpha.rs +++ b/pallets/subtensor/src/staking/recycle_alpha.rs @@ -20,7 +20,7 @@ impl Pallet { amount: u64, netuid: u16, ) -> DispatchResult { - let coldkey = ensure_signed(origin)?; + let coldkey: T::AccountId = ensure_signed(origin)?; ensure!( Self::if_subnet_exist(netuid), @@ -28,12 +28,19 @@ impl Pallet { ); ensure!( - Self::coldkey_owns_hotkey(&coldkey, &hotkey), - Error::::NonAssociatedColdKey + netuid != Self::get_root_netuid(), + Error::::CannotBurnOrRecycleOnRootSubnet ); + // Ensure that the hotkey account exists this is only possible through registration. ensure!( - TotalHotkeyAlpha::::get(&hotkey, netuid) >= amount, + Self::hotkey_account_exists(&hotkey), + Error::::HotKeyAccountNotExists + ); + + // Ensure that the hotkey has enough stake to withdraw. + ensure!( + Self::has_enough_stake_on_subnet(&hotkey, &coldkey, netuid, amount), Error::::NotEnoughStakeToWithdraw ); @@ -42,19 +49,22 @@ impl Pallet { Error::::InsufficientLiquidity ); - if TotalHotkeyAlpha::::mutate(&hotkey, netuid, |v| { - *v = v.saturating_sub(amount); - *v - }) == 0 - { - TotalHotkeyAlpha::::remove(&hotkey, netuid); - } + // Deduct from the coldkey's stake. + let actual_alpha_decrease = Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, amount, + ); + // Recycle means we should decrease the alpha issuance tracker. SubnetAlphaOut::::mutate(netuid, |total| { - *total = total.saturating_sub(amount); + *total = total.saturating_sub(actual_alpha_decrease); }); - Self::deposit_event(Event::AlphaRecycled(coldkey, hotkey, amount, netuid)); + Self::deposit_event(Event::AlphaRecycled( + coldkey, + hotkey, + actual_alpha_decrease, + netuid, + )); Ok(()) } @@ -85,12 +95,19 @@ impl Pallet { ); ensure!( - Self::coldkey_owns_hotkey(&coldkey, &hotkey), - Error::::NonAssociatedColdKey + netuid != Self::get_root_netuid(), + Error::::CannotBurnOrRecycleOnRootSubnet ); + // Ensure that the hotkey account exists this is only possible through registration. ensure!( - TotalHotkeyAlpha::::get(&hotkey, netuid) >= amount, + Self::hotkey_account_exists(&hotkey), + Error::::HotKeyAccountNotExists + ); + + // Ensure that the hotkey has enough stake to withdraw. + ensure!( + Self::has_enough_stake_on_subnet(&hotkey, &coldkey, netuid, amount), Error::::NotEnoughStakeToWithdraw ); @@ -99,16 +116,20 @@ impl Pallet { Error::::InsufficientLiquidity ); - if TotalHotkeyAlpha::::mutate(&hotkey, netuid, |v| { - *v = v.saturating_sub(amount); - *v - }) == 0 - { - TotalHotkeyAlpha::::remove(&hotkey, netuid); - } + // Deduct from the coldkey's stake. + let actual_alpha_decrease = Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, amount, + ); + + // This is a burn, so we don't need to update AlphaOut. // Deposit event - Self::deposit_event(Event::AlphaBurned(coldkey, hotkey, amount, netuid)); + Self::deposit_event(Event::AlphaBurned( + coldkey, + hotkey, + actual_alpha_decrease, + netuid, + )); Ok(()) } diff --git a/pallets/subtensor/src/tests/recycle_alpha.rs b/pallets/subtensor/src/tests/recycle_alpha.rs index 894a8887a4..b142e5d3c9 100644 --- a/pallets/subtensor/src/tests/recycle_alpha.rs +++ b/pallets/subtensor/src/tests/recycle_alpha.rs @@ -1,3 +1,4 @@ +use approx::assert_abs_diff_eq; use frame_support::{assert_noop, assert_ok, traits::Currency}; use sp_core::U256; @@ -44,6 +45,153 @@ fn test_recycle_success() { assert!(TotalHotkeyAlpha::::get(hotkey, netuid) < initial_alpha); assert!(SubnetAlphaOut::::get(netuid) < initial_net_alpha); + assert!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid) + < initial_alpha + ); + + assert!(System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AlphaRecycled(..)) + ) + })); + }); +} + +#[test] +fn test_recycle_two_stakers() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let other_coldkey = U256::from(3); + + let owner_coldkey = U256::from(1001); + let owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + let initial_balance = 1_000_000_000; + Balances::make_free_balance_be(&coldkey, initial_balance); + + // associate coldkey and hotkey + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + assert!(SubtensorModule::if_subnet_exist(netuid)); + + // add stake to coldkey-hotkey pair so we can recycle it + let stake = 200_000; + increase_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, stake, netuid); + + // add some stake to other coldkey on same hotkey. + increase_stake_on_coldkey_hotkey_account(&other_coldkey, &hotkey, stake, netuid); + + // get initial total issuance and alpha out + let initial_alpha = TotalHotkeyAlpha::::get(hotkey, netuid); + let initial_net_alpha = SubnetAlphaOut::::get(netuid); + + // amount to recycle + let recycle_amount = stake / 2; + + // recycle + assert_ok!(SubtensorModule::recycle_alpha( + RuntimeOrigin::signed(coldkey), + hotkey, + recycle_amount, + netuid + )); + + assert!(TotalHotkeyAlpha::::get(hotkey, netuid) < initial_alpha); + assert!(SubnetAlphaOut::::get(netuid) < initial_net_alpha); + assert!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid) + < stake + ); + // Make sure the other coldkey has no change + assert_abs_diff_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &other_coldkey, + netuid + ), + stake, + epsilon = 2 + ); + + assert!(System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AlphaRecycled(..)) + ) + })); + }); +} + +#[test] +fn test_recycle_staker_is_nominator() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let other_coldkey = U256::from(3); + + let owner_coldkey = U256::from(1001); + let owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + let initial_balance = 1_000_000_000; + Balances::make_free_balance_be(&coldkey, initial_balance); + + // associate coldkey and hotkey + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + assert!(SubtensorModule::if_subnet_exist(netuid)); + + // add stake to coldkey-hotkey pair so we can recycle it + let stake = 200_000; + increase_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, stake, netuid); + + // add some stake to other coldkey on same hotkey. + // Note: this coldkey DOES NOT own the hotkey, so it is a nominator. + increase_stake_on_coldkey_hotkey_account(&other_coldkey, &hotkey, stake, netuid); + // Verify the ownership + assert_ne!( + SubtensorModule::get_owning_coldkey_for_hotkey(&hotkey), + other_coldkey + ); + + // get initial total issuance and alpha out + let initial_alpha = TotalHotkeyAlpha::::get(hotkey, netuid); + let initial_net_alpha = SubnetAlphaOut::::get(netuid); + + // amount to recycle + let recycle_amount = stake / 2; + + // recycle from nominator coldkey + assert_ok!(SubtensorModule::recycle_alpha( + RuntimeOrigin::signed(other_coldkey), + hotkey, + recycle_amount, + netuid + )); + + assert!(TotalHotkeyAlpha::::get(hotkey, netuid) < initial_alpha); + assert!(SubnetAlphaOut::::get(netuid) < initial_net_alpha); + assert!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &other_coldkey, + netuid + ) < stake + ); + // Make sure the other coldkey has no change + assert_abs_diff_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid), + stake, + epsilon = 2 + ); assert!(System::events().iter().any(|e| { matches!( @@ -94,6 +242,148 @@ fn test_burn_success() { assert!(TotalHotkeyAlpha::::get(hotkey, netuid) < initial_alpha); assert!(SubnetAlphaOut::::get(netuid) == initial_net_alpha); + assert!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid) + < stake + ); + + assert!(System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AlphaBurned(..)) + ) + })); + }); +} + +#[test] +fn test_burn_staker_is_nominator() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let other_coldkey = U256::from(3); + + let owner_coldkey = U256::from(1001); + let owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + let initial_balance = 1_000_000_000; + Balances::make_free_balance_be(&coldkey, initial_balance); + + // associate coldkey and hotkey + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + assert!(SubtensorModule::if_subnet_exist(netuid)); + + // add stake to coldkey-hotkey pair so we can recycle it + let stake = 200_000; + increase_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, stake, netuid); + + // add some stake to other coldkey on same hotkey. + // Note: this coldkey DOES NOT own the hotkey, so it is a nominator. + increase_stake_on_coldkey_hotkey_account(&other_coldkey, &hotkey, stake, netuid); + + // get initial total issuance and alpha out + let initial_alpha = TotalHotkeyAlpha::::get(hotkey, netuid); + let initial_net_alpha = SubnetAlphaOut::::get(netuid); + + // amount to recycle + let burn_amount = stake / 2; + + // burn from nominator coldkey + assert_ok!(SubtensorModule::burn_alpha( + RuntimeOrigin::signed(other_coldkey), + hotkey, + burn_amount, + netuid + )); + + assert!(TotalHotkeyAlpha::::get(hotkey, netuid) < initial_alpha); + assert!(SubnetAlphaOut::::get(netuid) == initial_net_alpha); + assert!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &other_coldkey, + netuid + ) < stake + ); + // Make sure the other coldkey has no change + assert_abs_diff_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid), + stake, + epsilon = 2 + ); + + assert!(System::events().iter().any(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::AlphaBurned(..)) + ) + })); + }); +} + +#[test] +fn test_burn_two_stakers() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let other_coldkey = U256::from(3); + + let owner_coldkey = U256::from(1001); + let owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + let initial_balance = 1_000_000_000; + Balances::make_free_balance_be(&coldkey, initial_balance); + + // associate coldkey and hotkey + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + register_ok_neuron(netuid, hotkey, coldkey, 0); + + assert!(SubtensorModule::if_subnet_exist(netuid)); + + // add stake to coldkey-hotkey pair so we can recycle it + let stake = 200_000; + increase_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, stake, netuid); + + // add some stake to other coldkey on same hotkey. + increase_stake_on_coldkey_hotkey_account(&other_coldkey, &hotkey, stake, netuid); + + // get initial total issuance and alpha out + let initial_alpha = TotalHotkeyAlpha::::get(hotkey, netuid); + let initial_net_alpha = SubnetAlphaOut::::get(netuid); + + // amount to recycle + let burn_amount = stake / 2; + + // burn from coldkey + assert_ok!(SubtensorModule::burn_alpha( + RuntimeOrigin::signed(coldkey), + hotkey, + burn_amount, + netuid + )); + + assert!(TotalHotkeyAlpha::::get(hotkey, netuid) < initial_alpha); + assert!(SubnetAlphaOut::::get(netuid) == initial_net_alpha); + assert!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid) + < stake + ); + // Make sure the other coldkey has no change + assert_abs_diff_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &other_coldkey, + netuid + ), + stake, + epsilon = 2 + ); assert!(System::events().iter().any(|e| { matches!( @@ -109,12 +399,15 @@ fn test_recycle_errors() { new_test_ext(1).execute_with(|| { let coldkey = U256::from(1); let hotkey = U256::from(2); - let wrong_coldkey = U256::from(3); + let wrong_hotkey = U256::from(3); 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); + // Create root subnet + migrations::migrate_create_root_network::migrate_create_root_network::(); + let initial_balance = 1_000_000_000; Balances::make_free_balance_be(&coldkey, initial_balance); @@ -136,12 +429,22 @@ fn test_recycle_errors() { assert_noop!( SubtensorModule::recycle_alpha( - RuntimeOrigin::signed(wrong_coldkey), + RuntimeOrigin::signed(coldkey), hotkey, 100_000, + SubtensorModule::get_root_netuid(), + ), + Error::::CannotBurnOrRecycleOnRootSubnet + ); + + assert_noop!( + SubtensorModule::recycle_alpha( + RuntimeOrigin::signed(coldkey), + wrong_hotkey, + 100_000, netuid ), - Error::::NonAssociatedColdKey + Error::::HotKeyAccountNotExists ); assert_noop!( @@ -154,8 +457,12 @@ fn test_recycle_errors() { Error::::NotEnoughStakeToWithdraw ); - // make it pass the hotkey alpha check - TotalHotkeyAlpha::::set(hotkey, netuid, SubnetAlphaOut::::get(netuid) + 1); + // make it pass the stake check + TotalHotkeyAlpha::::set( + hotkey, + netuid, + SubnetAlphaOut::::get(netuid).saturating_mul(2), + ); assert_noop!( SubtensorModule::recycle_alpha( @@ -174,12 +481,15 @@ fn test_burn_errors() { new_test_ext(1).execute_with(|| { let coldkey = U256::from(1); let hotkey = U256::from(2); - let wrong_coldkey = U256::from(3); + let wrong_hotkey = U256::from(3); 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); + // Create root subnet + migrations::migrate_create_root_network::migrate_create_root_network::(); + let initial_balance = 1_000_000_000; Balances::make_free_balance_be(&coldkey, initial_balance); @@ -201,12 +511,22 @@ fn test_burn_errors() { assert_noop!( SubtensorModule::burn_alpha( - RuntimeOrigin::signed(wrong_coldkey), + RuntimeOrigin::signed(coldkey), hotkey, 100_000, + SubtensorModule::get_root_netuid(), + ), + Error::::CannotBurnOrRecycleOnRootSubnet + ); + + assert_noop!( + SubtensorModule::burn_alpha( + RuntimeOrigin::signed(coldkey), + wrong_hotkey, + 100_000, netuid ), - Error::::NonAssociatedColdKey + Error::::HotKeyAccountNotExists ); assert_noop!( @@ -220,7 +540,11 @@ fn test_burn_errors() { ); // make it pass the hotkey alpha check - TotalHotkeyAlpha::::set(hotkey, netuid, SubnetAlphaOut::::get(netuid) + 1); + TotalHotkeyAlpha::::set( + hotkey, + netuid, + SubnetAlphaOut::::get(netuid).saturating_mul(2), + ); assert_noop!( SubtensorModule::burn_alpha( diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 1aec928978..e9ead1812f 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -207,7 +207,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 256, + spec_version: 257, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1,