From 14cd19c6744f8362e23bf91b1afd46891a789f06 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 26 Jun 2026 18:16:34 +0300 Subject: [PATCH 01/24] reduce balancer exp precision from 1024 to 512 --- pallets/swap/src/pallet/balancer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pallets/swap/src/pallet/balancer.rs b/pallets/swap/src/pallet/balancer.rs index 2ffd04fdba..5230612ec6 100644 --- a/pallets/swap/src/pallet/balancer.rs +++ b/pallets/swap/src/pallet/balancer.rs @@ -149,7 +149,7 @@ impl Balancer { let w1: u128 = self.get_base_weight().deconstruct() as u128; let w2: u128 = self.get_quote_weight().deconstruct() as u128; - let precision = 1024; + let precision = 512; let x_safe = SafeInt::from(x); let w1_safe = SafeInt::from(w1); let w2_safe = SafeInt::from(w2); From bc5c0246bdd1927abbc38ca838547c1409f3af54 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 26 Jun 2026 19:00:58 +0300 Subject: [PATCH 02/24] reduce balancer exp precision from 512 to 128 --- pallets/swap/src/pallet/balancer.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pallets/swap/src/pallet/balancer.rs b/pallets/swap/src/pallet/balancer.rs index 5230612ec6..0b248a63f5 100644 --- a/pallets/swap/src/pallet/balancer.rs +++ b/pallets/swap/src/pallet/balancer.rs @@ -149,7 +149,7 @@ impl Balancer { let w1: u128 = self.get_base_weight().deconstruct() as u128; let w2: u128 = self.get_quote_weight().deconstruct() as u128; - let precision = 512; + let precision = 128; let x_safe = SafeInt::from(x); let w1_safe = SafeInt::from(w1); let w2_safe = SafeInt::from(w2); @@ -928,6 +928,7 @@ mod tests { } } + // cargo test --package pallet-subtensor-swap --lib -- pallet::balancer::tests::test_exp_quote_fuzzy --include-ignored --exact --nocapture #[ignore] #[test] fn test_exp_quote_fuzzy() { From 503170f0126b29fec26ff4364570235c5afdf3dc Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 26 Jun 2026 19:01:53 +0300 Subject: [PATCH 03/24] Increase fuzzy test output frequency --- pallets/swap/src/pallet/balancer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pallets/swap/src/pallet/balancer.rs b/pallets/swap/src/pallet/balancer.rs index 0b248a63f5..b04532eb00 100644 --- a/pallets/swap/src/pallet/balancer.rs +++ b/pallets/swap/src/pallet/balancer.rs @@ -994,7 +994,7 @@ mod tests { // Print progress let done = counter.fetch_add(1, Ordering::Relaxed) + 1; - if done % 100_000_000 == 0 { + if done % 10_000_000 == 0 { let progress = done as f64 / ITERATIONS as f64 * 100.0; // Replace with println for real-time progress log::debug!("progress = {progress:.4}%"); From 98151ecd2eaea92b5966e81aca7fcc1897495076 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 26 Jun 2026 19:11:16 +0300 Subject: [PATCH 04/24] cap exp_scaled at 1, set balancer exp precision to 256 --- pallets/swap/src/pallet/balancer.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pallets/swap/src/pallet/balancer.rs b/pallets/swap/src/pallet/balancer.rs index b04532eb00..85676db7db 100644 --- a/pallets/swap/src/pallet/balancer.rs +++ b/pallets/swap/src/pallet/balancer.rs @@ -149,7 +149,7 @@ impl Balancer { let w1: u128 = self.get_base_weight().deconstruct() as u128; let w2: u128 = self.get_quote_weight().deconstruct() as u128; - let precision = 128; + let precision = 256; let x_safe = SafeInt::from(x); let w1_safe = SafeInt::from(w1); let w2_safe = SafeInt::from(w2); @@ -187,8 +187,9 @@ impl Balancer { if let Some(result_safe_int) = maybe_result_safe_int && let Some(result_u64) = result_safe_int.to_u64() { - return U64F64::saturating_from_num(result_u64) + let result = U64F64::saturating_from_num(result_u64) .safe_div(U64F64::saturating_from_num(ACCURACY)); + return result.min(U64F64::from_num(1)); } U64F64::saturating_from_num(0) } From 593de8a177ddc00c3202705ff6c90e923a431241 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 26 Jun 2026 19:26:28 +0300 Subject: [PATCH 05/24] Forbid swaps that are too large compared to available liauidity --- pallets/swap/src/pallet/impls.rs | 2 +- pallets/swap/src/pallet/mod.rs | 3 ++ pallets/swap/src/pallet/swap_step.rs | 28 +++++++++++++-- pallets/swap/src/pallet/tests.rs | 54 ++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 4 deletions(-) diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index c3e0b2f1d3..df56f29aa2 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -210,7 +210,7 @@ impl Pallet { amount_to_swap, limit_price, drop_fees, - ); + )?; let swap_result = swap_step.execute()?; diff --git a/pallets/swap/src/pallet/mod.rs b/pallets/swap/src/pallet/mod.rs index 1d2fd07c59..b9044d4e82 100644 --- a/pallets/swap/src/pallet/mod.rs +++ b/pallets/swap/src/pallet/mod.rs @@ -165,6 +165,9 @@ mod pallet { /// Swap reserves are too imbalanced ReservesOutOfBalance, + /// Swap input is too large relative to input-side liquidity + SwapInputTooLarge, + /// The extrinsic is deprecated Deprecated, } diff --git a/pallets/swap/src/pallet/swap_step.rs b/pallets/swap/src/pallet/swap_step.rs index 7f10bff65a..9b98259a6f 100644 --- a/pallets/swap/src/pallet/swap_step.rs +++ b/pallets/swap/src/pallet/swap_step.rs @@ -7,6 +7,8 @@ use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token, TokenRes use super::pallet::*; +const MAX_SWAP_INPUT_RESERVE_MULTIPLIER: u64 = 1_000; + /// A struct representing a single swap step with all its parameters and state pub(crate) struct BasicSwapStep where @@ -45,15 +47,16 @@ where amount_remaining: PaidIn, limit_price: U64F64, drop_fees: bool, - ) -> Self { + ) -> Result> { let fee = Pallet::::calculate_fee_amount(netuid, amount_remaining, drop_fees); let requested_delta_in = amount_remaining.saturating_sub(fee); + Self::ensure_input_within_reserve_limit(netuid, requested_delta_in)?; // Target and current prices let target_price = Self::price_target(netuid, requested_delta_in); let current_price = Pallet::::current_price(netuid); - Self { + Ok(Self { netuid, drop_fees, requested_delta_in, @@ -64,15 +67,23 @@ where final_price: target_price, fee, _phantom: PhantomData, - } + }) } /// Execute the swap step and return the result pub(crate) fn execute(&mut self) -> Result, Error> { self.determine_action(); + Self::ensure_input_within_reserve_limit(self.netuid, self.delta_in)?; self.process_swap() } + fn ensure_input_within_reserve_limit(netuid: NetUid, delta_in: PaidIn) -> Result<(), Error> { + let input_reserve = Self::input_reserve(netuid); + let max_delta_in = input_reserve.saturating_mul(MAX_SWAP_INPUT_RESERVE_MULTIPLIER.into()); + ensure!(delta_in <= max_delta_in, Error::::SwapInputTooLarge); + Ok(()) + } + /// Determine the appropriate action for this swap step fn determine_action(&mut self) { let mut recalculate_fee = false; @@ -176,6 +187,10 @@ impl SwapStep .saturating_to_num::(), ) } + + fn input_reserve(netuid: NetUid) -> TaoBalance { + T::TaoReserve::reserve(netuid.into()) + } } impl SwapStep @@ -220,6 +235,10 @@ impl SwapStep .saturating_to_num::(), ) } + + fn input_reserve(netuid: NetUid) -> AlphaBalance { + T::AlphaReserve::reserve(netuid.into()) + } } pub(crate) trait SwapStep @@ -244,6 +263,9 @@ where /// This is the core method of the swap that tells how much output token is given for an /// amount of input token within one price tick. fn convert_deltas(netuid: NetUid, delta_in: PaidIn) -> PaidOut; + + /// Return the reserve for the token being paid into this swap step. + fn input_reserve(netuid: NetUid) -> PaidIn; } #[derive(Debug, PartialEq)] diff --git a/pallets/swap/src/pallet/tests.rs b/pallets/swap/src/pallet/tests.rs index b1071294d3..833dc8d9c1 100644 --- a/pallets/swap/src/pallet/tests.rs +++ b/pallets/swap/src/pallet/tests.rs @@ -721,6 +721,60 @@ fn test_rollback_works() { }) } +#[test] +fn test_swap_rejects_input_over_1000x_input_reserve() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + TaoReserve::set_mock_reserve(netuid, TaoBalance::from(1_000)); + AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(1_000)); + + assert_noop!( + Pallet::::do_swap( + netuid, + GetTaoForAlpha::with_amount(1_000_001), + get_min_price(), + true, + false, + ), + Error::::SwapInputTooLarge + ); + assert_noop!( + Pallet::::do_swap( + netuid, + GetAlphaForTao::with_amount(1_000_001), + get_max_price(), + true, + false, + ), + Error::::SwapInputTooLarge + ); + }); +} + +#[test] +fn test_swap_allows_input_at_1000x_input_reserve() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + TaoReserve::set_mock_reserve(netuid, TaoBalance::from(1_000)); + AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(1_000)); + + assert_ok!(Pallet::::do_swap( + netuid, + GetTaoForAlpha::with_amount(1_000_000), + get_min_price(), + true, + true, + )); + assert_ok!(Pallet::::do_swap( + netuid, + GetAlphaForTao::with_amount(1_000_000), + get_max_price(), + true, + true, + )); + }); +} + #[allow(dead_code)] fn bbox(t: U64F64, a: U64F64, b: U64F64) -> U64F64 { if t < a { From 536c8938d7ff114508bcf6adaaa7290d22e73b75 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 26 Jun 2026 20:16:22 +0300 Subject: [PATCH 06/24] spec bump --- runtime/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index a39c473880..52a517873a 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -234,7 +234,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 423, + spec_version: 424, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, From c4c77a270208e97243e48621adc5bde9aadc0c39 Mon Sep 17 00:00:00 2001 From: "subtensor-ai-review[bot]" Date: Fri, 26 Jun 2026 17:17:36 +0000 Subject: [PATCH 07/24] chore: auditor auto-fix --- runtime/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index a39c473880..52a517873a 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -234,7 +234,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 423, + spec_version: 424, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, From 566fb8c5afd9a8fe323cde1224b19a98d75a9ad3 Mon Sep 17 00:00:00 2001 From: gztensor <166415444+gztensor@users.noreply.github.com> Date: Fri, 26 Jun 2026 10:21:51 -0700 Subject: [PATCH 08/24] Update pallets/swap/src/pallet/balancer.rs Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- pallets/swap/src/pallet/balancer.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pallets/swap/src/pallet/balancer.rs b/pallets/swap/src/pallet/balancer.rs index 85676db7db..5c51ee08ca 100644 --- a/pallets/swap/src/pallet/balancer.rs +++ b/pallets/swap/src/pallet/balancer.rs @@ -189,7 +189,11 @@ impl Balancer { { let result = U64F64::saturating_from_num(result_u64) .safe_div(U64F64::saturating_from_num(ACCURACY)); - return result.min(U64F64::from_num(1)); + return if dx >= 0 { + result.min(U64F64::from_num(1)) + } else { + result + }; } U64F64::saturating_from_num(0) } From d005de37e16fda9d4f2dac6d55604511ad4820de Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 26 Jun 2026 20:59:08 +0300 Subject: [PATCH 09/24] fix tests --- pallets/subtensor/src/tests/staking.rs | 28 ++++++++++++--------- pallets/swap/src/pallet/balancer.rs | 6 +++++ pallets/swap/src/pallet/impls.rs | 34 +++++++++++++++++++++++--- pallets/swap/src/pallet/swap_step.rs | 28 +++------------------ 4 files changed, 56 insertions(+), 40 deletions(-) diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index 660b6957d7..ee36fe8e4e 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -776,9 +776,9 @@ fn test_add_stake_insufficient_liquidity() { }); } -/// cargo test --package pallet-subtensor --lib -- tests::staking::test_add_stake_insufficient_liquidity_one_side_ok --exact --show-output +/// cargo test --package pallet-subtensor --lib -- tests::staking::test_add_stake_input_reserve_too_low_fails --exact --show-output #[test] -fn test_add_stake_insufficient_liquidity_one_side_ok() { +fn test_add_stake_input_reserve_too_low_fails() { new_test_ext(1).execute_with(|| { let subnet_owner_coldkey = U256::from(1001); let subnet_owner_hotkey = U256::from(1002); @@ -795,13 +795,17 @@ fn test_add_stake_insufficient_liquidity_one_side_ok() { let reserve_tao = u64::from(mock::SwapMinimumReserve::get()) - 1; mock::setup_reserves(netuid, reserve_tao.into(), reserve_alpha.into()); - // Check the error - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - amount_staked.into() - )); + // The output-side reserve is sufficient, but the input-side reserve is too small for the + // requested swap under the 1000x input-reserve cap. + assert_noop!( + SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + amount_staked.into() + ), + pallet_subtensor_swap::Error::::SwapInputTooLarge + ); }); } @@ -876,7 +880,7 @@ fn test_remove_stake_insufficient_liquidity() { // Mock more liquidity - remove becomes successful SubnetTAO::::insert(netuid, TaoBalance::from(amount_staked + 1)); - SubnetAlphaIn::::insert(netuid, AlphaBalance::from(1)); + SubnetAlphaIn::::insert(netuid, AlphaBalance::from(alpha.to_u64() / 1000 + 1)); assert_ok!(SubtensorModule::remove_stake( RuntimeOrigin::signed(coldkey), hotkey, @@ -5248,7 +5252,8 @@ fn test_large_swap() { // add network let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_000_u64.into()); - let tao = TaoBalance::from(100_000_000u64); + let swap_amount = TaoBalance::from(100_000_000_000_000_u64); + let tao = TaoBalance::from(swap_amount.to_u64() / 1000); let alpha = AlphaBalance::from(1_000_000_000_000_000_u64); SubnetTAO::::insert(netuid, tao); SubnetAlphaIn::::insert(netuid, alpha); @@ -5256,7 +5261,6 @@ fn test_large_swap() { // Force the swap to initialize ::SwapInterface::init_swap(netuid, None); - let swap_amount = TaoBalance::from(100_000_000_000_000_u64); assert_ok!(SubtensorModule::add_stake( RuntimeOrigin::signed(coldkey), owner_hotkey, diff --git a/pallets/swap/src/pallet/balancer.rs b/pallets/swap/src/pallet/balancer.rs index 5c51ee08ca..244e4ff9eb 100644 --- a/pallets/swap/src/pallet/balancer.rs +++ b/pallets/swap/src/pallet/balancer.rs @@ -796,6 +796,12 @@ mod tests { let dy1 = y_fixed * (one - e1); let dy2 = y_fixed * (one - e2); + if dx > x.saturating_mul(1_000) { + assert!(e1 <= one); + assert!(e2 <= one); + return; + } + let w1 = perquintill_to_f64(bal.get_base_weight()); let w2 = perquintill_to_f64(bal.get_quote_weight()); let e1_expected = (x as f64 / (x as f64 + dx as f64)).powf(w1 / w2); diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index df56f29aa2..7edbf2bed4 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -14,7 +14,7 @@ use subtensor_swap_interface::{ }; use super::pallet::*; -use super::swap_step::{BasicSwapStep, SwapStep}; +use super::swap_step::{BasicSwapStep, MAX_SWAP_INPUT_RESERVE_MULTIPLIER, SwapStep}; use crate::{pallet::Balancer, pallet::balancer::BalancerError}; impl Pallet { @@ -154,8 +154,17 @@ impl Pallet { transactional::with_transaction(|| { let reserve = Order::ReserveOut::reserve(netuid.into()); - let result = Self::swap_inner::(netuid, order, limit_price, drop_fees) - .map_err(Into::into); + let result = if simulate { + Ok(()) + } else { + Self::ensure_swap_input_within_reserve_limit::( + netuid, + order.amount(), + drop_fees, + ) + } + .and_then(|_| Self::swap_inner::(netuid, order, limit_price, drop_fees)) + .map_err(Into::into); if simulate || result.is_err() { // Simulation only @@ -177,6 +186,23 @@ impl Pallet { }) } + fn ensure_swap_input_within_reserve_limit( + netuid: NetUid, + amount: Order::PaidIn, + drop_fees: bool, + ) -> Result<(), Error> + where + Order: OrderT, + { + let fee = Self::calculate_fee_amount(netuid, amount, drop_fees); + let net_amount = amount.saturating_sub(fee); + let input_reserve = Order::ReserveIn::reserve(netuid); + let max_amount = input_reserve.saturating_mul(MAX_SWAP_INPUT_RESERVE_MULTIPLIER.into()); + + ensure!(net_amount <= max_amount, Error::::SwapInputTooLarge); + Ok(()) + } + fn swap_inner( netuid: NetUid, order: Order, @@ -210,7 +236,7 @@ impl Pallet { amount_to_swap, limit_price, drop_fees, - )?; + ); let swap_result = swap_step.execute()?; diff --git a/pallets/swap/src/pallet/swap_step.rs b/pallets/swap/src/pallet/swap_step.rs index 9b98259a6f..3d4d516d1f 100644 --- a/pallets/swap/src/pallet/swap_step.rs +++ b/pallets/swap/src/pallet/swap_step.rs @@ -7,7 +7,7 @@ use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token, TokenRes use super::pallet::*; -const MAX_SWAP_INPUT_RESERVE_MULTIPLIER: u64 = 1_000; +pub(crate) const MAX_SWAP_INPUT_RESERVE_MULTIPLIER: u64 = 1_000; /// A struct representing a single swap step with all its parameters and state pub(crate) struct BasicSwapStep @@ -47,16 +47,15 @@ where amount_remaining: PaidIn, limit_price: U64F64, drop_fees: bool, - ) -> Result> { + ) -> Self { let fee = Pallet::::calculate_fee_amount(netuid, amount_remaining, drop_fees); let requested_delta_in = amount_remaining.saturating_sub(fee); - Self::ensure_input_within_reserve_limit(netuid, requested_delta_in)?; // Target and current prices let target_price = Self::price_target(netuid, requested_delta_in); let current_price = Pallet::::current_price(netuid); - Ok(Self { + Self { netuid, drop_fees, requested_delta_in, @@ -67,23 +66,15 @@ where final_price: target_price, fee, _phantom: PhantomData, - }) + } } /// Execute the swap step and return the result pub(crate) fn execute(&mut self) -> Result, Error> { self.determine_action(); - Self::ensure_input_within_reserve_limit(self.netuid, self.delta_in)?; self.process_swap() } - fn ensure_input_within_reserve_limit(netuid: NetUid, delta_in: PaidIn) -> Result<(), Error> { - let input_reserve = Self::input_reserve(netuid); - let max_delta_in = input_reserve.saturating_mul(MAX_SWAP_INPUT_RESERVE_MULTIPLIER.into()); - ensure!(delta_in <= max_delta_in, Error::::SwapInputTooLarge); - Ok(()) - } - /// Determine the appropriate action for this swap step fn determine_action(&mut self) { let mut recalculate_fee = false; @@ -187,10 +178,6 @@ impl SwapStep .saturating_to_num::(), ) } - - fn input_reserve(netuid: NetUid) -> TaoBalance { - T::TaoReserve::reserve(netuid.into()) - } } impl SwapStep @@ -235,10 +222,6 @@ impl SwapStep .saturating_to_num::(), ) } - - fn input_reserve(netuid: NetUid) -> AlphaBalance { - T::AlphaReserve::reserve(netuid.into()) - } } pub(crate) trait SwapStep @@ -263,9 +246,6 @@ where /// This is the core method of the swap that tells how much output token is given for an /// amount of input token within one price tick. fn convert_deltas(netuid: NetUid, delta_in: PaidIn) -> PaidOut; - - /// Return the reserve for the token being paid into this swap step. - fn input_reserve(netuid: NetUid) -> PaidIn; } #[derive(Debug, PartialEq)] From 132b9cca3b0d8aa4cbadf54b402c2bde12bf8050 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 26 Jun 2026 21:15:43 +0300 Subject: [PATCH 10/24] Fix tests, enforce atomic add-stake recycle/burn operations by rolling back the initial stake when the recycle or burn leg fails. --- chain-extensions/src/tests.rs | 38 ++++++++-- .../subtensor/src/staking/recycle_alpha.rs | 75 ++++++++++++++----- 2 files changed, 85 insertions(+), 28 deletions(-) diff --git a/chain-extensions/src/tests.rs b/chain-extensions/src/tests.rs index 16619389bf..886f722aa7 100644 --- a/chain-extensions/src/tests.rs +++ b/chain-extensions/src/tests.rs @@ -1297,15 +1297,26 @@ fn add_stake_recycle_rollback_on_recycle_failure() { let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - // Set up very low reserves so recycle will fail with InsufficientLiquidity + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + pallet_subtensor::Pallet::::insert_lock_state( + &coldkey, + netuid, + &hotkey, + pallet_subtensor::staking::lock::LockState { + locked_mass: AlphaBalance::from(u64::MAX / 4), + conviction: U64F64::saturating_from_num(0), + last_update: pallet_subtensor::Pallet::::get_current_block_as_u64(), + }, + ); + + // Leave enough input-side liquidity for add_stake to pass the 1000x swap input cap. + // The lock above makes the recycle leg fail, exercising atomic rollback. mock::setup_reserves( netuid, - TaoBalance::from(1_000_u64), + TaoBalance::from(tao_amount_raw / 1000 + 1), AlphaBalance::from(1_000_u64), ); - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - add_balance_to_coldkey_account( &coldkey, TaoBalance::from(tao_amount_raw.saturating_add(1_000_000_000)), @@ -1368,15 +1379,26 @@ fn add_stake_burn_rollback_on_burn_failure() { let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - // Set up very low reserves so burn will fail with InsufficientLiquidity + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + pallet_subtensor::Pallet::::insert_lock_state( + &coldkey, + netuid, + &hotkey, + pallet_subtensor::staking::lock::LockState { + locked_mass: AlphaBalance::from(u64::MAX / 4), + conviction: U64F64::saturating_from_num(0), + last_update: pallet_subtensor::Pallet::::get_current_block_as_u64(), + }, + ); + + // Leave enough input-side liquidity for add_stake to pass the 1000x swap input cap. + // The lock above makes the burn leg fail, exercising atomic rollback. mock::setup_reserves( netuid, - TaoBalance::from(1_000_u64), + TaoBalance::from(tao_amount_raw / 1000 + 1), AlphaBalance::from(1_000_u64), ); - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - add_balance_to_coldkey_account( &coldkey, TaoBalance::from(tao_amount_raw.saturating_add(1_000_000_000)), diff --git a/pallets/subtensor/src/staking/recycle_alpha.rs b/pallets/subtensor/src/staking/recycle_alpha.rs index 7152c48cdb..aa0eced405 100644 --- a/pallets/subtensor/src/staking/recycle_alpha.rs +++ b/pallets/subtensor/src/staking/recycle_alpha.rs @@ -1,5 +1,6 @@ use super::*; use crate::{Error, system::ensure_signed}; +use frame_support::storage::{TransactionOutcome, with_transaction}; use subtensor_runtime_common::{AlphaBalance, NetUid}; impl Pallet { @@ -132,22 +133,38 @@ impl Pallet { amount: TaoBalance, limit: Option, ) -> DispatchResult { - let alpha = if let Some(limit) = limit { - Self::do_add_stake_limit(origin.clone(), hotkey.clone(), netuid, amount, limit, false)? - } else { - Self::do_add_stake(origin.clone(), hotkey.clone(), netuid, amount)? - }; - - Self::do_burn_alpha(origin, hotkey.clone(), alpha, netuid)?; - - Self::deposit_event(Event::AddStakeBurn { - netuid, - hotkey, - amount, - alpha, - }); - - Ok(()) + with_transaction(|| { + let result = (|| { + let alpha = if let Some(limit) = limit { + Self::do_add_stake_limit( + origin.clone(), + hotkey.clone(), + netuid, + amount, + limit, + false, + )? + } else { + Self::do_add_stake(origin.clone(), hotkey.clone(), netuid, amount)? + }; + + Self::do_burn_alpha(origin, hotkey.clone(), alpha, netuid)?; + + Self::deposit_event(Event::AddStakeBurn { + netuid, + hotkey, + amount, + alpha, + }); + + Ok(()) + })(); + + match result { + Ok(()) => TransactionOutcome::Commit(Ok(())), + Err(err) => TransactionOutcome::Rollback(Err(err)), + } + }) } /// Atomically stakes TAO and recycles the resulting alpha. @@ -160,8 +177,17 @@ impl Pallet { netuid: NetUid, amount: TaoBalance, ) -> Result { - let alpha = Self::do_add_stake(origin.clone(), hotkey.clone(), netuid, amount)?; - Self::do_recycle_alpha(origin, hotkey, alpha, netuid) + with_transaction(|| { + let result = (|| { + let alpha = Self::do_add_stake(origin.clone(), hotkey.clone(), netuid, amount)?; + Self::do_recycle_alpha(origin, hotkey, alpha, netuid) + })(); + + match result { + Ok(alpha) => TransactionOutcome::Commit(Ok(alpha)), + Err(err) => TransactionOutcome::Rollback(Err(err)), + } + }) } /// Atomically stakes TAO and burns the resulting alpha. Permissionless @@ -173,7 +199,16 @@ impl Pallet { netuid: NetUid, amount: TaoBalance, ) -> Result { - let alpha = Self::do_add_stake(origin.clone(), hotkey.clone(), netuid, amount)?; - Self::do_burn_alpha(origin, hotkey, alpha, netuid) + with_transaction(|| { + let result = (|| { + let alpha = Self::do_add_stake(origin.clone(), hotkey.clone(), netuid, amount)?; + Self::do_burn_alpha(origin, hotkey, alpha, netuid) + })(); + + match result { + Ok(alpha) => TransactionOutcome::Commit(Ok(alpha)), + Err(err) => TransactionOutcome::Rollback(Err(err)), + } + }) } } From 1ad16880f9a66f9b4b8506c470d3fb6468db457e Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 26 Jun 2026 21:30:30 +0300 Subject: [PATCH 11/24] Apply the 1000x swap input cap to simulations and limit-path max-amount probes. --- pallets/subtensor/src/staking/add_stake.rs | 24 +++++++- pallets/subtensor/src/staking/remove_stake.rs | 23 +++++++- pallets/subtensor/src/tests/staking.rs | 58 ++++++++++++++----- pallets/swap/src/pallet/impls.rs | 14 ++--- pallets/swap/src/pallet/tests.rs | 20 ++++++- 5 files changed, 111 insertions(+), 28 deletions(-) diff --git a/pallets/subtensor/src/staking/add_stake.rs b/pallets/subtensor/src/staking/add_stake.rs index 33cadf241b..d2bad04787 100644 --- a/pallets/subtensor/src/staking/add_stake.rs +++ b/pallets/subtensor/src/staking/add_stake.rs @@ -48,6 +48,8 @@ impl Pallet { "do_add_stake( origin:{coldkey:?} hotkey:{hotkey:?}, netuid:{netuid:?}, stake_to_be_added:{stake_to_be_added:?} )" ); + Self::ensure_add_stake_input_within_swap_limit(netuid, stake_to_be_added)?; + // 2. Validate user input Self::validate_add_stake( &coldkey, @@ -124,6 +126,8 @@ impl Pallet { "do_add_stake( origin:{coldkey:?} hotkey:{hotkey:?}, netuid:{netuid:?}, stake_to_be_added:{stake_to_be_added:?} )" ); + Self::ensure_add_stake_input_within_swap_limit(netuid, stake_to_be_added)?; + // 2. Calculate the maximum amount that can be executed with price limit let max_amount: TaoBalance = Self::get_max_amount_add(netuid, limit_price)?.into(); let mut possible_stake = stake_to_be_added; @@ -175,11 +179,27 @@ impl Pallet { } } - // Use reverting swap to estimate max limit amount - let order = GetAlphaForTao::::with_amount(u64::MAX); + // Use the largest supported input instead of probing the swap path with u64::MAX. + let max_supported_input = SubnetTAO::::get(netuid).saturating_mul(1_000.into()); + let order = GetAlphaForTao::::with_amount(max_supported_input); let result = T::SwapInterface::swap(netuid.into(), order, limit_price, false, true) .map(|r| r.amount_paid_in.saturating_add(r.fee_paid))?; Ok(result.into()) } + + fn ensure_add_stake_input_within_swap_limit( + netuid: NetUid, + amount: TaoBalance, + ) -> Result<(), Error> { + if !netuid.is_root() && SubnetMechanism::::get(netuid) == 1 { + let max_supported_input = SubnetTAO::::get(netuid).saturating_mul(1_000.into()); + ensure!( + amount <= max_supported_input, + Error::::InsufficientLiquidity + ); + } + + Ok(()) + } } diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index cf640dc661..0b460612b9 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -55,6 +55,7 @@ impl Pallet { let alpha_available = Self::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); let alpha_unstaked = alpha_unstaked.min(alpha_available); + Self::ensure_remove_stake_input_within_swap_limit(netuid, alpha_unstaked)?; // 2. Validate the user input Self::validate_remove_stake( @@ -336,6 +337,8 @@ impl Pallet { "do_remove_stake( origin:{coldkey:?} hotkey:{hotkey:?}, netuid: {netuid:?}, alpha_unstaked:{alpha_unstaked:?} )" ); + Self::ensure_remove_stake_input_within_swap_limit(netuid, alpha_unstaked)?; + // 2. Calculate the maximum amount that can be executed with price limit let max_amount = Self::get_max_amount_remove(netuid, limit_price)?; let mut possible_alpha = alpha_unstaked; @@ -394,14 +397,30 @@ impl Pallet { } } - // Use reverting swap to estimate max limit amount - let order = GetTaoForAlpha::::with_amount(u64::MAX); + // Use the largest supported input instead of probing the swap path with u64::MAX. + let max_supported_input = SubnetAlphaIn::::get(netuid).saturating_mul(1_000.into()); + let order = GetTaoForAlpha::::with_amount(max_supported_input); let result = T::SwapInterface::swap(netuid.into(), order, limit_price.into(), false, true) .map(|r| r.amount_paid_in.saturating_add(r.fee_paid))?; Ok(result) } + fn ensure_remove_stake_input_within_swap_limit( + netuid: NetUid, + amount: AlphaBalance, + ) -> Result<(), Error> { + if !netuid.is_root() && SubnetMechanism::::get(netuid) == 1 { + let max_supported_input = SubnetAlphaIn::::get(netuid).saturating_mul(1_000.into()); + ensure!( + amount <= max_supported_input, + Error::::InsufficientLiquidity + ); + } + + Ok(()) + } + pub fn do_remove_stake_full_limit( origin: OriginFor, hotkey: T::AccountId, diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index ee36fe8e4e..3d759e200c 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -804,7 +804,7 @@ fn test_add_stake_input_reserve_too_low_fails() { netuid, amount_staked.into() ), - pallet_subtensor_swap::Error::::SwapInputTooLarge + Error::::InsufficientLiquidity ); }); } @@ -3046,14 +3046,14 @@ fn test_max_amount_remove_dynamic() { pallet_subtensor_swap::Error::::PriceLimitExceeded, )), ), - (10_000_000_000, 10_000_000_000, 0, Ok(u64::MAX)), + (10_000_000_000, 10_000_000_000, 0, Ok(10_000_000_000_000)), // Low bounds (numbers are empirical, it is only important that result // is sharply decreasing when limit price increases) - (1_000, 1_000, 0, Ok(u64::MAX)), - (1_001, 1_001, 0, Ok(u64::MAX)), - (1_001, 1_001, 1, Ok(17_472)), - (1_001, 1_001, 2, Ok(17_472)), - (1_001, 1_001, 1_001, Ok(17_472)), + (1_000, 1_000, 0, Ok(1_000_000)), + (1_001, 1_001, 0, Ok(1_001_000)), + (1_001, 1_001, 1, Ok(1_001_000)), + (1_001, 1_001, 2, Ok(1_001_000)), + (1_001, 1_001, 1_001, Ok(1_001_000)), (1_001, 1_001, 10_000, Ok(17_472)), (1_001, 1_001, 100_000, Ok(17_472)), (1_001, 1_001, 1_000_000, Ok(17_472)), @@ -3069,7 +3069,7 @@ fn test_max_amount_remove_dynamic() { Ok(3_030_000_000_000), ), // Normal range values with edge cases and sanity checks - (200_000_000_000, 100_000_000_000, 0, Ok(u64::MAX)), + (200_000_000_000, 100_000_000_000, 0, Ok(100_000_000_000_000)), ( 200_000_000_000, 100_000_000_000, @@ -3157,10 +3157,13 @@ fn test_max_amount_remove_dynamic() { ), Ok(v) => { let v = AlphaBalance::from(v); - assert_abs_diff_eq!( - SubtensorModule::get_max_amount_remove(netuid, limit_price.into()).unwrap(), - v, - epsilon = v / 100.into() + let actual = + SubtensorModule::get_max_amount_remove(netuid, limit_price.into()).unwrap(); + let epsilon = v / 100.into(); + let diff = actual.max(v).saturating_sub(actual.min(v)); + assert!( + diff <= epsilon, + "max remove mismatch: tao_in={tao_in}, alpha_in={alpha_in:?}, limit_price={limit_price}, actual={actual:?}, expected={v:?}, epsilon={epsilon:?}", ); } } @@ -3417,10 +3420,10 @@ fn test_max_amount_move_dynamic_stable() { // The tests below just mimic the remove_stake_limit tests - // 0 price => max is u64::MAX + // 0 price => max is capped at 1000x input reserve assert_eq!( SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, TaoBalance::ZERO), - Ok(AlphaBalance::MAX) + Ok(alpha_in.saturating_mul(1_000.into())) ); // Low price values don't blow things up @@ -3876,6 +3879,33 @@ fn test_add_stake_limit_fill_or_kill() { }); } +#[test] +fn test_add_stake_limit_rejects_input_over_swap_reserve_cap() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(533454); + let coldkey_account_id = U256::from(55454); + + let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + let tao_reserve = TaoBalance::from(1_000_u64); + mock::setup_reserves(netuid, tao_reserve, AlphaBalance::from(1_000_000_000_u64)); + + let amount = tao_reserve.saturating_mul(1_000.into()) + TaoBalance::from(1_u64); + add_balance_to_coldkey_account(&coldkey_account_id, amount); + + assert_noop!( + SubtensorModule::add_stake_limit( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount, + ::SwapInterface::max_price(), + true + ), + Error::::InsufficientLiquidity + ); + }); +} + #[test] fn test_add_stake_limit_partial_zero_max_stake_amount_error() { new_test_ext(1).execute_with(|| { diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index 7edbf2bed4..c5b9b11a29 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -154,15 +154,11 @@ impl Pallet { transactional::with_transaction(|| { let reserve = Order::ReserveOut::reserve(netuid.into()); - let result = if simulate { - Ok(()) - } else { - Self::ensure_swap_input_within_reserve_limit::( - netuid, - order.amount(), - drop_fees, - ) - } + let result = Self::ensure_swap_input_within_reserve_limit::( + netuid, + order.amount(), + drop_fees, + ) .and_then(|_| Self::swap_inner::(netuid, order, limit_price, drop_fees)) .map_err(Into::into); diff --git a/pallets/swap/src/pallet/tests.rs b/pallets/swap/src/pallet/tests.rs index 833dc8d9c1..5e9712d63d 100644 --- a/pallets/swap/src/pallet/tests.rs +++ b/pallets/swap/src/pallet/tests.rs @@ -11,7 +11,7 @@ use sp_arithmetic::Perquintill; use sp_runtime::DispatchError; use substrate_fixed::types::U64F64; use subtensor_runtime_common::{NetUid, Token}; -use subtensor_swap_interface::Order as OrderT; +use subtensor_swap_interface::{Order as OrderT, SwapHandler}; use super::*; use crate::mock::*; @@ -751,6 +751,24 @@ fn test_swap_rejects_input_over_1000x_input_reserve() { }); } +#[test] +fn test_sim_swap_rejects_input_over_1000x_input_reserve() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + TaoReserve::set_mock_reserve(netuid, TaoBalance::from(1_000)); + AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(1_000)); + + assert_noop!( + Pallet::::sim_swap(netuid, GetTaoForAlpha::with_amount(1_001_000)), + Error::::SwapInputTooLarge + ); + assert_noop!( + Pallet::::sim_swap(netuid, GetAlphaForTao::with_amount(1_001_000)), + Error::::SwapInputTooLarge + ); + }); +} + #[test] fn test_swap_allows_input_at_1000x_input_reserve() { new_test_ext().execute_with(|| { From 156eecadb2f9c3fbeed1b810a0ca94199cd9612f Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Mon, 29 Jun 2026 14:50:14 +0300 Subject: [PATCH 12/24] Allow locked alpha transfers in coldkey swaps --- pallets/subtensor/src/macros/dispatches.rs | 10 +-- pallets/subtensor/src/staking/lock.rs | 20 +++++ pallets/subtensor/src/swap/swap_coldkey.rs | 4 + pallets/subtensor/src/tests/swap_coldkey.rs | 99 +++++++++++++++++++++ 4 files changed, 124 insertions(+), 9 deletions(-) diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 08e5bb8fdf..a4596c9add 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -2639,15 +2639,7 @@ mod dispatches { ))] pub fn set_reject_locked_alpha(origin: OriginFor, enabled: bool) -> DispatchResult { let coldkey = ensure_signed(origin)?; - AccountFlags::::mutate_exists(&coldkey, |maybe_flags| { - let mut flags = maybe_flags.unwrap_or_default(); - if enabled { - flags &= !crate::ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA; - } else { - flags |= crate::ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA; - } - *maybe_flags = if flags == 0 { None } else { Some(flags) }; - }); + Self::set_accept_locked_alpha(&coldkey, !enabled); Self::deposit_event(Event::RejectLockedAlphaUpdated { coldkey, enabled }); Ok(()) } diff --git a/pallets/subtensor/src/staking/lock.rs b/pallets/subtensor/src/staking/lock.rs index fcf699619d..3e1b2ec8a7 100644 --- a/pallets/subtensor/src/staking/lock.rs +++ b/pallets/subtensor/src/staking/lock.rs @@ -471,6 +471,18 @@ impl Pallet { AccountFlags::::get(coldkey) & crate::ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA != 1 } + pub fn set_accept_locked_alpha(coldkey: &T::AccountId, enabled: bool) { + AccountFlags::::mutate_exists(coldkey, |maybe_flags| { + let mut flags = maybe_flags.unwrap_or_default(); + if enabled { + flags |= crate::ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA; + } else { + flags &= !crate::ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA; + } + *maybe_flags = if flags == 0 { None } else { Some(flags) }; + }); + } + pub fn ensure_can_receive_locked_alpha( coldkey: &T::AccountId, amount: AlphaBalance, @@ -1434,6 +1446,14 @@ impl Pallet { } } + let flags = AccountFlags::::get(old_coldkey); + AccountFlags::::remove(old_coldkey); + if flags != 0 { + AccountFlags::::insert(new_coldkey, flags); + } else { + AccountFlags::::remove(new_coldkey); + } + // Insert locks for the new coldkey and add to the destination aggregate // buckets after the flags have moved. for (netuid, hotkey, old_lock, perpetual_lock) in rolled_locks_to_transfer { diff --git a/pallets/subtensor/src/swap/swap_coldkey.rs b/pallets/subtensor/src/swap/swap_coldkey.rs index 608c61fd57..7b1b4d838c 100644 --- a/pallets/subtensor/src/swap/swap_coldkey.rs +++ b/pallets/subtensor/src/swap/swap_coldkey.rs @@ -23,6 +23,10 @@ impl Pallet { IdentitiesV2::::insert(new_coldkey.clone(), identity); } + // Temporarily allow the destination coldkey to receive this stake even if some of it is + // locked; swap_coldkey_locks will copy the source AccountFlags over afterward. + Self::set_accept_locked_alpha(new_coldkey, true); + for netuid in Self::get_all_subnet_netuids() { Self::transfer_subnet_ownership(netuid, old_coldkey, new_coldkey); Self::transfer_auto_stake_destination(netuid, old_coldkey, new_coldkey); diff --git a/pallets/subtensor/src/tests/swap_coldkey.rs b/pallets/subtensor/src/tests/swap_coldkey.rs index fd0281ad35..ca269fe3d1 100644 --- a/pallets/subtensor/src/tests/swap_coldkey.rs +++ b/pallets/subtensor/src/tests/swap_coldkey.rs @@ -499,6 +499,105 @@ fn test_swap_coldkey_works() { }); } +#[test] +fn test_swap_coldkey_announced_transfers_locked_alpha() { + new_test_ext(1000).execute_with(|| { + let old_coldkey = U256::from(1); + let new_coldkey = U256::from(2); + let new_coldkey_hash = ::Hashing::hash_of(&new_coldkey); + let hotkey1 = U256::from(1001); + let hotkey2 = U256::from(1002); + let hotkey3 = U256::from(1003); + let ed = ExistentialDeposit::get(); + let swap_cost = SubtensorModule::get_key_swap_cost(); + let min_stake = DefaultMinStake::::get(); + let stake1 = min_stake * 10.into(); + let stake2 = min_stake * 20.into(); + let stake3 = min_stake * 30.into(); + + add_balance_to_coldkey_account(&old_coldkey, swap_cost + stake1 + stake2 + stake3 + ed); + + let ( + netuid1, + _netuid2, + _hotkeys, + hk1_alpha, + _hk2_alpha, + _hk3_alpha, + _total_ck_stake, + _identity, + _balance_before, + _total_stake_before, + ) = comprehensive_setup!( + old_coldkey, + new_coldkey, + new_coldkey_hash, + stake1, + stake2, + stake3, + hotkey1, + hotkey2, + hotkey3, + swap_cost + ed + ); + + let lock_amount = hk1_alpha / 2.into(); + assert!(!lock_amount.is_zero()); + assert_ok!(SubtensorModule::do_lock_stake( + &old_coldkey, + netuid1, + &hotkey1, + lock_amount, + )); + + let old_stake_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &old_coldkey, + netuid1, + ); + let new_stake_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &new_coldkey, + netuid1, + ); + let old_lock_before = + Lock::::get((old_coldkey, netuid1, hotkey1)).expect("lock should exist"); + + ColdkeySwapAnnouncements::::insert( + old_coldkey, + (System::block_number(), new_coldkey_hash), + ); + + assert_ok!(SubtensorModule::swap_coldkey_announced( + ::RuntimeOrigin::signed(old_coldkey), + new_coldkey, + )); + + assert!(ColdkeySwapAnnouncements::::get(old_coldkey).is_none()); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &old_coldkey, + netuid1, + ), + AlphaBalance::ZERO + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &new_coldkey, + netuid1, + ), + old_stake_before + new_stake_before + ); + assert!(Lock::::get((old_coldkey, netuid1, hotkey1)).is_none()); + assert_eq!( + Lock::::get((new_coldkey, netuid1, hotkey1)), + Some(old_lock_before) + ); + }); +} + // cargo test --package pallet-subtensor --lib -- tests::swap_coldkey::test_swap_coldkey_works_with_zero_cost --exact --nocapture #[test] fn test_swap_coldkey_works_with_zero_cost() { From b749a634a62bd409e13777deac404e32d6585975 Mon Sep 17 00:00:00 2001 From: Loris Moulin Date: Mon, 29 Jun 2026 13:34:40 -0300 Subject: [PATCH 13/24] Apply dispatch extension conditionally --- .../src/guards/check_coldkey_swap.rs | 25 ++- .../src/guards/check_delegate_take.rs | 57 +++++- .../src/guards/check_evm_key_association.rs | 54 ++++- .../subtensor/src/guards/check_rate_limits.rs | 81 +++++++- .../src/guards/check_serving_endpoints.rs | 61 +++++- pallets/subtensor/src/guards/check_weights.rs | 190 ++++++++++++------ pallets/subtensor/src/guards/mod.rs | 19 ++ 7 files changed, 384 insertions(+), 103 deletions(-) diff --git a/pallets/subtensor/src/guards/check_coldkey_swap.rs b/pallets/subtensor/src/guards/check_coldkey_swap.rs index 5f124be219..907fed1d3b 100644 --- a/pallets/subtensor/src/guards/check_coldkey_swap.rs +++ b/pallets/subtensor/src/guards/check_coldkey_swap.rs @@ -1,3 +1,4 @@ +use super::{CallOf, DispatchableOriginOf}; use crate::weights::WeightInfo; use crate::{Call, ColdkeySwapAnnouncements, ColdkeySwapDisputes, Config, Error}; use frame_support::{ @@ -8,9 +9,6 @@ use frame_support::{ use sp_runtime::traits::Dispatchable; use sp_std::marker::PhantomData; -type CallOf = ::RuntimeCall; -type DispatchableOriginOf = as Dispatchable>::RuntimeOrigin; - /// Dispatch extension that blocks most calls when a coldkey swap is active. /// /// When a coldkey swap has been announced for the signing account: @@ -96,9 +94,14 @@ where #[allow(clippy::expect_used, clippy::unwrap_used)] mod tests { use super::CheckColdkeySwap; - use crate::{ColdkeySwapAnnouncements, ColdkeySwapDisputes, Error, tests::mock::*}; + use crate::{ + ColdkeySwapAnnouncements, ColdkeySwapDisputes, Error, tests::mock::*, + weights::WeightInfo as _, + }; use frame_support::{ - BoundedVec, assert_ok, dispatch::DispatchResultWithPostInfo, traits::ExtendedDispatchable, + BoundedVec, assert_ok, + dispatch::{DispatchExtension, DispatchResultWithPostInfo}, + traits::ExtendedDispatchable, }; use frame_system::Call as SystemCall; use pallet_subtensor_proxy::Call as ProxyCall; @@ -176,6 +179,18 @@ mod tests { ) } + #[test] + fn weight_charges_all_calls_because_swap_state_can_block_any_signed_call() { + let expected = ::WeightInfo::check_coldkey_swap_extension(); + + for call in forbidden_calls().into_iter().chain(authorized_calls()) { + assert_eq!( + as DispatchExtension>::weight(&call), + expected + ); + } + } + #[test] fn no_active_swap_allows_calls() { new_test_ext(1).execute_with(|| { diff --git a/pallets/subtensor/src/guards/check_delegate_take.rs b/pallets/subtensor/src/guards/check_delegate_take.rs index c9f54d4cb5..c80d969afc 100644 --- a/pallets/subtensor/src/guards/check_delegate_take.rs +++ b/pallets/subtensor/src/guards/check_delegate_take.rs @@ -1,3 +1,4 @@ +use super::{CallOf, DispatchableOriginOf, applicable_call}; use crate::weights::WeightInfo; use crate::{Call, Config, Error, Pallet}; use frame_support::{ @@ -8,9 +9,6 @@ use frame_support::{ use sp_runtime::traits::Dispatchable; use sp_std::marker::PhantomData; -type CallOf = ::RuntimeCall; -type DispatchableOriginOf = as Dispatchable>::RuntimeOrigin; - /// Dispatch extension for delegate-take bounds and ownership preconditions. /// /// Signed increase/decrease take calls are checked before dispatch; unrelated @@ -18,6 +16,13 @@ type DispatchableOriginOf = as Dispatchable>::RuntimeOrigin; pub struct CheckDelegateTake(PhantomData); impl CheckDelegateTake { + pub(crate) fn applies_to(call: &Call) -> bool { + matches!( + call, + Call::increase_take { .. } | Call::decrease_take { .. } + ) + } + pub fn check(who: &T::AccountId, call: &Call) -> Result<(), Error> { match call { Call::increase_take { hotkey, take } | Call::decrease_take { hotkey, take } => { @@ -42,8 +47,10 @@ where { type Pre = (); - fn weight(_call: &CallOf) -> Weight { - ::WeightInfo::check_delegate_take_extension() + fn weight(call: &CallOf) -> Weight { + applicable_call(call, Self::applies_to) + .map(|_| ::WeightInfo::check_delegate_take_extension()) + .unwrap_or(Weight::zero()) } fn pre_dispatch( @@ -54,7 +61,7 @@ where return Ok(()); }; - let Some(call) = call.is_sub_type() else { + let Some(call) = applicable_call(call, Self::applies_to) else { return Ok(()); }; @@ -68,7 +75,10 @@ mod tests { use super::*; use crate::{Error, tests::mock::*}; use frame_support::{ - assert_ok, dispatch::DispatchResultWithPostInfo, traits::ExtendedDispatchable, + assert_ok, + dispatch::{DispatchExtension, DispatchResultWithPostInfo}, + traits::ExtendedDispatchable, + weights::Weight, }; use sp_core::U256; use sp_runtime::DispatchError; @@ -91,6 +101,39 @@ mod tests { result.unwrap_err().error } + fn add_stake_call() -> RuntimeCall { + RuntimeCall::SubtensorModule(SubtensorCall::add_stake { + hotkey: U256::from(1), + netuid: 1u16.into(), + amount_staked: 1_000u64.into(), + }) + } + + #[test] + fn weight_only_charges_delegate_take_calls() { + let expected = ::WeightInfo::check_delegate_take_extension(); + + for call in [ + RuntimeCall::System(frame_system::Call::remark { remark: vec![] }), + add_stake_call(), + ] { + assert_eq!( + as DispatchExtension>::weight(&call), + Weight::zero() + ); + } + + for call in [ + increase_take_call(U256::from(1), 0), + decrease_take_call(U256::from(1), 0), + ] { + assert_eq!( + as DispatchExtension>::weight(&call), + expected + ); + } + } + #[test] fn accepts_owner_with_valid_take() { new_test_ext(0).execute_with(|| { diff --git a/pallets/subtensor/src/guards/check_evm_key_association.rs b/pallets/subtensor/src/guards/check_evm_key_association.rs index d7e2847e99..d9b69e1a7d 100644 --- a/pallets/subtensor/src/guards/check_evm_key_association.rs +++ b/pallets/subtensor/src/guards/check_evm_key_association.rs @@ -1,3 +1,4 @@ +use super::{CallOf, DispatchableOriginOf, applicable_call}; use crate::weights::WeightInfo; use crate::{Call, Config, Error, Pallet}; use frame_support::{ @@ -8,9 +9,6 @@ use frame_support::{ use sp_runtime::traits::Dispatchable; use sp_std::marker::PhantomData; -type CallOf = ::RuntimeCall; -type DispatchableOriginOf = as Dispatchable>::RuntimeOrigin; - /// Dispatch extension for EVM-key association preconditions. /// /// Signed EVM-key association calls are checked for subnet registration and @@ -18,6 +16,10 @@ type DispatchableOriginOf = as Dispatchable>::RuntimeOrigin; pub struct CheckEvmKeyAssociation(PhantomData); impl CheckEvmKeyAssociation { + pub(crate) fn applies_to(call: &Call) -> bool { + matches!(call, Call::associate_evm_key { .. }) + } + pub fn check(who: &T::AccountId, call: &Call) -> Result<(), Error> { match call { Call::associate_evm_key { netuid, .. } => { @@ -40,8 +42,10 @@ where { type Pre = (); - fn weight(_call: &CallOf) -> Weight { - ::WeightInfo::check_evm_key_association_extension() + fn weight(call: &CallOf) -> Weight { + applicable_call(call, Self::applies_to) + .map(|_| ::WeightInfo::check_evm_key_association_extension()) + .unwrap_or(Weight::zero()) } fn pre_dispatch( @@ -52,7 +56,7 @@ where return Ok(()); }; - let Some(call) = call.is_sub_type() else { + let Some(call) = applicable_call(call, Self::applies_to) else { return Ok(()); }; @@ -64,10 +68,13 @@ where #[allow(clippy::unwrap_used, clippy::arithmetic_side_effects)] mod tests { use super::CheckEvmKeyAssociation; - use crate::{AssociatedEvmAddress, Error, tests::mock::*}; + use crate::{AssociatedEvmAddress, Error, tests::mock::*, weights::WeightInfo as _}; use codec::Encode; use frame_support::{ - assert_ok, dispatch::DispatchResultWithPostInfo, traits::ExtendedDispatchable, + assert_ok, + dispatch::{DispatchExtension, DispatchResultWithPostInfo}, + traits::ExtendedDispatchable, + weights::Weight, }; use frame_system::Call as SystemCall; use sp_core::{H160, Pair, U256, ecdsa, keccak_256}; @@ -139,6 +146,37 @@ mod tests { ) } + fn add_stake_call() -> RuntimeCall { + RuntimeCall::SubtensorModule(SubtensorCall::add_stake { + hotkey: U256::from(1), + netuid: 1u16.into(), + amount_staked: 1_000u64.into(), + }) + } + + #[test] + fn weight_only_charges_evm_key_association_calls() { + let netuid = NetUid::from(1); + let expected = ::WeightInfo::check_evm_key_association_extension(); + + for call in [ + RuntimeCall::System(SystemCall::remark { remark: vec![] }), + add_stake_call(), + ] { + assert_eq!( + as DispatchExtension>::weight(&call), + Weight::zero() + ); + } + + assert_eq!( + as DispatchExtension>::weight( + &dummy_associate_call(netuid) + ), + expected + ); + } + #[test] fn unrelated_calls_pass_through() { new_test_ext(0).execute_with(|| { diff --git a/pallets/subtensor/src/guards/check_rate_limits.rs b/pallets/subtensor/src/guards/check_rate_limits.rs index d2c021dd5d..e12c9d064b 100644 --- a/pallets/subtensor/src/guards/check_rate_limits.rs +++ b/pallets/subtensor/src/guards/check_rate_limits.rs @@ -1,3 +1,4 @@ +use super::{CallOf, DispatchableOriginOf, applicable_call}; use crate::weights::WeightInfo; use crate::{Call, Config, Error, Pallet, TransactionType}; use frame_support::{ @@ -9,9 +10,6 @@ use sp_runtime::traits::Dispatchable; use sp_std::marker::PhantomData; use subtensor_runtime_common::{NetUid, NetUidStorageIndex}; -type CallOf = ::RuntimeCall; -type DispatchableOriginOf = as Dispatchable>::RuntimeOrigin; - /// Dispatch extension for rate-limit checks that are safe to reject before dispatch. /// /// Signed weight and network-registration calls are checked before dispatch; @@ -19,6 +17,17 @@ type DispatchableOriginOf = as Dispatchable>::RuntimeOrigin; pub struct CheckRateLimits(PhantomData); impl CheckRateLimits { + pub(crate) fn applies_to(call: &Call) -> bool { + matches!( + call, + Call::commit_weights { .. } + | Call::commit_mechanism_weights { .. } + | Call::set_weights { .. } + | Call::set_mechanism_weights { .. } + | Call::register_network { .. } + ) + } + fn check_weights_rate_limit( who: &T::AccountId, netuid: NetUid, @@ -89,8 +98,10 @@ where { type Pre = (); - fn weight(_call: &CallOf) -> Weight { - ::WeightInfo::check_rate_limits_extension() + fn weight(call: &CallOf) -> Weight { + applicable_call(call, Self::applies_to) + .map(|_| ::WeightInfo::check_rate_limits_extension()) + .unwrap_or(Weight::zero()) } fn pre_dispatch( @@ -101,7 +112,7 @@ where return Ok(()); }; - let Some(call) = call.is_sub_type() else { + let Some(call) = applicable_call(call, Self::applies_to) else { return Ok(()); }; @@ -113,9 +124,12 @@ where #[allow(clippy::unwrap_used)] mod tests { use super::CheckRateLimits; - use crate::{Error, tests::mock::*}; + use crate::{Error, tests::mock::*, weights::WeightInfo as _}; use frame_support::{ - assert_ok, dispatch::DispatchResultWithPostInfo, traits::ExtendedDispatchable, + assert_ok, + dispatch::{DispatchExtension, DispatchResultWithPostInfo}, + traits::ExtendedDispatchable, + weights::Weight, }; use frame_system::Call as SystemCall; use sp_core::U256; @@ -155,6 +169,57 @@ mod tests { add_balance_to_coldkey_account(&coldkey, amount); } + fn add_stake_call() -> RuntimeCall { + RuntimeCall::SubtensorModule(SubtensorCall::add_stake { + hotkey: U256::from(1), + netuid: 1u16.into(), + amount_staked: 1_000u64.into(), + }) + } + + #[test] + fn weight_only_charges_rate_limited_calls() { + let netuid = NetUid::from(1); + let expected = ::WeightInfo::check_rate_limits_extension(); + let charged_calls = [ + RuntimeCall::SubtensorModule(SubtensorCall::commit_weights { + netuid, + commit_hash: sp_core::H256::zero(), + }), + RuntimeCall::SubtensorModule(SubtensorCall::commit_mechanism_weights { + netuid, + mecid: MechId::MAIN, + commit_hash: sp_core::H256::zero(), + }), + set_weights_call(netuid, 0), + RuntimeCall::SubtensorModule(SubtensorCall::set_mechanism_weights { + netuid, + mecid: MechId::MAIN, + dests: vec![0], + weights: vec![1], + version_key: 0, + }), + register_network_call(U256::from(1)), + ]; + + for call in [ + RuntimeCall::System(SystemCall::remark { remark: vec![] }), + add_stake_call(), + ] { + assert_eq!( + as DispatchExtension>::weight(&call), + Weight::zero() + ); + } + + for call in charged_calls { + assert_eq!( + as DispatchExtension>::weight(&call), + expected + ); + } + } + #[test] fn unrelated_calls_pass_through() { new_test_ext(0).execute_with(|| { diff --git a/pallets/subtensor/src/guards/check_serving_endpoints.rs b/pallets/subtensor/src/guards/check_serving_endpoints.rs index 46304d337f..f8b2da64ed 100644 --- a/pallets/subtensor/src/guards/check_serving_endpoints.rs +++ b/pallets/subtensor/src/guards/check_serving_endpoints.rs @@ -1,3 +1,4 @@ +use super::{CallOf, DispatchableOriginOf, applicable_call}; use crate::weights::WeightInfo; use crate::{Call, Config, Error, Pallet}; use frame_support::{ @@ -8,9 +9,6 @@ use frame_support::{ use sp_runtime::traits::Dispatchable; use sp_std::marker::PhantomData; -type CallOf = ::RuntimeCall; -type DispatchableOriginOf = as Dispatchable>::RuntimeOrigin; - /// Dispatch extension for axon/prometheus endpoint validation. /// /// Signed serving calls are checked before dispatch; unrelated calls and @@ -18,6 +16,13 @@ type DispatchableOriginOf = as Dispatchable>::RuntimeOrigin; pub struct CheckServingEndpoints(PhantomData); impl CheckServingEndpoints { + pub(crate) fn applies_to(call: &Call) -> bool { + matches!( + call, + Call::serve_axon { .. } | Call::serve_axon_tls { .. } | Call::serve_prometheus { .. } + ) + } + pub fn check(who: &T::AccountId, call: &Call) -> Result<(), Error> { match call { Call::serve_axon { @@ -74,8 +79,10 @@ where { type Pre = (); - fn weight(_call: &CallOf) -> Weight { - ::WeightInfo::check_serving_endpoints_extension() + fn weight(call: &CallOf) -> Weight { + applicable_call(call, Self::applies_to) + .map(|_| ::WeightInfo::check_serving_endpoints_extension()) + .unwrap_or(Weight::zero()) } fn pre_dispatch( @@ -86,7 +93,7 @@ where return Ok(()); }; - let Some(call) = call.is_sub_type() else { + let Some(call) = applicable_call(call, Self::applies_to) else { return Ok(()); }; @@ -98,9 +105,12 @@ where #[allow(clippy::unwrap_used)] mod tests { use super::CheckServingEndpoints; - use crate::{Error, tests::mock::*}; + use crate::{Error, tests::mock::*, weights::WeightInfo as _}; use frame_support::{ - assert_ok, dispatch::DispatchResultWithPostInfo, traits::ExtendedDispatchable, + assert_ok, + dispatch::{DispatchExtension, DispatchResultWithPostInfo}, + traits::ExtendedDispatchable, + weights::Weight, }; use frame_system::Call as SystemCall; use sp_core::U256; @@ -160,6 +170,41 @@ mod tests { register_ok_neuron(netuid, hotkey, coldkey, 0); } + fn add_stake_call() -> RuntimeCall { + RuntimeCall::SubtensorModule(SubtensorCall::add_stake { + hotkey: U256::from(1), + netuid: 1u16.into(), + amount_staked: 1_000u64.into(), + }) + } + + #[test] + fn weight_only_charges_serving_endpoint_calls() { + let netuid = NetUid::from(1); + let expected = ::WeightInfo::check_serving_endpoints_extension(); + + for call in [ + RuntimeCall::System(SystemCall::remark { remark: vec![] }), + add_stake_call(), + ] { + assert_eq!( + as DispatchExtension>::weight(&call), + Weight::zero() + ); + } + + for call in [ + serve_axon_call(netuid), + serve_axon_tls_call(netuid), + serve_prometheus_call(netuid), + ] { + assert_eq!( + as DispatchExtension>::weight(&call), + expected + ); + } + } + #[test] fn unrelated_calls_pass_through() { new_test_ext(0).execute_with(|| { diff --git a/pallets/subtensor/src/guards/check_weights.rs b/pallets/subtensor/src/guards/check_weights.rs index d10e7b8b8c..c116071ba6 100644 --- a/pallets/subtensor/src/guards/check_weights.rs +++ b/pallets/subtensor/src/guards/check_weights.rs @@ -1,3 +1,4 @@ +use super::{CallOf, DispatchableOriginOf, applicable_call}; use crate::weights::WeightInfo; use crate::{Call, Config, Error, Pallet, WeightCommits}; use frame_support::{ @@ -10,8 +11,6 @@ use sp_runtime::traits::Dispatchable; use sp_std::{collections::vec_deque::VecDeque, marker::PhantomData, vec::Vec}; use subtensor_runtime_common::{NetUid, NetUidStorageIndex}; -type CallOf = ::RuntimeCall; -type DispatchableOriginOf = as Dispatchable>::RuntimeOrigin; type WeightCommitQueue = VecDeque<(H256, u64, u64, u64)>; /// Dispatch extension for weight-setting preconditions. @@ -21,6 +20,24 @@ type WeightCommitQueue = VecDeque<(H256, u64, u64, u64)>; pub struct CheckWeights(PhantomData); impl CheckWeights { + pub(crate) fn applies_to(call: &Call) -> bool { + matches!( + call, + Call::batch_commit_weights { .. } + | Call::batch_reveal_weights { .. } + | Call::batch_set_weights { .. } + | Call::commit_weights { .. } + | Call::commit_mechanism_weights { .. } + | Call::reveal_weights { .. } + | Call::reveal_mechanism_weights { .. } + | Call::set_weights { .. } + | Call::set_mechanism_weights { .. } + | Call::commit_timelocked_weights { .. } + | Call::commit_timelocked_mechanism_weights { .. } + | Call::commit_crv3_mechanism_weights { .. } + ) + } + pub fn check(who: &T::AccountId, call: &Call) -> Result<(), Error> { Self::check_input_lengths(call)?; Self::check_min_stake(who, call)?; @@ -227,8 +244,10 @@ where { type Pre = (); - fn weight(_call: &CallOf) -> Weight { - ::WeightInfo::check_weights_extension() + fn weight(call: &CallOf) -> Weight { + applicable_call(call, Self::applies_to) + .map(|_| ::WeightInfo::check_weights_extension()) + .unwrap_or(Weight::zero()) } fn pre_dispatch( @@ -239,7 +258,7 @@ where return Ok(()); }; - let Some(call) = call.is_sub_type() else { + let Some(call) = applicable_call(call, Self::applies_to) else { return Ok(()); }; @@ -251,11 +270,14 @@ where #[allow(clippy::unwrap_used)] mod tests { use super::CheckWeights; - use crate::{Error, MAX_CRV3_COMMIT_SIZE_BYTES, tests::mock::*}; + use crate::{Error, MAX_CRV3_COMMIT_SIZE_BYTES, tests::mock::*, weights::WeightInfo as _}; use codec::Compact; use frame_support::{ - BoundedVec, assert_ok, dispatch::DispatchResultWithPostInfo, traits::ConstU32, + BoundedVec, assert_ok, + dispatch::{DispatchExtension, DispatchResultWithPostInfo}, + traits::ConstU32, traits::ExtendedDispatchable, + weights::Weight, }; use frame_system::Call as SystemCall; use pallet_drand::LastStoredRound; @@ -309,6 +331,99 @@ mod tests { }) } + fn add_stake_call() -> RuntimeCall { + RuntimeCall::SubtensorModule(SubtensorCall::add_stake { + hotkey: U256::from(1), + netuid: 1u16.into(), + amount_staked: 1_000u64.into(), + }) + } + + fn checked_weight_calls(netuid: NetUid) -> Vec { + let bounded_commit = + BoundedVec::>::try_from(vec![0]).unwrap(); + + vec![ + set_weights_call(netuid, 0), + RuntimeCall::SubtensorModule(SubtensorCall::set_mechanism_weights { + netuid, + mecid: MechId::MAIN, + dests: vec![0], + weights: vec![1], + version_key: 0, + }), + RuntimeCall::SubtensorModule(SubtensorCall::batch_set_weights { + netuids: vec![Compact(netuid)], + weights: vec![vec![(Compact(0_u16), Compact(1_u16))]], + version_keys: vec![Compact(0_u64)], + }), + RuntimeCall::SubtensorModule(SubtensorCall::commit_weights { + netuid, + commit_hash: H256::zero(), + }), + RuntimeCall::SubtensorModule(SubtensorCall::commit_mechanism_weights { + netuid, + mecid: MechId::MAIN, + commit_hash: H256::zero(), + }), + RuntimeCall::SubtensorModule(SubtensorCall::batch_commit_weights { + netuids: vec![Compact(netuid)], + commit_hashes: vec![H256::zero()], + }), + reveal_weights_call(netuid), + reveal_mechanism_weights_call(netuid, MechId::MAIN), + RuntimeCall::SubtensorModule(SubtensorCall::batch_reveal_weights { + netuid, + uids_list: vec![vec![0]], + values_list: vec![vec![1]], + salts_list: vec![vec![1]], + version_keys: vec![0], + }), + RuntimeCall::SubtensorModule(SubtensorCall::commit_timelocked_weights { + netuid, + commit: bounded_commit.clone(), + reveal_round: 0, + commit_reveal_version: 0, + }), + RuntimeCall::SubtensorModule(SubtensorCall::commit_timelocked_mechanism_weights { + netuid, + mecid: MechId::MAIN, + commit: bounded_commit.clone(), + reveal_round: 0, + commit_reveal_version: 0, + }), + RuntimeCall::SubtensorModule(SubtensorCall::commit_crv3_mechanism_weights { + netuid, + mecid: MechId::MAIN, + commit: bounded_commit, + reveal_round: 0, + }), + ] + } + + #[test] + fn weight_only_charges_weight_related_calls() { + let netuid = NetUid::from(1); + let expected = ::WeightInfo::check_weights_extension(); + + for call in [ + RuntimeCall::System(SystemCall::remark { remark: vec![] }), + add_stake_call(), + ] { + assert_eq!( + as DispatchExtension>::weight(&call), + Weight::zero() + ); + } + + for call in checked_weight_calls(netuid) { + assert_eq!( + as DispatchExtension>::weight(&call), + expected + ); + } + } + #[test] fn unrelated_calls_pass_through() { new_test_ext(0).execute_with(|| { @@ -360,72 +475,13 @@ mod tests { let netuid = NetUid::from(1); let hotkey = U256::from(1); let coldkey = U256::from(2); - let bounded_commit = - BoundedVec::>::try_from(vec![0]).unwrap(); add_network_disable_commit_reveal(netuid, 1, 0); setup_reserves(netuid, DEFAULT_RESERVE.into(), DEFAULT_RESERVE.into()); SubtensorModule::append_neuron(netuid, &hotkey, 0); crate::Owner::::insert(hotkey, coldkey); SubtensorModule::set_stake_threshold(1_000_000_000_000_u64); - let calls = [ - set_weights_call(netuid, 0), - RuntimeCall::SubtensorModule(SubtensorCall::set_mechanism_weights { - netuid, - mecid: MechId::MAIN, - dests: vec![0], - weights: vec![1], - version_key: 0, - }), - RuntimeCall::SubtensorModule(SubtensorCall::batch_set_weights { - netuids: vec![Compact(netuid)], - weights: vec![vec![(Compact(0_u16), Compact(1_u16))]], - version_keys: vec![Compact(0_u64)], - }), - RuntimeCall::SubtensorModule(SubtensorCall::commit_weights { - netuid, - commit_hash: H256::zero(), - }), - RuntimeCall::SubtensorModule(SubtensorCall::commit_mechanism_weights { - netuid, - mecid: MechId::MAIN, - commit_hash: H256::zero(), - }), - RuntimeCall::SubtensorModule(SubtensorCall::batch_commit_weights { - netuids: vec![Compact(netuid)], - commit_hashes: vec![H256::zero()], - }), - reveal_weights_call(netuid), - reveal_mechanism_weights_call(netuid, MechId::MAIN), - RuntimeCall::SubtensorModule(SubtensorCall::batch_reveal_weights { - netuid, - uids_list: vec![vec![0]], - values_list: vec![vec![1]], - salts_list: vec![vec![1]], - version_keys: vec![0], - }), - RuntimeCall::SubtensorModule(SubtensorCall::commit_timelocked_weights { - netuid, - commit: bounded_commit.clone(), - reveal_round: 0, - commit_reveal_version: 0, - }), - RuntimeCall::SubtensorModule(SubtensorCall::commit_timelocked_mechanism_weights { - netuid, - mecid: MechId::MAIN, - commit: bounded_commit.clone(), - reveal_round: 0, - commit_reveal_version: 0, - }), - RuntimeCall::SubtensorModule(SubtensorCall::commit_crv3_mechanism_weights { - netuid, - mecid: MechId::MAIN, - commit: bounded_commit, - reveal_round: 0, - }), - ]; - - for call in calls { + for call in checked_weight_calls(netuid) { assert_eq!( err(dispatch_with_ext(call, RuntimeOrigin::signed(hotkey))), Error::::NotEnoughStakeToSetWeights.into() diff --git a/pallets/subtensor/src/guards/mod.rs b/pallets/subtensor/src/guards/mod.rs index 485fc65a04..3865352858 100644 --- a/pallets/subtensor/src/guards/mod.rs +++ b/pallets/subtensor/src/guards/mod.rs @@ -5,9 +5,28 @@ mod check_rate_limits; mod check_serving_endpoints; mod check_weights; +use crate::{Call, Config}; +use frame_support::traits::IsSubType; +use sp_runtime::traits::Dispatchable; + pub use check_coldkey_swap::*; pub use check_delegate_take::*; pub use check_evm_key_association::*; pub use check_rate_limits::*; pub use check_serving_endpoints::*; pub use check_weights::*; + +pub(crate) type CallOf = ::RuntimeCall; +pub(crate) type DispatchableOriginOf = as Dispatchable>::RuntimeOrigin; + +pub(crate) fn applicable_call( + call: &CallOf, + applies_to: impl FnOnce(&Call) -> bool, +) -> Option<&Call> +where + T: Config, + CallOf: IsSubType>, +{ + let call = call.is_sub_type()?; + applies_to(call).then_some(call) +} From ebe1ac631e04498b3e295ac87030e0c68f566585 Mon Sep 17 00:00:00 2001 From: Loris Moulin Date: Mon, 29 Jun 2026 13:34:53 -0300 Subject: [PATCH 14/24] Apply subtensor extension conditionally --- pallets/subtensor/src/extensions/subtensor.rs | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/pallets/subtensor/src/extensions/subtensor.rs b/pallets/subtensor/src/extensions/subtensor.rs index ea91c87c6e..7899ed855e 100644 --- a/pallets/subtensor/src/extensions/subtensor.rs +++ b/pallets/subtensor/src/extensions/subtensor.rs @@ -1,6 +1,6 @@ use crate::{ Call, CheckColdkeySwap, CheckDelegateTake, CheckEvmKeyAssociation, CheckRateLimits, - CheckServingEndpoints, CheckWeights, Config, Error, + CheckServingEndpoints, CheckWeights, Config, Error, guards::applicable_call, }; use codec::{Decode, DecodeWithMemTracking, Encode}; use frame_support::{ @@ -89,15 +89,23 @@ impl SubtensorTransactionExtension { CheckColdkeySwap::::check(who, call)?; - let Some(call) = call.is_sub_type() else { - return Ok(()); - }; + if let Some(call) = applicable_call(call, CheckWeights::::applies_to) { + CheckWeights::::check(who, call)?; + } + if let Some(call) = applicable_call(call, CheckRateLimits::::applies_to) { + CheckRateLimits::::check(who, call)?; + } + if let Some(call) = applicable_call(call, CheckDelegateTake::::applies_to) { + CheckDelegateTake::::check(who, call)?; + } + if let Some(call) = applicable_call(call, CheckServingEndpoints::::applies_to) { + CheckServingEndpoints::::check(who, call)?; + } + if let Some(call) = applicable_call(call, CheckEvmKeyAssociation::::applies_to) { + CheckEvmKeyAssociation::::check(who, call)?; + } - CheckWeights::::check(who, call)?; - CheckRateLimits::::check(who, call)?; - CheckDelegateTake::::check(who, call)?; - CheckServingEndpoints::::check(who, call)?; - CheckEvmKeyAssociation::::check(who, call) + Ok(()) } } From 9759f677d2b05d7f996ea272a7fd78cb1c6b261b Mon Sep 17 00:00:00 2001 From: yeuyeuh Date: Mon, 29 Jun 2026 23:16:17 +0200 Subject: [PATCH 15/24] Add eco-tests protecting Mentat indexer storage reads --- eco-tests/src/lib.rs | 3 + eco-tests/src/mock.rs | 37 +- eco-tests/src/tests_mentat_indexer.rs | 612 ++++++++++++++++++++++++++ 3 files changed, 650 insertions(+), 2 deletions(-) create mode 100644 eco-tests/src/tests_mentat_indexer.rs diff --git a/eco-tests/src/lib.rs b/eco-tests/src/lib.rs index 980de0f3da..4613d7dbe2 100644 --- a/eco-tests/src/lib.rs +++ b/eco-tests/src/lib.rs @@ -7,3 +7,6 @@ mod tests; #[cfg(test)] mod tests_taocom_indexer; + +#[cfg(test)] +mod tests_mentat_indexer; diff --git a/eco-tests/src/mock.rs b/eco-tests/src/mock.rs index 5ce8bc9b6b..a3a8ce1e39 100644 --- a/eco-tests/src/mock.rs +++ b/eco-tests/src/mock.rs @@ -612,11 +612,19 @@ pub fn add_balance_to_coldkey_account(coldkey: &U256, tao: TaoBalance) { mod api_mocks { use codec::Compact; use pallet_subtensor::rpc_info::delegate_info::DelegateInfo; + use pallet_subtensor::rpc_info::dynamic_info::DynamicInfo; + use pallet_subtensor::rpc_info::metagraph::{Metagraph, SelectiveMetagraph}; + use pallet_subtensor::rpc_info::show_subnet::SubnetState; use pallet_subtensor::rpc_info::stake_info::StakeInfo; + use pallet_subtensor::rpc_info::subnet_info::{ + SubnetHyperparams, SubnetHyperparamsV2, SubnetHyperparamsV3, SubnetInfo, SubnetInfov2, + }; use pallet_subtensor_swap_runtime_api::{SimSwapResult, SubnetPrice, SwapRuntimeApi}; use sp_runtime::AccountId32; - use subtensor_custom_rpc_runtime_api::{DelegateInfoRuntimeApi, StakeInfoRuntimeApi}; - use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance}; + use subtensor_custom_rpc_runtime_api::{ + DelegateInfoRuntimeApi, StakeInfoRuntimeApi, SubnetInfoRuntimeApi, + }; + use subtensor_runtime_common::{AlphaBalance, MechId, NetUid, TaoBalance}; use super::Block; @@ -660,6 +668,31 @@ mod api_mocks { } } + impl SubnetInfoRuntimeApi for MockApi { + fn get_subnet_info(_netuid: NetUid) -> Option> { None } + fn get_subnets_info() -> Vec>> { Vec::new() } + fn get_subnet_info_v2(_netuid: NetUid) -> Option> { None } + fn get_subnets_info_v2() -> Vec>> { Vec::new() } + #[allow(deprecated)] + fn get_subnet_hyperparams(_netuid: NetUid) -> Option { None } + #[allow(deprecated)] + fn get_subnet_hyperparams_v2(_netuid: NetUid) -> Option { None } + fn get_subnet_hyperparams_v3(_netuid: NetUid) -> Option { None } + fn get_all_dynamic_info() -> Vec>> { Vec::new() } + fn get_all_metagraphs() -> Vec>> { Vec::new() } + fn get_metagraph(_netuid: NetUid) -> Option> { None } + fn get_all_mechagraphs() -> Vec>> { Vec::new() } + fn get_mechagraph(_netuid: NetUid, _mecid: MechId) -> Option> { None } + fn get_dynamic_info(_netuid: NetUid) -> Option> { None } + fn get_subnet_state(_netuid: NetUid) -> Option> { None } + fn get_selective_metagraph(_netuid: NetUid, _metagraph_indexes: Vec) -> Option> { None } + fn get_coldkey_auto_stake_hotkey(_coldkey: AccountId32, _netuid: NetUid) -> Option { None } + fn get_selective_mechagraph(_netuid: NetUid, _subid: MechId, _metagraph_indexes: Vec) -> Option> { None } + fn get_subnet_to_prune() -> Option { None } + fn get_subnet_account_id(_netuid: NetUid) -> Option { None } + fn get_next_epoch_start_block(_netuid: NetUid) -> Option { None } + } + impl SwapRuntimeApi for MockApi { fn current_alpha_price(_netuid: NetUid) -> u64 { 0 } fn current_alpha_price_all() -> Vec { Vec::new() } diff --git a/eco-tests/src/tests_mentat_indexer.rs b/eco-tests/src/tests_mentat_indexer.rs new file mode 100644 index 0000000000..0968100a35 --- /dev/null +++ b/eco-tests/src/tests_mentat_indexer.rs @@ -0,0 +1,612 @@ +//! Indexer-contract tests for Mentat +//! Any modification in these tests will notify the member responsible +//! for the communication between protocol and the Mentat team. + + +#![allow(clippy::unwrap_used)] +#![allow(clippy::arithmetic_side_effects)] + +use frame_system as system; +use pallet_subtensor::*; +use pallet_subtensor_proxy::{Proxies, RealPaysFee}; +use pallet_subtensor_swap::FeeRate; +use pallet_subtensor_swap_runtime_api::SwapRuntimeApi; +use sp_core::U256; +use sp_runtime::traits::Block as BlockT; +use substrate_fixed::types::{I96F32, U64F64}; +use subtensor_custom_rpc_runtime_api::{StakeInfoRuntimeApi, SubnetInfoRuntimeApi}; +use subtensor_runtime_common::{AlphaBalance, MechId, NetUid, NetUidStorageIndex, TaoBalance}; + +use super::helpers::*; +use super::mock::*; + +// --------------------------------------------------------------------------- +// Storage queries — SubtensorModule +// --------------------------------------------------------------------------- + +#[test] +fn indexer_hotkey_ownership() { + new_test_ext(1).execute_with(|| { + let hotkey = U256::from(1); + + let _: U256 = Owner::::get(hotkey); + }); +} + +#[test] +fn indexer_staking_hotkeys() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + + let _: Vec = StakingHotkeys::::get(coldkey); + }); +} + +#[test] +fn indexer_alpha_shares_and_stake() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + let hotkey = U256::from(1); + let coldkey = U256::from(2); + + let _: AlphaBalance = TotalHotkeyAlpha::::get(hotkey, netuid); + let _: U64F64 = Alpha::::get((hotkey, coldkey, netuid)); + }); +} + +#[test] +fn indexer_subnet_pool_data() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + + let _: u16 = TotalNetworks::::get(); + let _: TaoBalance = SubnetTAO::::get(netuid); + let _: AlphaBalance = SubnetAlphaIn::::get(netuid); + let _: AlphaBalance = SubnetAlphaOut::::get(netuid); + let _: I96F32 = SubnetMovingPrice::::get(netuid); + }); +} + +#[test] +fn indexer_subnet_metadata() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + + let _: MechId = MechanismCountCurrent::::get(netuid); + let _: u64 = NetworkImmunityPeriod::::get(); + let _: u64 = NetworkRegisteredAt::::get(netuid); + let _: Option = SubnetIdentitiesV3::::get(netuid); + }); +} + +#[test] +fn indexer_neuron_per_subnet_vectors() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + + let _: Vec = Active::::get(netuid); + let _: Vec = Dividends::::get(netuid); + let _: Vec = Emission::::get(netuid); + let _: Vec = Incentive::::get(NetUidStorageIndex::from(netuid)); + let _: Vec = ValidatorTrust::::get(netuid); + }); +} + +#[test] +fn indexer_neuron_uid_maps() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + let uid: u16 = 0; + + let _: u16 = SubnetworkN::::get(netuid); + let _: U256 = Keys::::get(netuid, uid); + }); +} + +#[test] +fn indexer_childkey_and_parentkey_graph() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + let hotkey = U256::from(1); + + let _: Vec<(u64, U256)> = ChildKeys::::get(hotkey, netuid); + let _: Vec<(u64, U256)> = ParentKeys::::get(hotkey, netuid); + let _: u16 = ChildkeyTake::::get(hotkey, netuid); + }); +} + +#[test] +fn indexer_subnet_hyperparams() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + + let _: u16 = Tempo::::get(netuid); + let _: u16 = MaxAllowedUids::::get(netuid); + }); +} + +#[test] +fn indexer_validator_dividends() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + let hotkey = U256::from(1); + + let _: AlphaBalance = AlphaDividendsPerSubnet::::get(netuid, hotkey); + }); +} + +#[test] +fn indexer_network_economics() { + new_test_ext(1).execute_with(|| { + let _: u64 = TaoWeight::::get(); + }); +} + +#[test] +fn indexer_swap_fee_rate() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + + let _: u16 = FeeRate::::get(netuid); + }); +} + +#[test] +fn indexer_mechanism_emission() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + + let _: MechId = MechanismCountCurrent::::get(netuid); + let _: Option> = MechanismEmissionSplit::::get(netuid); + }); +} + +#[test] +fn indexer_root_claim_type() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + + let _: RootClaimTypeEnum = RootClaimType::::get(coldkey); + }); +} + +#[test] +fn indexer_pending_childkey_cooldown() { + new_test_ext(1).execute_with(|| { + let _: u64 = PendingChildKeyCooldown::::get(); + }); +} + +#[test] +fn indexer_root_alpha_dividends_per_subnet() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + let hotkey = U256::from(1); + + let _: AlphaBalance = RootAlphaDividendsPerSubnet::::get(netuid, hotkey); + }); +} + +// --------------------------------------------------------------------------- +// Storage queries — proxy pallet +// --------------------------------------------------------------------------- + +#[test] +fn indexer_proxy_proxies() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + + let _ = Proxies::::get(coldkey); + }); +} + +#[test] +fn indexer_proxy_real_pays_fee() { + new_test_ext(1).execute_with(|| { + let real = U256::from(1); + let delegate = U256::from(2); + + let _: Option<()> = RealPaysFee::::get(real, delegate); + }); +} + +// --------------------------------------------------------------------------- +// Extrinsics — SubtensorModule (call signatures) +// +// These run inside externalities and the Result is intentionally ignored: +// the call will typically return Err (no funds / no network), which is fine. +// We only lock that the dispatchable's argument list and types are unchanged. +// --------------------------------------------------------------------------- + +#[test] +fn indexer_extrinsic_add_stake() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = NetUid::from(1u16); + let amount = TaoBalance::from(1_000_000_000u64); + + let _ = SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + amount, + ); + }); +} + +#[test] +fn indexer_extrinsic_remove_stake() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = NetUid::from(1u16); + let amount = AlphaBalance::from(1_000_000_000u64); + + let _ = SubtensorModule::remove_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + amount, + ); + }); +} + +#[test] +fn indexer_extrinsic_add_stake_limit() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = NetUid::from(1u16); + let amount = TaoBalance::from(1_000_000_000u64); + + let _ = SubtensorModule::add_stake_limit( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + amount, + TaoBalance::from(0u64), + false, + ); + }); +} + +#[test] +fn indexer_extrinsic_remove_stake_limit() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = NetUid::from(1u16); + let alpha_amount = AlphaBalance::from(1_000_000_000u64); + + let _ = SubtensorModule::remove_stake_limit( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + alpha_amount, + TaoBalance::from(0u64), + false, + ); + }); +} + +#[test] +fn indexer_extrinsic_swap_stake() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid_origin = NetUid::from(1u16); + let netuid_dest = NetUid::from(2u16); + let alpha_amount = AlphaBalance::from(1_000_000_000u64); + + let _ = SubtensorModule::swap_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid_origin, + netuid_dest, + alpha_amount, + ); + }); +} + +#[test] +fn indexer_extrinsic_swap_stake_limit() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid_origin = NetUid::from(1u16); + let netuid_dest = NetUid::from(2u16); + let alpha_amount = AlphaBalance::from(1_000_000_000u64); + + let _ = SubtensorModule::swap_stake_limit( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid_origin, + netuid_dest, + alpha_amount, + TaoBalance::from(0u64), + false, + ); + }); +} + +#[test] +fn indexer_extrinsic_move_stake() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey_origin = U256::from(2); + let hotkey_dest = U256::from(3); + let netuid = NetUid::from(1u16); + let alpha_amount = AlphaBalance::from(1_000_000_000u64); + + let _ = SubtensorModule::move_stake( + RuntimeOrigin::signed(coldkey), + hotkey_origin, + hotkey_dest, + netuid, + netuid, + alpha_amount, + ); + }); +} + +#[test] +fn indexer_extrinsic_set_children() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child = U256::from(3); + let netuid = NetUid::from(1u16); + let children: Vec<(u64, U256)> = vec![(u64::MAX, child)]; + + let _ = SubtensorModule::set_children( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + children, + ); + }); +} + +#[test] +fn indexer_extrinsic_decrease_take() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let take_u16: u16 = 1000; + + let _ = SubtensorModule::decrease_take( + RuntimeOrigin::signed(coldkey), + hotkey, + take_u16, + ); + }); +} + +#[test] +fn indexer_extrinsic_set_root_claim_type() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + + let _ = SubtensorModule::set_root_claim_type( + RuntimeOrigin::signed(coldkey), + RootClaimTypeEnum::Swap, + ); + }); +} + +// --------------------------------------------------------------------------- +// Extrinsics — proxy pallet +// --------------------------------------------------------------------------- + +#[test] +fn indexer_extrinsic_proxy_add_proxy() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let delegate = U256::from(2); + + let _ = pallet_subtensor_proxy::Pallet::::add_proxy( + RuntimeOrigin::signed(coldkey), + delegate, + subtensor_runtime_common::ProxyType::Any, + 0u64.into(), + ); + }); +} + +#[test] +fn indexer_extrinsic_proxy_proxy() { + new_test_ext(1).execute_with(|| { + let delegate = U256::from(1); + let real = U256::from(2); + let call = Box::new(RuntimeCall::System(system::Call::remark { remark: vec![] })); + + let _ = pallet_subtensor_proxy::Pallet::::proxy( + RuntimeOrigin::signed(delegate), + real, + Some(subtensor_runtime_common::ProxyType::Any), + call, + ); + }); +} + +#[test] +fn indexer_extrinsic_proxy_remove_proxy() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let delegate = U256::from(2); + + let _ = pallet_subtensor_proxy::Pallet::::remove_proxy( + RuntimeOrigin::signed(coldkey), + delegate, + subtensor_runtime_common::ProxyType::Any, + 0u64.into(), + ); + }); +} + +#[test] +fn indexer_extrinsic_proxy_set_real_pays_fee() { + new_test_ext(1).execute_with(|| { + let real = U256::from(1); + let delegate = U256::from(2); + + let _ = pallet_subtensor_proxy::Pallet::::set_real_pays_fee( + RuntimeOrigin::signed(real), + delegate, + true, + ); + }); +} + +// --------------------------------------------------------------------------- +// Extrinsics — balances pallet +// --------------------------------------------------------------------------- + +#[test] +fn indexer_extrinsic_balances_transfer_keep_alive() { + new_test_ext(1).execute_with(|| { + let from = U256::from(1); + let dest = U256::from(2); + let value = TaoBalance::from(1_000_000_000u64); + + let _ = Balances::transfer_keep_alive( + RuntimeOrigin::signed(from), + dest, + value, + ); + }); +} + +// --------------------------------------------------------------------------- +// Extrinsics — system pallet +// --------------------------------------------------------------------------- + +#[test] +fn indexer_extrinsic_system_remark() { + new_test_ext(1).execute_with(|| { + let who = U256::from(1); + let remark = vec![0u8; 32]; + + let _ = system::Pallet::::remark( + RuntimeOrigin::signed(who), + remark, + ); + }); +} + +// --------------------------------------------------------------------------- +// Extrinsics — utility pallet +// --------------------------------------------------------------------------- + +#[test] +fn indexer_extrinsic_utility_batch() { + new_test_ext(1).execute_with(|| { + let who = U256::from(1); + let calls: Vec = vec![ + RuntimeCall::System(system::Call::remark { remark: vec![] }), + ]; + + let _ = pallet_subtensor_utility::Pallet::::batch( + RuntimeOrigin::signed(who), + calls, + ); + }); +} + +#[test] +fn indexer_extrinsic_utility_batch_all() { + new_test_ext(1).execute_with(|| { + let who = U256::from(1); + let calls: Vec = vec![ + RuntimeCall::System(system::Call::remark { remark: vec![] }), + ]; + + let _ = pallet_subtensor_utility::Pallet::::batch_all( + RuntimeOrigin::signed(who), + calls, + ); + }); +} + +#[test] +fn indexer_extrinsic_utility_force_batch() { + new_test_ext(1).execute_with(|| { + let who = U256::from(1); + let calls: Vec = vec![ + RuntimeCall::System(system::Call::remark { remark: vec![] }), + ]; + + let _ = pallet_subtensor_utility::Pallet::::force_batch( + RuntimeOrigin::signed(who), + calls, + ); + }); +} + +// --------------------------------------------------------------------------- +// Runtime API signatures +// --------------------------------------------------------------------------- + +#[test] +fn indexer_runtime_api_current_alpha_price() { + let at = ::Hash::default(); + let netuid = NetUid::from(1u16); + + let _: u64 = SwapRuntimeApi::current_alpha_price(&MockApi, at, netuid).unwrap(); +} + +#[test] +fn indexer_runtime_api_sim_swap() { + let at = ::Hash::default(); + let netuid = NetUid::from(1u16); + let tao = TaoBalance::from(1_000_000_000u64); + let alpha = AlphaBalance::from(1_000_000_000u64); + + let _: pallet_subtensor_swap_runtime_api::SimSwapResult = + SwapRuntimeApi::sim_swap_tao_for_alpha(&MockApi, at, netuid, tao).unwrap(); + let _: pallet_subtensor_swap_runtime_api::SimSwapResult = + SwapRuntimeApi::sim_swap_alpha_for_tao(&MockApi, at, netuid, alpha).unwrap(); +} + +#[test] +fn indexer_runtime_api_get_metagraph() { + let at = ::Hash::default(); + let netuid = NetUid::from(1u16); + + let _: Option> = + SubnetInfoRuntimeApi::get_metagraph(&MockApi, at, netuid).unwrap(); +} + +#[test] +fn indexer_runtime_api_get_mechagraph() { + let at = ::Hash::default(); + let netuid = NetUid::from(1u16); + let mecid = MechId::from(0u8); + + let _: Option> = + SubnetInfoRuntimeApi::get_mechagraph(&MockApi, at, netuid, mecid).unwrap(); +} + +#[test] +fn indexer_runtime_api_stake_info_for_coldkey() { + let at = ::Hash::default(); + let acct = sp_runtime::AccountId32::new([0u8; 32]); + + let _: Vec> = + StakeInfoRuntimeApi::get_stake_info_for_coldkey(&MockApi, at, acct).unwrap(); +} + +#[test] +fn indexer_runtime_api_stake_info_for_hotkey_coldkey_netuid() { + let at = ::Hash::default(); + let hotkey = sp_runtime::AccountId32::new([1u8; 32]); + let coldkey = sp_runtime::AccountId32::new([2u8; 32]); + let netuid = NetUid::from(1u16); + + let _: Option> = + StakeInfoRuntimeApi::get_stake_info_for_hotkey_coldkey_netuid( + &MockApi, at, hotkey, coldkey, netuid, + ) + .unwrap(); +} \ No newline at end of file From f77dcce6ef395f3200d9a7c359106605ba09986c Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Tue, 30 Jun 2026 16:38:50 +0300 Subject: [PATCH 16/24] Add AlphaV2 presence test --- eco-tests/src/tests_mentat_indexer.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/eco-tests/src/tests_mentat_indexer.rs b/eco-tests/src/tests_mentat_indexer.rs index 0968100a35..f67567a797 100644 --- a/eco-tests/src/tests_mentat_indexer.rs +++ b/eco-tests/src/tests_mentat_indexer.rs @@ -11,6 +11,7 @@ use pallet_subtensor::*; use pallet_subtensor_proxy::{Proxies, RealPaysFee}; use pallet_subtensor_swap::FeeRate; use pallet_subtensor_swap_runtime_api::SwapRuntimeApi; +use share_pool::SafeFloat; use sp_core::U256; use sp_runtime::traits::Block as BlockT; use substrate_fixed::types::{I96F32, U64F64}; @@ -51,6 +52,7 @@ fn indexer_alpha_shares_and_stake() { let _: AlphaBalance = TotalHotkeyAlpha::::get(hotkey, netuid); let _: U64F64 = Alpha::::get((hotkey, coldkey, netuid)); + let _: SafeFloat = AlphaV2::::get((hotkey, coldkey, netuid)); }); } @@ -609,4 +611,4 @@ fn indexer_runtime_api_stake_info_for_hotkey_coldkey_netuid() { &MockApi, at, hotkey, coldkey, netuid, ) .unwrap(); -} \ No newline at end of file +} From 9d49f17b23cfee9e4958a9a13816b1871ae1d37a Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Tue, 30 Jun 2026 17:23:27 +0300 Subject: [PATCH 17/24] Remove Alpha and AlphaV2 presence test from mentat eco --- eco-tests/src/tests_mentat_indexer.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/eco-tests/src/tests_mentat_indexer.rs b/eco-tests/src/tests_mentat_indexer.rs index f67567a797..055ee859a6 100644 --- a/eco-tests/src/tests_mentat_indexer.rs +++ b/eco-tests/src/tests_mentat_indexer.rs @@ -51,8 +51,6 @@ fn indexer_alpha_shares_and_stake() { let coldkey = U256::from(2); let _: AlphaBalance = TotalHotkeyAlpha::::get(hotkey, netuid); - let _: U64F64 = Alpha::::get((hotkey, coldkey, netuid)); - let _: SafeFloat = AlphaV2::::get((hotkey, coldkey, netuid)); }); } From d3404368251fcb2c3af8821a0a79adaebd0fa228 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Fri, 26 Jun 2026 11:41:33 +0300 Subject: [PATCH 18/24] Enforce conviction, add test, bump spec --- .../subtensor/src/coinbase/run_coinbase.rs | 5 +- pallets/subtensor/src/tests/locks.rs | 54 +++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index d7c75964b2..84fdac3012 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -411,9 +411,8 @@ impl Pallet { ); epochs_run_this_block = epochs_run_this_block.saturating_add(1); - // Reserved for potential future enhancements. - // Ownership update logic based on conviction is currently inactive by design. - // Self::change_subnet_owner_if_needed(netuid); + // Change subnet owner based on conviction. + Self::change_subnet_owner_if_needed(netuid); } else { // Schedule advances below; execution skipped. Pending emissions accumulate // and will be drained by the next successful epoch. diff --git a/pallets/subtensor/src/tests/locks.rs b/pallets/subtensor/src/tests/locks.rs index 91b48b7881..17e45595cd 100644 --- a/pallets/subtensor/src/tests/locks.rs +++ b/pallets/subtensor/src/tests/locks.rs @@ -2951,6 +2951,60 @@ fn test_change_subnet_owner_if_needed_reassigns_to_subnet_king() { }); } +#[test] +fn test_run_coinbase_reassigns_subnet_owner_by_conviction_on_epoch() { + new_test_ext(1).execute_with(|| { + let old_owner_coldkey = U256::from(1); + let old_owner_hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(old_owner_coldkey, old_owner_hotkey, 100_000_000_000); + SubnetOwner::::insert(netuid, old_owner_coldkey); + SubnetOwnerHotkey::::insert(netuid, old_owner_hotkey); + + let new_owner_coldkey = U256::from(5); + let king_hotkey = U256::from(6); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &new_owner_coldkey, + &king_hotkey + )); + + let now = crate::staking::lock::ONE_YEAR + 1; + System::set_block_number(now); + NetworkRegisteredAt::::insert(netuid, 1); + SubnetAlphaOut::::insert(netuid, AlphaBalance::from(10_000u64)); + SubtensorModule::set_tempo_unchecked(netuid, 1); + LastEpochBlock::::insert(netuid, now.saturating_sub(1)); + PendingEpochAt::::insert(netuid, 0); + + let locked_mass = AlphaBalance::from(1_000u64); + Lock::::insert( + (new_owner_coldkey, netuid, king_hotkey), + LockState { + locked_mass, + conviction: U64F64::from_num(1_000), + last_update: now, + }, + ); + HotkeyLock::::insert( + netuid, + king_hotkey, + LockState { + locked_mass, + conviction: U64F64::from_num(1_000), + last_update: now, + }, + ); + + assert_eq!(SubnetOwner::::get(netuid), old_owner_coldkey); + assert_eq!(SubnetOwnerHotkey::::get(netuid), old_owner_hotkey); + + SubtensorModule::run_coinbase(SubtensorModule::mint_tao(0.into())); + + assert_eq!(SubnetOwner::::get(netuid), new_owner_coldkey); + assert_eq!(SubnetOwnerHotkey::::get(netuid), king_hotkey); + assert_eq!(LastEpochBlock::::get(netuid), now); + }); +} + #[test] fn test_change_subnet_owner_rebuilds_old_owner_hotkey_by_lock_mode() { new_test_ext(1).execute_with(|| { From bb3591e715a4e283e4d3996b8c12058e0c7d8f3a Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 30 Jun 2026 14:17:16 -0700 Subject: [PATCH 19/24] remove root prop from emission calculation --- pallets/subtensor/src/coinbase/subnet_emissions.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs index f378e3e930..b82bb2b0fb 100644 --- a/pallets/subtensor/src/coinbase/subnet_emissions.rs +++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs @@ -36,7 +36,7 @@ impl Pallet { block_emission: U96F32, ) -> BTreeMap { // Disabled subnets get zero TAO-side emission, redistributed to enabled subnets. - // They stay in the map so the normal alpha_out/root-prop path still runs. + // They stay in the map so the normal alpha_out path still runs. let shares = Self::get_shares(subnets_to_emit_to); log::debug!("Subnet emission shares = {shares:?}"); @@ -354,11 +354,9 @@ impl Pallet { pub(crate) fn get_shares(subnets_to_emit_to: &[NetUid]) -> BTreeMap { let price_shares = Self::get_shares_price_ema(subnets_to_emit_to); - // Weight each subnet's price share by root_proportion * (1 - miner_burned), then - // renormalize. The effective emission is therefore proportional to - // root_proportion_i * price_i * (1 - miner_burned_i). - // - root_proportion shrinks as a subnet's alpha issuance grows, so emission is - // reallocated away from older subnets toward newer ones (easier entrance). + // Weight each subnet's price share by (1 - miner_burned), then + // renormalize. The effective emission is proportional to + // price_i * (1 - miner_burned_i). // - (1 - miner_burned) reallocates away from subnets that withhold miner emission. let zero = U64F64::saturating_from_num(0); let one = U64F64::saturating_from_num(1); @@ -366,8 +364,8 @@ impl Pallet { .iter() .map(|(netuid, share)| { let burned = U64F64::saturating_from_num(MinerBurned::::get(netuid)).min(one); - let root_prop = U64F64::saturating_from_num(Self::root_proportion(*netuid)); - let factor = root_prop.saturating_mul(one.saturating_sub(burned)); + let factor = one.saturating_sub(burned); + (*netuid, share.saturating_mul(factor)) }) .collect(); From 3b94406a1b6dfabe5b82ce0fa574f1218299e04f Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 30 Jun 2026 14:35:40 -0700 Subject: [PATCH 20/24] test_get_shares_ignores_root_prop --- .../subtensor/src/tests/subnet_emissions.rs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/pallets/subtensor/src/tests/subnet_emissions.rs b/pallets/subtensor/src/tests/subnet_emissions.rs index 9218eabe76..ff56be851c 100644 --- a/pallets/subtensor/src/tests/subnet_emissions.rs +++ b/pallets/subtensor/src/tests/subnet_emissions.rs @@ -151,6 +151,47 @@ fn inplace_pow_normalize_fractional_exponent() { }) } +#[test] +fn get_shares_ignores_root_prop_storage_when_prices_and_burns_match() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(70); + let owner_coldkey = U256::from(71); + let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + // Equal prices and equal miner-burn values should produce equal shares, + // regardless of the stored root proportion. + SubnetMovingPrice::::insert(n1, i96f32(1.0)); + SubnetMovingPrice::::insert(n2, i96f32(1.0)); + MinerBurned::::insert(n1, U96F32::saturating_from_num(0.0)); + MinerBurned::::insert(n2, U96F32::saturating_from_num(0.0)); + + // Deliberately make root-prop unequal. The old formula would weight + // these equal-price subnets as 1.0 : 0.1 and fail the equality checks. + RootProp::::insert(n1, U96F32::saturating_from_num(1.0)); + RootProp::::insert(n2, U96F32::saturating_from_num(0.1)); + + assert_abs_diff_eq!( + RootProp::::get(n1).to_num::(), + 1.0_f64, + epsilon = 1e-9 + ); + assert_abs_diff_eq!( + RootProp::::get(n2).to_num::(), + 0.1_f64, + epsilon = 1e-9 + ); + + let shares = SubtensorModule::get_shares(&[n1, n2]); + let s1 = shares.get(&n1).copied().unwrap().to_num::(); + let s2 = shares.get(&n2).copied().unwrap().to_num::(); + + assert_abs_diff_eq!(s1, 0.5_f64, epsilon = 1e-9); + assert_abs_diff_eq!(s2, 0.5_f64, epsilon = 1e-9); + assert_abs_diff_eq!(s1 + s2, 1.0_f64, epsilon = 1e-9); + }); +} + // /// Normal (moderate, non-zero) EMA flows across 3 subnets. // /// Expect: shares sum to ~1 and are monotonic with flows. // #[test] From 6506ac687cc47e51dfeedec3a43de26a337af13c Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:31:29 -0700 Subject: [PATCH 21/24] bump spec --- runtime/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 52a517873a..5f10e03469 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -234,7 +234,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 424, + spec_version: 425, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, From 5c1c7ffab0ea25c05dada4861a859b7cbd44fa17 Mon Sep 17 00:00:00 2001 From: gztensor <166415444+gztensor@users.noreply.github.com> Date: Wed, 1 Jul 2026 07:47:16 -0700 Subject: [PATCH 22/24] Update eco-tests/src/mock.rs Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- eco-tests/src/mock.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/eco-tests/src/mock.rs b/eco-tests/src/mock.rs index a3a8ce1e39..c19e49906a 100644 --- a/eco-tests/src/mock.rs +++ b/eco-tests/src/mock.rs @@ -691,6 +691,7 @@ mod api_mocks { fn get_subnet_to_prune() -> Option { None } fn get_subnet_account_id(_netuid: NetUid) -> Option { None } fn get_next_epoch_start_block(_netuid: NetUid) -> Option { None } + fn get_block_emission() -> TaoBalance { TaoBalance::ZERO } } impl SwapRuntimeApi for MockApi { From 71044fd163c25caf7e446f367872f020cedff799 Mon Sep 17 00:00:00 2001 From: gztensor <166415444+gztensor@users.noreply.github.com> Date: Wed, 1 Jul 2026 07:47:30 -0700 Subject: [PATCH 23/24] Update eco-tests/src/mock.rs Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- eco-tests/src/mock.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/eco-tests/src/mock.rs b/eco-tests/src/mock.rs index c19e49906a..ac685684ef 100644 --- a/eco-tests/src/mock.rs +++ b/eco-tests/src/mock.rs @@ -691,6 +691,7 @@ mod api_mocks { fn get_subnet_to_prune() -> Option { None } fn get_subnet_account_id(_netuid: NetUid) -> Option { None } fn get_next_epoch_start_block(_netuid: NetUid) -> Option { None } + fn get_block_emission() -> TaoBalance { TaoBalance::from(0u64) } fn get_block_emission() -> TaoBalance { TaoBalance::ZERO } } From 026b8f3966e54416251b1c4eeba72c1c803ec25f Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Wed, 1 Jul 2026 17:49:25 +0300 Subject: [PATCH 24/24] Fix AI fix --- eco-tests/src/mock.rs | 1 - eco-tests/src/tests_mentat_indexer.rs | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/eco-tests/src/mock.rs b/eco-tests/src/mock.rs index ac685684ef..2a064c8840 100644 --- a/eco-tests/src/mock.rs +++ b/eco-tests/src/mock.rs @@ -692,7 +692,6 @@ mod api_mocks { fn get_subnet_account_id(_netuid: NetUid) -> Option { None } fn get_next_epoch_start_block(_netuid: NetUid) -> Option { None } fn get_block_emission() -> TaoBalance { TaoBalance::from(0u64) } - fn get_block_emission() -> TaoBalance { TaoBalance::ZERO } } impl SwapRuntimeApi for MockApi { diff --git a/eco-tests/src/tests_mentat_indexer.rs b/eco-tests/src/tests_mentat_indexer.rs index 055ee859a6..8b0f6ae69f 100644 --- a/eco-tests/src/tests_mentat_indexer.rs +++ b/eco-tests/src/tests_mentat_indexer.rs @@ -11,10 +11,9 @@ use pallet_subtensor::*; use pallet_subtensor_proxy::{Proxies, RealPaysFee}; use pallet_subtensor_swap::FeeRate; use pallet_subtensor_swap_runtime_api::SwapRuntimeApi; -use share_pool::SafeFloat; use sp_core::U256; use sp_runtime::traits::Block as BlockT; -use substrate_fixed::types::{I96F32, U64F64}; +use substrate_fixed::types::I96F32; use subtensor_custom_rpc_runtime_api::{StakeInfoRuntimeApi, SubnetInfoRuntimeApi}; use subtensor_runtime_common::{AlphaBalance, MechId, NetUid, NetUidStorageIndex, TaoBalance}; @@ -48,7 +47,6 @@ fn indexer_alpha_shares_and_stake() { new_test_ext(1).execute_with(|| { let netuid = NetUid::from(1u16); let hotkey = U256::from(1); - let coldkey = U256::from(2); let _: AlphaBalance = TotalHotkeyAlpha::::get(hotkey, netuid); });