diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index d508a0162b..10e0a5dbee 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -210,7 +210,7 @@ impl Pallet { Error::::SubnetNotExists ); - Self::finalize_all_subnet_root_dividends(netuid); + Self::clear_root_claim_root_data(netuid); // --- Perform the cleanup before removing the network. T::SwapInterface::dissolve_all_liquidity_providers(netuid)?; diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index ce0781b536..37163f4010 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -324,6 +324,20 @@ pub mod pallet { /// ==== Staking + Accounts ==== /// ============================ + #[crate::freeze_struct("e453805be107a384")] + #[derive( + Encode, Decode, Default, TypeInfo, Clone, PartialEq, Eq, Debug, DecodeWithMemTracking, + )] + /// Accounting root claim data per subnet per block. + pub struct PendingRootClaimedData { + /// Total alpha kept this block + pub alpha_kept: AlphaCurrency, + /// Total alpha swapped this block + pub alpha_swapped: AlphaCurrency, + /// Total TAO swapped this block + pub tao_swapped: TaoCurrency, + } + #[derive( Encode, Decode, Default, TypeInfo, Clone, PartialEq, Eq, Debug, DecodeWithMemTracking, )] @@ -357,6 +371,12 @@ pub mod pallet { RootClaimTypeEnum::default() } + /// Default root claim accounting data. + #[pallet::type_value] + pub fn DefaultRootClaimedData() -> PendingRootClaimedData { + PendingRootClaimedData::default() + } + /// Default number of root claims per claim call. /// Ideally this is calculated using the number of staking coldkey /// and the block time. @@ -2265,6 +2285,17 @@ pub mod pallet { #[pallet::storage] // --- Value --> num_root_claim | Number of coldkeys to claim each auto-claim. pub type NumRootClaim = StorageValue<_, u64, ValueQuery, DefaultNumRootClaim>; + /// --- MAP ( subnet ) --> pending accounting root claimed data. In-block storage only - to be cleaned each block. + #[pallet::storage] + pub type PendingSubnetRootClaimData = StorageMap< + _, + Blake2_128Concat, + NetUid, + PendingRootClaimedData, + ValueQuery, + DefaultRootClaimedData, + >; + /// ============================= /// ==== EVM related storage ==== /// ============================= diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index a06e035d86..3fa2236e49 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -479,5 +479,11 @@ mod events { /// The amount of alpha distributed alpha: AlphaCurrency, }, + + /// Root claimed data for this block. + RootClaimedData { + /// Root claim data for this block + data: BTreeMap, + }, } } diff --git a/pallets/subtensor/src/macros/hooks.rs b/pallets/subtensor/src/macros/hooks.rs index 31be3e5e4f..7affd73d04 100644 --- a/pallets/subtensor/src/macros/hooks.rs +++ b/pallets/subtensor/src/macros/hooks.rs @@ -47,6 +47,8 @@ mod hooks { for _ in StakingOperationRateLimiter::::drain() { // Clear all entries each block } + + Self::deposit_root_claim_accounting_event(); } fn on_runtime_upgrade() -> frame_support::weights::Weight { diff --git a/pallets/subtensor/src/staking/claim_root.rs b/pallets/subtensor/src/staking/claim_root.rs index 24a26d154c..f96267523c 100644 --- a/pallets/subtensor/src/staking/claim_root.rs +++ b/pallets/subtensor/src/staking/claim_root.rs @@ -1,6 +1,7 @@ use super::*; use frame_support::weights::Weight; use sp_core::Get; +use sp_std::collections::btree_map::BTreeMap; use sp_std::collections::btree_set::BTreeSet; use substrate_fixed::types::I96F32; use subtensor_swap_interface::SwapHandler; @@ -191,6 +192,12 @@ impl Pallet { coldkey, owed_tao.amount_paid_out.into(), ); + + Self::register_root_claim_swapped_alpha( + netuid, + owed_u64.into(), + owed_tao.amount_paid_out, + ); } else /* Keep */ { @@ -201,6 +208,8 @@ impl Pallet { netuid, owed_u64.into(), ); + + Self::register_root_claim_kept_alpha(netuid, owed_u64.into()); } // Increase root claimed by owed amount. @@ -209,6 +218,18 @@ impl Pallet { }); } + fn register_root_claim_swapped_alpha(netuid: NetUid, alpha: AlphaCurrency, tao: TaoCurrency) { + PendingSubnetRootClaimData::::mutate(netuid, |data| { + data.alpha_swapped = data.alpha_swapped.saturating_add(alpha); + data.tao_swapped = data.tao_swapped.saturating_add(tao); + }); + } + fn register_root_claim_kept_alpha(netuid: NetUid, alpha: AlphaCurrency) { + PendingSubnetRootClaimData::::mutate(netuid, |data| { + data.alpha_kept = data.alpha_kept.saturating_add(alpha); + }); + } + 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)) @@ -347,6 +368,17 @@ impl Pallet { weight } + pub fn deposit_root_claim_accounting_event() { + let root_claim_data = PendingSubnetRootClaimData::::iter().collect::>(); + + Self::deposit_event(Event::RootClaimedData { + data: root_claim_data, + }); + + // Subnet Number = 100+ + let _ = PendingSubnetRootClaimData::::clear(u32::MAX, None); + } + pub fn change_root_claim_type(coldkey: &T::AccountId, new_type: RootClaimTypeEnum) { RootClaimType::::insert(coldkey.clone(), new_type.clone()); @@ -388,8 +420,8 @@ impl Pallet { RootClaimable::::insert(new_hotkey, dst_root_claimable); } - /// Claim all root dividends for subnet and remove all associated data. - pub fn finalize_all_subnet_root_dividends(netuid: NetUid) { + /// Remove all root claim data for subnet. + pub fn clear_root_claim_root_data(netuid: NetUid) { let hotkeys = RootClaimable::::iter_keys().collect::>(); for hotkey in hotkeys.iter() { diff --git a/pallets/subtensor/src/tests/claim_root.rs b/pallets/subtensor/src/tests/claim_root.rs index 9cb94b4d1a..65b796bf1a 100644 --- a/pallets/subtensor/src/tests/claim_root.rs +++ b/pallets/subtensor/src/tests/claim_root.rs @@ -1,15 +1,16 @@ #![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, step_block, }; use crate::{ - DefaultMinRootClaimAmount, Error, MAX_NUM_ROOT_CLAIMS, MAX_ROOT_CLAIM_THRESHOLD, NetworksAdded, - NumRootClaim, NumStakingColdkeys, PendingRootAlphaDivs, RootClaimable, RootClaimableThreshold, - StakingColdkeys, StakingColdkeysByIndex, SubnetAlphaIn, SubnetMechanism, SubnetMovingPrice, - SubnetTAO, SubnetTaoFlow, SubtokenEnabled, Tempo, pallet, + DefaultMinRootClaimAmount, Error, Event, MAX_NUM_ROOT_CLAIMS, MAX_ROOT_CLAIM_THRESHOLD, + NetworksAdded, NumRootClaim, NumStakingColdkeys, PendingRootAlphaDivs, PendingRootClaimedData, + RootClaimable, RootClaimableThreshold, StakingColdkeys, StakingColdkeysByIndex, SubnetAlphaIn, + SubnetMechanism, SubnetMovingPrice, SubnetTAO, SubnetTaoFlow, SubtokenEnabled, Tempo, pallet, }; +use crate::{PendingSubnetRootClaimData, RootAlphaDividendsPerSubnet}; use crate::{RootClaimType, RootClaimTypeEnum, RootClaimed}; use approx::assert_abs_diff_eq; use frame_support::dispatch::RawOrigin; @@ -18,7 +19,7 @@ use frame_support::traits::Get; use frame_support::{assert_err, assert_noop, assert_ok}; use sp_core::{H256, U256}; use sp_runtime::DispatchError; -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use substrate_fixed::types::{I96F32, U64F64, U96F32}; use subtensor_runtime_common::{AlphaCurrency, Currency, NetUid, TaoCurrency}; use subtensor_swap_interface::SwapHandler; @@ -1817,6 +1818,247 @@ fn test_claim_root_keep_subnets_swap_claim_type() { }); } +#[test] +fn test_claim_root_subnet_root_claim_map_keep() { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(1001); + let hotkey = U256::from(1002); + let coldkey = U256::from(1003); + let netuid = add_dynamic_network(&hotkey, &owner_coldkey); + + SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 + + let root_stake = 2_000_000u64; + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + NetUid::ROOT, + root_stake.into(), + ); + + 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(), + ); + + // Distribute pending root alpha + + let pending_root_alpha = 1_000_000u64; + SubtensorModule::distribute_emission( + netuid, + AlphaCurrency::ZERO, + AlphaCurrency::ZERO, + pending_root_alpha.into(), + AlphaCurrency::ZERO, + ); + + // Claim root alpha + + assert_ok!(SubtensorModule::set_root_claim_type( + RuntimeOrigin::signed(coldkey), + RootClaimTypeEnum::Keep + ),); + assert_eq!(RootClaimType::::get(coldkey), RootClaimTypeEnum::Keep); + assert!(!PendingSubnetRootClaimData::::contains_key(netuid)); + + assert_ok!(SubtensorModule::claim_root( + RuntimeOrigin::signed(coldkey), + BTreeSet::from([netuid]) + )); + + // Check SubnetRootClaimKeep + + let new_stake = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); + + assert!(PendingSubnetRootClaimData::::contains_key(netuid)); + + let root_claim_data = PendingSubnetRootClaimData::::get(netuid); + + assert_eq!(new_stake, root_claim_data.alpha_kept); + + assert_eq!(root_claim_data.alpha_swapped, AlphaCurrency::ZERO); + assert_eq!(root_claim_data.tao_swapped, TaoCurrency::ZERO); + + // Check PendingSubnetRootClaimData after on_finalize() + + step_block(1); + assert!(!PendingSubnetRootClaimData::::contains_key(netuid)); + + // Check for the event + let event = System::events() + .into_iter() + .find(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::RootClaimedData { .. }) + ) + }) + .expect("Event must be found"); + + let RuntimeEvent::SubtensorModule(Event::RootClaimedData { data }) = event.event else { + panic!("Event must be RootClaimedData") + }; + + assert_eq!( + BTreeMap::from([( + netuid, + PendingRootClaimedData { + alpha_swapped: AlphaCurrency::ZERO, + tao_swapped: TaoCurrency::ZERO, + alpha_kept: new_stake, + } + )]), + data + ); + }); +} + +#[test] +fn test_claim_root_subnet_root_claim_map_swap() { + new_test_ext(1).execute_with(|| { + let owner_coldkey = U256::from(1001); + let other_coldkey = U256::from(10010); + let hotkey = U256::from(1002); + let coldkey = U256::from(1003); + let netuid = add_dynamic_network(&hotkey, &owner_coldkey); + + SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 + SubnetMechanism::::insert(netuid, 1); + + 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); + let current_price = + ::SwapInterface::current_alpha_price(netuid.into()) + .saturating_to_num::(); + assert_eq!(current_price, 0.5f64); + + let root_stake = 2_000_000u64; + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + NetUid::ROOT, + root_stake.into(), + ); + let root_stake_rate = 0.1f64; + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &other_coldkey, + NetUid::ROOT, + (9 * root_stake).into(), + ); + + 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(), + ); + + // Distribute pending root alpha + + let pending_root_alpha = 10_000_000u64; + SubtensorModule::distribute_emission( + netuid, + AlphaCurrency::ZERO, + AlphaCurrency::ZERO, + pending_root_alpha.into(), + AlphaCurrency::ZERO, + ); + + // Claim root alpha + + let validator_take_percent = 0.18f64; + + assert_ok!(SubtensorModule::set_root_claim_type( + RuntimeOrigin::signed(coldkey), + RootClaimTypeEnum::Swap + ),); + assert_eq!(RootClaimType::::get(coldkey), RootClaimTypeEnum::Swap); + assert!(!PendingSubnetRootClaimData::::contains_key(netuid)); + + assert_ok!(SubtensorModule::claim_root( + RuntimeOrigin::signed(coldkey), + BTreeSet::from([netuid]) + )); + + // Check new stake + + let new_stake: u64 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + NetUid::ROOT, + ) + .into(); + + let estimated_stake_increment = (pending_root_alpha as f64) + * (1f64 - validator_take_percent) + * current_price + * root_stake_rate; + + assert_abs_diff_eq!( + new_stake, + root_stake + estimated_stake_increment as u64, + epsilon = 10000u64, + ); + + // Check SubnetRootClaimSwap + + let root_claim_data = PendingSubnetRootClaimData::::get(netuid); + + let saved_swapped_alpha: u64 = root_claim_data.alpha_swapped.into(); + + let saved_swapped_tao: u64 = root_claim_data.tao_swapped.into(); + + assert_eq!(root_claim_data.alpha_kept, AlphaCurrency::ZERO); + + assert_abs_diff_eq!( + estimated_stake_increment, + saved_swapped_tao as f64, + epsilon = 10f64, + ); + + assert_abs_diff_eq!( + estimated_stake_increment / current_price, + saved_swapped_alpha as f64, + epsilon = 10f64, + ); + + // Check PendingSubnetRootClaimData after block finalization + + step_block(1); + assert!(!PendingSubnetRootClaimData::::contains_key(netuid)); + + // Check for the event + let event = System::events() + .into_iter() + .find(|e| { + matches!( + &e.event, + RuntimeEvent::SubtensorModule(Event::RootClaimedData { .. }) + ) + }) + .expect("Event must be found"); + + let RuntimeEvent::SubtensorModule(Event::RootClaimedData { data }) = event.event else { + panic!("Event must be RootClaimedData") + }; + + for (event_netuid, root_claim_data) in data.into_iter() { + assert_eq!(event_netuid, netuid); + assert_eq!(root_claim_data.alpha_kept, AlphaCurrency::ZERO); + assert_eq!(root_claim_data.alpha_swapped, saved_swapped_alpha.into()); + assert_eq!(root_claim_data.tao_swapped, saved_swapped_tao.into()); + } + }); +} + #[test] fn test_claim_root_default_mode_keep() { new_test_ext(1).execute_with(|| {