From 26e638c4768db694ec57c8f7cc4c015b33420acd Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Tue, 16 Dec 2025 12:08:42 -0500 Subject: [PATCH 1/5] add precisionloss error --- pallets/subtensor/src/macros/errors.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index 5a15330075..6c3d7a35df 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, + /// Unintended precision loss when unstaking alpha + PrecisionLoss, } } From 64fc846648ac3b69f9e37047c855c7b46a83d6a7 Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Tue, 16 Dec 2025 12:09:14 -0500 Subject: [PATCH 2/5] handle decrease_stake fn --- pallets/subtensor/src/staking/recycle_alpha.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pallets/subtensor/src/staking/recycle_alpha.rs b/pallets/subtensor/src/staking/recycle_alpha.rs index 7334c8126a..5229971ed0 100644 --- a/pallets/subtensor/src/staking/recycle_alpha.rs +++ b/pallets/subtensor/src/staking/recycle_alpha.rs @@ -55,8 +55,10 @@ impl Pallet { &hotkey, &coldkey, netuid, amount, ); + ensure!(actual_alpha_decrease <= amount, Error::::PrecisionLoss); + // Recycle means we should decrease the alpha issuance tracker. - Self::recycle_subnet_alpha(netuid, amount); + Self::recycle_subnet_alpha(netuid, actual_alpha_decrease); Self::deposit_event(Event::AlphaRecycled( coldkey, @@ -120,7 +122,9 @@ impl Pallet { &hotkey, &coldkey, netuid, amount, ); - Self::burn_subnet_alpha(netuid, amount); + ensure!(actual_alpha_decrease <= amount, Error::::PrecisionLoss); + + Self::burn_subnet_alpha(netuid, actual_alpha_decrease); // Deposit event Self::deposit_event(Event::AlphaBurned( From 3f79fb2f197887ddcd29502b99e8d36fa358071b Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Tue, 16 Dec 2025 12:09:27 -0500 Subject: [PATCH 3/5] add tests for precision loss during recycle/burn --- pallets/subtensor/src/tests/recycle_alpha.rs | 83 ++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/pallets/subtensor/src/tests/recycle_alpha.rs b/pallets/subtensor/src/tests/recycle_alpha.rs index 173a03aea1..dc888b051e 100644 --- a/pallets/subtensor/src/tests/recycle_alpha.rs +++ b/pallets/subtensor/src/tests/recycle_alpha.rs @@ -1,6 +1,7 @@ use approx::assert_abs_diff_eq; use frame_support::{assert_noop, assert_ok, traits::Currency}; use sp_core::U256; +use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AlphaCurrency, Currency as CurrencyT}; use super::mock; @@ -543,3 +544,85 @@ fn test_burn_errors() { ); }); } + +#[test] +fn test_recycle_precision_loss() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + + Balances::make_free_balance_be(&coldkey, 1_000_000_000); + // sanity check + assert!(SubtensorModule::if_subnet_exist(netuid)); + + // add stake to coldkey-hotkey pair so we can recycle it + let stake = 200_000; + increase_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, stake.into(), netuid); + + // get initial total issuance and alpha out + let initial_alpha = TotalHotkeyAlpha::::get(hotkey, netuid); + let initial_net_alpha = SubnetAlphaOut::::get(netuid); + + // amount to recycle + let recycle_amount = AlphaCurrency::from(stake); + + // Modify the alpha pool denominator so it's low-precision + let denominator = U64F64::from_num(0.0000001); + TotalHotkeyShares::::insert(hotkey, netuid, denominator); + Alpha::::insert((&hotkey, &coldkey, netuid), denominator); + + // recycle, expect error due to precision loss + assert_noop!( + SubtensorModule::recycle_alpha( + RuntimeOrigin::signed(coldkey), + hotkey, + recycle_amount, + netuid + ), + Error::::PrecisionLoss + ); + }); +} + +#[test] +fn test_burn_precision_loss() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + + let netuid = add_dynamic_network(&hotkey, &coldkey); + + Balances::make_free_balance_be(&coldkey, 1_000_000_000); + // sanity check + assert!(SubtensorModule::if_subnet_exist(netuid)); + + // add stake to coldkey-hotkey pair so we can recycle it + let stake = 200_000; + increase_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, stake.into(), netuid); + + // get initial total issuance and alpha out + let initial_alpha = TotalHotkeyAlpha::::get(hotkey, netuid); + let initial_net_alpha = SubnetAlphaOut::::get(netuid); + + // amount to recycle + let burn_amount = AlphaCurrency::from(stake); + + // Modify the alpha pool denominator so it's low-precision + let denominator = U64F64::from_num(0.0000001); + TotalHotkeyShares::::insert(hotkey, netuid, denominator); + Alpha::::insert((&hotkey, &coldkey, netuid), denominator); + + // burn, expect error due to precision loss + assert_noop!( + SubtensorModule::burn_alpha( + RuntimeOrigin::signed(coldkey), + hotkey, + burn_amount, + netuid + ), + Error::::PrecisionLoss + ); + }); +} From 94355dec711a5043abfedcc7717999380eb7f742 Mon Sep 17 00:00:00 2001 From: camfairchild Date: Tue, 16 Dec 2025 12:52:17 -0500 Subject: [PATCH 4/5] chore: clippy --- pallets/subtensor/src/tests/recycle_alpha.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pallets/subtensor/src/tests/recycle_alpha.rs b/pallets/subtensor/src/tests/recycle_alpha.rs index dc888b051e..f6cc54f379 100644 --- a/pallets/subtensor/src/tests/recycle_alpha.rs +++ b/pallets/subtensor/src/tests/recycle_alpha.rs @@ -561,10 +561,6 @@ fn test_recycle_precision_loss() { let stake = 200_000; increase_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, stake.into(), netuid); - // get initial total issuance and alpha out - let initial_alpha = TotalHotkeyAlpha::::get(hotkey, netuid); - let initial_net_alpha = SubnetAlphaOut::::get(netuid); - // amount to recycle let recycle_amount = AlphaCurrency::from(stake); @@ -602,10 +598,6 @@ fn test_burn_precision_loss() { let stake = 200_000; increase_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, stake.into(), netuid); - // get initial total issuance and alpha out - let initial_alpha = TotalHotkeyAlpha::::get(hotkey, netuid); - let initial_net_alpha = SubnetAlphaOut::::get(netuid); - // amount to recycle let burn_amount = AlphaCurrency::from(stake); From 47fdfb810f55995d51f3b6900ca9769ec45fcd94 Mon Sep 17 00:00:00 2001 From: camfairchild Date: Tue, 16 Dec 2025 16:31:22 -0500 Subject: [PATCH 5/5] reduce precision and lower amount for test --- pallets/subtensor/src/tests/recycle_alpha.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pallets/subtensor/src/tests/recycle_alpha.rs b/pallets/subtensor/src/tests/recycle_alpha.rs index f6cc54f379..32a95c700d 100644 --- a/pallets/subtensor/src/tests/recycle_alpha.rs +++ b/pallets/subtensor/src/tests/recycle_alpha.rs @@ -562,10 +562,10 @@ fn test_recycle_precision_loss() { increase_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, stake.into(), netuid); // amount to recycle - let recycle_amount = AlphaCurrency::from(stake); + let recycle_amount = AlphaCurrency::from(stake / 2); // Modify the alpha pool denominator so it's low-precision - let denominator = U64F64::from_num(0.0000001); + let denominator = U64F64::from_num(0.00000001); TotalHotkeyShares::::insert(hotkey, netuid, denominator); Alpha::::insert((&hotkey, &coldkey, netuid), denominator); @@ -599,10 +599,10 @@ fn test_burn_precision_loss() { increase_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, stake.into(), netuid); // amount to recycle - let burn_amount = AlphaCurrency::from(stake); + let burn_amount = AlphaCurrency::from(stake / 2); // Modify the alpha pool denominator so it's low-precision - let denominator = U64F64::from_num(0.0000001); + let denominator = U64F64::from_num(0.00000001); TotalHotkeyShares::::insert(hotkey, netuid, denominator); Alpha::::insert((&hotkey, &coldkey, netuid), denominator);