diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index c3e0b2f1d3..c3d8d73edf 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -34,6 +34,28 @@ impl Pallet { } } + /// 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, 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)); + let sqrt_price = price.checked_sqrt(epsilon).unwrap_or_default(); + 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, @@ -76,6 +98,9 @@ impl Pallet { PalSwapInitialized::::insert(netuid, true); + // Seed the legacy AlphaSqrtPrice mirror for backwards compatibility. + Self::refresh_alpha_sqrt_price(netuid); + Ok(()) } @@ -112,6 +137,9 @@ impl Pallet { (TaoBalance::ZERO, AlphaBalance::ZERO) } else { SwapBalancer::::insert(netuid, balancer); + // 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) } } @@ -218,6 +246,12 @@ impl Pallet { log::trace!("Fees: {}", swap_result.fee_paid); log::trace!("======== End Swap ========"); + // 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, amount_paid_out: swap_result.delta_out, @@ -276,6 +310,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..1811d66c40 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,17 @@ 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/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 b1071294d3..631eaae9fc 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)); @@ -867,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)); + }); +} 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,