From baa16587af39c5ce8bec19e7b709eceaf028cefa Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Tue, 2 Jun 2026 14:29:23 -0400 Subject: [PATCH] Implement custom dust collection type in subtensor --- pallets/subtensor/src/coinbase/tao.rs | 18 +----- pallets/subtensor/src/lib.rs | 15 +++++ .../migrate_fix_total_issuance_evm_fees.rs | 29 +++++---- pallets/subtensor/src/tests/migration.rs | 18 +++++- pallets/subtensor/src/tests/mock.rs | 2 +- pallets/subtensor/src/tests/mock_high_ed.rs | 2 +- runtime/src/lib.rs | 2 +- runtime/tests/balances_dust.rs | 60 +++++++++++++++++++ 8 files changed, 115 insertions(+), 31 deletions(-) create mode 100644 runtime/tests/balances_dust.rs diff --git a/pallets/subtensor/src/coinbase/tao.rs b/pallets/subtensor/src/coinbase/tao.rs index 33dbda57fb..b605407763 100644 --- a/pallets/subtensor/src/coinbase/tao.rs +++ b/pallets/subtensor/src/coinbase/tao.rs @@ -33,18 +33,14 @@ impl Pallet { SubnetTAO::::get(netuid) } - /// Internal function that transfers and updates subtensor pallet total issuance - /// in case of dust collection. + /// Internal function that transfers TAO and allows the origin account to be reaped. + /// + /// Dust collection is handled by the runtime's Balances `DustRemoval` implementation. fn transfer_allow_death_update_ti( origin_coldkey: &T::AccountId, destination_coldkey: &T::AccountId, amount: BalanceOf, ) -> DispatchResult { - // If account balance remainder drops below ED, then account is killed, balance - // is lost, and we need to reduce total issuance in subtensor pallet. Measure - // balance TI before and after to detect the dust. - let balances_ti_before = ::Currency::total_issuance(); - ::Currency::transfer( origin_coldkey, destination_coldkey, @@ -52,14 +48,6 @@ impl Pallet { Preservation::Expendable, )?; - let balances_ti_after = ::Currency::total_issuance(); - if balances_ti_after < balances_ti_before { - let burned = balances_ti_before.saturating_sub(balances_ti_after); - TotalIssuance::::mutate(|total| { - *total = total.saturating_sub(burned); - }); - } - Ok(()) } diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index d5aee84243..641c816e3c 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -69,6 +69,21 @@ pub const MAX_SUBNET_CLAIMS: usize = 5; pub const MAX_ROOT_CLAIM_THRESHOLD: u64 = 10_000_000; +pub struct SubtensorDustRemoval(PhantomData); +impl frame_support::traits::OnUnbalanced> + for SubtensorDustRemoval +where + T: Config + pallet_balances::Config, + ::Balance: Into + Copy, +{ + fn on_nonzero_unbalanced(dust: pallet_balances::CreditOf) { + let amount: TaoBalance = frame_support::traits::Imbalance::peek(&dust).into(); + TotalIssuance::::mutate(|total| { + *total = total.saturating_sub(amount); + }); + } +} + #[allow(deprecated)] #[deny(missing_docs)] #[import_section(errors::errors)] diff --git a/pallets/subtensor/src/migrations/migrate_fix_total_issuance_evm_fees.rs b/pallets/subtensor/src/migrations/migrate_fix_total_issuance_evm_fees.rs index b4851745cb..476a9d8b4f 100644 --- a/pallets/subtensor/src/migrations/migrate_fix_total_issuance_evm_fees.rs +++ b/pallets/subtensor/src/migrations/migrate_fix_total_issuance_evm_fees.rs @@ -3,24 +3,31 @@ use frame_support::traits::fungible::Inspect; use frame_support::weights::Weight; pub fn migrate_fix_total_issuance_evm_fees() -> Weight { - let migration_name = b"migrate_fix_total_issuance_evm_fees".to_vec(); - let mut weight = T::DbWeight::get().reads(1); - - if HasMigrationRun::::get(&migration_name) { - log::info!( - "Migration '{:?}' has already run. Skipping.", - String::from_utf8_lossy(&migration_name) - ); + let migration_names: [&[u8]; 2] = [ + // Fix testnet TotalIssuance after the earlier EVM fees issue caused the + // Subtensor pallet's accounting to diverge from the balances pallet. + b"migrate_fix_total_issuance_evm_fees", + // Fix Subtensor TotalIssuance after dust collection caused accounting drift. + b"migrate_fix_total_issuance_after_dust_collection", + ]; + let mut weight = T::DbWeight::get().reads(migration_names.len() as u64); + + let Some(migration_name) = migration_names + .iter() + .map(|name| name.to_vec()) + .find(|name| !HasMigrationRun::::get(name)) + else { + log::info!("All total issuance fix migrations have already run. Skipping."); return weight; - } + }; log::info!( "Running migration '{}'", String::from_utf8_lossy(&migration_name) ); - // Fix testnet TotalIssuance after the earlier EVM fees issue caused the - // Subtensor pallet's accounting to diverge from the balances pallet. + // All migration instances reset Subtensor TotalIssuance to the authoritative + // Balances pallet total issuance. let balances_total_issuance = ::Currency::total_issuance(); let subtensor_total_issuance_before = TotalIssuance::::get(); TotalIssuance::::put(balances_total_issuance); diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index f13c2ae186..a68f02d250 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -4362,6 +4362,7 @@ fn test_migrate_subnet_balances() { fn test_migrate_fix_total_issuance_evm_fees() { new_test_ext(1).execute_with(|| { const MIGRATION_NAME: &[u8] = b"migrate_fix_total_issuance_evm_fees"; + const DUST_MIGRATION_NAME: &[u8] = b"migrate_fix_total_issuance_after_dust_collection"; let account = U256::from(42); let balances_total_issuance = TaoBalance::from(123_456_789_u64); @@ -4382,16 +4383,29 @@ fn test_migrate_fix_total_issuance_evm_fees() { assert!(!weight.is_zero(), "weight must be non-zero"); assert_eq!(TotalIssuance::::get(), balances_total_issuance); assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); + assert!(!HasMigrationRun::::get( + DUST_MIGRATION_NAME.to_vec() + )); let second_wrong_value = TaoBalance::from(555_u64); TotalIssuance::::put(second_wrong_value); crate::migrations::migrate_fix_total_issuance_evm_fees::migrate_fix_total_issuance_evm_fees::(); + assert_eq!(TotalIssuance::::get(), balances_total_issuance); + assert!(HasMigrationRun::::get( + DUST_MIGRATION_NAME.to_vec() + )); + + let third_wrong_value = TaoBalance::from(777_u64); + TotalIssuance::::put(third_wrong_value); + + crate::migrations::migrate_fix_total_issuance_evm_fees::migrate_fix_total_issuance_evm_fees::(); + assert_eq!( TotalIssuance::::get(), - second_wrong_value, - "migration must not run more than once" + third_wrong_value, + "migration must not run after all known migration keys have run" ); }); } diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 277162dde4..0d07251f13 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -88,7 +88,7 @@ pub type BlockNumber = u64; impl pallet_balances::Config for Test { type Balance = Balance; type RuntimeEvent = RuntimeEvent; - type DustRemoval = (); + type DustRemoval = crate::SubtensorDustRemoval; type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; type MaxLocks = (); diff --git a/pallets/subtensor/src/tests/mock_high_ed.rs b/pallets/subtensor/src/tests/mock_high_ed.rs index 5f19edf455..00cdc5c2bb 100644 --- a/pallets/subtensor/src/tests/mock_high_ed.rs +++ b/pallets/subtensor/src/tests/mock_high_ed.rs @@ -70,7 +70,7 @@ pub type BlockNumber = u64; impl pallet_balances::Config for Test { type Balance = Balance; type RuntimeEvent = RuntimeEvent; - type DustRemoval = (); + type DustRemoval = crate::SubtensorDustRemoval; type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; type MaxLocks = (); diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 207997d7df..999a64fbc8 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -519,7 +519,7 @@ impl pallet_balances::Config for Runtime { type Balance = Balance; // The ubiquitous event type. type RuntimeEvent = RuntimeEvent; - type DustRemoval = (); + type DustRemoval = pallet_subtensor::SubtensorDustRemoval; type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; type WeightInfo = pallet_balances::weights::SubstrateWeight; diff --git a/runtime/tests/balances_dust.rs b/runtime/tests/balances_dust.rs new file mode 100644 index 0000000000..b56ada26bc --- /dev/null +++ b/runtime/tests/balances_dust.rs @@ -0,0 +1,60 @@ +#![allow(clippy::unwrap_used)] + +use frame_support::traits::fungible::{Inspect, Mutate}; +use frame_support::traits::tokens::Preservation; +use node_subtensor_runtime::{Balances, BuildStorage, Runtime, RuntimeGenesisConfig}; +use sp_core::crypto::AccountId32; +use subtensor_runtime_common::TaoBalance; + +fn new_test_ext() -> sp_io::TestExternalities { + let mut ext: sp_io::TestExternalities = RuntimeGenesisConfig { + ..Default::default() + } + .build_storage() + .unwrap() + .into(); + ext.execute_with(|| frame_system::Pallet::::set_block_number(1)); + ext +} + +fn add_balance_to_account(account: &AccountId32, tao: TaoBalance) { + let credit = pallet_subtensor::Pallet::::mint_tao(tao); + let _ = pallet_subtensor::Pallet::::spend_tao(account, credit, tao).unwrap(); +} + +#[test] +fn balances_dust_removal_updates_subtensor_total_issuance() { + new_test_ext().execute_with(|| { + let origin = AccountId32::new([1u8; 32]); + let destination = AccountId32::new([2u8; 32]); + let existential_deposit = TaoBalance::from(500u64); + let dust = TaoBalance::from(1u64); + let transfer_amount = existential_deposit; + + add_balance_to_account(&origin, transfer_amount + dust); + + let balances_issuance_before = Balances::total_issuance(); + let subtensor_issuance_before = pallet_subtensor::Pallet::::get_total_issuance(); + assert_eq!(balances_issuance_before, subtensor_issuance_before); + + >::transfer( + &origin, + &destination, + transfer_amount, + Preservation::Expendable, + ) + .unwrap(); + + assert_eq!(Balances::total_balance(&origin), 0u64.into()); + assert_eq!(Balances::total_balance(&destination), transfer_amount); + assert_eq!(balances_issuance_before - Balances::total_issuance(), dust); + assert_eq!( + subtensor_issuance_before - pallet_subtensor::Pallet::::get_total_issuance(), + dust + ); + assert_eq!( + Balances::total_issuance(), + pallet_subtensor::Pallet::::get_total_issuance() + ); + }); +}