From e1a5db349efdda9f5e0085634bad510597e7e5af Mon Sep 17 00:00:00 2001 From: Sam Johnson Date: Thu, 25 Jun 2026 17:53:43 -0400 Subject: [PATCH 1/3] Re-add Swap::AlphaSqrtPrice for backwards compatibility; bump spec to 424 The balancer migration (v3.4.8-423) deleted the Swap::AlphaSqrtPrice storage map as collateral of the Uniswap-V3 -> balancer engine rewrite, where price became a derived quantity instead of stored state. External consumers (indexers, dashboards, wallets, SDKs) read that map directly to obtain the subnet price and broke. Re-add the storage map with its exact prior definition and maintain it as a read-only mirror of the derived balancer price (sqrt of current_price), refreshed on every price change (init, swap, protocol-liquidity adjust) and removed on dissolve. The migration now retains the V3 values as the initial seed instead of clearing them, so there is no gap at upgrade. No V3 engine logic is reintroduced; the map is purely for back-compat. --- pallets/swap/src/pallet/impls.rs | 24 +++++++++++++++++++ .../migrations/migrate_swapv3_to_balancer.rs | 11 ++++----- pallets/swap/src/pallet/mod.rs | 13 ++++++++++ pallets/swap/src/pallet/tests.rs | 23 ++++++++++-------- runtime/src/lib.rs | 2 +- 5 files changed, 55 insertions(+), 18 deletions(-) diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index c3e0b2f1d3..f23c973bbd 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -34,6 +34,20 @@ impl Pallet { } } + /// Refresh the backwards-compatibility `AlphaSqrtPrice` storage for a subnet. + /// + /// The balancer derives price on the fly, but external consumers still read the legacy + /// `Swap::AlphaSqrtPrice` map directly. Call this whenever the price may have changed + /// (swaps, protocol-liquidity adjustments, initialization) to keep that map in sync. + pub(crate) fn refresh_alpha_sqrt_price(netuid: NetUid) { + let price = Self::current_price(netuid); + // Epsilon for the bisection sqrt: 1e-9 is well below the price precision consumers need. + let epsilon = U64F64::saturating_from_num(1) + .safe_div(U64F64::saturating_from_num(1_000_000_000_u64)); + let sqrt_price = price.checked_sqrt(epsilon).unwrap_or_default(); + AlphaSqrtPrice::::insert(netuid, sqrt_price); + } + // initializes pal-swap (balancer) for a subnet if needed pub fn maybe_initialize_palswap( netuid: NetUid, @@ -76,6 +90,9 @@ impl Pallet { PalSwapInitialized::::insert(netuid, true); + // Keep the legacy AlphaSqrtPrice map in sync for backwards compatibility. + Self::refresh_alpha_sqrt_price(netuid); + Ok(()) } @@ -112,6 +129,8 @@ impl Pallet { (TaoBalance::ZERO, AlphaBalance::ZERO) } else { SwapBalancer::::insert(netuid, balancer); + // Keep the legacy AlphaSqrtPrice map in sync for backwards compatibility. + Self::refresh_alpha_sqrt_price(netuid); (tao_delta, alpha_delta) } } @@ -218,6 +237,10 @@ impl Pallet { log::trace!("Fees: {}", swap_result.fee_paid); log::trace!("======== End Swap ========"); + // Keep the legacy AlphaSqrtPrice map in sync for backwards compatibility. This runs + // inside the swap's transactional scope, so it is rolled back on simulation. + Self::refresh_alpha_sqrt_price(netuid); + Ok(SwapResult { amount_paid_in: swap_result.delta_in, amount_paid_out: swap_result.delta_out, @@ -276,6 +299,7 @@ impl Pallet { FeeRate::::remove(netuid); SwapBalancer::::remove(netuid); + AlphaSqrtPrice::::remove(netuid); log::debug!( "clear_protocol_liquidity: netuid={netuid:?}, protocol_burned: τ={burned_tao:?}, α={burned_alpha:?}; state cleared" diff --git a/pallets/swap/src/pallet/migrations/migrate_swapv3_to_balancer.rs b/pallets/swap/src/pallet/migrations/migrate_swapv3_to_balancer.rs index 2f06d88a00..ebfb0c6bbb 100644 --- a/pallets/swap/src/pallet/migrations/migrate_swapv3_to_balancer.rs +++ b/pallets/swap/src/pallet/migrations/migrate_swapv3_to_balancer.rs @@ -2,15 +2,10 @@ use super::*; use crate::HasMigrationRun; use frame_support::{storage_alias, traits::Get, weights::Weight}; use scale_info::prelude::string::String; -use substrate_fixed::types::U64F64; pub mod deprecated_swap_maps { use super::*; - #[storage_alias] - pub type AlphaSqrtPrice = - StorageMap, Twox64Concat, NetUid, U64F64, ValueQuery>; - /// TAO reservoir for scraps of protocol claimed fees. #[storage_alias] pub type ScrapReservoirTao = @@ -42,7 +37,10 @@ pub fn migrate_swapv3_to_balancer() -> Weight { // ------------------------------ // Step 1: Initialize swaps with price before price removal // ------------------------------ - for (netuid, price_sqrt) in deprecated_swap_maps::AlphaSqrtPrice::::iter() { + // NOTE: `AlphaSqrtPrice` is intentionally NOT cleared below. It is retained as a + // backwards-compatibility map (see its definition in the pallet) and the V3 values read + // here serve as its initial seed; it is refreshed on every subsequent price change. + for (netuid, price_sqrt) in AlphaSqrtPrice::::iter() { let price = price_sqrt.saturating_mul(price_sqrt); if let Err(error) = crate::Pallet::::maybe_initialize_palswap(netuid, Some(price)) { log::warn!( @@ -60,7 +58,6 @@ pub fn migrate_swapv3_to_balancer() -> Weight { // ------------------------------ // Step 2: Clear Map entries // ------------------------------ - remove_prefix::("Swap", "AlphaSqrtPrice", &mut weight); remove_prefix::("Swap", "CurrentTick", &mut weight); remove_prefix::("Swap", "EnabledUserLiquidity", &mut weight); remove_prefix::("Swap", "FeeGlobalTao", &mut weight); diff --git a/pallets/swap/src/pallet/mod.rs b/pallets/swap/src/pallet/mod.rs index 1d2fd07c59..aa4c69a344 100644 --- a/pallets/swap/src/pallet/mod.rs +++ b/pallets/swap/src/pallet/mod.rs @@ -2,6 +2,7 @@ use core::num::NonZeroU64; use frame_support::{PalletId, pallet_prelude::*, traits::Get}; use frame_system::pallet_prelude::*; +use substrate_fixed::types::U64F64; use subtensor_runtime_common::{ AlphaBalance, BalanceOps, NetUid, SubnetInfo, TaoBalance, TokenReserve, }; @@ -113,6 +114,18 @@ mod pallet { #[pallet::storage] pub type PalSwapInitialized = StorageMap<_, Twox64Concat, NetUid, bool, ValueQuery>; + /// Square root of the current alpha price per subnet. + /// + /// This map is NOT used by the balancer (price is derived on the fly from reserves and + /// weights via [`Pallet::current_price`]). It is maintained purely for backwards + /// compatibility: external consumers (indexers, dashboards, wallets, SDKs) read the + /// `Swap::AlphaSqrtPrice` storage directly to obtain the subnet price, as they did under + /// the Uniswap V3 implementation. It is refreshed whenever the price changes via + /// [`Pallet::refresh_alpha_sqrt_price`]. + #[pallet::storage] + pub type AlphaSqrtPrice = + StorageMap<_, Twox64Concat, NetUid, U64F64, ValueQuery>; + /// --- Storage for migration run status #[pallet::storage] pub type HasMigrationRun = diff --git a/pallets/swap/src/pallet/tests.rs b/pallets/swap/src/pallet/tests.rs index b1071294d3..0a21ece36d 100644 --- a/pallets/swap/src/pallet/tests.rs +++ b/pallets/swap/src/pallet/tests.rs @@ -806,8 +806,9 @@ fn test_migrate_swapv3_to_balancer() { crate::migrations::migrate_swapv3_to_balancer::migrate_swapv3_to_balancer::; let netuid = NetUid::from(1); - // Insert deprecated maps values - deprecated_swap_maps::AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1.23)); + // Insert deprecated maps values. AlphaSqrtPrice is retained for backwards + // compatibility, so it is inserted via the live pallet storage. + AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1.23)); deprecated_swap_maps::ScrapReservoirTao::::insert(netuid, TaoBalance::from(9876)); deprecated_swap_maps::ScrapReservoirAlpha::::insert(netuid, AlphaBalance::from(9876)); @@ -818,10 +819,13 @@ fn test_migrate_swapv3_to_balancer() { // Run migration migration(); - // Test that values are removed from state - assert!(!deprecated_swap_maps::AlphaSqrtPrice::::contains_key( - netuid - )); + // V3-only state is removed, but AlphaSqrtPrice is retained as a backwards-compat seed. + assert!(AlphaSqrtPrice::::contains_key(netuid)); + assert_abs_diff_eq!( + AlphaSqrtPrice::::get(netuid).to_num::(), + 1.23, + epsilon = 0.001 + ); assert!(!deprecated_swap_maps::ScrapReservoirAlpha::::contains_key(netuid)); // Test that subnet price is still 1.23^2 @@ -845,7 +849,7 @@ fn test_migrate_swapv3_to_balancer_falls_back_to_default_when_price_init_fails() frame_support::BoundedVec::truncate_from(b"migrate_swapv3_to_balancer".to_vec()); let netuid = NetUid::from(1); - deprecated_swap_maps::AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1)); + AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1)); deprecated_swap_maps::ScrapReservoirTao::::insert(netuid, TaoBalance::from(9876)); deprecated_swap_maps::ScrapReservoirAlpha::::insert(netuid, AlphaBalance::from(9876)); @@ -854,9 +858,8 @@ fn test_migrate_swapv3_to_balancer_falls_back_to_default_when_price_init_fails() migration(); - assert!(!deprecated_swap_maps::AlphaSqrtPrice::::contains_key( - netuid - )); + // AlphaSqrtPrice is retained even when the balancer falls back to default. + assert!(AlphaSqrtPrice::::contains_key(netuid)); assert!(!deprecated_swap_maps::ScrapReservoirTao::::contains_key(netuid)); assert!(!deprecated_swap_maps::ScrapReservoirAlpha::::contains_key(netuid)); assert!(PalSwapInitialized::::get(netuid)); 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 d39e0bf5f1b720229af9b3bbb9410ffa27ee46b8 Mon Sep 17 00:00:00 2001 From: Sam Johnson Date: Thu, 25 Jun 2026 18:16:28 -0400 Subject: [PATCH 2/3] cargo fmt --- pallets/swap/src/pallet/impls.rs | 4 ++-- pallets/swap/src/pallet/mod.rs | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index f23c973bbd..1bd060ccab 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -42,8 +42,8 @@ impl Pallet { pub(crate) fn refresh_alpha_sqrt_price(netuid: NetUid) { let price = Self::current_price(netuid); // Epsilon for the bisection sqrt: 1e-9 is well below the price precision consumers need. - let epsilon = U64F64::saturating_from_num(1) - .safe_div(U64F64::saturating_from_num(1_000_000_000_u64)); + let epsilon = + U64F64::saturating_from_num(1).safe_div(U64F64::saturating_from_num(1_000_000_000_u64)); let sqrt_price = price.checked_sqrt(epsilon).unwrap_or_default(); AlphaSqrtPrice::::insert(netuid, sqrt_price); } diff --git a/pallets/swap/src/pallet/mod.rs b/pallets/swap/src/pallet/mod.rs index aa4c69a344..1811d66c40 100644 --- a/pallets/swap/src/pallet/mod.rs +++ b/pallets/swap/src/pallet/mod.rs @@ -123,8 +123,7 @@ mod pallet { /// the Uniswap V3 implementation. It is refreshed whenever the price changes via /// [`Pallet::refresh_alpha_sqrt_price`]. #[pallet::storage] - pub type AlphaSqrtPrice = - StorageMap<_, Twox64Concat, NetUid, U64F64, ValueQuery>; + pub type AlphaSqrtPrice = StorageMap<_, Twox64Concat, NetUid, U64F64, ValueQuery>; /// --- Storage for migration run status #[pallet::storage] From 695cf0cdcfb00a9128ffcf0a71fae667cb809006 Mon Sep 17 00:00:00 2001 From: Sam Johnson Date: Thu, 25 Jun 2026 18:55:59 -0400 Subject: [PATCH 3/3] Mirror post-swap price into AlphaSqrtPrice via swap step; add test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The swap-path refresh previously derived the price from reserves inside swap_inner, but a swap's reserve deltas are applied by the caller (stake_utils), outside this pallet — so it read pre-swap reserves and mirrored a stale price. Surface the swap step's computed final price and store that instead, which is the correct post-swap price without needing committed reserves. Init and emission (adjust_protocol_liquidity) still derive from current_price, where reserves are already up to date. Add a dedicated test covering init seeding, post-swap tracking, simulated-swap rollback, and clear-on-dissolve. --- pallets/swap/src/pallet/impls.rs | 31 ++++++++---- pallets/swap/src/pallet/swap_step.rs | 4 ++ pallets/swap/src/pallet/tests.rs | 74 ++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 10 deletions(-) diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index 1bd060ccab..c3d8d73edf 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -34,13 +34,15 @@ impl Pallet { } } - /// Refresh the backwards-compatibility `AlphaSqrtPrice` storage for a subnet. + /// Store `sqrt(price)` into the backwards-compatibility `AlphaSqrtPrice` map for a subnet. /// /// The balancer derives price on the fly, but external consumers still read the legacy - /// `Swap::AlphaSqrtPrice` map directly. Call this whenever the price may have changed - /// (swaps, protocol-liquidity adjustments, initialization) to keep that map in sync. - pub(crate) fn refresh_alpha_sqrt_price(netuid: NetUid) { - let price = Self::current_price(netuid); + /// `Swap::AlphaSqrtPrice` map directly, so we mirror the price wherever it changes: + /// initialization, protocol-liquidity adjustment (emission), and each swap. The price is + /// passed in rather than re-read from reserves because a swap commits its reserve deltas in + /// the caller (outside this pallet) — at swap time we use the swap step's computed + /// post-swap price instead. + pub(crate) fn store_alpha_sqrt_price(netuid: NetUid, price: U64F64) { // Epsilon for the bisection sqrt: 1e-9 is well below the price precision consumers need. let epsilon = U64F64::saturating_from_num(1).safe_div(U64F64::saturating_from_num(1_000_000_000_u64)); @@ -48,6 +50,12 @@ impl Pallet { AlphaSqrtPrice::::insert(netuid, sqrt_price); } + /// Refresh `AlphaSqrtPrice` from the current derived price. Safe to call where reserves are + /// already up to date (initialization, emission) — not on the swap path. + pub(crate) fn refresh_alpha_sqrt_price(netuid: NetUid) { + Self::store_alpha_sqrt_price(netuid, Self::current_price(netuid)); + } + // initializes pal-swap (balancer) for a subnet if needed pub fn maybe_initialize_palswap( netuid: NetUid, @@ -90,7 +98,7 @@ impl Pallet { PalSwapInitialized::::insert(netuid, true); - // Keep the legacy AlphaSqrtPrice map in sync for backwards compatibility. + // Seed the legacy AlphaSqrtPrice mirror for backwards compatibility. Self::refresh_alpha_sqrt_price(netuid); Ok(()) @@ -129,7 +137,8 @@ impl Pallet { (TaoBalance::ZERO, AlphaBalance::ZERO) } else { SwapBalancer::::insert(netuid, balancer); - // Keep the legacy AlphaSqrtPrice map in sync for backwards compatibility. + // Emission changed reserves/weights; refresh the legacy AlphaSqrtPrice mirror. + // Reserves are already committed here, so deriving from current_price is correct. Self::refresh_alpha_sqrt_price(netuid); (tao_delta, alpha_delta) } @@ -237,9 +246,11 @@ impl Pallet { log::trace!("Fees: {}", swap_result.fee_paid); log::trace!("======== End Swap ========"); - // Keep the legacy AlphaSqrtPrice map in sync for backwards compatibility. This runs - // inside the swap's transactional scope, so it is rolled back on simulation. - Self::refresh_alpha_sqrt_price(netuid); + // Mirror the post-swap price into the legacy AlphaSqrtPrice map for backwards + // compatibility. We use the swap step's computed final price because the reserve deltas + // are applied by the caller (outside this pallet) and aren't visible yet. This runs in + // the swap's transactional scope, so it is rolled back on simulated swaps. + Self::store_alpha_sqrt_price(netuid, swap_result.final_price); Ok(SwapResult { amount_paid_in: swap_result.delta_in, diff --git a/pallets/swap/src/pallet/swap_step.rs b/pallets/swap/src/pallet/swap_step.rs index 7f10bff65a..79d0435868 100644 --- a/pallets/swap/src/pallet/swap_step.rs +++ b/pallets/swap/src/pallet/swap_step.rs @@ -130,6 +130,7 @@ where delta_in: self.delta_in, delta_out, fee_to_block_author, + final_price: self.final_price, }) } } @@ -256,4 +257,7 @@ where pub(crate) delta_in: PaidIn, pub(crate) delta_out: PaidOut, pub(crate) fee_to_block_author: PaidIn, + /// Price after this swap step. Used to keep the backwards-compatibility + /// `AlphaSqrtPrice` mirror in sync without re-reading not-yet-committed reserves. + pub(crate) final_price: U64F64, } diff --git a/pallets/swap/src/pallet/tests.rs b/pallets/swap/src/pallet/tests.rs index 0a21ece36d..631eaae9fc 100644 --- a/pallets/swap/src/pallet/tests.rs +++ b/pallets/swap/src/pallet/tests.rs @@ -870,3 +870,77 @@ fn test_migrate_swapv3_to_balancer_falls_back_to_default_when_price_init_fails() assert!(HasMigrationRun::::get(&migration_name)); }); } + +// Backwards-compat: the legacy `AlphaSqrtPrice` mirror is seeded on init, tracks the post-swap +// price, is unaffected by simulated swaps, and is cleared on dissolve. +// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_alpha_sqrt_price_backcompat_mirror --exact --nocapture +#[test] +fn test_alpha_sqrt_price_backcompat_mirror() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + + // No mirror entry before the subnet is initialized. + assert!(!AlphaSqrtPrice::::contains_key(netuid)); + + // Initialize at price 0.25 (tao/alpha = 1e9 / 4e9). + let initial_tao = TaoBalance::from(1_000_000_000u64); + let initial_alpha = AlphaBalance::from(4_000_000_000u64); + TaoReserve::set_mock_reserve(netuid, initial_tao); + AlphaReserve::set_mock_reserve(netuid, initial_alpha); + assert_ok!(Pallet::::maybe_initialize_palswap(netuid, None)); + + // Init seeds the mirror with sqrt(0.25) = 0.5, and mirror^2 == the derived price. + let seeded = AlphaSqrtPrice::::get(netuid).to_num::(); + assert_abs_diff_eq!(seeded, 0.5, epsilon = 0.0001); + assert_abs_diff_eq!( + seeded * seeded, + Pallet::::current_price(netuid).to_num::(), + epsilon = 0.0001 + ); + + // Buy alpha with tao -> price rises. The mirror tracks the post-swap price even though + // the reserve deltas are applied by the caller afterwards. + let order = GetAlphaForTao::with_amount(100_000_000); + let swap_result = + Pallet::::do_swap(netuid, order, U64F64::from_num(1000.0), false, false).unwrap(); + + // Apply the reserve deltas the way the real caller (stake_utils) does. + TaoReserve::set_mock_reserve( + netuid, + TaoBalance::from( + (u64::from(initial_tao) as i128 + swap_result.paid_in_reserve_delta()) as u64, + ), + ); + AlphaReserve::set_mock_reserve( + netuid, + AlphaBalance::from( + (u64::from(initial_alpha) as i128 + swap_result.paid_out_reserve_delta()) as u64, + ), + ); + + // Mirror rose and, once reserves are committed, mirror^2 == the new derived price. + let after = AlphaSqrtPrice::::get(netuid).to_num::(); + assert!(after > seeded); + assert_abs_diff_eq!( + after * after, + Pallet::::current_price(netuid).to_num::(), + epsilon = 0.001 + ); + + // A simulated swap must not move the mirror (the write is rolled back). + let before_sim = AlphaSqrtPrice::::get(netuid); + let _ = Pallet::::do_swap( + netuid, + GetAlphaForTao::with_amount(100_000_000), + U64F64::from_num(1000.0), + false, + true, // simulate + ) + .unwrap(); + assert_eq!(AlphaSqrtPrice::::get(netuid), before_sim); + + // Dissolve clears the mirror. + assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); + assert!(!AlphaSqrtPrice::::contains_key(netuid)); + }); +}