diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index 50b9ccd28..cc2ba65c0 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -333,7 +333,7 @@ parameter_types! { pub const SwapProtocolId: PalletId = PalletId(*b"ten/swap"); pub const SwapMaxFeeRate: u16 = 10000; // 15.26% pub const SwapMaxPositions: u32 = 100; - pub const SwapMinimumLiquidity: u64 = 1_000; + pub const SwapMinimumLiquidity: u128 = 1_000; pub const SwapMinimumReserve: NonZeroU64 = NonZeroU64::new(1_000_000).unwrap(); } diff --git a/pallets/subtensor/src/macros/hooks.rs b/pallets/subtensor/src/macros/hooks.rs index 20651f68f..68682a5f7 100644 --- a/pallets/subtensor/src/macros/hooks.rs +++ b/pallets/subtensor/src/macros/hooks.rs @@ -153,7 +153,9 @@ mod hooks { // Cleanup child/parent keys .saturating_add(migrations::migrate_fix_childkeys::migrate_fix_childkeys::()) // Migrate AutoStakeDestinationColdkeys - .saturating_add(migrations::migrate_auto_stake_destination::migrate_auto_stake_destination::()); + .saturating_add(migrations::migrate_auto_stake_destination::migrate_auto_stake_destination::()) + // Migrate and fix LP ticks that saturated + .saturating_add(migrations::migrate_fix_liquidity_ticks::migrate_fix_liquidity_ticks::()); weight } diff --git a/pallets/subtensor/src/migrations/migrate_fix_liquidity_ticks.rs b/pallets/subtensor/src/migrations/migrate_fix_liquidity_ticks.rs new file mode 100644 index 000000000..49e74a75b --- /dev/null +++ b/pallets/subtensor/src/migrations/migrate_fix_liquidity_ticks.rs @@ -0,0 +1,56 @@ +use super::*; +use alloc::string::String; +use subtensor_swap_interface::SwapHandler; + +/// Migrate and fix LP ticks that saturated +pub fn migrate_fix_liquidity_ticks() -> Weight { + let migration_name = b"migrate_fix_liquidity_ticks".to_vec(); + let mut weight = T::DbWeight::get().reads(1); + + if HasMigrationRun::::get(&migration_name) { + log::info!( + "Migration '{:?}' has already run. Skipping.", + String::from_utf8_lossy(&migration_name) + ); + return weight; + } + + log::info!( + "Running migration '{}'", + String::from_utf8_lossy(&migration_name) + ); + + //////////////////////////////////////////////////////// + // Actual migration + + T::SwapInterface::migrate_fix_liquidity_ticks(&mut weight); + T::SwapInterface::migrate_fix_current_liquidity(&mut weight); + + // Fix reserves for all subnets + let netuids: Vec = NetworksAdded::::iter() + .map(|(netuid, _)| netuid) + .collect(); + weight = weight.saturating_add(T::DbWeight::get().reads_writes(netuids.len() as u64, 0)); + + for netuid in netuids.into_iter() { + let (tao_reserve, alpha_reserve) = + T::SwapInterface::migrate_get_implied_reserves(netuid, &mut weight); + SubnetTaoProvided::::insert(netuid, TaoCurrency::from(0)); + SubnetTAO::::insert(netuid, tao_reserve); + SubnetAlphaInProvided::::insert(netuid, AlphaCurrency::from(0)); + SubnetAlphaIn::::insert(netuid, alpha_reserve); + weight = weight.saturating_add(T::DbWeight::get().writes(4)); + } + + //////////////////////////////////////////////////////// + + HasMigrationRun::::insert(&migration_name, true); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + log::info!( + target: "runtime", + "Migration '{}' completed successfully.", + String::from_utf8_lossy(&migration_name) + ); + weight +} diff --git a/pallets/subtensor/src/migrations/mod.rs b/pallets/subtensor/src/migrations/mod.rs index e92b86aa2..0577aef92 100644 --- a/pallets/subtensor/src/migrations/mod.rs +++ b/pallets/subtensor/src/migrations/mod.rs @@ -17,6 +17,7 @@ pub mod migrate_delete_subnet_3; pub mod migrate_disable_commit_reveal; pub mod migrate_fix_childkeys; pub mod migrate_fix_is_network_member; +pub mod migrate_fix_liquidity_ticks; pub mod migrate_fix_root_subnet_tao; pub mod migrate_fix_root_tao_and_alpha_in; pub mod migrate_identities_v2; diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index 39a964a06..29636f32d 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -2876,3 +2876,72 @@ fn test_incentive_goes_to_hotkey_when_no_autostake_destination() { ); }); } + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_coinbase_liquidity_reserves --exact --show-output +#[test] +fn test_coinbase_liquidity_reserves() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1001); + let owner_coldkey = U256::from(1002); + let coldkey = U256::from(1); + + // add network + let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + // Setup reserves that will make amount push the liquidity limits + // let tao_reserve_before = TaoCurrency::from(2_000_000_000_000_000); + // let alpha_reserve_before = AlphaCurrency::from(1_700_000_000_000_000); + let tao_reserve_before = TaoCurrency::from(1_000_000_000_000_000); + let alpha_reserve_before = AlphaCurrency::from(1_000_000_000_000_000); + mock::setup_reserves(netuid, tao_reserve_before, alpha_reserve_before); + + // Force the swap to initialize + SubtensorModule::swap_tao_for_alpha( + netuid, + TaoCurrency::ZERO, + 1_000_000_000_000.into(), + false, + ) + .unwrap(); + + // Calculate implied reserves before + let protocol_account_id = pallet_subtensor_swap::Pallet::::protocol_account_id(); + let position_before = pallet_subtensor_swap::Positions::::get(( + netuid, + protocol_account_id, + PositionId::from(1), + )) + .unwrap(); + let price_sqrt_before = pallet_subtensor_swap::AlphaSqrtPrice::::get(netuid); + let (tao_implied_before, _alpha_implied_before) = + position_before.to_token_amounts(price_sqrt_before).unwrap(); + + //////////////////////////////////////////// + // Do emissions + + // Set subnet ema price + SubnetMovingPrice::::insert(netuid, I96F32::from_num(1.0)); + + // Run the coinbase with the emission amount. + SubtensorModule::run_coinbase(U96F32::from_num(1_000_000_000)); + + // Calculate implied reserves after + let position_after = pallet_subtensor_swap::Positions::::get(( + netuid, + protocol_account_id, + PositionId::from(1), + )) + .unwrap(); + let price_sqrt_after = pallet_subtensor_swap::AlphaSqrtPrice::::get(netuid); + let (tao_implied_after, _alpha_implied_after) = + position_after.to_token_amounts(price_sqrt_after).unwrap(); + + // Verify implied and realized reserve diffs match + let tao_reserve_after = SubnetTAO::::get(netuid); + assert_abs_diff_eq!( + tao_reserve_after - tao_reserve_before, + (tao_implied_after - tao_implied_before).into(), + epsilon = 1.into() + ); + }); +} diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 14ec878e7..f9058b076 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -476,7 +476,7 @@ parameter_types! { pub const SwapProtocolId: PalletId = PalletId(*b"ten/swap"); pub const SwapMaxFeeRate: u16 = 10000; // 15.26% pub const SwapMaxPositions: u32 = 100; - pub const SwapMinimumLiquidity: u64 = 1_000; + pub const SwapMinimumLiquidity: u128 = 1_000; pub const SwapMinimumReserve: NonZeroU64 = NonZeroU64::new(100).unwrap(); } diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index d7b0e15ea..293509d34 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -2067,7 +2067,7 @@ fn massive_dissolve_refund_and_reregistration_flow_is_lossless_and_cleans_state( "subnet {net:?} still exists" ); assert!( - pallet_subtensor_swap::Ticks::::iter_prefix(net) + pallet_subtensor_swap::Ticks128::::iter_prefix(net) .next() .is_none(), "ticks not cleared for net {net:?}" @@ -2088,7 +2088,7 @@ fn massive_dissolve_refund_and_reregistration_flow_is_lossless_and_cleans_state( "FeeGlobalAlpha nonzero for net {net:?}" ); assert_eq!( - pallet_subtensor_swap::CurrentLiquidity::::get(net), + pallet_subtensor_swap::CurrentLiquidity128::::get(net), 0, "CurrentLiquidity not zero for net {net:?}" ); diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index 31826b681..7d95f554e 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -7,6 +7,7 @@ use frame_support::sp_runtime::DispatchError; use frame_support::{assert_err, assert_noop, assert_ok, traits::Currency}; use frame_system::RawOrigin; use pallet_subtensor_swap::Call as SwapCall; +use pallet_subtensor_swap::position::PositionId; use pallet_subtensor_swap::tick::TickIndex; use safe_math::FixedExt; use sp_core::{Get, H256, U256}; @@ -5576,3 +5577,84 @@ fn test_remove_root_updates_counters() { ); }); } + +// cargo test --package pallet-subtensor --lib -- tests::staking::test_add_stake_liquidity_reserves --exact --show-output +#[test] +fn test_add_stake_liquidity_reserves() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1001); + let owner_coldkey = U256::from(1002); + let coldkey = U256::from(1); + let amount = TaoCurrency::from(10_000_000_000_000_000); + + // add network + let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + // Give it some $$$ in his coldkey balance + SubtensorModule::add_balance_to_coldkey_account(&coldkey, amount.into()); + + // Setup reserves that will make amount push the liquidity limits + let tao_reserve_before = TaoCurrency::from(20_000_000_000_000); + let alpha_reserve_before = AlphaCurrency::from(1_700_000_000_000_000); + mock::setup_reserves(netuid, tao_reserve_before, alpha_reserve_before); + + // Force the swap to initialize + SubtensorModule::swap_tao_for_alpha( + netuid, + TaoCurrency::ZERO, + 1_000_000_000_000.into(), + false, + ) + .unwrap(); + + // Calculate implied reserves before + let protocol_account_id = pallet_subtensor_swap::Pallet::::protocol_account_id(); + let position_before = pallet_subtensor_swap::Positions::::get(( + netuid, + protocol_account_id, + PositionId::from(1), + )) + .unwrap(); + let price_sqrt_before = pallet_subtensor_swap::AlphaSqrtPrice::::get(netuid); + let (tao_implied_before, _alpha_implied_before) = + position_before.to_token_amounts(price_sqrt_before).unwrap(); + + //////////////////////////////////////////// + // Call add_stake extrinsic + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + owner_hotkey, + netuid, + amount + )); + + //////////////////////////////////////////// + // Call remove_stake extrinsic for a small alpha amount + let unstake_amount = AlphaCurrency::from(100_000_000); + remove_stake_rate_limit_for_tests(&owner_hotkey, &coldkey, netuid); + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(coldkey), + owner_hotkey, + netuid, + unstake_amount + )); + + // Calculate implied reserves after + let position_after = pallet_subtensor_swap::Positions::::get(( + netuid, + protocol_account_id, + PositionId::from(1), + )) + .unwrap(); + let price_sqrt_after = pallet_subtensor_swap::AlphaSqrtPrice::::get(netuid); + let (tao_implied_after, _alpha_implied_after) = + position_after.to_token_amounts(price_sqrt_after).unwrap(); + + // Verify implied and realized reserve diffs match + let tao_reserve_after = SubnetTAO::::get(netuid); + assert_eq!( + tao_reserve_after - tao_reserve_before, + (tao_implied_after - tao_implied_before).into(), + ); + }); +} diff --git a/pallets/swap-interface/src/lib.rs b/pallets/swap-interface/src/lib.rs index ae7d375f9..7acf21021 100644 --- a/pallets/swap-interface/src/lib.rs +++ b/pallets/swap-interface/src/lib.rs @@ -50,6 +50,14 @@ pub trait SwapHandler { fn dissolve_all_liquidity_providers(netuid: NetUid) -> DispatchResult; fn toggle_user_liquidity(netuid: NetUid, enabled: bool); fn clear_protocol_liquidity(netuid: NetUid) -> DispatchResult; + + // Migrations + fn migrate_fix_liquidity_ticks(weight: &mut Weight); + fn migrate_fix_current_liquidity(weight: &mut Weight); + fn migrate_get_implied_reserves( + netuid: NetUid, + weight: &mut Weight, + ) -> (TaoCurrency, AlphaCurrency); } pub trait DefaultPriceLimit diff --git a/pallets/swap/src/benchmarking.rs b/pallets/swap/src/benchmarking.rs index 0d926a640..19cab9a1e 100644 --- a/pallets/swap/src/benchmarking.rs +++ b/pallets/swap/src/benchmarking.rs @@ -12,8 +12,8 @@ use subtensor_runtime_common::NetUid; use crate::{ pallet::{ - AlphaSqrtPrice, Call, Config, CurrentLiquidity, CurrentTick, EnabledUserLiquidity, Pallet, - Positions, SwapV3Initialized, + AlphaSqrtPrice, Call, Config, CurrentLiquidity128, CurrentTick, EnabledUserLiquidity, + Pallet, Positions, SwapV3Initialized, }, position::{Position, PositionId}, tick::TickIndex, @@ -40,7 +40,7 @@ mod benchmarks { SwapV3Initialized::::insert(netuid, true); AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1)); CurrentTick::::insert(netuid, TickIndex::new(0).unwrap()); - CurrentLiquidity::::insert(netuid, T::MinimumLiquidity::get()); + CurrentLiquidity128::::insert(netuid, T::MinimumLiquidity::get()); } let caller: T::AccountId = whitelisted_caller(); @@ -67,7 +67,7 @@ mod benchmarks { SwapV3Initialized::::insert(netuid, true); AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1)); CurrentTick::::insert(netuid, TickIndex::new(0).unwrap()); - CurrentLiquidity::::insert(netuid, T::MinimumLiquidity::get()); + CurrentLiquidity128::::insert(netuid, T::MinimumLiquidity::get()); } let caller: T::AccountId = whitelisted_caller(); @@ -100,7 +100,7 @@ mod benchmarks { SwapV3Initialized::::insert(netuid, true); AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1)); CurrentTick::::insert(netuid, TickIndex::new(0).unwrap()); - CurrentLiquidity::::insert(netuid, T::MinimumLiquidity::get()); + CurrentLiquidity128::::insert(netuid, T::MinimumLiquidity::get()); } let caller: T::AccountId = whitelisted_caller(); diff --git a/pallets/swap/src/mock.rs b/pallets/swap/src/mock.rs index aacdf9083..c89a60304 100644 --- a/pallets/swap/src/mock.rs +++ b/pallets/swap/src/mock.rs @@ -83,7 +83,7 @@ parameter_types! { pub const SwapProtocolId: PalletId = PalletId(*b"ten/swap"); pub const MaxFeeRate: u16 = 10000; // 15.26% pub const MaxPositions: u32 = 100; - pub const MinimumLiquidity: u64 = 1_000; + pub const MinimumLiquidity: u128 = 1_000; pub const MinimumReserves: NonZeroU64 = NonZeroU64::new(1).unwrap(); } diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index 34b5e624e..a0b22d41b 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -1,10 +1,11 @@ use core::ops::Neg; +use frame_support::pallet_prelude::Weight; use frame_support::storage::{TransactionOutcome, transactional}; use frame_support::{ensure, pallet_prelude::DispatchError, traits::Get}; use safe_math::*; -use sp_arithmetic::helpers_128bit; use sp_runtime::{DispatchResult, Vec, traits::AccountIdConversion}; +use sp_std::collections::btree_map::BTreeMap; use substrate_fixed::types::{I64F64, U64F64, U96F32}; use subtensor_runtime_common::{ AlphaCurrency, BalanceOps, Currency, CurrencyReserve, NetUid, SubnetInfo, TaoCurrency, @@ -18,7 +19,7 @@ use super::swap_step::{BasicSwapStep, SwapStep, SwapStepAction}; use crate::{ SqrtPrice, position::{Position, PositionId}, - tick::{ActiveTickIndexManager, Tick, TickIndex}, + tick::{ActiveTickIndexManager, Tick128, TickIndex}, }; const MAX_SWAP_ITERATIONS: u16 = 1000; @@ -67,6 +68,18 @@ impl Pallet { } } + /// Converts tao amount to liquidity assuming there's proportionally + /// matching alpha amount beside tao. + pub fn token_amounts_to_protocol_liquidity( + tao: TaoCurrency, + sqrt_price_curr: SqrtPrice, + ) -> u64 { + let sqrt_price_low = TickIndex::min_sqrt_price(); + U64F64::saturating_from_num(tao) + .safe_div(sqrt_price_curr.saturating_sub(sqrt_price_low)) + .saturating_to_num::() + } + // initializes V3 swap for a subnet if needed pub(super) fn maybe_initialize_v3(netuid: NetUid) -> Result<(), Error> { if SwapV3Initialized::::get(netuid) { @@ -95,9 +108,7 @@ impl Pallet { // Set initial (protocol owned) liquidity and positions // Protocol liquidity makes one position from TickIndex::MIN to TickIndex::MAX // We are using the sp_arithmetic sqrt here, which works for u128 - let liquidity = helpers_128bit::sqrt( - (tao_reserve.to_u64() as u128).saturating_mul(alpha_reserve.to_u64() as u128), - ) as u64; + let liquidity = Self::token_amounts_to_protocol_liquidity(tao_reserve, current_sqrt_price); let protocol_account_id = Self::protocol_account_id(); let (position, _, _) = Self::add_liquidity_not_insert( @@ -168,7 +179,7 @@ impl Pallet { let current_sqrt_price = AlphaSqrtPrice::::get(netuid); let tao_reservoir = ScrapReservoirTao::::get(netuid); let alpha_reservoir = ScrapReservoirAlpha::::get(netuid); - let (corrected_tao_delta, corrected_alpha_delta, tao_scrap, alpha_scrap) = + let (corrected_tao_delta, _corrected_alpha_delta, tao_scrap, alpha_scrap) = Self::get_proportional_alpha_tao_and_remainders( current_sqrt_price, tao_delta @@ -185,18 +196,15 @@ impl Pallet { // Adjust liquidity let maybe_token_amounts = position.to_token_amounts(current_sqrt_price); - if let Ok((tao, alpha)) = maybe_token_amounts { + if let Ok((tao, _alpha)) = maybe_token_amounts { // Get updated reserves, calculate liquidity let new_tao_reserve = tao.saturating_add(corrected_tao_delta.to_u64()); - let new_alpha_reserve = alpha.saturating_add(corrected_alpha_delta.to_u64()); - let new_liquidity = helpers_128bit::sqrt( - (new_tao_reserve as u128).saturating_mul(new_alpha_reserve as u128), - ) as u64; + let new_liquidity = Self::token_amounts_to_protocol_liquidity(new_tao_reserve.into(), current_sqrt_price); let liquidity_delta = new_liquidity.saturating_sub(position.liquidity); // Update current liquidity - CurrentLiquidity::::mutate(netuid, |current_liquidity| { - *current_liquidity = current_liquidity.saturating_add(liquidity_delta); + CurrentLiquidity128::::mutate(netuid, |current_liquidity| { + *current_liquidity = current_liquidity.saturating_add(liquidity_delta as u128); }); // Update protocol position @@ -311,7 +319,7 @@ impl Pallet { log::trace!("\nIteration: {iteration_counter}"); log::trace!( "\tCurrent Liquidity: {}", - CurrentLiquidity::::get(netuid) + CurrentLiquidity128::::get(netuid) ); // Create and execute a swap step @@ -381,21 +389,14 @@ impl Pallet { } } - pub fn find_closest_lower_active_tick(netuid: NetUid, index: TickIndex) -> Option { + pub fn find_closest_lower_active_tick(netuid: NetUid, index: TickIndex) -> Option { ActiveTickIndexManager::::find_closest_lower(netuid, index) - .and_then(|ti| Ticks::::get(netuid, ti)) + .and_then(|ti| Ticks128::::get(netuid, ti)) } - pub fn find_closest_higher_active_tick(netuid: NetUid, index: TickIndex) -> Option { + pub fn find_closest_higher_active_tick(netuid: NetUid, index: TickIndex) -> Option { ActiveTickIndexManager::::find_closest_higher(netuid, index) - .and_then(|ti| Ticks::::get(netuid, ti)) - } - - /// Here we subtract minimum safe liquidity from current liquidity to stay in the safe range - pub(crate) fn current_liquidity_safe(netuid: NetUid) -> U64F64 { - U64F64::saturating_from_num( - CurrentLiquidity::::get(netuid).saturating_sub(T::MinimumLiquidity::get()), - ) + .and_then(|ti| Ticks128::::get(netuid, ti)) } /// Adds liquidity to the specified price range. @@ -461,7 +462,7 @@ impl Pallet { // Small delta is not allowed ensure!( - liquidity >= T::MinimumLiquidity::get(), + liquidity as u128 >= T::MinimumLiquidity::get(), Error::::InvalidLiquidityValue ); @@ -598,11 +599,11 @@ impl Pallet { } else { // Update current liquidity if price is in range let new_liquidity_curr = if liquidity_delta > 0 { - CurrentLiquidity::::get(netuid).saturating_add(delta_liquidity_abs) + CurrentLiquidity128::::get(netuid).saturating_add(delta_liquidity_abs as u128) } else { - CurrentLiquidity::::get(netuid).saturating_sub(delta_liquidity_abs) + CurrentLiquidity128::::get(netuid).saturating_sub(delta_liquidity_abs as u128) }; - CurrentLiquidity::::set(netuid, new_liquidity_curr); + CurrentLiquidity128::::set(netuid, new_liquidity_curr); current_price_sqrt }; @@ -639,7 +640,8 @@ impl Pallet { // withdraw full amounts let mut remove = false; if (liquidity_delta < 0) - && (position.liquidity.saturating_sub(delta_liquidity_abs) < T::MinimumLiquidity::get()) + && (position.liquidity.saturating_sub(delta_liquidity_abs) + < T::MinimumLiquidity::get() as u64) { delta_liquidity_abs = position.liquidity; remove = true; @@ -694,10 +696,10 @@ impl Pallet { liquidity as i128 }; - Ticks::::mutate(netuid, tick_index, |maybe_tick| match maybe_tick { + Ticks128::::mutate(netuid, tick_index, |maybe_tick| match maybe_tick { Some(tick) => { tick.liquidity_net = tick.liquidity_net.saturating_add(net_liquidity_change); - tick.liquidity_gross = tick.liquidity_gross.saturating_add(liquidity); + tick.liquidity_gross = tick.liquidity_gross.saturating_add(liquidity as u128); } None => { let current_tick = TickIndex::current_bounded::(netuid); @@ -713,9 +715,9 @@ impl Pallet { I64F64::saturating_from_num(0), ) }; - *maybe_tick = Some(Tick { + *maybe_tick = Some(Tick128 { liquidity_net: net_liquidity_change, - liquidity_gross: liquidity, + liquidity_gross: liquidity as u128, fees_out_tao, fees_out_alpha, }); @@ -740,10 +742,10 @@ impl Pallet { liquidity as i128 }; - Ticks::::mutate_exists(netuid, tick_index, |maybe_tick| { + Ticks128::::mutate_exists(netuid, tick_index, |maybe_tick| { if let Some(tick) = maybe_tick { tick.liquidity_net = tick.liquidity_net.saturating_sub(net_reduction); - tick.liquidity_gross = tick.liquidity_gross.saturating_sub(liquidity); + tick.liquidity_gross = tick.liquidity_gross.saturating_sub(liquidity as u128); // If no liquidity is left at the tick, remove it if tick.liquidity_gross == 0 { @@ -760,7 +762,7 @@ impl Pallet { /// range /// /// This function handles both increasing and decreasing liquidity based on the sign of the - /// liquidity parameter. It uses i128 to safely handle values up to u64::MAX in both positive + /// liquidity parameter. It uses i128 to safely handle values up to u128::MAX/2 in both positive /// and negative directions. fn update_liquidity_if_needed( netuid: NetUid, @@ -770,9 +772,9 @@ impl Pallet { ) { let current_tick_index = TickIndex::current_bounded::(netuid); if (tick_low <= current_tick_index) && (current_tick_index < tick_high) { - CurrentLiquidity::::mutate(netuid, |current_liquidity| { + CurrentLiquidity128::::mutate(netuid, |current_liquidity| { let is_neg = liquidity.is_negative(); - let liquidity = liquidity.abs().min(u64::MAX as i128) as u64; + let liquidity = liquidity.unsigned_abs(); if is_neg { *current_liquidity = current_liquidity.saturating_sub(liquidity); } else { @@ -990,18 +992,19 @@ impl Pallet { } // 2) Clear active tick index entries, then all swap state (idempotent even if empty/non‑V3). - let active_ticks: sp_std::vec::Vec = - Ticks::::iter_prefix(netuid).map(|(ti, _)| ti).collect(); + let active_ticks: sp_std::vec::Vec = Ticks128::::iter_prefix(netuid) + .map(|(ti, _)| ti) + .collect(); for ti in active_ticks { ActiveTickIndexManager::::remove(netuid, ti); } let _ = Positions::::clear_prefix((netuid,), u32::MAX, None); - let _ = Ticks::::clear_prefix(netuid, u32::MAX, None); + let _ = Ticks128::::clear_prefix(netuid, u32::MAX, None); FeeGlobalTao::::remove(netuid); FeeGlobalAlpha::::remove(netuid); - CurrentLiquidity::::remove(netuid); + CurrentLiquidity128::::remove(netuid); CurrentTick::::remove(netuid); AlphaSqrtPrice::::remove(netuid); SwapV3Initialized::::remove(netuid); @@ -1016,6 +1019,17 @@ impl Pallet { Ok(()) } + + fn load_current_ticks() -> BTreeMap { + let mut map: BTreeMap = BTreeMap::new(); + + // CurrentTick: (netuid) -> TickIndex + for (netuid, tick) in CurrentTick::::iter() { + map.insert(netuid, tick); + } + + map + } } impl DefaultPriceLimit for Pallet { @@ -1140,4 +1154,91 @@ impl SwapHandler for Pallet { fn clear_protocol_liquidity(netuid: NetUid) -> DispatchResult { Self::do_clear_protocol_liquidity(netuid) } + + /// Migrate Ticks to Ticks128 fixing the overflown ticks + fn migrate_fix_liquidity_ticks(weight: &mut Weight) { + let mut moved: u64 = 0; + + let current_tick_map = Self::load_current_ticks(); + let mut keys_to_remove: Vec<(NetUid, TickIndex)> = Vec::new(); + for (netuid, tick_idx, tick) in Ticks::::iter() { + let current_tick = current_tick_map.get(&netuid).copied().unwrap_or_default(); + + // Fix ticks that saturated (some low ticks) + let tick128 = if (tick_idx < TickIndex::try_from(-400000).unwrap_or_default()) + && (tick_idx < current_tick) + && (tick.liquidity_gross == 0xFFFFFFFFFFFFFFFF) + { + Tick128 { + liquidity_net: tick.liquidity_net, + liquidity_gross: tick.liquidity_net.unsigned_abs(), + fees_out_tao: tick.fees_out_tao, + fees_out_alpha: tick.fees_out_alpha, + } + } else { + Tick128 { + liquidity_net: tick.liquidity_net, + liquidity_gross: tick.liquidity_net as u128, + fees_out_tao: tick.fees_out_tao, + fees_out_alpha: tick.fees_out_alpha, + } + }; + + Ticks128::::insert(netuid, tick_idx, tick128); + keys_to_remove.push((netuid, tick_idx)); + moved = moved.saturating_add(1); + } + + for (netuid, tick_idx) in keys_to_remove.iter() { + Ticks::::remove(netuid, tick_idx); + } + + // Return a conservative weight estimate: moved reads + moved writes * 2. + *weight = + weight.saturating_add(T::DbWeight::get().reads_writes(moved, moved.saturating_mul(2))); + } + + /// Migrate CurrentLiquidity to CurrentLiquidity128 + fn migrate_fix_current_liquidity(weight: &mut Weight) { + let mut keys_to_remove: Vec = Vec::new(); + let mut moved: u64 = 0; + for (netuid, liquidity) in CurrentLiquidity::::iter() { + CurrentLiquidity128::::insert(netuid, liquidity as u128); + keys_to_remove.push(netuid); + moved = moved.saturating_add(1); + } + for netuid in keys_to_remove.iter() { + CurrentLiquidity::::remove(netuid); + } + + // Return a conservative weight estimate: moved reads + moved writes * 2. + *weight = + weight.saturating_add(T::DbWeight::get().reads_writes(moved, moved.saturating_mul(2))); + } + + /// Get implied reserves for a subnet + fn migrate_get_implied_reserves( + netuid: NetUid, + weight: &mut Weight, + ) -> (TaoCurrency, AlphaCurrency) { + // current sqrt(price) for this subnet + let current_price_sqrt = AlphaSqrtPrice::::get(netuid); + let mut reads: u64 = 1; + + let mut total_tao: TaoCurrency = Default::default(); + let mut total_alpha: AlphaCurrency = Default::default(); + + // Iterate all LP positions for this subnet (triple-key NMap: (netuid, account, position_id)) + // `iter_prefix(netuid)` yields ((account, position_id), Position) + for ((_account, _pos_id), position) in Positions::::iter_prefix((netuid,)) { + if let Ok((tao, alpha)) = position.to_token_amounts(current_price_sqrt) { + total_tao = total_tao.saturating_add(TaoCurrency::from(tao)); + total_alpha = total_alpha.saturating_add(AlphaCurrency::from(alpha)); + } + reads = reads.saturating_add(1); + } + *weight = weight.saturating_add(T::DbWeight::get().reads_writes(0, reads)); + + (total_tao, total_alpha) + } } diff --git a/pallets/swap/src/pallet/mod.rs b/pallets/swap/src/pallet/mod.rs index 1501f9cb3..b04e5ab6c 100644 --- a/pallets/swap/src/pallet/mod.rs +++ b/pallets/swap/src/pallet/mod.rs @@ -10,7 +10,7 @@ use subtensor_runtime_common::{ use crate::{ position::{Position, PositionId}, - tick::{LayerLevel, Tick, TickIndex}, + tick::{LayerLevel, Tick, Tick128, TickIndex}, weights::WeightInfo, }; @@ -62,7 +62,7 @@ mod pallet { /// Minimum liquidity that is safe for rounding and integer math. #[pallet::constant] - type MinimumLiquidity: Get; + type MinimumLiquidity: Get; /// Minimum reserve for tao and alpha #[pallet::constant] @@ -93,6 +93,9 @@ mod pallet { /// Storage for all ticks, using subnet ID as the primary key and tick index as the secondary key #[pallet::storage] pub type Ticks = StorageDoubleMap<_, Twox64Concat, NetUid, Twox64Concat, TickIndex, Tick>; + #[pallet::storage] + pub type Ticks128 = + StorageDoubleMap<_, Twox64Concat, NetUid, Twox64Concat, TickIndex, Tick128>; /// Storage to determine whether swap V3 was initialized for a specific subnet. #[pallet::storage] @@ -109,6 +112,8 @@ mod pallet { /// Storage for the current liquidity amount for each subnet. #[pallet::storage] pub type CurrentLiquidity = StorageMap<_, Twox64Concat, NetUid, u64, ValueQuery>; + #[pallet::storage] + pub type CurrentLiquidity128 = StorageMap<_, Twox64Concat, NetUid, u128, ValueQuery>; /// Indicates whether a subnet has been switched to V3 swap from V2. /// If `true`, the subnet is permanently on V3 swap mode allowing add/remove liquidity diff --git a/pallets/swap/src/pallet/swap_step.rs b/pallets/swap/src/pallet/swap_step.rs index 6791835b1..02ec01460 100644 --- a/pallets/swap/src/pallet/swap_step.rs +++ b/pallets/swap/src/pallet/swap_step.rs @@ -65,7 +65,7 @@ where let possible_delta_in = amount_remaining.saturating_sub(fee); // Target price and quantities - let current_liquidity = U64F64::saturating_from_num(CurrentLiquidity::::get(netuid)); + let current_liquidity = U64F64::saturating_from_num(CurrentLiquidity128::::get(netuid)); let target_sqrt_price = Self::sqrt_price_target(current_liquidity, current_sqrt_price, possible_delta_in); @@ -187,23 +187,20 @@ where /// Process a single step of a swap fn process_swap(&self) -> Result, Error> { // Hold the fees - Self::add_fees( - self.netuid, - Pallet::::current_liquidity_safe(self.netuid), - self.fee, - ); + let current_liquidity = CurrentLiquidity128::::get(self.netuid); + Self::add_fees(self.netuid, current_liquidity, self.fee); let delta_out = Self::convert_deltas(self.netuid, self.delta_in); // log::trace!("\tDelta Out : {delta_out:?}"); if self.action == SwapStepAction::Crossing { - let mut tick = Ticks::::get(self.netuid, self.edge_tick).unwrap_or_default(); + let mut tick = Ticks128::::get(self.netuid, self.edge_tick).unwrap_or_default(); tick.fees_out_tao = I64F64::saturating_from_num(FeeGlobalTao::::get(self.netuid)) .saturating_sub(tick.fees_out_tao); tick.fees_out_alpha = I64F64::saturating_from_num(FeeGlobalAlpha::::get(self.netuid)) .saturating_sub(tick.fees_out_alpha); Self::update_liquidity_at_crossing(self.netuid)?; - Ticks::::insert(self.netuid, self.edge_tick, tick); + Ticks128::::insert(self.netuid, self.edge_tick, tick); } // Update current price @@ -275,16 +272,36 @@ impl SwapStep SwapStepAction::Crossing } - fn add_fees(netuid: NetUid, current_liquidity: U64F64, fee: TaoCurrency) { + fn add_fees(netuid: NetUid, current_liquidity: u128, fee: TaoCurrency) { if current_liquidity == 0 { return; } - let fee_fixed = U64F64::saturating_from_num(fee.to_u64()); + if current_liquidity <= u64::MAX as u128 { + let fee_fixed = U64F64::saturating_from_num(fee.to_u64()); + let current_liquidity_fixed = U64F64::saturating_from_num(current_liquidity); + FeeGlobalTao::::mutate(netuid, |value| { + *value = value.saturating_add(fee_fixed.safe_div(current_liquidity_fixed)) + }); + } else { + // Decompose current_liquidity = hi * 2^64 + lo + const TWO_POW_64: u128 = 1u128 << 64; + let hi: u64 = (current_liquidity.safe_div(TWO_POW_64)) as u64; + let lo: u64 = (current_liquidity + .checked_rem(TWO_POW_64) + .unwrap_or_default()) as u64; + + // Build liquidity_factor = 2^-64 / (hi + lo/2^64) in U64F64 + let s: U64F64 = U64F64::saturating_from_num(hi).saturating_add( + U64F64::saturating_from_num(lo).safe_div(U64F64::saturating_from_num(TWO_POW_64)), + ); + // 2^-64 = from_bits(1) + let liquidity_factor: U64F64 = U64F64::from_bits(1).safe_div(s); - FeeGlobalTao::::mutate(netuid, |value| { - *value = value.saturating_add(fee_fixed.safe_div(current_liquidity)) - }); + // result = fee * inv + let fee_delta = U64F64::saturating_from_num(fee).saturating_mul(liquidity_factor); + FeeGlobalTao::::mutate(netuid, |value| *value = value.saturating_add(fee_delta)); + } } fn convert_deltas(netuid: NetUid, delta_in: TaoCurrency) -> AlphaCurrency { @@ -293,7 +310,7 @@ impl SwapStep return AlphaCurrency::ZERO; } - let liquidity_curr = SqrtPrice::saturating_from_num(CurrentLiquidity::::get(netuid)); + let liquidity_curr = SqrtPrice::saturating_from_num(CurrentLiquidity128::::get(netuid)); let sqrt_price_curr = AlphaSqrtPrice::::get(netuid); let delta_fixed = SqrtPrice::saturating_from_num(delta_in.to_u64()); @@ -315,7 +332,7 @@ impl SwapStep } fn update_liquidity_at_crossing(netuid: NetUid) -> Result<(), Error> { - let mut liquidity_curr = CurrentLiquidity::::get(netuid); + let mut liquidity_curr = CurrentLiquidity128::::get(netuid); let current_tick_index = TickIndex::current_bounded::(netuid); // Find the appropriate tick based on order type @@ -326,20 +343,20 @@ impl SwapStep current_tick_index.next().unwrap_or(TickIndex::MAX), ) .unwrap_or(TickIndex::MAX); - Ticks::::get(netuid, upper_tick) + Ticks128::::get(netuid, upper_tick) } .ok_or(Error::::InsufficientLiquidity)?; - let liquidity_update_abs_u64 = tick.liquidity_net_as_u64(); + let liquidity_update_abs_u128 = tick.liquidity_net_as_u128(); // Update liquidity based on the sign of liquidity_net and the order type liquidity_curr = if tick.liquidity_net >= 0 { - liquidity_curr.saturating_add(liquidity_update_abs_u64) + liquidity_curr.saturating_add(liquidity_update_abs_u128) } else { - liquidity_curr.saturating_sub(liquidity_update_abs_u64) + liquidity_curr.saturating_sub(liquidity_update_abs_u128) }; - CurrentLiquidity::::set(netuid, liquidity_curr); + CurrentLiquidity128::::set(netuid, liquidity_curr); Ok(()) } @@ -411,16 +428,36 @@ impl SwapStep SwapStepAction::Stop } - fn add_fees(netuid: NetUid, current_liquidity: U64F64, fee: AlphaCurrency) { + fn add_fees(netuid: NetUid, current_liquidity: u128, fee: AlphaCurrency) { if current_liquidity == 0 { return; } - let fee_fixed = U64F64::saturating_from_num(fee.to_u64()); + if current_liquidity <= u64::MAX as u128 { + let fee_fixed = U64F64::saturating_from_num(fee.to_u64()); + let current_liquidity_fixed = U64F64::saturating_from_num(current_liquidity); + FeeGlobalAlpha::::mutate(netuid, |value| { + *value = value.saturating_add(fee_fixed.safe_div(current_liquidity_fixed)) + }); + } else { + // Decompose current_liquidity = hi * 2^64 + lo + const TWO_POW_64: u128 = 1u128 << 64; + let hi: u64 = (current_liquidity.safe_div(TWO_POW_64)) as u64; + let lo: u64 = (current_liquidity + .checked_rem(TWO_POW_64) + .unwrap_or_default()) as u64; + + // Build liquidity_factor = 2^-64 / (hi + lo/2^64) in U64F64 + let s: U64F64 = U64F64::saturating_from_num(hi).saturating_add( + U64F64::saturating_from_num(lo).safe_div(U64F64::saturating_from_num(TWO_POW_64)), + ); + // 2^-64 = from_bits(1) + let liquidity_factor: U64F64 = U64F64::from_bits(1).safe_div(s); - FeeGlobalAlpha::::mutate(netuid, |value| { - *value = value.saturating_add(fee_fixed.safe_div(current_liquidity)) - }); + // result = fee * inv + let fee_delta = U64F64::saturating_from_num(fee).saturating_mul(liquidity_factor); + FeeGlobalAlpha::::mutate(netuid, |value| *value = value.saturating_add(fee_delta)); + } } fn convert_deltas(netuid: NetUid, delta_in: AlphaCurrency) -> TaoCurrency { @@ -429,7 +466,7 @@ impl SwapStep return TaoCurrency::ZERO; } - let liquidity_curr = SqrtPrice::saturating_from_num(CurrentLiquidity::::get(netuid)); + let liquidity_curr = SqrtPrice::saturating_from_num(CurrentLiquidity128::::get(netuid)); let sqrt_price_curr = AlphaSqrtPrice::::get(netuid); let delta_fixed = SqrtPrice::saturating_from_num(delta_in.to_u64()); @@ -452,7 +489,7 @@ impl SwapStep } fn update_liquidity_at_crossing(netuid: NetUid) -> Result<(), Error> { - let mut liquidity_curr = CurrentLiquidity::::get(netuid); + let mut liquidity_curr = CurrentLiquidity128::::get(netuid); let current_tick_index = TickIndex::current_bounded::(netuid); // Find the appropriate tick based on order type @@ -472,20 +509,20 @@ impl SwapStep ) .unwrap_or(TickIndex::MIN) }; - Ticks::::get(netuid, lower_tick) + Ticks128::::get(netuid, lower_tick) } .ok_or(Error::::InsufficientLiquidity)?; - let liquidity_update_abs_u64 = tick.liquidity_net_as_u64(); + let liquidity_update_abs_u128 = tick.liquidity_net_as_u128(); // Update liquidity based on the sign of liquidity_net and the order type liquidity_curr = if tick.liquidity_net >= 0 { - liquidity_curr.saturating_sub(liquidity_update_abs_u64) + liquidity_curr.saturating_sub(liquidity_update_abs_u128) } else { - liquidity_curr.saturating_add(liquidity_update_abs_u64) + liquidity_curr.saturating_add(liquidity_update_abs_u128) }; - CurrentLiquidity::::set(netuid, liquidity_curr); + CurrentLiquidity128::::set(netuid, liquidity_curr); Ok(()) } @@ -531,7 +568,7 @@ where fn action_on_edge_sqrt_price() -> SwapStepAction; /// Add fees to the global fee counters - fn add_fees(netuid: NetUid, current_liquidity: U64F64, fee: PaidIn); + fn add_fees(netuid: NetUid, current_liquidity: u128, fee: PaidIn); /// Convert input amount (delta_in) to output amount (delta_out) /// diff --git a/pallets/swap/src/pallet/tests.rs b/pallets/swap/src/pallet/tests.rs index 5b8cca643..b65c0e838 100644 --- a/pallets/swap/src/pallet/tests.rs +++ b/pallets/swap/src/pallet/tests.rs @@ -182,7 +182,7 @@ fn test_swap_initialization() { // Calculate expected liquidity let expected_liquidity = helpers_128bit::sqrt((tao.to_u64() as u128).saturating_mul(alpha.to_u64() as u128)) - as u64; + as i128; // Get the protocol account let protocol_account_id = Pallet::::protocol_account_id(); @@ -193,24 +193,27 @@ fn test_swap_initialization() { assert_eq!(positions.len(), 1); let position = &positions[0]; - assert_eq!(position.liquidity, expected_liquidity); + assert_eq!(position.liquidity, expected_liquidity as u64); assert_eq!(position.tick_low, TickIndex::MIN); assert_eq!(position.tick_high, TickIndex::MAX); assert_eq!(position.fees_tao, 0); assert_eq!(position.fees_alpha, 0); // Verify ticks were created - let tick_low = Ticks::::get(netuid, TickIndex::MIN).unwrap(); - let tick_high = Ticks::::get(netuid, TickIndex::MAX).unwrap(); + let tick_low = Ticks128::::get(netuid, TickIndex::MIN).unwrap(); + let tick_high = Ticks128::::get(netuid, TickIndex::MAX).unwrap(); // Check liquidity values - assert_eq!(tick_low.liquidity_net, expected_liquidity as i128); - assert_eq!(tick_low.liquidity_gross, expected_liquidity); - assert_eq!(tick_high.liquidity_net, -(expected_liquidity as i128)); - assert_eq!(tick_high.liquidity_gross, expected_liquidity); + assert_eq!(tick_low.liquidity_net, expected_liquidity); + assert_eq!(tick_low.liquidity_gross, expected_liquidity as u128); + assert_eq!(tick_high.liquidity_net, -(expected_liquidity)); + assert_eq!(tick_high.liquidity_gross, expected_liquidity as u128); // Verify current liquidity is set - assert_eq!(CurrentLiquidity::::get(netuid), expected_liquidity); + assert_eq!( + CurrentLiquidity128::::get(netuid), + expected_liquidity as u128 + ); }); } @@ -275,10 +278,11 @@ fn test_add_liquidity_basic() { let tick_high = price_to_tick(price_high); // Get tick infos and liquidity before adding (to account for protocol liquidity) - let tick_low_info_before = Ticks::::get(netuid, tick_low).unwrap_or_default(); + let tick_low_info_before = + Ticks128::::get(netuid, tick_low).unwrap_or_default(); let tick_high_info_before = - Ticks::::get(netuid, tick_high).unwrap_or_default(); - let liquidity_before = CurrentLiquidity::::get(netuid); + Ticks128::::get(netuid, tick_high).unwrap_or_default(); + let liquidity_before = CurrentLiquidity128::::get(netuid); // Add liquidity let (position_id, tao, alpha) = Pallet::::do_add_liquidity( @@ -295,12 +299,12 @@ fn test_add_liquidity_basic() { assert_abs_diff_eq!(alpha, expected_alpha, epsilon = alpha / 1000); // Check that low and high ticks appear in the state and are properly updated - let tick_low_info = Ticks::::get(netuid, tick_low).unwrap(); - let tick_high_info = Ticks::::get(netuid, tick_high).unwrap(); + let tick_low_info = Ticks128::::get(netuid, tick_low).unwrap(); + let tick_high_info = Ticks128::::get(netuid, tick_high).unwrap(); let expected_liquidity_net_low = liquidity as i128; - let expected_liquidity_gross_low = liquidity; + let expected_liquidity_gross_low = liquidity as u128; let expected_liquidity_net_high = -(liquidity as i128); - let expected_liquidity_gross_high = liquidity; + let expected_liquidity_gross_high = liquidity as u128; assert_eq!( tick_low_info.liquidity_net - tick_low_info_before.liquidity_net, @@ -336,12 +340,12 @@ fn test_add_liquidity_basic() { // Current liquidity is updated only when price range includes the current price let expected_liquidity = if (price_high > current_price) && (price_low <= current_price) { - liquidity_before + liquidity + liquidity_before + liquidity as u128 } else { liquidity_before }; - assert_eq!(CurrentLiquidity::::get(netuid), expected_liquidity) + assert_eq!(CurrentLiquidity128::::get(netuid), expected_liquidity) }, ); }); @@ -538,7 +542,7 @@ fn test_remove_liquidity_basic() { let tick_high = price_to_tick(price_high); assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - let liquidity_before = CurrentLiquidity::::get(netuid); + let liquidity_before = CurrentLiquidity128::::get(netuid); // Add liquidity let (position_id, _, _) = Pallet::::do_add_liquidity( @@ -572,7 +576,7 @@ fn test_remove_liquidity_basic() { assert!(Positions::::get((netuid, OK_COLDKEY_ACCOUNT_ID, position_id)).is_none()); // Current liquidity is updated (back where it was) - assert_eq!(CurrentLiquidity::::get(netuid), liquidity_before); + assert_eq!(CurrentLiquidity128::::get(netuid), liquidity_before); }); }); } @@ -664,8 +668,8 @@ fn test_modify_position_basic() { .unwrap(); // Get tick infos before the swap/update - let tick_low_info_before = Ticks::::get(netuid, tick_low).unwrap(); - let tick_high_info_before = Ticks::::get(netuid, tick_high).unwrap(); + let tick_low_info_before = Ticks128::::get(netuid, tick_low).unwrap(); + let tick_high_info_before = Ticks128::::get(netuid, tick_high).unwrap(); // Swap to create fees on the position let sqrt_limit_price = SqrtPrice::from_num((limit_price).sqrt()); @@ -673,7 +677,7 @@ fn test_modify_position_basic() { Pallet::::do_swap(netuid, order, sqrt_limit_price, false, false).unwrap(); // Modify liquidity (also causes claiming of fees) - let liquidity_before = CurrentLiquidity::::get(netuid); + let liquidity_before = CurrentLiquidity128::::get(netuid); let modify_result = Pallet::::do_modify_position( netuid, &OK_COLDKEY_ACCOUNT_ID, @@ -697,7 +701,7 @@ fn test_modify_position_basic() { ); // Current liquidity is reduced with modify_position - assert!(CurrentLiquidity::::get(netuid) < liquidity_before); + assert!(CurrentLiquidity128::::get(netuid) < liquidity_before); // Position liquidity is reduced let position = @@ -707,15 +711,15 @@ fn test_modify_position_basic() { assert_eq!(position.tick_high, tick_high); // Tick liquidity is updated properly for low and high position ticks - let tick_low_info_after = Ticks::::get(netuid, tick_low).unwrap(); - let tick_high_info_after = Ticks::::get(netuid, tick_high).unwrap(); + let tick_low_info_after = Ticks128::::get(netuid, tick_low).unwrap(); + let tick_high_info_after = Ticks128::::get(netuid, tick_high).unwrap(); assert_eq!( tick_low_info_before.liquidity_net - (liquidity / 10) as i128, tick_low_info_after.liquidity_net, ); assert_eq!( - tick_low_info_before.liquidity_gross - (liquidity / 10), + tick_low_info_before.liquidity_gross - (liquidity / 10) as u128, tick_low_info_after.liquidity_gross, ); assert_eq!( @@ -723,7 +727,7 @@ fn test_modify_position_basic() { tick_high_info_after.liquidity_net, ); assert_eq!( - tick_high_info_before.liquidity_gross - (liquidity / 10), + tick_high_info_before.liquidity_gross - (liquidity / 10) as u128, tick_high_info_after.liquidity_gross, ); @@ -773,9 +777,10 @@ fn test_swap_basic() { assert_ok!(Pallet::::maybe_initialize_v3(netuid)); // Get tick infos before the swap - let tick_low_info_before = Ticks::::get(netuid, tick_low).unwrap_or_default(); - let tick_high_info_before = Ticks::::get(netuid, tick_high).unwrap_or_default(); - let liquidity_before = CurrentLiquidity::::get(netuid); + let tick_low_info_before = Ticks128::::get(netuid, tick_low).unwrap_or_default(); + let tick_high_info_before = + Ticks128::::get(netuid, tick_high).unwrap_or_default(); + let liquidity_before = CurrentLiquidity128::::get(netuid); // Get current price let current_price = Pallet::::current_price(netuid); @@ -803,8 +808,8 @@ fn test_swap_basic() { ); // Check that low and high ticks' fees were updated properly, and liquidity values were not updated - let tick_low_info = Ticks::::get(netuid, tick_low).unwrap(); - let tick_high_info = Ticks::::get(netuid, tick_high).unwrap(); + let tick_low_info = Ticks128::::get(netuid, tick_low).unwrap(); + let tick_high_info = Ticks128::::get(netuid, tick_high).unwrap(); let expected_liquidity_net_low = tick_low_info_before.liquidity_net; let expected_liquidity_gross_low = tick_low_info_before.liquidity_gross; let expected_liquidity_net_high = tick_high_info_before.liquidity_net; @@ -849,7 +854,7 @@ fn test_swap_basic() { assert_eq!(position.fees_tao, 0); // Current liquidity is not updated - assert_eq!(CurrentLiquidity::::get(netuid), liquidity_before); + assert_eq!(CurrentLiquidity128::::get(netuid), liquidity_before); // Assert that price movement is in correct direction let sqrt_current_price_after = AlphaSqrtPrice::::get(netuid); @@ -958,10 +963,11 @@ fn test_swap_single_position() { ); // Get tick infos before the swap - let tick_low_info_before = Ticks::::get(netuid, tick_low).unwrap_or_default(); + let tick_low_info_before = + Ticks128::::get(netuid, tick_low).unwrap_or_default(); let tick_high_info_before = - Ticks::::get(netuid, tick_high).unwrap_or_default(); - let liquidity_before = CurrentLiquidity::::get(netuid); + Ticks128::::get(netuid, tick_high).unwrap_or_default(); + let liquidity_before = CurrentLiquidity128::::get(netuid); assert_abs_diff_eq!( liquidity_before as f64, protocol_liquidity + position_liquidity as f64, @@ -1018,8 +1024,8 @@ fn test_swap_single_position() { } // Check that low and high ticks' fees were updated properly - let tick_low_info = Ticks::::get(netuid, tick_low).unwrap(); - let tick_high_info = Ticks::::get(netuid, tick_high).unwrap(); + let tick_low_info = Ticks128::::get(netuid, tick_low).unwrap(); + let tick_high_info = Ticks128::::get(netuid, tick_high).unwrap(); let expected_liquidity_net_low = tick_low_info_before.liquidity_net; let expected_liquidity_gross_low = tick_low_info_before.liquidity_gross; let expected_liquidity_net_high = tick_high_info_before.liquidity_net; @@ -1211,7 +1217,7 @@ fn test_swap_multiple_positions() { let sqrt_current_price = AlphaSqrtPrice::::get(netuid); let current_price = (sqrt_current_price * sqrt_current_price).to_num::(); - let liquidity_before = CurrentLiquidity::::get(netuid); + let liquidity_before = CurrentLiquidity128::::get(netuid); let output_amount = >::approx_expected_swap_output( sqrt_current_price.to_num(), liquidity_before as f64, @@ -1575,7 +1581,7 @@ fn test_current_liquidity_updates() { // Calculate ticks (assuming tick math is tested separately) let tick_low = price_to_tick(price_low); let tick_high = price_to_tick(price_high); - let liquidity_before = CurrentLiquidity::::get(netuid); + let liquidity_before = CurrentLiquidity128::::get(netuid); // Add liquidity assert_ok!(Pallet::::do_add_liquidity( @@ -1591,13 +1597,13 @@ fn test_current_liquidity_updates() { let expected_liquidity = if (price_high > current_price) && (price_low <= current_price) { assert!(expect_to_update); - liquidity_before + liquidity + liquidity_before + liquidity as u128 } else { assert!(!expect_to_update); liquidity_before }; - assert_eq!(CurrentLiquidity::::get(netuid), expected_liquidity) + assert_eq!(CurrentLiquidity128::::get(netuid), expected_liquidity) }); }); } @@ -1991,9 +1997,9 @@ fn test_liquidate_v3_removes_positions_ticks_and_state() { .collect::>(); assert_eq!(user_positions.len(), 1); - assert!(Ticks::::get(netuid, TickIndex::MIN).is_some()); - assert!(Ticks::::get(netuid, TickIndex::MAX).is_some()); - assert!(CurrentLiquidity::::get(netuid) > 0); + assert!(Ticks128::::get(netuid, TickIndex::MIN).is_some()); + assert!(Ticks128::::get(netuid, TickIndex::MAX).is_some()); + assert!(CurrentLiquidity128::::get(netuid) > 0); let had_bitmap_words = TickIndexBitmapWords::::iter_prefix((netuid,)) .next() @@ -2018,9 +2024,9 @@ fn test_liquidate_v3_removes_positions_ticks_and_state() { assert!(user_positions_after.is_empty()); // ASSERT: ticks cleared - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert!(Ticks::::get(netuid, TickIndex::MIN).is_none()); - assert!(Ticks::::get(netuid, TickIndex::MAX).is_none()); + assert!(Ticks128::::iter_prefix(netuid).next().is_none()); + assert!(Ticks128::::get(netuid, TickIndex::MIN).is_none()); + assert!(Ticks128::::get(netuid, TickIndex::MAX).is_none()); // ASSERT: fee globals cleared assert!(!FeeGlobalTao::::contains_key(netuid)); @@ -2029,7 +2035,7 @@ fn test_liquidate_v3_removes_positions_ticks_and_state() { // ASSERT: price/tick/liquidity flags cleared assert!(!AlphaSqrtPrice::::contains_key(netuid)); assert!(!CurrentTick::::contains_key(netuid)); - assert!(!CurrentLiquidity::::contains_key(netuid)); + assert!(!CurrentLiquidity128::::contains_key(netuid)); assert!(!SwapV3Initialized::::contains_key(netuid)); // ASSERT: active tick bitmap cleared @@ -2099,7 +2105,7 @@ fn test_liquidate_v3_with_user_liquidity_disabled() { .next() .is_none() ); - assert!(Ticks::::iter_prefix(netuid).next().is_none()); + assert!(Ticks128::::iter_prefix(netuid).next().is_none()); assert!( TickIndexBitmapWords::::iter_prefix((netuid,)) .next() @@ -2108,7 +2114,7 @@ fn test_liquidate_v3_with_user_liquidity_disabled() { assert!(!SwapV3Initialized::::contains_key(netuid)); assert!(!AlphaSqrtPrice::::contains_key(netuid)); assert!(!CurrentTick::::contains_key(netuid)); - assert!(!CurrentLiquidity::::contains_key(netuid)); + assert!(!CurrentLiquidity128::::contains_key(netuid)); assert!(!FeeGlobalTao::::contains_key(netuid)); assert!(!FeeGlobalAlpha::::contains_key(netuid)); @@ -2140,7 +2146,7 @@ fn test_liquidate_non_v3_uninitialized_ok_and_clears() { .next() .is_none() ); - assert!(Ticks::::iter_prefix(netuid).next().is_none()); + assert!(Ticks128::::iter_prefix(netuid).next().is_none()); assert!( TickIndexBitmapWords::::iter_prefix((netuid,)) .next() @@ -2150,7 +2156,7 @@ fn test_liquidate_non_v3_uninitialized_ok_and_clears() { // All single-key maps should not have the key after liquidation assert!(!FeeGlobalTao::::contains_key(netuid)); assert!(!FeeGlobalAlpha::::contains_key(netuid)); - assert!(!CurrentLiquidity::::contains_key(netuid)); + assert!(!CurrentLiquidity128::::contains_key(netuid)); assert!(!CurrentTick::::contains_key(netuid)); assert!(!AlphaSqrtPrice::::contains_key(netuid)); assert!(!SwapV3Initialized::::contains_key(netuid)); @@ -2197,7 +2203,7 @@ fn test_liquidate_idempotent() { .next() .is_none() ); - assert!(Ticks::::iter_prefix(netuid).next().is_none()); + assert!(Ticks128::::iter_prefix(netuid).next().is_none()); assert!( TickIndexBitmapWords::::iter_prefix((netuid,)) .next() @@ -2219,7 +2225,7 @@ fn test_liquidate_idempotent() { .next() .is_none() ); - assert!(Ticks::::iter_prefix(netuid).next().is_none()); + assert!(Ticks128::::iter_prefix(netuid).next().is_none()); assert!( TickIndexBitmapWords::::iter_prefix((netuid,)) .next() @@ -2301,7 +2307,7 @@ fn liquidate_v3_refunds_user_funds_and_clears_state() { // User position(s) are gone and all V3 state cleared. assert_eq!(Pallet::::count_positions(netuid, &cold), 0); - assert!(Ticks::::iter_prefix(netuid).next().is_none()); + assert!(Ticks128::::iter_prefix(netuid).next().is_none()); assert!(!SwapV3Initialized::::contains_key(netuid)); }); } @@ -2362,7 +2368,7 @@ fn refund_alpha_single_provider_exact() { assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); // --- State is cleared. - assert!(Ticks::::iter_prefix(netuid).next().is_none()); + assert!(Ticks128::::iter_prefix(netuid).next().is_none()); assert_eq!(Pallet::::count_positions(netuid, &cold), 0); assert!(!SwapV3Initialized::::contains_key(netuid)); }); @@ -2640,10 +2646,10 @@ fn test_dissolve_v3_green_path_refund_tao_stake_alpha_and_clear_state() { "protocol positions must be removed" ); - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert!(Ticks::::get(netuid, TickIndex::MIN).is_none()); - assert!(Ticks::::get(netuid, TickIndex::MAX).is_none()); - assert!(!CurrentLiquidity::::contains_key(netuid)); + assert!(Ticks128::::iter_prefix(netuid).next().is_none()); + assert!(Ticks128::::get(netuid, TickIndex::MIN).is_none()); + assert!(Ticks128::::get(netuid, TickIndex::MAX).is_none()); + assert!(!CurrentLiquidity128::::contains_key(netuid)); assert!(!CurrentTick::::contains_key(netuid)); assert!(!AlphaSqrtPrice::::contains_key(netuid)); assert!(!SwapV3Initialized::::contains_key(netuid)); @@ -2706,7 +2712,7 @@ fn test_clear_protocol_liquidity_green_path() { // --- Assert: V3 data wiped (idempotent even if some maps were empty) --- // Ticks / active tick bitmap - assert!(Ticks::::iter_prefix(netuid).next().is_none()); + assert!(Ticks128::::iter_prefix(netuid).next().is_none()); assert!( TickIndexBitmapWords::::iter_prefix((netuid,)) .next() @@ -2721,7 +2727,7 @@ fn test_clear_protocol_liquidity_green_path() { // Price / tick / liquidity / flags assert!(!AlphaSqrtPrice::::contains_key(netuid)); assert!(!CurrentTick::::contains_key(netuid)); - assert!(!CurrentLiquidity::::contains_key(netuid)); + assert!(!CurrentLiquidity128::::contains_key(netuid)); assert!(!SwapV3Initialized::::contains_key(netuid)); // Knobs removed @@ -2735,7 +2741,7 @@ fn test_clear_protocol_liquidity_green_path() { .next() .is_none() ); - assert!(Ticks::::iter_prefix(netuid).next().is_none()); + assert!(Ticks128::::iter_prefix(netuid).next().is_none()); assert!( TickIndexBitmapWords::::iter_prefix((netuid,)) .next() diff --git a/pallets/swap/src/tick.rs b/pallets/swap/src/tick.rs index d3493fde4..bd294caaa 100644 --- a/pallets/swap/src/tick.rs +++ b/pallets/swap/src/tick.rs @@ -18,7 +18,7 @@ use subtensor_runtime_common::NetUid; use crate::SqrtPrice; use crate::pallet::{ - Config, CurrentTick, FeeGlobalAlpha, FeeGlobalTao, TickIndexBitmapWords, Ticks, + Config, CurrentTick, FeeGlobalAlpha, FeeGlobalTao, TickIndexBitmapWords, Ticks128, }; const U256_1: U256 = U256::from_limbs([1, 0, 0, 0]); @@ -88,9 +88,18 @@ pub struct Tick { pub fees_out_alpha: I64F64, } -impl Tick { - pub fn liquidity_net_as_u64(&self) -> u64 { - self.liquidity_net.abs().min(u64::MAX as i128) as u64 +#[freeze_struct("eaa387751e7584f8")] +#[derive(Debug, Default, Clone, Encode, Decode, TypeInfo, MaxEncodedLen, PartialEq, Eq)] +pub struct Tick128 { + pub liquidity_net: i128, + pub liquidity_gross: u128, + pub fees_out_tao: I64F64, + pub fees_out_alpha: I64F64, +} + +impl Tick128 { + pub fn liquidity_net_as_u128(&self) -> u128 { + self.liquidity_net.unsigned_abs() } } @@ -217,7 +226,7 @@ impl TickIndex { pub fn fees_above(&self, netuid: NetUid, quote: bool) -> I64F64 { let current_tick = Self::current_bounded::(netuid); - let tick = Ticks::::get(netuid, *self).unwrap_or_default(); + let tick = Ticks128::::get(netuid, *self).unwrap_or_default(); if *self <= current_tick { if quote { I64F64::saturating_from_num(FeeGlobalTao::::get(netuid)) @@ -237,7 +246,7 @@ impl TickIndex { pub fn fees_below(&self, netuid: NetUid, quote: bool) -> I64F64 { let current_tick = Self::current_bounded::(netuid); - let tick = Ticks::::get(netuid, *self).unwrap_or_default(); + let tick = Ticks128::::get(netuid, *self).unwrap_or_default(); if *self <= current_tick { if quote { tick.fees_out_tao diff --git a/pallets/transaction-fee/src/tests/mock.rs b/pallets/transaction-fee/src/tests/mock.rs index de5d13e34..048eb26d0 100644 --- a/pallets/transaction-fee/src/tests/mock.rs +++ b/pallets/transaction-fee/src/tests/mock.rs @@ -398,7 +398,7 @@ parameter_types! { pub const SwapProtocolId: PalletId = PalletId(*b"ten/swap"); pub const SwapMaxFeeRate: u16 = 10000; // 15.26% pub const SwapMaxPositions: u32 = 100; - pub const SwapMinimumLiquidity: u64 = 1_000; + pub const SwapMinimumLiquidity: u128 = 1_000; pub const SwapMinimumReserve: NonZeroU64 = NonZeroU64::new(1_000_000).unwrap(); } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 5654def01..d61a490ae 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1296,7 +1296,7 @@ parameter_types! { pub const SwapProtocolId: PalletId = PalletId(*b"ten/swap"); pub const SwapMaxFeeRate: u16 = 10000; // 15.26% pub const SwapMaxPositions: u32 = 100; - pub const SwapMinimumLiquidity: u64 = 1_000; + pub const SwapMinimumLiquidity: u128 = 1_000; pub const SwapMinimumReserve: NonZeroU64 = unsafe { NonZeroU64::new_unchecked(1_000_000) }; }