Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions pallets/swap/src/pallet/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,28 @@ impl<T: Config> Pallet<T> {
}
}

/// 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::<T>::insert(netuid, sqrt_price);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] New price-mirror storage write is not reflected in weights

refresh_alpha_sqrt_price performs a persistent storage write and is now called from hot paths including adjust_protocol_liquidity and swap_inner, but this PR does not update the affected runtime weights. Those paths are reachable from per-block emission handling and user-facing swap/stake extrinsics, so blocks can now do at least one extra storage write, plus the helper's price reads, beyond what the declared weights account for. Regenerate or manually update the affected weights and block-emission accounting before merging.

Comment on lines +45 to +50

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] New price-mirror storage write is not reflected in weights

store_alpha_sqrt_price now performs a fixed-point sqrt and writes Swap::AlphaSqrtPrice from initialization, adjust_protocol_liquidity, and the real/simulated swap path, but this PR does not update the benchmarked weights for the Subtensor extrinsics and block paths that call SwapInterface::swap or protocol-liquidity adjustment. That undercharges repeated swaps/emission for both CPU and an additional storage write, which is a runtime resource-accounting issue. Rerun/update the affected benchmarks or otherwise add the extra DB write and computation cost to the relevant weights before merging.

}

/// 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,
Expand Down Expand Up @@ -76,6 +98,9 @@ impl<T: Config> Pallet<T> {

PalSwapInitialized::<T>::insert(netuid, true);

// Seed the legacy AlphaSqrtPrice mirror for backwards compatibility.
Self::refresh_alpha_sqrt_price(netuid);

Ok(())
}

Expand Down Expand Up @@ -112,6 +137,9 @@ impl<T: Config> Pallet<T> {
(TaoBalance::ZERO, AlphaBalance::ZERO)
} else {
SwapBalancer::<T>::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)
}
}
Expand Down Expand Up @@ -218,6 +246,12 @@ impl<T: Config> Pallet<T> {
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,
Expand Down Expand Up @@ -276,6 +310,7 @@ impl<T: Config> Pallet<T> {

FeeRate::<T>::remove(netuid);
SwapBalancer::<T>::remove(netuid);
AlphaSqrtPrice::<T>::remove(netuid);

log::debug!(
"clear_protocol_liquidity: netuid={netuid:?}, protocol_burned: τ={burned_tao:?}, α={burned_alpha:?}; state cleared"
Expand Down
11 changes: 4 additions & 7 deletions pallets/swap/src/pallet/migrations/migrate_swapv3_to_balancer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: Config> =
StorageMap<Pallet<T>, Twox64Concat, NetUid, U64F64, ValueQuery>;

/// TAO reservoir for scraps of protocol claimed fees.
#[storage_alias]
pub type ScrapReservoirTao<T: Config> =
Expand Down Expand Up @@ -42,7 +37,10 @@ pub fn migrate_swapv3_to_balancer<T: Config>() -> Weight {
// ------------------------------
// Step 1: Initialize swaps with price before price removal
// ------------------------------
for (netuid, price_sqrt) in deprecated_swap_maps::AlphaSqrtPrice::<T>::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::<T>::iter() {
let price = price_sqrt.saturating_mul(price_sqrt);
if let Err(error) = crate::Pallet::<T>::maybe_initialize_palswap(netuid, Some(price)) {
log::warn!(
Expand All @@ -60,7 +58,6 @@ pub fn migrate_swapv3_to_balancer<T: Config>() -> Weight {
// ------------------------------
// Step 2: Clear Map entries
// ------------------------------
remove_prefix::<T>("Swap", "AlphaSqrtPrice", &mut weight);
remove_prefix::<T>("Swap", "CurrentTick", &mut weight);
remove_prefix::<T>("Swap", "EnabledUserLiquidity", &mut weight);
remove_prefix::<T>("Swap", "FeeGlobalTao", &mut weight);
Expand Down
12 changes: 12 additions & 0 deletions pallets/swap/src/pallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -113,6 +114,17 @@ mod pallet {
#[pallet::storage]
pub type PalSwapInitialized<T> = 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<T> = StorageMap<_, Twox64Concat, NetUid, U64F64, ValueQuery>;

/// --- Storage for migration run status
#[pallet::storage]
pub type HasMigrationRun<T: Config> =
Expand Down
4 changes: 4 additions & 0 deletions pallets/swap/src/pallet/swap_step.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ where
delta_in: self.delta_in,
delta_out,
fee_to_block_author,
final_price: self.final_price,
})
}
}
Expand Down Expand Up @@ -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,
}
97 changes: 87 additions & 10 deletions pallets/swap/src/pallet/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -806,8 +806,9 @@ fn test_migrate_swapv3_to_balancer() {
crate::migrations::migrate_swapv3_to_balancer::migrate_swapv3_to_balancer::<Test>;
let netuid = NetUid::from(1);

// Insert deprecated maps values
deprecated_swap_maps::AlphaSqrtPrice::<Test>::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::<Test>::insert(netuid, U64F64::from_num(1.23));
deprecated_swap_maps::ScrapReservoirTao::<Test>::insert(netuid, TaoBalance::from(9876));
deprecated_swap_maps::ScrapReservoirAlpha::<Test>::insert(netuid, AlphaBalance::from(9876));

Expand All @@ -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::<Test>::contains_key(
netuid
));
// V3-only state is removed, but AlphaSqrtPrice is retained as a backwards-compat seed.
assert!(AlphaSqrtPrice::<Test>::contains_key(netuid));
assert_abs_diff_eq!(
AlphaSqrtPrice::<Test>::get(netuid).to_num::<f64>(),
1.23,
epsilon = 0.001
);
assert!(!deprecated_swap_maps::ScrapReservoirAlpha::<Test>::contains_key(netuid));

// Test that subnet price is still 1.23^2
Expand All @@ -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::<Test>::insert(netuid, U64F64::from_num(1));
AlphaSqrtPrice::<Test>::insert(netuid, U64F64::from_num(1));
deprecated_swap_maps::ScrapReservoirTao::<Test>::insert(netuid, TaoBalance::from(9876));
deprecated_swap_maps::ScrapReservoirAlpha::<Test>::insert(netuid, AlphaBalance::from(9876));

Expand All @@ -854,9 +858,8 @@ fn test_migrate_swapv3_to_balancer_falls_back_to_default_when_price_init_fails()

migration();

assert!(!deprecated_swap_maps::AlphaSqrtPrice::<Test>::contains_key(
netuid
));
// AlphaSqrtPrice is retained even when the balancer falls back to default.
assert!(AlphaSqrtPrice::<Test>::contains_key(netuid));
assert!(!deprecated_swap_maps::ScrapReservoirTao::<Test>::contains_key(netuid));
assert!(!deprecated_swap_maps::ScrapReservoirAlpha::<Test>::contains_key(netuid));
assert!(PalSwapInitialized::<Test>::get(netuid));
Expand All @@ -867,3 +870,77 @@ fn test_migrate_swapv3_to_balancer_falls_back_to_default_when_price_init_fails()
assert!(HasMigrationRun::<Test>::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::<Test>::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::<Test>::maybe_initialize_palswap(netuid, None));

// Init seeds the mirror with sqrt(0.25) = 0.5, and mirror^2 == the derived price.
let seeded = AlphaSqrtPrice::<Test>::get(netuid).to_num::<f64>();
assert_abs_diff_eq!(seeded, 0.5, epsilon = 0.0001);
assert_abs_diff_eq!(
seeded * seeded,
Pallet::<Test>::current_price(netuid).to_num::<f64>(),
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::<Test>::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::<Test>::get(netuid).to_num::<f64>();
assert!(after > seeded);
assert_abs_diff_eq!(
after * after,
Pallet::<Test>::current_price(netuid).to_num::<f64>(),
epsilon = 0.001
);

// A simulated swap must not move the mirror (the write is rolled back).
let before_sim = AlphaSqrtPrice::<Test>::get(netuid);
let _ = Pallet::<Test>::do_swap(
netuid,
GetAlphaForTao::with_amount(100_000_000),
U64F64::from_num(1000.0),
false,
true, // simulate
)
.unwrap();
assert_eq!(AlphaSqrtPrice::<Test>::get(netuid), before_sim);

// Dissolve clears the mirror.
assert_ok!(Pallet::<Test>::do_clear_protocol_liquidity(netuid));
assert!(!AlphaSqrtPrice::<Test>::contains_key(netuid));
});
}
2 changes: 1 addition & 1 deletion runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading