diff --git a/Cargo.lock b/Cargo.lock index eb568725ab..d6eb890244 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6834,6 +6834,23 @@ dependencies = [ "w3f-bls", ] +[[package]] +name = "pallet-subtensor-swap" +version = "0.1.0" +dependencies = [ + "alloy-primitives", + "approx", + "pallet-subtensor-swap-interface", + "safe-math", + "sp-arithmetic", + "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-stable2409)", + "substrate-fixed", +] + +[[package]] +name = "pallet-subtensor-swap-interface" +version = "0.1.0" + [[package]] name = "pallet-sudo" version = "38.0.0" @@ -11551,18 +11568,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "swap" -version = "0.1.0" -dependencies = [ - "alloy-primitives", - "approx", - "safe-math", - "sp-arithmetic", - "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-stable2409)", - "substrate-fixed", -] - [[package]] name = "syn" version = "1.0.109" diff --git a/Cargo.toml b/Cargo.toml index d3cedd96d0..fa6c2b396a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,7 @@ subtensor-custom-rpc = { default-features = false, path = "pallets/subtensor/rpc subtensor-custom-rpc-runtime-api = { default-features = false, path = "pallets/subtensor/runtime-api" } subtensor-precompiles = { default-features = false, path = "precompiles" } subtensor-runtime-common = { default-features = false, path = "common" } +pallet-subtensor-swap-interface = { default-features = false, path = "pallets/swap-interface" } async-trait = "0.1" cargo-husky = { version = "1", default-features = false } diff --git a/pallets/swap-interface/Cargo.toml b/pallets/swap-interface/Cargo.toml new file mode 100644 index 0000000000..28c05cac3e --- /dev/null +++ b/pallets/swap-interface/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pallet-subtensor-swap-interface" +version = "0.1.0" +edition.workspace = true + +[dependencies] + +[lints] +workspace = true + +[features] +default = ["std"] +std = [] diff --git a/pallets/swap-interface/src/lib.rs b/pallets/swap-interface/src/lib.rs new file mode 100644 index 0000000000..a5dd7f415e --- /dev/null +++ b/pallets/swap-interface/src/lib.rs @@ -0,0 +1,23 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +use alloc::boxed::Box; +use core::error::Error; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OrderType { + Sell, + Buy, +} + +pub trait SwapHandler { + fn swap(order_t: OrderType, amount: u64) -> Result<(), Box>; + fn add_liquidity(account_id: AccountId, liquidity: u64) -> Result<(), Box>; + fn remove_liquidity(account_id: AccountId) -> Result<(), Box>; +} + +pub trait LiquidityDataProvider { + fn first_reserve() -> First; + fn second_reserve() -> Second; +} diff --git a/primitives/swap/Cargo.toml b/pallets/swap/Cargo.toml similarity index 83% rename from primitives/swap/Cargo.toml rename to pallets/swap/Cargo.toml index d69c31fa0d..362ed0e429 100644 --- a/primitives/swap/Cargo.toml +++ b/pallets/swap/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "swap" +name = "pallet-subtensor-swap" version = "0.1.0" edition = { workspace = true } @@ -11,6 +11,8 @@ sp-arithmetic = { workspace = true } sp-std = { workspace = true } substrate-fixed = { workspace = true } +pallet-subtensor-swap-interface = { workspace = true } + [lints] workspace = true diff --git a/primitives/swap/src/error.rs b/pallets/swap/src/error.rs similarity index 100% rename from primitives/swap/src/error.rs rename to pallets/swap/src/error.rs diff --git a/primitives/swap/src/lib.rs b/pallets/swap/src/lib.rs similarity index 99% rename from primitives/swap/src/lib.rs rename to pallets/swap/src/lib.rs index 24f9bff96e..1e3aaf6379 100644 --- a/primitives/swap/src/lib.rs +++ b/pallets/swap/src/lib.rs @@ -1,6 +1,7 @@ use core::marker::PhantomData; use std::ops::Neg; +use pallet_subtensor_swap_interface::OrderType; use safe_math::*; use sp_arithmetic::helpers_128bit::sqrt; use substrate_fixed::types::U64F64; @@ -13,16 +14,9 @@ use self::tick::{ mod error; mod tick; -mod tick_math; type SqrtPrice = U64F64; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum OrderType { - Sell, - Buy, -} - pub enum SwapStepAction { Crossing, StopOn, @@ -84,7 +78,7 @@ impl Position { /// else: /// tao = L * (self.sqrt_price_curr - sqrt_pa) /// alpha = L * (1 / self.sqrt_price_curr - 1 / sqrt_pb) - /// + /// pub fn to_token_amounts(&self, sqrt_price_curr: SqrtPrice) -> Result<(u64, u64), SwapError> { let one: U64F64 = U64F64::saturating_from_num(1); @@ -1692,11 +1686,7 @@ mod tests { // Test case is (price_low, price_high, liquidity) [ // Repeat the protocol liquidity at maximum range: Expect all the same values - ( - min_price, - max_price, - 2_000_000_000_u64, - ), + (min_price, max_price, 2_000_000_000_u64), ] .iter() .for_each(|(price_low, price_high, liquidity)| { @@ -1724,5 +1714,4 @@ mod tests { ); }); } - } diff --git a/primitives/swap/src/tick_math.rs b/pallets/swap/src/tick.rs similarity index 72% rename from primitives/swap/src/tick_math.rs rename to pallets/swap/src/tick.rs index 3dfcb0d514..91a1ba05b0 100644 --- a/primitives/swap/src/tick_math.rs +++ b/pallets/swap/src/tick.rs @@ -1,4 +1,4 @@ -//! This module is adopted from github.com/0xKitsune/uniswap-v3-math +//! The math is adapted from github.com/0xKitsune/uniswap-v3-math use core::error::Error; use core::fmt; use core::ops::{BitOr, Neg, Shl, Shr}; @@ -6,6 +6,14 @@ use core::ops::{BitOr, Neg, Shl, Shr}; use alloy_primitives::{I256, U256}; use substrate_fixed::types::U64F64; +use crate::{SqrtPrice, SwapDataOperations}; + +/// Maximum and minimum values of the tick index +/// The tick_math library uses different bitness, so we have to divide by 2. +/// Do not use tick_math::MIN_TICK and tick_math::MAX_TICK +pub const MAX_TICK_INDEX: i32 = MAX_TICK / 2; +pub const MIN_TICK_INDEX: i32 = MIN_TICK / 2; + const U256_1: U256 = U256::from_limbs([1, 0, 0, 0]); const U256_2: U256 = U256::from_limbs([2, 0, 0, 0]); const U256_3: U256 = U256::from_limbs([3, 0, 0, 0]); @@ -37,8 +45,8 @@ const U256_524288: U256 = U256::from_limbs([524288, 0, 0, 0]); const U256_MAX_TICK: U256 = U256::from_limbs([887272, 0, 0, 0]); -pub(crate) const MIN_TICK: i32 = -887272; -pub(crate) const MAX_TICK: i32 = -MIN_TICK; +const MIN_TICK: i32 = -887272; +const MAX_TICK: i32 = -MIN_TICK; const MIN_SQRT_RATIO: U256 = U256::from_limbs([4295128739, 0, 0, 0]); const MAX_SQRT_RATIO: U256 = @@ -58,7 +66,95 @@ const TICK_HIGH: I256 = I256::from_raw(U256::from_limbs([ 0, ])); -pub(crate) fn get_sqrt_ratio_at_tick(tick: i32) -> Result { +/// Tick is the price range determined by tick index (not part of this struct, +/// but is the key at which the Tick is stored in state hash maps). Tick struct +/// stores liquidity and fee information. +/// +/// - Net liquidity +/// - Gross liquidity +/// - Fees (above global) in both currencies +/// +#[derive(Debug, Default, Clone)] +pub struct Tick { + pub liquidity_net: i128, + pub liquidity_gross: u64, + pub fees_out_tao: U64F64, + pub fees_out_alpha: U64F64, +} + +/// Converts tick index into SQRT of lower price of this tick +/// In order to find the higher price of this tick, call +/// tick_index_to_sqrt_price(tick_idx + 1) +pub fn tick_index_to_sqrt_price(tick_idx: i32) -> Result { + // because of u256->u128 conversion we have twice less values for min/max ticks + if !(MIN_TICK / 2..=MAX_TICK / 2).contains(&tick_idx) { + return Err(TickMathError::TickOutOfBounds); + } + get_sqrt_ratio_at_tick(tick_idx).and_then(u256_q64_96_to_u64f64) +} + +/// Converts SQRT price to tick index +/// Because the tick is the range of prices [sqrt_lower_price, sqrt_higher_price), +/// the resulting tick index matches the price by the following inequality: +/// sqrt_lower_price <= sqrt_price < sqrt_higher_price +pub fn sqrt_price_to_tick_index(sqrt_price: SqrtPrice) -> Result { + let tick = get_tick_at_sqrt_ratio(u64f64_to_u256_q64_96(sqrt_price))?; + + // Correct for rounding error during conversions between different fixed-point formats + Ok(if tick == 0 { + tick + } else { + tick.saturating_add(1) + }) +} + +pub fn find_closest_lower_active_tick_index( + ops: &Ops, + index: i32, +) -> Option +where + AccountIdType: Eq, + Ops: SwapDataOperations, +{ + // TODO: Implement without iteration + let mut current_index = index; + loop { + if current_index < MIN_TICK { + return None; + } + if ops.get_tick_by_index(current_index).is_some() { + return Some(current_index); + } + + // Intentionally using unsafe math here to trigger CI + current_index -= 1; + } +} + +pub fn find_closest_higher_active_tick_index( + ops: &Ops, + index: i32, +) -> Option +where + AccountIdType: Eq, + Ops: SwapDataOperations, +{ + // TODO: Implement without iteration + let mut current_index = index; + loop { + if current_index > MAX_TICK { + return None; + } + if ops.get_tick_by_index(current_index).is_some() { + return Some(current_index); + } + + // Intentionally using unsafe math here to trigger CI + current_index += 1; + } +} + +fn get_sqrt_ratio_at_tick(tick: i32) -> Result { let abs_tick = if tick < 0 { U256::from(tick.neg()) } else { @@ -149,7 +245,7 @@ pub(crate) fn get_sqrt_ratio_at_tick(tick: i32) -> Result { }) } -pub(crate) fn get_tick_at_sqrt_ratio(sqrt_price_x_96: U256) -> Result { +fn get_tick_at_sqrt_ratio(sqrt_price_x_96: U256) -> Result { if !(sqrt_price_x_96 >= MIN_SQRT_RATIO && sqrt_price_x_96 < MAX_SQRT_RATIO) { return Err(TickMathError::SqrtPriceOutOfBounds); } @@ -262,10 +358,7 @@ pub(crate) fn get_tick_at_sqrt_ratio(sqrt_price_x_96: U256) -> Result` - Converted value or error if too large -pub(crate) fn u256_to_u64f64( - value: U256, - source_fractional_bits: u32, -) -> Result { +fn u256_to_u64f64(value: U256, source_fractional_bits: u32) -> Result { if value > U256::from(u128::MAX) { return Err(TickMathError::ConversionError); } @@ -296,7 +389,7 @@ pub(crate) fn u256_to_u64f64( /// /// # Returns /// * `U256` - Converted value -pub(crate) fn u64f64_to_u256(value: U64F64, target_fractional_bits: u32) -> U256 { +fn u64f64_to_u256(value: U64F64, target_fractional_bits: u32) -> U256 { let mut bits = value.to_bits(); // Adjust to target fractional bits @@ -313,12 +406,12 @@ pub(crate) fn u64f64_to_u256(value: U64F64, target_fractional_bits: u32) -> U256 } /// Convert U256 in Q64.96 format (Uniswap's sqrt price format) to U64F64 -pub(crate) fn u256_q64_96_to_u64f64(value: U256) -> Result { +fn u256_q64_96_to_u64f64(value: U256) -> Result { u256_to_u64f64(value, 96) } /// Convert U64F64 to U256 in Q64.96 format (Uniswap's sqrt price format) -pub(crate) fn u64f64_to_u256_q64_96(value: U64F64) -> U256 { +fn u64f64_to_u256_q64_96(value: U64F64) -> U256 { u64f64_to_u256(value, 96) } @@ -344,10 +437,13 @@ impl fmt::Display for TickMathError { impl Error for TickMathError {} #[cfg(test)] -mod test { - use super::*; +mod tests { use std::{ops::Sub, str::FromStr}; + use safe_math::FixedExt; + + use super::*; + #[test] fn test_get_sqrt_ratio_at_tick_bounds() { // the function should return an error if the tick is out of bounds @@ -536,4 +632,87 @@ mod test { let back_to_u256 = u64f64_to_u256(fixed_value, 32); assert_eq!(back_to_u256, value_32frac); } + #[test] + fn test_tick_index_to_sqrt_price() { + let tick_spacing = SqrtPrice::from_num(1.0001); + + // check tick bounds + assert_eq!( + tick_index_to_sqrt_price(MIN_TICK), + Err(TickMathError::TickOutOfBounds) + ); + + assert_eq!( + tick_index_to_sqrt_price(MAX_TICK), + Err(TickMathError::TickOutOfBounds), + ); + + // At tick index 0, the sqrt price should be 1.0 + let sqrt_price = tick_index_to_sqrt_price(0).unwrap(); + assert_eq!(sqrt_price, SqrtPrice::from_num(1.0)); + + let sqrt_price = tick_index_to_sqrt_price(2).unwrap(); + assert!(sqrt_price.abs_diff(tick_spacing) < SqrtPrice::from_num(1e-10)); + + let sqrt_price = tick_index_to_sqrt_price(4).unwrap(); + // Calculate the expected value: (1 + TICK_SPACING/1e9 + 1.0)^2 + let expected = tick_spacing * tick_spacing; + assert!(sqrt_price.abs_diff(expected) < SqrtPrice::from_num(1e-10)); + + // Test with tick index 10 + let sqrt_price = tick_index_to_sqrt_price(10).unwrap(); + // Calculate the expected value: (1 + TICK_SPACING/1e9 + 1.0)^5 + let expected = tick_spacing.checked_pow(5).unwrap(); + assert!( + sqrt_price.abs_diff(expected) < SqrtPrice::from_num(1e-10), + "diff: {}", + sqrt_price.abs_diff(expected), + ); + } + + #[test] + fn test_sqrt_price_to_tick_index() { + let tick_spacing = SqrtPrice::from_num(1.0001); + let tick_index = sqrt_price_to_tick_index(SqrtPrice::from_num(1.0)).unwrap(); + assert_eq!(tick_index, 0); + + // Test with sqrt price equal to tick_spacing_tao (should be tick index 2) + let tick_index = sqrt_price_to_tick_index(tick_spacing).unwrap(); + assert_eq!(tick_index, 2); + + // Test with sqrt price equal to tick_spacing_tao^2 (should be tick index 4) + let sqrt_price = tick_spacing * tick_spacing; + let tick_index = sqrt_price_to_tick_index(sqrt_price).unwrap(); + assert_eq!(tick_index, 4); + + // Test with sqrt price equal to tick_spacing_tao^5 (should be tick index 10) + let sqrt_price = tick_spacing.checked_pow(5).unwrap(); + let tick_index = sqrt_price_to_tick_index(sqrt_price).unwrap(); + assert_eq!(tick_index, 10); + } + + #[test] + fn test_roundtrip_tick_index_sqrt_price() { + for tick_index in [ + MIN_TICK / 2, + -1000, + -100, + -10, + -4, + -2, + 0, + 2, + 4, + 10, + 100, + 1000, + MAX_TICK / 2, + ] + .iter() + { + let sqrt_price = tick_index_to_sqrt_price(*tick_index).unwrap(); + let round_trip_tick_index = sqrt_price_to_tick_index(sqrt_price).unwrap(); + assert_eq!(round_trip_tick_index, *tick_index); + } + } } diff --git a/primitives/swap/src/tick.rs b/primitives/swap/src/tick.rs deleted file mode 100644 index e5e7eefc16..0000000000 --- a/primitives/swap/src/tick.rs +++ /dev/null @@ -1,191 +0,0 @@ -use substrate_fixed::types::U64F64; - -use crate::tick_math::{ - MAX_TICK, MIN_TICK, TickMathError, get_sqrt_ratio_at_tick, get_tick_at_sqrt_ratio, - u64f64_to_u256_q64_96, u256_q64_96_to_u64f64, -}; -use crate::{SqrtPrice, SwapDataOperations}; - -// Maximum and minimum values of the tick index -// The tick_math library uses different bitness, so we have to divide by 2. -// Do not use tick_math::MIN_TICK and tick_math::MAX_TICK -pub const MAX_TICK_INDEX: i32 = MAX_TICK / 2; -pub const MIN_TICK_INDEX: i32 = MIN_TICK / 2; - -/// Tick is the price range determined by tick index (not part of this struct, -/// but is the key at which the Tick is stored in state hash maps). Tick struct -/// stores liquidity and fee information. -/// -/// - Net liquidity -/// - Gross liquidity -/// - Fees (above global) in both currencies -/// -#[derive(Debug, Default, Clone)] -pub struct Tick { - pub liquidity_net: i128, - pub liquidity_gross: u64, - pub fees_out_tao: U64F64, - pub fees_out_alpha: U64F64, -} - -/// Converts tick index into SQRT of lower price of this tick -/// In order to find the higher price of this tick, call -/// tick_index_to_sqrt_price(tick_idx + 1) -pub fn tick_index_to_sqrt_price(tick_idx: i32) -> Result { - // because of u256->u128 conversion we have twice less values for min/max ticks - if !(MIN_TICK / 2..=MAX_TICK / 2).contains(&tick_idx) { - return Err(TickMathError::TickOutOfBounds); - } - get_sqrt_ratio_at_tick(tick_idx).and_then(u256_q64_96_to_u64f64) -} - -/// Converts SQRT price to tick index -/// Because the tick is the range of prices [sqrt_lower_price, sqrt_higher_price), -/// the resulting tick index matches the price by the following inequality: -/// sqrt_lower_price <= sqrt_price < sqrt_higher_price -pub fn sqrt_price_to_tick_index(sqrt_price: SqrtPrice) -> Result { - let tick = get_tick_at_sqrt_ratio(u64f64_to_u256_q64_96(sqrt_price))?; - - // Correct for rounding error during conversions between different fixed-point formats - Ok(if tick == 0 { - tick - } else { - tick.saturating_add(1) - }) -} - -pub fn find_closest_lower_active_tick_index( - ops: &Ops, - index: i32, -) -> Option -where - AccountIdType: Eq, - Ops: SwapDataOperations, -{ - // TODO: Implement without iteration - let mut current_index = index; - loop { - if current_index < MIN_TICK { - return None; - } - if ops.get_tick_by_index(current_index).is_some() { - return Some(current_index); - } - - // Intentionally using unsafe math here to trigger CI - current_index -= 1; - } -} - -pub fn find_closest_higher_active_tick_index( - ops: &Ops, - index: i32, -) -> Option -where - AccountIdType: Eq, - Ops: SwapDataOperations, -{ - // TODO: Implement without iteration - let mut current_index = index; - loop { - if current_index > MAX_TICK { - return None; - } - if ops.get_tick_by_index(current_index).is_some() { - return Some(current_index); - } - - // Intentionally using unsafe math here to trigger CI - current_index += 1; - } -} - -#[cfg(test)] -mod tests { - use super::*; - use safe_math::FixedExt; - - #[test] - fn test_tick_index_to_sqrt_price() { - let tick_spacing = SqrtPrice::from_num(1.0001); - - // check tick bounds - assert_eq!( - tick_index_to_sqrt_price(MIN_TICK), - Err(TickMathError::TickOutOfBounds) - ); - - assert_eq!( - tick_index_to_sqrt_price(MAX_TICK), - Err(TickMathError::TickOutOfBounds), - ); - - // At tick index 0, the sqrt price should be 1.0 - let sqrt_price = tick_index_to_sqrt_price(0).unwrap(); - assert_eq!(sqrt_price, SqrtPrice::from_num(1.0)); - - let sqrt_price = tick_index_to_sqrt_price(2).unwrap(); - assert!(sqrt_price.abs_diff(tick_spacing) < SqrtPrice::from_num(1e-10)); - - let sqrt_price = tick_index_to_sqrt_price(4).unwrap(); - // Calculate the expected value: (1 + TICK_SPACING/1e9 + 1.0)^2 - let expected = tick_spacing * tick_spacing; - assert!(sqrt_price.abs_diff(expected) < SqrtPrice::from_num(1e-10)); - - // Test with tick index 10 - let sqrt_price = tick_index_to_sqrt_price(10).unwrap(); - // Calculate the expected value: (1 + TICK_SPACING/1e9 + 1.0)^5 - let expected = tick_spacing.checked_pow(5).unwrap(); - assert!( - sqrt_price.abs_diff(expected) < SqrtPrice::from_num(1e-10), - "diff: {}", - sqrt_price.abs_diff(expected), - ); - } - - #[test] - fn test_sqrt_price_to_tick_index() { - let tick_spacing = SqrtPrice::from_num(1.0001); - let tick_index = sqrt_price_to_tick_index(SqrtPrice::from_num(1.0)).unwrap(); - assert_eq!(tick_index, 0); - - // Test with sqrt price equal to tick_spacing_tao (should be tick index 2) - let tick_index = sqrt_price_to_tick_index(tick_spacing).unwrap(); - assert_eq!(tick_index, 2); - - // Test with sqrt price equal to tick_spacing_tao^2 (should be tick index 4) - let sqrt_price = tick_spacing * tick_spacing; - let tick_index = sqrt_price_to_tick_index(sqrt_price).unwrap(); - assert_eq!(tick_index, 4); - - // Test with sqrt price equal to tick_spacing_tao^5 (should be tick index 10) - let sqrt_price = tick_spacing.checked_pow(5).unwrap(); - let tick_index = sqrt_price_to_tick_index(sqrt_price).unwrap(); - assert_eq!(tick_index, 10); - } - - #[test] - fn test_roundtrip_tick_index_sqrt_price() { - for tick_index in [ - MIN_TICK / 2, - -1000, - -100, - -10, - -4, - -2, - 0, - 2, - 4, - 10, - 100, - 1000, - MAX_TICK / 2, - ] - .iter() - { - let sqrt_price = tick_index_to_sqrt_price(*tick_index).unwrap(); - let round_trip_tick_index = sqrt_price_to_tick_index(sqrt_price).unwrap(); - assert_eq!(round_trip_tick_index, *tick_index); - } - } -}