@@ -508,18 +508,6 @@ impl<T: Config> Pallet<T> {
508508 alpha_dividends : BTreeMap < T :: AccountId , U96F32 > ,
509509 root_alpha_dividends : BTreeMap < T :: AccountId , U96F32 > ,
510510 ) {
511- // Compute and store EffectiveRootProp for the NEXT round before distributing.
512- // This intentionally computes the effective root proportion for the next epoch based on
513- // the current epoch's dividend distribution (using raw, pre-distribution dividend values).
514- // It is calculated once per epoch from the actual dividend proportions that occurred.
515- // Exploitation via temporary stake placement before this calculation is mitigated because
516- // apply_effective_root_prop_scaling uses min(EffectiveRootProp, RootProp), which caps the
517- // value at the protocol-level RootProp setting.
518- Self :: compute_and_store_effective_root_prop (
519- netuid,
520- & alpha_dividends,
521- & root_alpha_dividends,
522- ) ;
523511
524512 // Distribute the owner cut.
525513 if let Ok ( owner_coldkey) = SubnetOwner :: < T > :: try_get ( netuid)
@@ -651,25 +639,26 @@ impl<T: Config> Pallet<T> {
651639 }
652640 }
653641
654- /// Computes and stores the EffectiveRootProp for a subnet.
642+ /// Computes and stores the EffectiveRootProp for a subnet. Returns the utilization value.
655643 ///
656644 /// EffectiveRootProp = raw_root_prop * utilization
657645 ///
658646 /// Where:
659647 /// raw_root_prop = sum(root_alpha_dividends) / (sum(alpha_dividends) + sum(root_alpha_dividends))
660- /// utilization = active_root_stake / total_root_stake
648+ /// utilization = sum(root_stake_i * efficiency_i) / total_root_stake
649+ /// efficiency_i = min(actual_share_i / expected_share_i, 1.0)
650+ /// expected_share_i = root_stake_i / total_root_stake
651+ /// actual_share_i = root_alpha_dividends[i] / sum(root_alpha_dividends)
661652 ///
662- /// active_root_stake is the root stake of validators that earned root dividends this epoch.
663- /// total_root_stake is the root stake of ALL validators registered on the subnet.
664- ///
665- /// This weighting ensures that subnets where most root stake is idle (validators not setting
666- /// weights) get a much lower EffectiveRootProp than subnets where all root stake is active.
653+ /// Only root stake of validators with UIDs on this subnet is counted.
654+ /// TotalIssuance, unstaked TAO, and root stake on other subnets are irrelevant.
667655 pub fn compute_and_store_effective_root_prop (
668656 netuid : NetUid ,
669657 alpha_dividends : & BTreeMap < T :: AccountId , U96F32 > ,
670658 root_alpha_dividends : & BTreeMap < T :: AccountId , U96F32 > ,
671- ) {
659+ ) -> U96F32 {
672660 let zero = U96F32 :: saturating_from_num ( 0 ) ;
661+ let one = U96F32 :: saturating_from_num ( 1 ) ;
673662
674663 let total_alpha_divs: U96F32 = alpha_dividends
675664 . values ( )
@@ -687,42 +676,62 @@ impl<T: Config> Pallet<T> {
687676 zero
688677 } ;
689678
690- // Compute root stake utilization: fraction of total root stake that actively earns dividends.
691- // Iterate all UIDs on the subnet and sum their root stakes. Hotkeys that appear in
692- // root_alpha_dividends with a nonzero value are considered "active".
679+ // Compute dividend-efficiency-based utilization.
680+ // For each root-staked validator registered on this subnet:
681+ // expected_share = root_stake_i / total_root_stake
682+ // actual_share = root_dividends_i / total_root_divs
683+ // efficiency = min(actual_share / expected_share, 1.0)
684+ // utilization = sum(root_stake_i * efficiency_i) / total_root_stake
693685 let n = SubnetworkN :: < T > :: get ( netuid) ;
694686 let mut total_root_stake = zero;
695- let mut active_root_stake = zero;
696687
688+ // First pass: compute total root stake on this subnet
689+ let mut hotkey_root_stakes: Vec < ( T :: AccountId , U96F32 ) > = Vec :: new ( ) ;
697690 for uid in 0 ..n {
698691 if let Ok ( hotkey) = Keys :: < T > :: try_get ( netuid, uid) {
699692 let root_stake = Self :: get_stake_for_hotkey_on_subnet ( & hotkey, NetUid :: ROOT ) ;
700693 let rs = U96F32 :: saturating_from_num ( root_stake. to_u64 ( ) ) ;
701694 total_root_stake = total_root_stake. saturating_add ( rs) ;
702- if root_alpha_dividends
703- . get ( & hotkey)
704- . is_some_and ( |v| * v > zero)
705- {
706- active_root_stake = active_root_stake. saturating_add ( rs) ;
695+ if rs > zero {
696+ hotkey_root_stakes. push ( ( hotkey, rs) ) ;
707697 }
708698 }
709699 }
710700
711- let utilization = if total_root_stake > zero {
712- active_root_stake
701+ let utilization = if total_root_stake > zero && total_root_divs > zero {
702+ // Second pass: compute weighted efficiency
703+ let mut weighted_efficiency_sum = zero;
704+ for ( hotkey, rs) in & hotkey_root_stakes {
705+ let expected_share = rs. checked_div ( total_root_stake) . unwrap_or ( zero) ;
706+ let actual_div = root_alpha_dividends. get ( hotkey) . copied ( ) . unwrap_or ( zero) ;
707+ let actual_share = actual_div. checked_div ( total_root_divs) . unwrap_or ( zero) ;
708+ let efficiency = if expected_share > zero {
709+ let raw_eff = actual_share. checked_div ( expected_share) . unwrap_or ( zero) ;
710+ raw_eff. min ( one)
711+ } else {
712+ zero
713+ } ;
714+ weighted_efficiency_sum =
715+ weighted_efficiency_sum. saturating_add ( rs. saturating_mul ( efficiency) ) ;
716+ }
717+ weighted_efficiency_sum
713718 . checked_div ( total_root_stake)
714719 . unwrap_or ( zero)
720+ } else if total_root_stake > zero {
721+ // No root dividends at all → utilization = 0
722+ zero
715723 } else {
716724 zero
717725 } ;
718726
719727 let effective_root_prop = raw_root_prop. saturating_mul ( utilization) ;
720728
721729 log:: debug!(
722- "EffectiveRootProp for netuid {netuid:?}: {effective_root_prop:?} (raw: {raw_root_prop:?}, utilization: {utilization:?}, active_root_stake: {active_root_stake:?}, total_root_stake: {total_root_stake:?})"
730+ "EffectiveRootProp for netuid {netuid:?}: {effective_root_prop:?} (raw: {raw_root_prop:?}, utilization: {utilization:?}, total_root_stake: {total_root_stake:?})"
723731 ) ;
724732
725733 EffectiveRootProp :: < T > :: insert ( netuid, effective_root_prop) ;
734+ utilization
726735 }
727736
728737 pub fn get_stake_map (
@@ -811,7 +820,7 @@ impl<T: Config> Pallet<T> {
811820 let root_alpha = pending_root_alpha;
812821 let owner_cut = pending_owner_cut;
813822
814- let ( incentives, ( alpha_dividends, root_alpha_dividends) ) =
823+ let ( incentives, ( mut alpha_dividends, mut root_alpha_dividends) ) =
815824 Self :: calculate_dividend_and_incentive_distribution (
816825 netuid,
817826 root_alpha,
@@ -820,6 +829,94 @@ impl<T: Config> Pallet<T> {
820829 tao_weight,
821830 ) ;
822831
832+ // Compute and store EffectiveRootProp, getting back utilization for scaling.
833+ let utilization = Self :: compute_and_store_effective_root_prop (
834+ netuid,
835+ & alpha_dividends,
836+ & root_alpha_dividends,
837+ ) ;
838+
839+ let half = U96F32 :: saturating_from_num ( 0.5 ) ;
840+ let one = U96F32 :: saturating_from_num ( 1 ) ;
841+ let zero = U96F32 :: saturating_from_num ( 0 ) ;
842+
843+ if utilization < half {
844+ // Hard cap: recycle ALL root alpha dividends
845+ let total_root: U96F32 = root_alpha_dividends
846+ . values ( )
847+ . fold ( zero, |acc, v| acc. saturating_add ( * v) ) ;
848+ Self :: recycle_subnet_alpha ( netuid, AlphaCurrency :: from ( tou64 ! ( total_root) ) ) ;
849+ root_alpha_dividends. clear ( ) ;
850+
851+ // Zero root-staked portion of alpha_dividends
852+ for ( _hotkey, alpha_div) in alpha_dividends. iter_mut ( ) {
853+ let root_stake = Self :: get_stake_for_hotkey_on_subnet ( _hotkey, NetUid :: ROOT ) ;
854+ let root_stake_f = asfloat ! ( root_stake. to_u64( ) ) ;
855+ if root_stake_f > zero {
856+ let root_alpha_weighted = root_stake_f. saturating_mul ( tao_weight) ;
857+ let alpha_stake =
858+ Self :: get_stake_for_hotkey_on_subnet ( _hotkey, netuid) ;
859+ let alpha_stake_f = asfloat ! ( alpha_stake. to_u64( ) ) ;
860+ let total_stake = alpha_stake_f. saturating_add ( root_alpha_weighted) ;
861+ if total_stake > zero {
862+ let root_fraction =
863+ root_alpha_weighted. checked_div ( total_stake) . unwrap_or ( zero) ;
864+ let recycle_amount = ( * alpha_div) . saturating_mul ( root_fraction) ;
865+ * alpha_div = ( * alpha_div) . saturating_sub ( recycle_amount) ;
866+ Self :: recycle_subnet_alpha (
867+ netuid,
868+ AlphaCurrency :: from ( tou64 ! ( recycle_amount) ) ,
869+ ) ;
870+ }
871+ }
872+ }
873+
874+ // Overwrite EffectiveRootProp to 0
875+ EffectiveRootProp :: < T > :: insert ( netuid, U96F32 :: saturating_from_num ( 0 ) ) ;
876+
877+ log:: debug!(
878+ "Hard cap triggered for netuid {netuid:?}: utilization {utilization:?} < 0.5, all root dividends recycled"
879+ ) ;
880+ } else if utilization < one {
881+ // Scale root_alpha_dividends by utilization
882+ for ( _hotkey, root_div) in root_alpha_dividends. iter_mut ( ) {
883+ let scaled = ( * root_div) . saturating_mul ( utilization) ;
884+ let reduction = ( * root_div) . saturating_sub ( scaled) ;
885+ * root_div = scaled;
886+ Self :: recycle_subnet_alpha ( netuid, AlphaCurrency :: from ( tou64 ! ( reduction) ) ) ;
887+ }
888+
889+ // Scale root-staked portion of alpha_dividends by utilization
890+ for ( _hotkey, alpha_div) in alpha_dividends. iter_mut ( ) {
891+ let root_stake = Self :: get_stake_for_hotkey_on_subnet ( _hotkey, NetUid :: ROOT ) ;
892+ let root_stake_f = asfloat ! ( root_stake. to_u64( ) ) ;
893+ if root_stake_f > zero {
894+ let root_alpha_weighted = root_stake_f. saturating_mul ( tao_weight) ;
895+ let alpha_stake =
896+ Self :: get_stake_for_hotkey_on_subnet ( _hotkey, netuid) ;
897+ let alpha_stake_f = asfloat ! ( alpha_stake. to_u64( ) ) ;
898+ let total_stake = alpha_stake_f. saturating_add ( root_alpha_weighted) ;
899+ if total_stake > zero {
900+ let root_fraction =
901+ root_alpha_weighted. checked_div ( total_stake) . unwrap_or ( zero) ;
902+ let root_portion = ( * alpha_div) . saturating_mul ( root_fraction) ;
903+ let reduction =
904+ root_portion. saturating_mul ( one. saturating_sub ( utilization) ) ;
905+ * alpha_div = ( * alpha_div) . saturating_sub ( reduction) ;
906+ Self :: recycle_subnet_alpha (
907+ netuid,
908+ AlphaCurrency :: from ( tou64 ! ( reduction) ) ,
909+ ) ;
910+ }
911+ }
912+ }
913+
914+ log:: debug!(
915+ "Utilization scaling for netuid {netuid:?}: utilization {utilization:?}, dividends scaled"
916+ ) ;
917+ }
918+ // else: utilization >= 1.0, no scaling needed
919+
823920 Self :: distribute_dividends_and_incentives (
824921 netuid,
825922 owner_cut,
0 commit comments