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, } } 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( diff --git a/pallets/subtensor/src/tests/recycle_alpha.rs b/pallets/subtensor/src/tests/recycle_alpha.rs index 173a03aea1..32a95c700d 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,77 @@ 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); + + // amount to recycle + let recycle_amount = AlphaCurrency::from(stake / 2); + + // Modify the alpha pool denominator so it's low-precision + let denominator = U64F64::from_num(0.00000001); + 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); + + // amount to recycle + let burn_amount = AlphaCurrency::from(stake / 2); + + // Modify the alpha pool denominator so it's low-precision + let denominator = U64F64::from_num(0.00000001); + 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 + ); + }); +}