diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index ce0781b536..1a48418b96 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -330,15 +330,17 @@ pub mod pallet { /// Enum for the per-coldkey root claim setting. pub enum RootClaimTypeEnum { /// Swap any alpha emission for TAO. + #[default] Swap, /// Keep all alpha emission. - #[default] Keep, /// Keep all alpha emission for specified subnets. KeepSubnets { /// Subnets to keep alpha emissions (swap everything else). subnets: BTreeSet, }, + /// Delegate choice to subnet. + Delegated, } /// Default minimum root claim amount. @@ -354,7 +356,12 @@ pub mod pallet { /// This is set by the user. Either swap to TAO or keep as alpha. #[pallet::type_value] pub fn DefaultRootClaimType() -> RootClaimTypeEnum { - RootClaimTypeEnum::default() + RootClaimTypeEnum::Delegated + } + /// Default value for delegate claim type storage + #[pallet::type_value] + pub fn DefaultValidatorClaimType() -> RootClaimTypeEnum { + RootClaimTypeEnum::Swap } /// Default number of root claims per claim call. @@ -2253,6 +2260,17 @@ pub mod pallet { ValueQuery, DefaultRootClaimType, >; + #[pallet::storage] // -- MAP ( hotkey, netuid ) --> delegate_claim_type enum + pub type ValidatorClaimType = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, + Identity, + NetUid, + RootClaimTypeEnum, + ValueQuery, + DefaultValidatorClaimType, + >; #[pallet::storage] // --- MAP ( u64 ) --> coldkey | Maps coldkeys that have stake to an index pub type StakingColdkeysByIndex = StorageMap<_, Identity, u64, T::AccountId, OptionQuery>; diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 6d927f33a3..72d8c446a5 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -2431,5 +2431,42 @@ mod dispatches { Ok(()) } + + /// --- Sets delegate claim type for a hotkey on a subnet. + #[pallet::call_index(125)] + #[pallet::weight(( + Weight::from_parts(5_711_000, 0).saturating_add(T::DbWeight::get().writes(1_u64)), + DispatchClass::Operational, + Pays::Yes + ))] + pub fn set_validator_claim_type( + origin: OriginFor, + hotkey: T::AccountId, + netuid: NetUid, + new_claim_type: RootClaimTypeEnum, + ) -> DispatchResult { + let coldkey: T::AccountId = ensure_signed(origin)?; + ensure!( + Self::coldkey_owns_hotkey(&coldkey, &hotkey), + Error::::NonAssociatedColdKey + ); + + // Ensure the delegate claim type is not Delegated. + ensure!( + matches!( + new_claim_type, + RootClaimTypeEnum::Keep | RootClaimTypeEnum::Swap + ), + Error::::InvalidRootClaimType + ); + + ValidatorClaimType::::insert(hotkey.clone(), netuid, new_claim_type.clone()); + Self::deposit_event(Event::ValidatorClaimTypeSet { + hotkey: hotkey.clone(), + root_claim_type: new_claim_type, + netuid, + }); + Ok(()) + } } } diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index 5a15330075..c4456ee1fe 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -266,5 +266,7 @@ mod errors { InvalidRootClaimThreshold, /// Exceeded subnet limit number or zero. InvalidSubnetNumber, + /// Delegates cant set delegated as claim type + InvalidRootClaimType, } } diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index a06e035d86..7184cb0b79 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -470,6 +470,18 @@ mod events { root_claim_type: RootClaimTypeEnum, }, + /// Root claim type for a coldkey has been set. + /// Parameters: + /// (coldkey, u8) + ValidatorClaimTypeSet { + /// delegate hotkey + hotkey: T::AccountId, + /// root claim type enum + root_claim_type: RootClaimTypeEnum, + /// subnet UID + netuid: NetUid, + }, + /// Subnet lease dividends have been distributed. SubnetLeaseDividendsDistributed { /// The lease ID diff --git a/pallets/subtensor/src/staking/claim_root.rs b/pallets/subtensor/src/staking/claim_root.rs index 24a26d154c..bc38be406b 100644 --- a/pallets/subtensor/src/staking/claim_root.rs +++ b/pallets/subtensor/src/staking/claim_root.rs @@ -2,7 +2,7 @@ use super::*; use frame_support::weights::Weight; use sp_core::Get; use sp_std::collections::btree_set::BTreeSet; -use substrate_fixed::types::I96F32; +use substrate_fixed::types::{I96F32, U96F32}; use subtensor_swap_interface::SwapHandler; impl Pallet { @@ -157,10 +157,21 @@ impl Pallet { return; // no-op } - let swap = match root_claim_type { + let mut actual_root_claim = root_claim_type; + // If root_claim_type is Delegated, switch to the delegate's actual claim type. + if actual_root_claim == RootClaimTypeEnum::Delegated { + actual_root_claim = ValidatorClaimType::::get(hotkey, netuid); + } + + let swap = match actual_root_claim { RootClaimTypeEnum::Swap => true, RootClaimTypeEnum::Keep => false, RootClaimTypeEnum::KeepSubnets { subnets } => !subnets.contains(&netuid), + RootClaimTypeEnum::Delegated => { + // Should not reach here. Added for completeness. + log::error!("Delegated root_claim_type should have been switched. Skipping."); + return; + } }; if swap { @@ -179,6 +190,12 @@ impl Pallet { } }; + let recorded_tao_outflow = + Self::calculate_tao_outflow(netuid, owed_tao.amount_paid_out); + + // Importantly measures swap as flow. + Self::record_tao_outflow(netuid, recorded_tao_outflow.into()); + Self::increase_stake_for_hotkey_and_coldkey_on_subnet( hotkey, coldkey, @@ -194,7 +211,7 @@ impl Pallet { } else /* Keep */ { - // Increase the stake with the alpha owned + // Increase the stake with the alpha owed Self::increase_stake_for_hotkey_and_coldkey_on_subnet( hotkey, coldkey, @@ -209,6 +226,30 @@ impl Pallet { }); } + // Calculates root proportion for subnet, uses "1 / root_prop" (with 10 as an upper bound) as + // a multiplier for provided Tao amount. + pub(crate) fn calculate_tao_outflow(netuid: NetUid, amount: TaoCurrency) -> TaoCurrency { + let root_proportion = Self::root_proportion(netuid); + + let root_prop_multiplier = { + let root_prop_multiplier = U96F32::saturating_from_num(1) + .checked_div(root_proportion) + .unwrap_or(U96F32::saturating_from_num(0.0)); + let upper_bound = U96F32::saturating_from_num(10); + + if root_prop_multiplier < upper_bound { + root_prop_multiplier + } else { + upper_bound + } + }; + + let tao = U96F32::saturating_from_num(amount.to_u64()); + let recorded_tao_outflow: u64 = tao.saturating_mul(root_prop_multiplier).to_num(); + + recorded_tao_outflow.into() + } + fn root_claim_on_subnet_weight(_root_claim_type: RootClaimTypeEnum) -> Weight { Weight::from_parts(60_000_000, 6987) .saturating_add(T::DbWeight::get().reads(7_u64)) diff --git a/pallets/subtensor/src/tests/claim_root.rs b/pallets/subtensor/src/tests/claim_root.rs index 9cb94b4d1a..c8945d28e6 100644 --- a/pallets/subtensor/src/tests/claim_root.rs +++ b/pallets/subtensor/src/tests/claim_root.rs @@ -1,8 +1,8 @@ #![allow(clippy::expect_used)] -use crate::RootAlphaDividendsPerSubnet; use crate::tests::mock::{ - RuntimeOrigin, SubtensorModule, Test, add_dynamic_network, new_test_ext, run_to_block, + RuntimeEvent, RuntimeOrigin, SubtensorModule, System, Test, add_dynamic_network, new_test_ext, + run_to_block, }; use crate::{ DefaultMinRootClaimAmount, Error, MAX_NUM_ROOT_CLAIMS, MAX_ROOT_CLAIM_THRESHOLD, NetworksAdded, @@ -10,7 +10,8 @@ use crate::{ StakingColdkeys, StakingColdkeysByIndex, SubnetAlphaIn, SubnetMechanism, SubnetMovingPrice, SubnetTAO, SubnetTaoFlow, SubtokenEnabled, Tempo, pallet, }; -use crate::{RootClaimType, RootClaimTypeEnum, RootClaimed}; +use crate::{Event, RootAlphaDividendsPerSubnet}; +use crate::{RootClaimType, RootClaimTypeEnum, RootClaimed, ValidatorClaimType}; use approx::assert_abs_diff_eq; use frame_support::dispatch::RawOrigin; use frame_support::pallet_prelude::Weight; @@ -652,6 +653,10 @@ fn test_claim_root_with_drain_emissions_and_swap_claim_type() { RootClaimTypeEnum::Swap ),); assert_eq!(RootClaimType::::get(coldkey), RootClaimTypeEnum::Swap); + assert_eq!(SubnetTaoFlow::::get(netuid), 0); + + // Setup root prop + SubnetTAO::::insert(NetUid::ROOT, TaoCurrency::from(1_000_000_000_000u64)); assert_ok!(SubtensorModule::claim_root( RuntimeOrigin::signed(coldkey), @@ -678,6 +683,19 @@ fn test_claim_root_with_drain_emissions_and_swap_claim_type() { epsilon = 10000u64, ); + // Check tao flow + let tao_outflow: u64 = SubtensorModule::calculate_tao_outflow( + netuid, + (estimated_stake_increment as u64).into(), + ) + .into(); + + assert_abs_diff_eq!( + SubnetTaoFlow::::get(netuid), + -(tao_outflow as i64), + epsilon = 10i64, + ); + // Distribute and claim pending root alpha (round 2) SubtensorModule::distribute_emission( @@ -1556,6 +1574,338 @@ fn test_claim_root_with_unrelated_subnets() { }); } +#[test] +fn test_claim_root_with_set_validator_claim_type() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1001); + let hotkey = U256::from(1002); + let netuid = add_dynamic_network(&hotkey, &coldkey); + let new_claim_type = RootClaimTypeEnum::Swap; + + // Default check + assert_eq!( + ValidatorClaimType::::get(hotkey, netuid), + RootClaimTypeEnum::Swap + ); + + // Set new type + assert_ok!(SubtensorModule::set_validator_claim_type( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + new_claim_type.clone() + ),); + + // Result check + assert_eq!( + ValidatorClaimType::::get(hotkey, netuid), + new_claim_type + ); + + let event = System::events().into_iter().find(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::ValidatorClaimTypeSet { .. }) + ) + }); + assert!(event.is_some()); + + if let Some(RuntimeEvent::SubtensorModule(Event::ValidatorClaimTypeSet { + hotkey: ev_hotkey, + root_claim_type: ev_claim_type, + netuid: ev_netuid, + })) = event.map(|e| e.event.clone()) + { + assert_eq!(ev_hotkey, hotkey); + assert_eq!(ev_claim_type, new_claim_type); + assert_eq!(ev_netuid, netuid); + } + + // Check possible options + assert_ok!(SubtensorModule::set_validator_claim_type( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + RootClaimTypeEnum::Keep + ),); + assert_ok!(SubtensorModule::set_validator_claim_type( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + RootClaimTypeEnum::Swap + ),); + assert_err!( + SubtensorModule::set_validator_claim_type( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + RootClaimTypeEnum::Delegated + ), + Error::::InvalidRootClaimType + ); + assert_err!( + SubtensorModule::set_validator_claim_type( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + RootClaimTypeEnum::KeepSubnets { + subnets: BTreeSet::from([netuid]) + } + ), + Error::::InvalidRootClaimType + ); + }); +} + +#[test] +fn test_claim_root_with_delegated_claim_type() { + new_test_ext(1).execute_with(|| { + // Setup: Create network with validator (hotkey/owner_coldkey) and two stakers + let owner_coldkey = U256::from(1001); // Validator's coldkey + let other_coldkey = U256::from(10010); // Other staker (not tested) + let hotkey = U256::from(1002); // Validator's hotkey + let alice_coldkey = U256::from(1003); // Staker who will delegate claim type + let bob_coldkey = U256::from(1004); // Staker who will set explicit claim type + let netuid = add_dynamic_network(&hotkey, &owner_coldkey); + + // Configure TAO weight and subnet mechanism for swap functionality + SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 + SubnetMechanism::::insert(netuid, 1); // Enable subnet mechanism for swaps + + // Setup swap pool with reserves to enable Swap claim type + let tao_reserve = TaoCurrency::from(50_000_000_000); + let alpha_in = AlphaCurrency::from(100_000_000_000); + SubnetTAO::::insert(netuid, tao_reserve); + SubnetAlphaIn::::insert(netuid, alpha_in); + + // Verify the alpha-to-TAO exchange rate is 0.5 + let current_price = + ::SwapInterface::current_alpha_price(netuid.into()) + .saturating_to_num::(); + assert_eq!(current_price, 0.5f64); + + // Setup root network stakes: Alice and Bob each have 10% of total stake + let root_stake = 2_000_000u64; + let root_stake_rate = 0.1f64; // Each staker owns 10% of root stake + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &alice_coldkey, + NetUid::ROOT, + root_stake.into(), + ); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &bob_coldkey, + NetUid::ROOT, + root_stake.into(), + ); + // Other coldkey has remaining 80% of stake + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &other_coldkey, + NetUid::ROOT, + (8 * root_stake).into(), + ); + + // Setup subnet alpha stake for validator + let initial_total_hotkey_alpha = 10_000_000u64; + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &owner_coldkey, + netuid, + initial_total_hotkey_alpha.into(), + ); + + // SCENARIO 1: Validator sets Keep claim type, Alice uses default (Delegated) + // Alice should inherit the validator's Keep claim type and receive alpha stake + assert_ok!(SubtensorModule::set_validator_claim_type( + RuntimeOrigin::signed(owner_coldkey), + hotkey, + netuid, + RootClaimTypeEnum::Keep + ),); + assert_eq!( + ValidatorClaimType::::get(hotkey, netuid), + RootClaimTypeEnum::Keep + ); + + // Bob explicitly sets Keep claim type (same as validator, but not delegated) + assert_ok!(SubtensorModule::set_root_claim_type( + RuntimeOrigin::signed(bob_coldkey), + RootClaimTypeEnum::Keep + ),); + + // Alice has default Delegated claim type (not explicitly set) + assert_eq!( + RootClaimType::::get(alice_coldkey), + RootClaimTypeEnum::Delegated + ); + + // Distribute pending root alpha emissions to create claimable rewards + let pending_root_alpha = 10_000_000u64; + SubtensorModule::distribute_emission( + netuid, + AlphaCurrency::ZERO, + AlphaCurrency::ZERO, + pending_root_alpha.into(), + AlphaCurrency::ZERO, + ); + + // Alice claims with delegated claim type (should use validator's Keep) + assert_ok!(SubtensorModule::claim_root( + RuntimeOrigin::signed(alice_coldkey), + BTreeSet::from([netuid]) + )); + + // Bob claims with explicit Keep claim type + assert_ok!(SubtensorModule::claim_root( + RuntimeOrigin::signed(bob_coldkey), + BTreeSet::from([netuid]) + )); + + // Verify both stakers received alpha stake (Keep claim type behavior) + // With Keep, rewards are staked as alpha on the subnet + let validator_take_percent = 0.18f64; + let expected_stake_per_user = + (pending_root_alpha as f64) * (1f64 - validator_take_percent) * root_stake_rate; + + let alice_alpha_stake: u64 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &alice_coldkey, + netuid, + ) + .into(); + + let bob_alpha_stake: u64 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &bob_coldkey, + netuid, + ) + .into(); + + // Both should have equal alpha stakes since they both used Keep claim type + assert_eq!(alice_alpha_stake, bob_alpha_stake); + assert_abs_diff_eq!( + alice_alpha_stake, + expected_stake_per_user as u64, + epsilon = 100u64 + ); + + // Verify neither received TAO stake (would happen with Swap claim type) + let alice_tao_stake: u64 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &alice_coldkey, + NetUid::ROOT, + ) + .into(); + let bob_tao_stake: u64 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &bob_coldkey, + NetUid::ROOT, + ) + .into(); + // TAO stake should remain unchanged at initial amount + assert_eq!(alice_tao_stake, root_stake); + assert_eq!(bob_tao_stake, root_stake); + + // SCENARIO 2: Validator changes to Swap claim type + // Alice (with Delegated) should now use Swap, Bob (explicit Keep) stays with Keep + assert_ok!(SubtensorModule::set_validator_claim_type( + RuntimeOrigin::signed(owner_coldkey), + hotkey, + netuid, + RootClaimTypeEnum::Swap + ),); + + // Distribute more pending root alpha for second round of claims + SubtensorModule::distribute_emission( + netuid, + AlphaCurrency::ZERO, + AlphaCurrency::ZERO, + pending_root_alpha.into(), + AlphaCurrency::ZERO, + ); + + // Both stakers claim again + assert_ok!(SubtensorModule::claim_root( + RuntimeOrigin::signed(alice_coldkey), + BTreeSet::from([netuid]) + )); + assert_ok!(SubtensorModule::claim_root( + RuntimeOrigin::signed(bob_coldkey), + BTreeSet::from([netuid]) + )); + + // Alice's alpha stake should remain the same (Swap doesn't add alpha stake) + let alice_alpha_stake_round2: u64 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &alice_coldkey, + netuid, + ) + .into(); + + // Bob's alpha stake should increase (Keep adds alpha stake) + let bob_alpha_stake_round2: u64 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &bob_coldkey, + netuid, + ) + .into(); + + // Alice used Swap (delegated from validator), so no new alpha stake + assert_abs_diff_eq!( + alice_alpha_stake_round2, + alice_alpha_stake, + epsilon = 100u64 + ); + + // Bob used Keep (explicit), so alpha stake increased + assert_abs_diff_eq!( + bob_alpha_stake_round2, + alice_alpha_stake + expected_stake_per_user as u64, + epsilon = 100u64 + ); + + // Alice used Swap, so TAO stake should increase + let alice_tao_stake_round2: u64 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &alice_coldkey, + NetUid::ROOT, + ) + .into(); + + // Bob used Keep, so TAO stake should remain the same + let bob_tao_stake_round2: u64 = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &bob_coldkey, + NetUid::ROOT, + ) + .into(); + + // Alice's TAO stake increased (swapped alpha to TAO and staked on root) + let expected_tao_increase = expected_stake_per_user * current_price; + assert_abs_diff_eq!( + alice_tao_stake_round2, + root_stake + expected_tao_increase as u64, + epsilon = 10000u64 + ); + + // Bob's TAO stake unchanged (used Keep, not Swap) + assert_eq!(bob_tao_stake_round2, root_stake); + + // SUMMARY: This test demonstrates that: + // 1. Stakers with Delegated claim type inherit the validator's claim type + // 2. Stakers with explicit claim types use their own setting regardless of validator + // 3. Keep claim type stakes rewards as alpha on the subnet + // 4. Swap claim type converts alpha to TAO and stakes on root network + // 5. Changing validator's claim type affects delegated stakers immediately + }); +} + #[test] fn test_claim_root_fill_root_alpha_dividends_per_subnet() { new_test_ext(1).execute_with(|| { @@ -1818,10 +2168,64 @@ fn test_claim_root_keep_subnets_swap_claim_type() { } #[test] -fn test_claim_root_default_mode_keep() { +fn test_claim_root_calculate_tao_outflow() { new_test_ext(1).execute_with(|| { + let hotkey = U256::from(1002); let coldkey = U256::from(1003); + let netuid = add_dynamic_network(&hotkey, &coldkey); - assert_eq!(RootClaimType::::get(coldkey), RootClaimTypeEnum::Keep); + // Setup root prop + SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 + let alpha_in = AlphaCurrency::from(100_000_000_000); + SubnetAlphaIn::::insert(netuid, alpha_in); + + let tao_amount = 10u64; + + // Check unbounded tao outflow (coefficient < 10) + let tao_reserve = TaoCurrency::from(1_000_000_000_000u64); + SubnetTAO::::insert(NetUid::ROOT, tao_reserve); + + let tao_outflow_unbounded: u64 = + SubtensorModule::calculate_tao_outflow(netuid, tao_amount.into()).into(); + + assert_abs_diff_eq!(tao_outflow_unbounded, tao_amount, epsilon = 1u64,); + + // Check bounded tao outflow (coefficient > 10). + let tao_reserve = TaoCurrency::from(10_000_000_000u64); + SubnetTAO::::insert(NetUid::ROOT, tao_reserve); + + let tao_outflow_bounded: u64 = + SubtensorModule::calculate_tao_outflow(netuid, tao_amount.into()).into(); + let bounded_root_prop_multiplier = 10u64; + + assert_eq!( + tao_outflow_bounded, + bounded_root_prop_multiplier * tao_amount, + ); + }); +} + +#[test] +fn test_claim_root_default_mode() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1003); + + assert_eq!( + RootClaimType::::get(coldkey), + RootClaimTypeEnum::Delegated + ); + }); +} + +#[test] +fn test_claim_root_default_validator_mode() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1003); + let netuid = NetUid::from(100u16); + + assert_eq!( + ValidatorClaimType::::get(coldkey, netuid), + RootClaimTypeEnum::Swap + ); }); }