From b69af0908c0529c39bfa569abcdede9c77764386 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Thu, 24 Jul 2025 17:09:01 -0400 Subject: [PATCH 1/2] Burn incentive of all hotkeys associated with subnet owner's coldkey --- .../subtensor/src/coinbase/run_coinbase.rs | 27 +++++++--- pallets/subtensor/src/tests/coinbase.rs | 51 ++++++++++++++++++- 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index aaccb1f749..ab6363531c 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -429,19 +429,34 @@ impl Pallet { } // Distribute mining incentives. + let mut total_incentive = AlphaCurrency::from(0_u64); + let mut burned_incentive = AlphaCurrency::from(0_u64); for (hotkey, incentive) in incentives { log::debug!("incentives: hotkey: {:?}", incentive); + total_incentive = total_incentive.saturating_add(incentive); + + // Burn miner emission for all hotkeys associated with subnet owner coldkey + // Also, calculate the burned proportion + let mut skip_and_burn = false; if let Ok(owner_hotkey) = SubnetOwnerHotkey::::try_get(netuid) { if hotkey == owner_hotkey { - log::debug!( - "incentives: hotkey: {:?} is SN owner hotkey, skipping {:?}", - hotkey, - incentive - ); - continue; // Skip/burn miner-emission for SN owner hotkey. + skip_and_burn = true; } } + if Owner::::get(&hotkey) == SubnetOwner::::get(netuid) { + skip_and_burn = true; + } + if skip_and_burn { + burned_incentive = burned_incentive.saturating_add(incentive); + log::debug!( + "incentives: hotkey: {:?} is SN owner hotkey, skipping {:?}", + hotkey, + incentive + ); + continue; + } + // Increase stake for miner. Self::increase_stake_for_hotkey_and_coldkey_on_subnet( &hotkey.clone(), diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index 03fdf4227e..31e4e61d3c 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -1627,7 +1627,7 @@ fn test_get_root_children_drain_with_half_take() { // }); // } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_incentive_to_subnet_owner_is_burned --exact --show-output --nocapture +// cargo test --package pallet-subtensor --lib -- tests::coinbase::test_incentive_to_subnet_owner_is_burned --exact --show-output #[test] fn test_incentive_to_subnet_owner_is_burned() { new_test_ext(1).execute_with(|| { @@ -1636,6 +1636,7 @@ fn test_incentive_to_subnet_owner_is_burned() { let other_ck = U256::from(2); let other_hk = U256::from(3); + Owner::::insert(other_hk.clone(), other_ck.clone()); let netuid = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); @@ -1675,6 +1676,54 @@ fn test_incentive_to_subnet_owner_is_burned() { }); } +// cargo test --package pallet-subtensor --lib -- tests::coinbase::test_incentive_to_subnet_owner_owned_hotkey_is_burned --exact --show-output +#[test] +fn test_incentive_to_subnet_owner_owned_hotkey_is_burned() { + new_test_ext(1).execute_with(|| { + let subnet_owner_ck = U256::from(0); + let subnet_owner_hk = U256::from(1); + + let other_hk = U256::from(3); + Owner::::insert(other_hk.clone(), subnet_owner_ck.clone()); + + let netuid = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); + + let pending_tao: u64 = 1_000_000_000; + let pending_alpha = AlphaCurrency::ZERO; // None to valis + let owner_cut = AlphaCurrency::ZERO; + let mut incentives: BTreeMap = BTreeMap::new(); + + // Give incentive to other_hk + incentives.insert(other_hk, 10_000_000.into()); + + // Give incentives to subnet_owner_hk + incentives.insert(subnet_owner_hk, 10_000_000.into()); + + // Verify stake before + let subnet_owner_stake_before = + SubtensorModule::get_stake_for_hotkey_on_subnet(&subnet_owner_hk, netuid); + assert_eq!(subnet_owner_stake_before, 0.into()); + let other_stake_before = SubtensorModule::get_stake_for_hotkey_on_subnet(&other_hk, netuid); + assert_eq!(other_stake_before, 0.into()); + + // Distribute dividends and incentives + SubtensorModule::distribute_dividends_and_incentives( + netuid, + owner_cut, + incentives, + BTreeMap::new(), + BTreeMap::new(), + ); + + // Verify stake after + let subnet_owner_stake_after = + SubtensorModule::get_stake_for_hotkey_on_subnet(&subnet_owner_hk, netuid); + assert_eq!(subnet_owner_stake_after, 0.into()); + let other_stake_after = SubtensorModule::get_stake_for_hotkey_on_subnet(&other_hk, netuid); + assert_eq!(other_stake_after, 0.into()); + }); +} + #[test] fn test_calculate_dividend_distribution_totals() { new_test_ext(1).execute_with(|| { From cfe4f5a8ddcc1843b93220b732fe415afd377d0e Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 25 Jul 2025 15:55:30 -0400 Subject: [PATCH 2/2] Implement proportional cut and dividend burn --- .../subtensor/src/coinbase/run_coinbase.rs | 91 +++++++------ pallets/subtensor/src/tests/coinbase.rs | 124 ++++++++++++++++++ 2 files changed, 173 insertions(+), 42 deletions(-) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index ab6363531c..2cd113ed0d 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -1,7 +1,7 @@ use super::*; use alloc::collections::BTreeMap; use safe_math::*; -use substrate_fixed::types::U96F32; +use substrate_fixed::types::{U64F64, U96F32}; use subtensor_runtime_common::{AlphaCurrency, Currency, NetUid}; use subtensor_swap_interface::SwapHandler; @@ -405,36 +405,13 @@ impl Pallet { alpha_dividends: BTreeMap, tao_dividends: BTreeMap, ) { - // Distribute the owner cut. - if let Ok(owner_coldkey) = SubnetOwner::::try_get(netuid) { - if let Ok(owner_hotkey) = SubnetOwnerHotkey::::try_get(netuid) { - // Increase stake for owner hotkey and coldkey. - log::debug!( - "owner_hotkey: {:?} owner_coldkey: {:?}, owner_cut: {:?}", - owner_hotkey, - owner_coldkey, - owner_cut - ); - let real_owner_cut = Self::increase_stake_for_hotkey_and_coldkey_on_subnet( - &owner_hotkey, - &owner_coldkey, - netuid, - owner_cut, - ); - // If the subnet is leased, notify the lease logic that owner cut has been distributed. - if let Some(lease_id) = SubnetUidToLeaseId::::get(netuid) { - Self::distribute_leased_network_dividends(lease_id, real_owner_cut); - } - } - } - - // Distribute mining incentives. - let mut total_incentive = AlphaCurrency::from(0_u64); - let mut burned_incentive = AlphaCurrency::from(0_u64); + // Distribute mining incentives and calculate the proportion of burned incentive + let mut total_incentive = U64F64::saturating_from_num(0); + let mut burned_incentive = U64F64::saturating_from_num(0); for (hotkey, incentive) in incentives { log::debug!("incentives: hotkey: {:?}", incentive); - total_incentive = total_incentive.saturating_add(incentive); + total_incentive = total_incentive.saturating_add(U64F64::saturating_from_num(incentive)); // Burn miner emission for all hotkeys associated with subnet owner coldkey // Also, calculate the burned proportion @@ -448,7 +425,7 @@ impl Pallet { skip_and_burn = true; } if skip_and_burn { - burned_incentive = burned_incentive.saturating_add(incentive); + burned_incentive = burned_incentive.saturating_add(U64F64::saturating_from_num(incentive)); log::debug!( "incentives: hotkey: {:?} is SN owner hotkey, skipping {:?}", hotkey, @@ -465,15 +442,43 @@ impl Pallet { incentive, ); } + let remaining_proportion = total_incentive.saturating_sub(burned_incentive).safe_div(total_incentive); + + // Distribute the owner cut. + if let Ok(owner_coldkey) = SubnetOwner::::try_get(netuid) { + if let Ok(owner_hotkey) = SubnetOwnerHotkey::::try_get(netuid) { + let owner_cut_after_burn = AlphaCurrency::from(remaining_proportion.saturating_mul(U64F64::saturating_from_num(owner_cut)).saturating_to_num::()); + + // Increase stake for owner hotkey and coldkey. + log::debug!( + "owner_hotkey: {:?} owner_coldkey: {:?}, owner_cut: {:?}", + owner_hotkey, + owner_coldkey, + owner_cut_after_burn + ); + let real_owner_cut = Self::increase_stake_for_hotkey_and_coldkey_on_subnet( + &owner_hotkey, + &owner_coldkey, + netuid, + owner_cut_after_burn, + ); + // If the subnet is leased, notify the lease logic that owner cut has been distributed. + if let Some(lease_id) = SubnetUidToLeaseId::::get(netuid) { + Self::distribute_leased_network_dividends(lease_id, real_owner_cut); + } + } + } // Distribute alpha divs. let _ = AlphaDividendsPerSubnet::::clear_prefix(netuid, u32::MAX, None); - for (hotkey, mut alpha_divs) in alpha_dividends { + for (hotkey, alpha_divs) in alpha_dividends { + let mut alpha_divs_after_burn = U96F32::saturating_from_num(remaining_proportion).saturating_mul(alpha_divs); + // Get take prop let alpha_take: U96F32 = - Self::get_hotkey_take_float(&hotkey).saturating_mul(alpha_divs); + Self::get_hotkey_take_float(&hotkey).saturating_mul(alpha_divs_after_burn); // Remove take prop from alpha_divs - alpha_divs = alpha_divs.saturating_sub(alpha_take); + alpha_divs_after_burn = alpha_divs_after_burn.saturating_sub(alpha_take); // Give the validator their take. log::debug!("hotkey: {:?} alpha_take: {:?}", hotkey, alpha_take); Self::increase_stake_for_hotkey_and_coldkey_on_subnet( @@ -483,11 +488,11 @@ impl Pallet { tou64!(alpha_take).into(), ); // Give all other nominators. - log::debug!("hotkey: {:?} alpha_divs: {:?}", hotkey, alpha_divs); - Self::increase_stake_for_hotkey_on_subnet(&hotkey, netuid, tou64!(alpha_divs).into()); + log::debug!("hotkey: {:?} alpha_divs: {:?}", hotkey, alpha_divs_after_burn); + Self::increase_stake_for_hotkey_on_subnet(&hotkey, netuid, tou64!(alpha_divs_after_burn).into()); // Record dividends for this hotkey. AlphaDividendsPerSubnet::::mutate(netuid, &hotkey, |divs| { - *divs = divs.saturating_add(tou64!(alpha_divs).into()); + *divs = divs.saturating_add(tou64!(alpha_divs_after_burn).into()); }); // Record total hotkey alpha based on which this value of AlphaDividendsPerSubnet // was calculated @@ -497,11 +502,13 @@ impl Pallet { // Distribute root tao divs. let _ = TaoDividendsPerSubnet::::clear_prefix(netuid, u32::MAX, None); - for (hotkey, mut root_tao) in tao_dividends { + for (hotkey, root_tao) in tao_dividends { + let mut root_divs_after_burn = U96F32::saturating_from_num(remaining_proportion).saturating_mul(root_tao); + // Get take prop - let tao_take: U96F32 = Self::get_hotkey_take_float(&hotkey).saturating_mul(root_tao); + let tao_take: U96F32 = Self::get_hotkey_take_float(&hotkey).saturating_mul(root_divs_after_burn); // Remove take prop from root_tao - root_tao = root_tao.saturating_sub(tao_take); + root_divs_after_burn = root_divs_after_burn.saturating_sub(tao_take); // Give the validator their take. log::debug!("hotkey: {:?} tao_take: {:?}", hotkey, tao_take); let validator_stake = Self::increase_stake_for_hotkey_and_coldkey_on_subnet( @@ -511,21 +518,21 @@ impl Pallet { tou64!(tao_take).into(), ); // Give rest to nominators. - log::debug!("hotkey: {:?} root_tao: {:?}", hotkey, root_tao); + log::debug!("hotkey: {:?} root_tao: {:?}", hotkey, root_divs_after_burn); Self::increase_stake_for_hotkey_on_subnet( &hotkey, NetUid::ROOT, - tou64!(root_tao).into(), + tou64!(root_divs_after_burn).into(), ); // Record root dividends for this validator on this subnet. TaoDividendsPerSubnet::::mutate(netuid, hotkey.clone(), |divs| { - *divs = divs.saturating_add(tou64!(root_tao)); + *divs = divs.saturating_add(tou64!(root_divs_after_burn)); }); // Update the total TAO on the subnet with root tao dividends. SubnetTAO::::mutate(NetUid::ROOT, |total| { *total = total .saturating_add(validator_stake.to_u64()) - .saturating_add(tou64!(root_tao)); + .saturating_add(tou64!(root_divs_after_burn)); }); } } diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index 31e4e61d3c..723597bc87 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -1724,6 +1724,130 @@ fn test_incentive_to_subnet_owner_owned_hotkey_is_burned() { }); } +// cargo test --package pallet-subtensor --lib -- tests::coinbase::test_owner_cut_burn_proportional_to_incentive --exact --show-output +#[test] +fn test_owner_cut_burn_proportional_to_incentive() { + new_test_ext(1).execute_with(|| { + let subnet_owner_ck = U256::from(0); + let subnet_owner_hk = U256::from(1); + + let other_ck = U256::from(2); + let other_hk = U256::from(3); + Owner::::insert(other_hk.clone(), other_ck.clone()); + + let netuid = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); + + let pending_tao: u64 = 1_000_000_000; + let pending_alpha = AlphaCurrency::ZERO; // None to valis + let owner_cut = AlphaCurrency::from(1_000_000_000); + let mut incentives: BTreeMap = BTreeMap::new(); + + // Give incentive to other_hk + incentives.insert(other_hk, 10_000_000.into()); + + // Give incentives to subnet_owner_hk, total is 50/50 split + incentives.insert(subnet_owner_hk, 10_000_000.into()); + let expected_owner_cut = owner_cut / AlphaCurrency::from(2); + + // Verify stake before + let subnet_owner_stake_before = + SubtensorModule::get_stake_for_hotkey_on_subnet(&subnet_owner_hk, netuid); + assert_eq!(subnet_owner_stake_before, 0.into()); + let other_stake_before = SubtensorModule::get_stake_for_hotkey_on_subnet(&other_hk, netuid); + assert_eq!(other_stake_before, 0.into()); + + // Distribute dividends and incentives + SubtensorModule::distribute_dividends_and_incentives( + netuid, + owner_cut, + incentives, + BTreeMap::new(), + BTreeMap::new(), + ); + + // Verify stake after + let subnet_owner_stake_after = + SubtensorModule::get_stake_for_hotkey_on_subnet(&subnet_owner_hk, netuid); + assert_eq!(subnet_owner_stake_after, expected_owner_cut); + let other_stake_after = SubtensorModule::get_stake_for_hotkey_on_subnet(&other_hk, netuid); + assert!(other_stake_after > 0.into()); + }); +} + +// cargo test --package pallet-subtensor --lib -- tests::coinbase::test_dividends_burn_proportional_to_incentive --exact --show-output +#[test] +fn test_dividends_burn_proportional_to_incentive() { + new_test_ext(1).execute_with(|| { + let subnet_owner_ck = U256::from(0); + let subnet_owner_hk = U256::from(1); + + let other_ck = U256::from(2); + let other_hk = U256::from(3); + Owner::::insert(other_hk.clone(), other_ck.clone()); + + let validator_ck = U256::from(4); + let validator_hk = U256::from(5); + Owner::::insert(validator_hk.clone(), validator_ck.clone()); + + let netuid = add_dynamic_network(&subnet_owner_hk, &subnet_owner_ck); + + let pending_tao: u64 = 1_000_000_000; + let pending_alpha = AlphaCurrency::ZERO; // None to valis + let owner_cut = AlphaCurrency::from(1_000_000_000); + let mut incentives: BTreeMap = BTreeMap::new(); + let mut alpha_divs: BTreeMap = BTreeMap::new(); + let mut tao_divs: BTreeMap = BTreeMap::new(); + + // Give incentive to other_hk + incentives.insert(other_hk, 10_000_000.into()); + alpha_divs.insert(validator_hk, U96F32::from_num(1_000_000_000)); + tao_divs.insert(validator_hk, U96F32::from_num(1_000_000_000)); + + // Give incentives to subnet_owner_hk, total is 50/50 split + incentives.insert(subnet_owner_hk, 10_000_000.into()); + let expected_owner_cut = owner_cut / AlphaCurrency::from(2); + + // Verify stake before + let subnet_owner_stake_before = + SubtensorModule::get_stake_for_hotkey_on_subnet(&subnet_owner_hk, netuid); + assert_eq!(subnet_owner_stake_before, 0.into()); + let other_stake_before = SubtensorModule::get_stake_for_hotkey_on_subnet(&other_hk, netuid); + assert_eq!(other_stake_before, 0.into()); + + // Distribute dividends and incentives + SubtensorModule::distribute_dividends_and_incentives( + netuid, + owner_cut, + incentives, + alpha_divs, + tao_divs, + ); + + // Verify stake after + let subnet_owner_stake_after = + SubtensorModule::get_stake_for_hotkey_on_subnet(&subnet_owner_hk, netuid); + assert_eq!(subnet_owner_stake_after, expected_owner_cut); + let other_stake_after = SubtensorModule::get_stake_for_hotkey_on_subnet(&other_hk, netuid); + assert!(other_stake_after > 0.into()); + + let validator_alpha_stake_after = SubtensorModule::get_stake_for_hotkey_on_subnet(&validator_hk, netuid); + let expected_alpha_stake = AlphaCurrency::from(500_000_000); + assert_abs_diff_eq!( + validator_alpha_stake_after, + expected_alpha_stake, + epsilon = 1.into() + ); + + let validator_tao_stake_after = SubtensorModule::get_stake_for_hotkey_on_subnet(&validator_hk, NetUid::from(0)); + let expected_root_stake = AlphaCurrency::from(500_000_000); + assert_abs_diff_eq!( + validator_alpha_stake_after, + expected_root_stake, + epsilon = 1.into() + ); + }); +} + #[test] fn test_calculate_dividend_distribution_totals() { new_test_ext(1).execute_with(|| {