diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index dd7645e8ff..f404146ffd 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -339,6 +339,8 @@ pub mod pallet { /// 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::Keep } /// Default number of root claims per claim call. @@ -2252,6 +2259,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 edf561810e..6ef75976bc 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -2431,5 +2431,38 @@ 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::Delegated), + 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, + }); + 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 d015205d4d..2b66a14924 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -468,6 +468,16 @@ 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 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..dd72134988 100644 --- a/pallets/subtensor/src/staking/claim_root.rs +++ b/pallets/subtensor/src/staking/claim_root.rs @@ -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,9 @@ impl Pallet { } }; + // Importantly measures swap as flow. + Self::record_tao_outflow(netuid, owed_tao.amount_paid_out.into()); + Self::increase_stake_for_hotkey_and_coldkey_on_subnet( hotkey, coldkey, @@ -194,7 +208,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, diff --git a/pallets/subtensor/src/tests/claim_root.rs b/pallets/subtensor/src/tests/claim_root.rs index b910fa1e83..48d0c0f798 100644 --- a/pallets/subtensor/src/tests/claim_root.rs +++ b/pallets/subtensor/src/tests/claim_root.rs @@ -10,7 +10,7 @@ use crate::{ StakingColdkeys, StakingColdkeysByIndex, SubnetAlphaIn, SubnetMechanism, SubnetMovingPrice, SubnetTAO, SubnetTaoFlow, SubtokenEnabled, Tempo, pallet, }; -use crate::{RootClaimType, RootClaimTypeEnum, RootClaimed}; +use crate::{RootClaimType, RootClaimTypeEnum, RootClaimed, ValidatorClaimType}; use approx::assert_abs_diff_eq; use frame_support::dispatch::RawOrigin; use frame_support::pallet_prelude::Weight; @@ -1556,6 +1556,255 @@ fn test_claim_root_with_unrelated_subnets() { }); } +#[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(|| {