diff --git a/Cargo.lock b/Cargo.lock index 441af10269..ec5c8f9526 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1267,6 +1267,7 @@ dependencies = [ name = "cf-amm" version = "0.1.0" dependencies = [ + "cf-amm-math", "cf-primitives", "parity-scale-codec", "rand", @@ -1277,6 +1278,19 @@ dependencies = [ "utilities", ] +[[package]] +name = "cf-amm-math" +version = "0.1.0" +dependencies = [ + "cf-primitives", + "parity-scale-codec", + "rand", + "scale-info", + "serde", + "sp-core 34.0.0", + "sp-std 14.0.0 (git+https://github.com/chainflip-io/polkadot-sdk.git?tag=chainflip-substrate-1.15.2+2)", +] + [[package]] name = "cf-chains" version = "0.1.0" @@ -1287,6 +1301,7 @@ dependencies = [ "bincode 2.0.0-rc.3", "borsh", "bs58 0.5.1", + "cf-amm-math", "cf-primitives", "cf-runtime-utilities", "cf-test-utilities", diff --git a/Cargo.toml b/Cargo.toml index 0a765a32a5..09ba22181b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -257,6 +257,7 @@ pallet-cf-vaults = { path = "state-chain/pallets/cf-vaults", default-features = pallet-cf-witnesser = { path = "state-chain/pallets/cf-witnesser", default-features = false } cf-amm = { path = "state-chain/amm", default-features = false } +cf-amm-math = { path = "state-chain/amm-math", default-features = false } cf-chains = { path = "state-chain/chains", default-features = false } cf-primitives = { path = "state-chain/primitives", default-features = false } cf-session-benchmarking = { path = "state-chain/cf-session-benchmarking", default-features = false } diff --git a/api/lib/src/lp.rs b/api/lib/src/lp.rs index c47197adbd..4198f18301 100644 --- a/api/lib/src/lp.rs +++ b/api/lib/src/lp.rs @@ -2,7 +2,8 @@ use super::SimpleSubmissionApi; use anyhow::{bail, Result}; use async_trait::async_trait; pub use cf_amm::{ - common::{Amount, PoolPairsMap, Side, Tick}, + common::{PoolPairsMap, Side}, + math::{Amount, Tick}, range_orders::Liquidity, }; use cf_chains::{ diff --git a/engine/src/witness/btc/vault_swaps.rs b/engine/src/witness/btc/vault_swaps.rs index c277e575d0..8d6074257a 100644 --- a/engine/src/witness/btc/vault_swaps.rs +++ b/engine/src/witness/btc/vault_swaps.rs @@ -1,5 +1,5 @@ use bitcoin::{hashes::Hash as btcHash, opcodes::all::OP_RETURN, ScriptBuf}; -use cf_amm::common::{bounded_sqrt_price, sqrt_price_to_price}; +use cf_amm::math::{bounded_sqrt_price, sqrt_price_to_price}; use cf_chains::{ assets::btc::Asset as BtcAsset, btc::{ diff --git a/state-chain/amm-math/Cargo.toml b/state-chain/amm-math/Cargo.toml new file mode 100644 index 0000000000..2c755e4a48 --- /dev/null +++ b/state-chain/amm-math/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "cf-amm-math" +version = "0.1.0" +authors = ["Chainflip "] +edition = "2021" +description = "Chainflip AMM Math Primitives" + +[dependencies] +cf-primitives = { workspace = true } +serde = { workspace = true, features = ["derive", "alloc"] } + +# Parity deps +codec = { workspace = true, features = ["derive"] } +scale-info = { workspace = true, features = ["derive"] } + +sp-core = { workspace = true } +sp-std = { workspace = true } + +rand = { workspace = true, features = ["std"], optional = true } + +[features] +default = ["std"] +slow-tests = ["dep:rand"] +std = [ + "cf-primitives/std", + "codec/std", + "scale-info/std", + "sp-core/std", + "sp-std/std", + "serde/std", +] diff --git a/state-chain/amm-math/src/lib.rs b/state-chain/amm-math/src/lib.rs new file mode 100644 index 0000000000..645b7e7210 --- /dev/null +++ b/state-chain/amm-math/src/lib.rs @@ -0,0 +1,609 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +pub mod test_utilities; + +use sp_core::{U256, U512}; + +// TODO: Consider alternative representation for Price: +// +// increasing Price to U512 or switch to a f64 (f64 would only be for the external +// price representation), as at low ticks the precision in the price is VERY LOW, but this does not +// cause any problems for the AMM code in terms of correctness + +/// This is the ratio of equivalently valued amounts of asset One and asset Zero. +/// +/// The price is always measured in amount of asset One per unit of asset Zero. Therefore as asset +/// zero becomes more valuable relative to asset one the price's literal value goes up, and vice +/// versa. This ratio is represented as a fixed point number with `PRICE_FRACTIONAL_BITS` fractional +/// bits. +pub type Price = U256; // TODO: Delete ths from cf-primitives + +/// Represents an amount of an asset, in its smallest unit i.e. Ethereum has 10^-18 precision, and +/// therefore an `Amount` with the literal value of `1` would represent 10^-18 Ethereum. +pub type Amount = U256; + +/// The `log1.0001(price)` rounded to the nearest integer. Note [Price] is always +/// in units of asset One. +pub type Tick = i32; + +/// The square root of the price. +/// +/// Represented as a fixed point integer with 96 fractional bits and +/// 64 integer bits (The higher bits past 96+64 th aren't used). [SqrtPriceQ64F96] is always in sqrt +/// units of asset one. +pub type SqrtPriceQ64F96 = U256; + +/// The number of fractional bits used by `SqrtPriceQ64F96`. +pub const SQRT_PRICE_FRACTIONAL_BITS: u32 = 96; + +pub fn mul_div_floor>(a: U256, b: U256, c: C) -> U256 { + let c: U512 = c.into(); + (U256::full_mul(a, b) / c).try_into().unwrap() +} + +pub fn mul_div_ceil>(a: U256, b: U256, c: C) -> U256 { + mul_div(a, b, c).1 +} + +pub fn mul_div>(a: U256, b: U256, c: C) -> (U256, U256) { + let c: U512 = c.into(); + + let (d, m) = U512::div_mod(U256::full_mul(a, b), c); + + ( + d.try_into().unwrap(), + if m > U512::from(0) { + // cannot overflow as for m > 0, c must be > 1, and as (a*b) < U512::MAX, therefore + // a*b/c < U512::MAX + d + 1 + } else { + d + } + .try_into() + .unwrap(), + ) +} + +pub fn bounded_sqrt_price(quote: Amount, base: Amount) -> SqrtPriceQ64F96 { + assert!(!quote.is_zero() || !base.is_zero()); + + if base.is_zero() { + MAX_SQRT_PRICE + } else { + let unbounded_sqrt_price = U256::try_from( + ((U512::from(quote) << 256) / U512::from(base)).integer_sqrt() >> + (128 - SQRT_PRICE_FRACTIONAL_BITS), + ) + .unwrap(); + + if unbounded_sqrt_price < MIN_SQRT_PRICE { + MIN_SQRT_PRICE + } else if unbounded_sqrt_price > MAX_SQRT_PRICE { + MAX_SQRT_PRICE + } else { + unbounded_sqrt_price + } + } +} + +pub fn output_amount_floor(input: Amount, price: Price) -> Amount { + mul_div_floor(input, price, U256::one() << PRICE_FRACTIONAL_BITS) +} + +pub fn output_amount_ceil(input: Amount, price: Price) -> Amount { + mul_div_ceil(input, price, U256::one() << PRICE_FRACTIONAL_BITS) +} + +pub const PRICE_FRACTIONAL_BITS: u32 = 128; + +/// Converts from a [SqrtPriceQ64F96] to a [Price]. +/// +/// Will panic for `sqrt_price`'s outside `MIN_SQRT_PRICE..=MAX_SQRT_PRICE` +pub fn sqrt_price_to_price(sqrt_price: SqrtPriceQ64F96) -> Price { + assert!(is_sqrt_price_valid(sqrt_price)); + + // Note the value here cannot ever be zero as MIN_SQRT_PRICE has its 33th bit set, so sqrt_price + // will always include a bit pass the 64th bit that is set, so when we shift down below that set + // bit will not be removed. + mul_div_floor( + sqrt_price, + sqrt_price, + SqrtPriceQ64F96::one() << (2 * SQRT_PRICE_FRACTIONAL_BITS - PRICE_FRACTIONAL_BITS), + ) +} + +/// Converts from a `price` to a `sqrt_price` +/// +/// This function never panics. +pub fn price_to_sqrt_price(price: Price) -> SqrtPriceQ64F96 { + ((U512::from(price) << PRICE_FRACTIONAL_BITS).integer_sqrt() >> + (PRICE_FRACTIONAL_BITS - SQRT_PRICE_FRACTIONAL_BITS)) + .try_into() + .unwrap_or(SqrtPriceQ64F96::MAX) +} + +/// Converts a `tick` to a `price`. Will return `None` for ticks outside MIN_TICK..=MAX_TICK +/// +/// This function never panics. +pub fn price_at_tick(tick: Tick) -> Option { + if is_tick_valid(tick) { + Some(sqrt_price_to_price(sqrt_price_at_tick(tick))) + } else { + None + } +} + +/// Converts a `price` to a `tick`. Will return `None` is the price is too high or low to be +/// represented by a valid tick i.e. one inside MIN_TICK..=MAX_TICK. +/// +/// This function never panics. +pub fn tick_at_price(price: Price) -> Option { + let sqrt_price = price_to_sqrt_price(price); + if is_sqrt_price_valid(sqrt_price) { + Some(tick_at_sqrt_price(sqrt_price)) + } else { + None + } +} + +/// The minimum tick that may be passed to `sqrt_price_at_tick` computed from log base 1.0001 of +/// 2**-128 +pub const MIN_TICK: Tick = -887272; +/// The maximum tick that may be passed to `sqrt_price_at_tick` computed from log base 1.0001 of +/// 2**128 +pub const MAX_TICK: Tick = -MIN_TICK; +/// The minimum value that can be returned from `sqrt_price_at_tick`. Equivalent to +/// `sqrt_price_at_tick(MIN_TICK)` +pub const MIN_SQRT_PRICE: SqrtPriceQ64F96 = U256([0x1000276a3u64, 0x0, 0x0, 0x0]); +/// The maximum value that can be returned from `sqrt_price_at_tick`. Equivalent to +/// `sqrt_price_at_tick(MAX_TICK)`. +pub const MAX_SQRT_PRICE: SqrtPriceQ64F96 = + U256([0x5d951d5263988d26u64, 0xefd1fc6a50648849u64, 0xfffd8963u64, 0x0u64]); + +pub fn is_sqrt_price_valid(sqrt_price: SqrtPriceQ64F96) -> bool { + (MIN_SQRT_PRICE..=MAX_SQRT_PRICE).contains(&sqrt_price) +} + +pub fn is_tick_valid(tick: Tick) -> bool { + (MIN_TICK..=MAX_TICK).contains(&tick) +} + +pub fn sqrt_price_at_tick(tick: Tick) -> SqrtPriceQ64F96 { + assert!(is_tick_valid(tick)); + + let abs_tick = tick.unsigned_abs(); + + let mut r = if abs_tick & 0x1u32 != 0 { + U256::from(0xfffcb933bd6fad37aa2d162d1a594001u128) + } else { + U256::one() << 128u128 + }; + + macro_rules! handle_tick_bit { + ($bit:literal, $constant:literal) => { + /* Proof that `checked_mul` does not overflow: + Note that the value ratio is initialized with above is such that `ratio <= (U256::one() << 128u128)`, alternatively `ratio <= (u128::MAX + 1)` + First consider the case of applying the macro once assuming `ratio <= (U256::one() << 128u128)`: + If ∀r ∈ U256, `r <= (U256::one() << 128u128)`, ∀C ∈ "Set of constants the macro is used with (Listed below)" + Then `C * r <= U256::MAX` (See `debug_assertions` below) + Therefore the `checked_mul` will not overflow + Also note that above `(C * r >> 128u128) <= UINT128_MAX` + Therefore if the if branch is taken ratio will be assigned a value `<= u128::MAX` + else ratio is unchanged and remains `ratio <= u128::MAX + 1` + Therefore as the assumption `ratio <= u128::MAX + 1` is always maintained after applying the macro, + none of the checked_mul calls in any of the applications of the macro will overflow + */ + #[cfg(debug_assertions)] + U256::checked_mul(U256::one() << 128u128, $constant.into()).unwrap(); + if abs_tick & (0x1u32 << $bit) != 0 { + r = U256::checked_mul(r, U256::from($constant)).unwrap() >> 128u128 + } + } + } + + handle_tick_bit!(1, 0xfff97272373d413259a46990580e213au128); + handle_tick_bit!(2, 0xfff2e50f5f656932ef12357cf3c7fdccu128); + handle_tick_bit!(3, 0xffe5caca7e10e4e61c3624eaa0941cd0u128); + handle_tick_bit!(4, 0xffcb9843d60f6159c9db58835c926644u128); + handle_tick_bit!(5, 0xff973b41fa98c081472e6896dfb254c0u128); + handle_tick_bit!(6, 0xff2ea16466c96a3843ec78b326b52861u128); + handle_tick_bit!(7, 0xfe5dee046a99a2a811c461f1969c3053u128); + handle_tick_bit!(8, 0xfcbe86c7900a88aedcffc83b479aa3a4u128); + handle_tick_bit!(9, 0xf987a7253ac413176f2b074cf7815e54u128); + handle_tick_bit!(10, 0xf3392b0822b70005940c7a398e4b70f3u128); + handle_tick_bit!(11, 0xe7159475a2c29b7443b29c7fa6e889d9u128); + handle_tick_bit!(12, 0xd097f3bdfd2022b8845ad8f792aa5825u128); + handle_tick_bit!(13, 0xa9f746462d870fdf8a65dc1f90e061e5u128); + handle_tick_bit!(14, 0x70d869a156d2a1b890bb3df62baf32f7u128); + handle_tick_bit!(15, 0x31be135f97d08fd981231505542fcfa6u128); + handle_tick_bit!(16, 0x9aa508b5b7a84e1c677de54f3e99bc9u128); + handle_tick_bit!(17, 0x5d6af8dedb81196699c329225ee604u128); + handle_tick_bit!(18, 0x2216e584f5fa1ea926041bedfe98u128); + handle_tick_bit!(19, 0x48a170391f7dc42444e8fa2u128); + // Note due to MIN_TICK and MAX_TICK bounds, past the 20th bit abs_tick is all zeros + + /* Proof that r is never zero (therefore avoiding the divide by zero case here): + We can think of an application of the `handle_tick_bit` macro as increasing the index I of r's MSB/`r.ilog2()` (mul by constant), and then decreasing it by 128 (the right shift). + + Note the increase in I caused by the constant mul will be at least constant.ilog2(). + + Also note each application of `handle_tick_bit` decreases (if the if branch is entered) or else maintains r's value as all the constants are less than 2^128. + + Therefore the largest decrease would be caused if all the macros application's if branches where entered. + + So we assuming all if branches are entered, after all the applications `I` would be at least I_initial + bigsum(constant.ilog2()) - 19*128. + + The test `r_non_zero` checks with value is >= 0, therefore imply the smallest value r could have is more than 0. + */ + let sqrt_price_q32f128 = if tick > 0 { U256::MAX / r } else { r }; + + // we round up in the division so tick_at_sqrt_price of the output price is always + // consistent + (sqrt_price_q32f128 >> 32u128) + + if sqrt_price_q32f128.low_u32() == 0 { U256::zero() } else { U256::one() } +} + +/// Calculates the greatest tick value such that `sqrt_price_at_tick(tick) <= sqrt_price` +pub fn tick_at_sqrt_price(sqrt_price: SqrtPriceQ64F96) -> Tick { + assert!(is_sqrt_price_valid(sqrt_price)); + + let sqrt_price_q64f128 = sqrt_price << 32u128; + + let (integer_log_2, mantissa) = { + let mut _bits_remaining = sqrt_price_q64f128; + let mut most_significant_bit = 0u8; + + // rustfmt chokes when formatting this macro. + // See: https://github.com/rust-lang/rustfmt/issues/5404 + #[rustfmt::skip] + macro_rules! add_integer_bit { + ($bit:literal, $lower_bits_mask:literal) => { + if _bits_remaining > U256::from($lower_bits_mask) { + most_significant_bit |= $bit; + _bits_remaining >>= $bit; + } + }; + } + + add_integer_bit!(128u8, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFu128); + add_integer_bit!(64u8, 0xFFFFFFFFFFFFFFFFu128); + add_integer_bit!(32u8, 0xFFFFFFFFu128); + add_integer_bit!(16u8, 0xFFFFu128); + add_integer_bit!(8u8, 0xFFu128); + add_integer_bit!(4u8, 0xFu128); + add_integer_bit!(2u8, 0x3u128); + add_integer_bit!(1u8, 0x1u128); + + ( + // most_significant_bit is the log2 of sqrt_price_q64f128 as an integer. This + // converts most_significant_bit to the integer log2 of sqrt_price_q64f128 as an + // q64f128 + ((most_significant_bit as i16) + (-128i16)) as i8, + // Calculate mantissa of sqrt_price_q64f128. + if most_significant_bit >= 128u8 { + // The bits we possibly drop when right shifting don't contribute to the log2 + // above the 14th fractional bit. + sqrt_price_q64f128 >> (most_significant_bit - 127u8) + } else { + sqrt_price_q64f128 << (127u8 - most_significant_bit) + } + .as_u128(), // Conversion to u128 is safe as top 128 bits are always zero + ) + }; + + let log_2_q63f64 = { + let mut log_2_q63f64 = (integer_log_2 as i128) << 64u8; + let mut _mantissa = mantissa; + + // rustfmt chokes when formatting this macro. + // See: https://github.com/rust-lang/rustfmt/issues/5404 + #[rustfmt::skip] + macro_rules! add_fractional_bit { + ($bit:literal) => { + // Note squaring a number doubles its log + let mantissa_sq = + (U256::checked_mul(_mantissa.into(), _mantissa.into()).unwrap() >> 127u8); + _mantissa = if mantissa_sq.bit(128) { + // is the 129th bit set, all higher bits must be zero due to 127 right bit + // shift + log_2_q63f64 |= 1i128 << $bit; + (mantissa_sq >> 1u8).as_u128() + } else { + mantissa_sq.as_u128() + } + }; + } + + add_fractional_bit!(63u8); + add_fractional_bit!(62u8); + add_fractional_bit!(61u8); + add_fractional_bit!(60u8); + add_fractional_bit!(59u8); + add_fractional_bit!(58u8); + add_fractional_bit!(57u8); + add_fractional_bit!(56u8); + add_fractional_bit!(55u8); + add_fractional_bit!(54u8); + add_fractional_bit!(53u8); + add_fractional_bit!(52u8); + add_fractional_bit!(51u8); + add_fractional_bit!(50u8); + + // We don't need more precision than (63..50) = 14 bits + + log_2_q63f64 + }; + + // Note we don't have a I256 type so I have to handle the negative mul case manually + let log_sqrt10001_q127f128 = U256::overflowing_mul( + if log_2_q63f64 < 0 { + (U256::from(u128::MAX) << 128u8) | U256::from(log_2_q63f64 as u128) + } else { + U256::from(log_2_q63f64 as u128) + }, + U256::from(255738958999603826347141u128), + ) + .0; + + let tick_low = (U256::overflowing_sub( + log_sqrt10001_q127f128, + U256::from(3402992956809132418596140100660247210u128), + ) + .0 >> 128u8) + .as_u128() as Tick; // Add Checks + let tick_high = (U256::overflowing_add( + log_sqrt10001_q127f128, + U256::from(291339464771989622907027621153398088495u128), + ) + .0 >> 128u8) + .as_u128() as Tick; // Add Checks + + if tick_low == tick_high { + tick_low + } else if sqrt_price_at_tick(tick_high) <= sqrt_price { + tick_high + } else { + tick_low + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[cfg(feature = "slow-tests")] + use rand::SeedableRng; + + #[cfg(feature = "slow-tests")] + use crate::test_utilities::rng_u256_inclusive_bound; + + #[cfg(feature = "slow-tests")] + #[test] + fn test_sqrt_price() { + let mut rng: rand::rngs::StdRng = rand::rngs::StdRng::from_seed([0; 32]); + + for _i in 0..10000000 { + assert!(is_sqrt_price_valid(bounded_sqrt_price( + rng_u256_inclusive_bound(&mut rng, Amount::one()..=Amount::MAX), + rng_u256_inclusive_bound(&mut rng, Amount::one()..=Amount::MAX), + ))); + } + } + + #[test] + fn test_mul_div_floor() { + assert_eq!(mul_div_floor(1.into(), 1.into(), 1), 1.into()); + assert_eq!(mul_div_floor(1.into(), 1.into(), 2), 0.into()); + assert_eq!(mul_div_floor(1.into(), 2.into(), 1), 2.into()); + assert_eq!(mul_div_floor(1.into(), 2.into(), 2), 1.into()); + assert_eq!(mul_div_floor(1.into(), 2.into(), 3), 0.into()); + assert_eq!(mul_div_floor(1.into(), 3.into(), 2), 1.into()); + assert_eq!(mul_div_floor(1.into(), 3.into(), 3), 1.into()); + assert_eq!(mul_div_floor(1.into(), 3.into(), 4), 0.into()); + assert_eq!(mul_div_floor(1.into(), 4.into(), 3), 1.into()); + assert_eq!(mul_div_floor(1.into(), 4.into(), 4), 1.into()); + assert_eq!(mul_div_floor(1.into(), 4.into(), 5), 0.into()); + assert_eq!(mul_div_floor(1.into(), 5.into(), 4), 1.into()); + assert_eq!(mul_div_floor(1.into(), 5.into(), 5), 1.into()); + assert_eq!(mul_div_floor(1.into(), 5.into(), 6), 0.into()); + + assert_eq!(mul_div_floor(2.into(), 1.into(), 2), 1.into()); + assert_eq!(mul_div_floor(2.into(), 1.into(), 3), 0.into()); + assert_eq!(mul_div_floor(3.into(), 1.into(), 2), 1.into()); + assert_eq!(mul_div_floor(3.into(), 1.into(), 3), 1.into()); + assert_eq!(mul_div_floor(3.into(), 1.into(), 4), 0.into()); + assert_eq!(mul_div_floor(4.into(), 1.into(), 3), 1.into()); + assert_eq!(mul_div_floor(4.into(), 1.into(), 4), 1.into()); + assert_eq!(mul_div_floor(4.into(), 1.into(), 5), 0.into()); + assert_eq!(mul_div_floor(5.into(), 1.into(), 4), 1.into()); + assert_eq!(mul_div_floor(5.into(), 1.into(), 5), 1.into()); + assert_eq!(mul_div_floor(5.into(), 1.into(), 6), 0.into()); + + assert_eq!(mul_div_floor(2.into(), 1.into(), 1), 2.into()); + assert_eq!(mul_div_floor(2.into(), 1.into(), 2), 1.into()); + + assert_eq!(mul_div_floor(U256::MAX, U256::MAX, U256::MAX), U256::MAX); + assert_eq!(mul_div_floor(U256::MAX, U256::MAX - 1, U256::MAX), U256::MAX - 1); + } + + #[test] + fn test_mul_div() { + assert_eq!(mul_div(U256::MAX, U256::MAX, U256::MAX), (U256::MAX, U256::MAX)); + assert_eq!(mul_div(U256::MAX, U256::MAX - 1, U256::MAX), (U256::MAX - 1, U256::MAX - 1)); + assert_eq!(mul_div(2.into(), 2.into(), 3), (1.into(), 2.into())); + assert_eq!(mul_div(2.into(), 2.into(), 4), (1.into(), 1.into())); + assert_eq!(mul_div(2.into(), 2.into(), 5), (0.into(), 1.into())); + assert_eq!(mul_div(2.into(), 2.into(), 6), (0.into(), 1.into())); + } + + #[cfg(feature = "slow-tests")] + #[test] + fn test_conversion_sqrt_price_back_and_forth() { + for tick in MIN_TICK..=MAX_TICK { + assert_eq!(tick, tick_at_sqrt_price(sqrt_price_at_tick(tick))); + } + } + + #[test] + fn test_sqrt_price_at_tick() { + assert_eq!(sqrt_price_at_tick(MIN_TICK), MIN_SQRT_PRICE); + assert_eq!(sqrt_price_at_tick(-738203), U256::from_dec_str("7409801140451").unwrap()); + assert_eq!(sqrt_price_at_tick(-500000), U256::from_dec_str("1101692437043807371").unwrap()); + assert_eq!( + sqrt_price_at_tick(-250000), + U256::from_dec_str("295440463448801648376846").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(-150000), + U256::from_dec_str("43836292794701720435367485").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(-50000), + U256::from_dec_str("6504256538020985011912221507").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(-5000), + U256::from_dec_str("61703726247759831737814779831").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(-4000), + U256::from_dec_str("64867181785621769311890333195").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(-3000), + U256::from_dec_str("68192822843687888778582228483").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(-2500), + U256::from_dec_str("69919044979842180277688105136").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(-1000), + U256::from_dec_str("75364347830767020784054125655").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(-500), + U256::from_dec_str("77272108795590369356373805297").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(-250), + U256::from_dec_str("78244023372248365697264290337").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(-100), + U256::from_dec_str("78833030112140176575862854579").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(-50), + U256::from_dec_str("79030349367926598376800521322").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(50), + U256::from_dec_str("79426470787362580746886972461").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(100), + U256::from_dec_str("79625275426524748796330556128").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(250), + U256::from_dec_str("80224679980005306637834519095").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(500), + U256::from_dec_str("81233731461783161732293370115").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(1000), + U256::from_dec_str("83290069058676223003182343270").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(2500), + U256::from_dec_str("89776708723587163891445672585").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(3000), + U256::from_dec_str("92049301871182272007977902845").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(4000), + U256::from_dec_str("96768528593268422080558758223").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(5000), + U256::from_dec_str("101729702841318637793976746270").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(50000), + U256::from_dec_str("965075977353221155028623082916").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(150000), + U256::from_dec_str("143194173941309278083010301478497").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(250000), + U256::from_dec_str("21246587762933397357449903968194344").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(500000), + U256::from_dec_str("5697689776495288729098254600827762987878").unwrap() + ); + assert_eq!( + sqrt_price_at_tick(738203), + U256::from_dec_str("847134979253254120489401328389043031315994541").unwrap() + ); + assert_eq!(sqrt_price_at_tick(MAX_TICK), MAX_SQRT_PRICE); + } + + #[test] + fn test_tick_at_sqrt_price() { + assert_eq!(tick_at_sqrt_price(MIN_SQRT_PRICE), MIN_TICK); + assert_eq!( + tick_at_sqrt_price(U256::from_dec_str("79228162514264337593543").unwrap()), + -276325 + ); + assert_eq!( + tick_at_sqrt_price(U256::from_dec_str("79228162514264337593543950").unwrap()), + -138163 + ); + assert_eq!( + tick_at_sqrt_price(U256::from_dec_str("9903520314283042199192993792").unwrap()), + -41591 + ); + assert_eq!( + tick_at_sqrt_price(U256::from_dec_str("28011385487393069959365969113").unwrap()), + -20796 + ); + assert_eq!( + tick_at_sqrt_price(U256::from_dec_str("56022770974786139918731938227").unwrap()), + -6932 + ); + assert_eq!( + tick_at_sqrt_price(U256::from_dec_str("79228162514264337593543950336").unwrap()), + 0 + ); + assert_eq!( + tick_at_sqrt_price(U256::from_dec_str("112045541949572279837463876454").unwrap()), + 6931 + ); + assert_eq!( + tick_at_sqrt_price(U256::from_dec_str("224091083899144559674927752909").unwrap()), + 20795 + ); + assert_eq!( + tick_at_sqrt_price(U256::from_dec_str("633825300114114700748351602688").unwrap()), + 41590 + ); + assert_eq!( + tick_at_sqrt_price(U256::from_dec_str("79228162514264337593543950336000").unwrap()), + 138162 + ); + assert_eq!( + tick_at_sqrt_price(U256::from_dec_str("79228162514264337593543950336000000").unwrap()), + 276324 + ); + assert_eq!(tick_at_sqrt_price(MAX_SQRT_PRICE - 1), MAX_TICK - 1); + assert_eq!(tick_at_sqrt_price(MAX_SQRT_PRICE), MAX_TICK); + } +} diff --git a/state-chain/amm/src/test_utilities.rs b/state-chain/amm-math/src/test_utilities.rs similarity index 96% rename from state-chain/amm/src/test_utilities.rs rename to state-chain/amm-math/src/test_utilities.rs index a01c404527..07fb358267 100644 --- a/state-chain/amm/src/test_utilities.rs +++ b/state-chain/amm-math/src/test_utilities.rs @@ -1,4 +1,4 @@ -#![cfg(test)] +#![cfg(feature = "slow-tests")] use rand::prelude::Distribution; use sp_core::U256; diff --git a/state-chain/amm/Cargo.toml b/state-chain/amm/Cargo.toml index f414fbd016..0148b0674b 100644 --- a/state-chain/amm/Cargo.toml +++ b/state-chain/amm/Cargo.toml @@ -9,6 +9,7 @@ description = "Chainflip's AMM Logic" workspace = true [dependencies] +cf-amm-math = { workspace = true } cf-primitives = { workspace = true } cf-utilities = { workspace = true } serde = { workspace = true, features = ["derive", "alloc"] } @@ -22,11 +23,13 @@ sp-std = { workspace = true } [dev-dependencies] rand = { workspace = true, features = ["std"] } +cf-amm-math = { workspace = true, features = ["slow-tests"] } [features] default = ["std"] slow-tests = [] std = [ + "cf-amm-math/std", "cf-primitives/std", "cf-utilities/std", "codec/std", diff --git a/state-chain/amm/src/common.rs b/state-chain/amm/src/common.rs index 48ef310b01..294e72380c 100644 --- a/state-chain/amm/src/common.rs +++ b/state-chain/amm/src/common.rs @@ -1,4 +1,4 @@ -pub use cf_primitives::Price; +use cf_amm_math::*; use codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use serde::{Deserialize, Serialize}; @@ -7,24 +7,6 @@ use sp_core::{U256, U512}; pub const ONE_IN_HUNDREDTH_PIPS: u32 = 1_000_000; pub const MAX_LP_FEE: u32 = ONE_IN_HUNDREDTH_PIPS / 2; -/// Represents an amount of an asset, in its smallest unit i.e. Ethereum has 10^-18 precision, and -/// therefore an `Amount` with the literal value of `1` would represent 10^-18 Ethereum. -pub type Amount = U256; - -/// The `log1.0001(price)` rounded to the nearest integer. Note [Price] is always -/// in units of asset One. -pub type Tick = i32; - -/// The square root of the price. -/// -/// Represented as a fixed point integer with 96 fractional bits and -/// 64 integer bits (The higher bits past 96+64 th aren't used). [SqrtPriceQ64F96] is always in sqrt -/// units of asset one. -pub type SqrtPriceQ64F96 = U256; - -/// The number of fractional bits used by `SqrtPriceQ64F96`. -pub const SQRT_PRICE_FRACTIONAL_BITS: u32 = 96; - #[derive(Debug)] pub enum SetFeesError { /// Fee must be between 0 - 50% @@ -190,64 +172,6 @@ impl, R> core::ops::Add> for PoolPairsMap>(a: U256, b: U256, c: C) -> U256 { - let c: U512 = c.into(); - (U256::full_mul(a, b) / c).try_into().unwrap() -} - -pub fn mul_div_ceil>(a: U256, b: U256, c: C) -> U256 { - mul_div(a, b, c).1 -} - -pub(super) fn mul_div>(a: U256, b: U256, c: C) -> (U256, U256) { - let c: U512 = c.into(); - - let (d, m) = U512::div_mod(U256::full_mul(a, b), c); - - ( - d.try_into().unwrap(), - if m > U512::from(0) { - // cannot overflow as for m > 0, c must be > 1, and as (a*b) < U512::MAX, therefore - // a*b/c < U512::MAX - d + 1 - } else { - d - } - .try_into() - .unwrap(), - ) -} - -pub fn bounded_sqrt_price(quote: Amount, base: Amount) -> SqrtPriceQ64F96 { - assert!(!quote.is_zero() || !base.is_zero()); - - if base.is_zero() { - MAX_SQRT_PRICE - } else { - let unbounded_sqrt_price = U256::try_from( - ((U512::from(quote) << 256) / U512::from(base)).integer_sqrt() >> - (128 - SQRT_PRICE_FRACTIONAL_BITS), - ) - .unwrap(); - - if unbounded_sqrt_price < MIN_SQRT_PRICE { - MIN_SQRT_PRICE - } else if unbounded_sqrt_price > MAX_SQRT_PRICE { - MAX_SQRT_PRICE - } else { - unbounded_sqrt_price - } - } -} - -pub fn output_amount_floor(input: Amount, price: Price) -> Amount { - mul_div_floor(input, price, U256::one() << PRICE_FRACTIONAL_BITS) -} - -pub fn output_amount_ceil(input: Amount, price: Price) -> Amount { - mul_div_ceil(input, price, U256::one() << PRICE_FRACTIONAL_BITS) -} - /// A marker type to represent a swap that buys asset Quote, and sells asset Base pub(super) struct BaseToQuote {} /// A marker type to represent a swap that buys asset Base, and sells asset Quote @@ -307,279 +231,6 @@ impl SwapDirection for QuoteToBase { } } -pub const PRICE_FRACTIONAL_BITS: u32 = 128; - -/// Converts from a [SqrtPriceQ64F96] to a [Price]. -/// -/// Will panic for `sqrt_price`'s outside `MIN_SQRT_PRICE..=MAX_SQRT_PRICE` -pub fn sqrt_price_to_price(sqrt_price: SqrtPriceQ64F96) -> Price { - assert!(is_sqrt_price_valid(sqrt_price)); - - // Note the value here cannot ever be zero as MIN_SQRT_PRICE has its 33th bit set, so sqrt_price - // will always include a bit pass the 64th bit that is set, so when we shift down below that set - // bit will not be removed. - mul_div_floor( - sqrt_price, - sqrt_price, - SqrtPriceQ64F96::one() << (2 * SQRT_PRICE_FRACTIONAL_BITS - PRICE_FRACTIONAL_BITS), - ) -} - -/// Converts from a `price` to a `sqrt_price` -/// -/// This function never panics. -pub fn price_to_sqrt_price(price: Price) -> SqrtPriceQ64F96 { - ((U512::from(price) << PRICE_FRACTIONAL_BITS).integer_sqrt() >> - (PRICE_FRACTIONAL_BITS - SQRT_PRICE_FRACTIONAL_BITS)) - .try_into() - .unwrap_or(SqrtPriceQ64F96::MAX) -} - -/// Converts a `tick` to a `price`. Will return `None` for ticks outside MIN_TICK..=MAX_TICK -/// -/// This function never panics. -pub fn price_at_tick(tick: Tick) -> Option { - if is_tick_valid(tick) { - Some(sqrt_price_to_price(sqrt_price_at_tick(tick))) - } else { - None - } -} - -/// Converts a `price` to a `tick`. Will return `None` is the price is too high or low to be -/// represented by a valid tick i.e. one inside MIN_TICK..=MAX_TICK. -/// -/// This function never panics. -pub fn tick_at_price(price: Price) -> Option { - let sqrt_price = price_to_sqrt_price(price); - if is_sqrt_price_valid(sqrt_price) { - Some(tick_at_sqrt_price(sqrt_price)) - } else { - None - } -} - -/// The minimum tick that may be passed to `sqrt_price_at_tick` computed from log base 1.0001 of -/// 2**-128 -pub const MIN_TICK: Tick = -887272; -/// The maximum tick that may be passed to `sqrt_price_at_tick` computed from log base 1.0001 of -/// 2**128 -pub const MAX_TICK: Tick = -MIN_TICK; -/// The minimum value that can be returned from `sqrt_price_at_tick`. Equivalent to -/// `sqrt_price_at_tick(MIN_TICK)` -pub(super) const MIN_SQRT_PRICE: SqrtPriceQ64F96 = U256([0x1000276a3u64, 0x0, 0x0, 0x0]); -/// The maximum value that can be returned from `sqrt_price_at_tick`. Equivalent to -/// `sqrt_price_at_tick(MAX_TICK)`. -pub(super) const MAX_SQRT_PRICE: SqrtPriceQ64F96 = - U256([0x5d951d5263988d26u64, 0xefd1fc6a50648849u64, 0xfffd8963u64, 0x0u64]); - -pub(super) fn is_sqrt_price_valid(sqrt_price: SqrtPriceQ64F96) -> bool { - (MIN_SQRT_PRICE..=MAX_SQRT_PRICE).contains(&sqrt_price) -} - -pub fn is_tick_valid(tick: Tick) -> bool { - (MIN_TICK..=MAX_TICK).contains(&tick) -} - -pub(super) fn sqrt_price_at_tick(tick: Tick) -> SqrtPriceQ64F96 { - assert!(is_tick_valid(tick)); - - let abs_tick = tick.unsigned_abs(); - - let mut r = if abs_tick & 0x1u32 != 0 { - U256::from(0xfffcb933bd6fad37aa2d162d1a594001u128) - } else { - U256::one() << 128u128 - }; - - macro_rules! handle_tick_bit { - ($bit:literal, $constant:literal) => { - /* Proof that `checked_mul` does not overflow: - Note that the value ratio is initialized with above is such that `ratio <= (U256::one() << 128u128)`, alternatively `ratio <= (u128::MAX + 1)` - First consider the case of applying the macro once assuming `ratio <= (U256::one() << 128u128)`: - If ∀r ∈ U256, `r <= (U256::one() << 128u128)`, ∀C ∈ "Set of constants the macro is used with (Listed below)" - Then `C * r <= U256::MAX` (See `debug_assertions` below) - Therefore the `checked_mul` will not overflow - Also note that above `(C * r >> 128u128) <= UINT128_MAX` - Therefore if the if branch is taken ratio will be assigned a value `<= u128::MAX` - else ratio is unchanged and remains `ratio <= u128::MAX + 1` - Therefore as the assumption `ratio <= u128::MAX + 1` is always maintained after applying the macro, - none of the checked_mul calls in any of the applications of the macro will overflow - */ - #[cfg(debug_assertions)] - U256::checked_mul(U256::one() << 128u128, $constant.into()).unwrap(); - if abs_tick & (0x1u32 << $bit) != 0 { - r = U256::checked_mul(r, U256::from($constant)).unwrap() >> 128u128 - } - } - } - - handle_tick_bit!(1, 0xfff97272373d413259a46990580e213au128); - handle_tick_bit!(2, 0xfff2e50f5f656932ef12357cf3c7fdccu128); - handle_tick_bit!(3, 0xffe5caca7e10e4e61c3624eaa0941cd0u128); - handle_tick_bit!(4, 0xffcb9843d60f6159c9db58835c926644u128); - handle_tick_bit!(5, 0xff973b41fa98c081472e6896dfb254c0u128); - handle_tick_bit!(6, 0xff2ea16466c96a3843ec78b326b52861u128); - handle_tick_bit!(7, 0xfe5dee046a99a2a811c461f1969c3053u128); - handle_tick_bit!(8, 0xfcbe86c7900a88aedcffc83b479aa3a4u128); - handle_tick_bit!(9, 0xf987a7253ac413176f2b074cf7815e54u128); - handle_tick_bit!(10, 0xf3392b0822b70005940c7a398e4b70f3u128); - handle_tick_bit!(11, 0xe7159475a2c29b7443b29c7fa6e889d9u128); - handle_tick_bit!(12, 0xd097f3bdfd2022b8845ad8f792aa5825u128); - handle_tick_bit!(13, 0xa9f746462d870fdf8a65dc1f90e061e5u128); - handle_tick_bit!(14, 0x70d869a156d2a1b890bb3df62baf32f7u128); - handle_tick_bit!(15, 0x31be135f97d08fd981231505542fcfa6u128); - handle_tick_bit!(16, 0x9aa508b5b7a84e1c677de54f3e99bc9u128); - handle_tick_bit!(17, 0x5d6af8dedb81196699c329225ee604u128); - handle_tick_bit!(18, 0x2216e584f5fa1ea926041bedfe98u128); - handle_tick_bit!(19, 0x48a170391f7dc42444e8fa2u128); - // Note due to MIN_TICK and MAX_TICK bounds, past the 20th bit abs_tick is all zeros - - /* Proof that r is never zero (therefore avoiding the divide by zero case here): - We can think of an application of the `handle_tick_bit` macro as increasing the index I of r's MSB/`r.ilog2()` (mul by constant), and then decreasing it by 128 (the right shift). - - Note the increase in I caused by the constant mul will be at least constant.ilog2(). - - Also note each application of `handle_tick_bit` decreases (if the if branch is entered) or else maintains r's value as all the constants are less than 2^128. - - Therefore the largest decrease would be caused if all the macros application's if branches where entered. - - So we assuming all if branches are entered, after all the applications `I` would be at least I_initial + bigsum(constant.ilog2()) - 19*128. - - The test `r_non_zero` checks with value is >= 0, therefore imply the smallest value r could have is more than 0. - */ - let sqrt_price_q32f128 = if tick > 0 { U256::MAX / r } else { r }; - - // we round up in the division so tick_at_sqrt_price of the output price is always - // consistent - (sqrt_price_q32f128 >> 32u128) + - if sqrt_price_q32f128.low_u32() == 0 { U256::zero() } else { U256::one() } -} - -/// Calculates the greatest tick value such that `sqrt_price_at_tick(tick) <= sqrt_price` -pub fn tick_at_sqrt_price(sqrt_price: SqrtPriceQ64F96) -> Tick { - assert!(is_sqrt_price_valid(sqrt_price)); - - let sqrt_price_q64f128 = sqrt_price << 32u128; - - let (integer_log_2, mantissa) = { - let mut _bits_remaining = sqrt_price_q64f128; - let mut most_significant_bit = 0u8; - - // rustfmt chokes when formatting this macro. - // See: https://github.com/rust-lang/rustfmt/issues/5404 - #[rustfmt::skip] - macro_rules! add_integer_bit { - ($bit:literal, $lower_bits_mask:literal) => { - if _bits_remaining > U256::from($lower_bits_mask) { - most_significant_bit |= $bit; - _bits_remaining >>= $bit; - } - }; - } - - add_integer_bit!(128u8, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFu128); - add_integer_bit!(64u8, 0xFFFFFFFFFFFFFFFFu128); - add_integer_bit!(32u8, 0xFFFFFFFFu128); - add_integer_bit!(16u8, 0xFFFFu128); - add_integer_bit!(8u8, 0xFFu128); - add_integer_bit!(4u8, 0xFu128); - add_integer_bit!(2u8, 0x3u128); - add_integer_bit!(1u8, 0x1u128); - - ( - // most_significant_bit is the log2 of sqrt_price_q64f128 as an integer. This - // converts most_significant_bit to the integer log2 of sqrt_price_q64f128 as an - // q64f128 - ((most_significant_bit as i16) + (-128i16)) as i8, - // Calculate mantissa of sqrt_price_q64f128. - if most_significant_bit >= 128u8 { - // The bits we possibly drop when right shifting don't contribute to the log2 - // above the 14th fractional bit. - sqrt_price_q64f128 >> (most_significant_bit - 127u8) - } else { - sqrt_price_q64f128 << (127u8 - most_significant_bit) - } - .as_u128(), // Conversion to u128 is safe as top 128 bits are always zero - ) - }; - - let log_2_q63f64 = { - let mut log_2_q63f64 = (integer_log_2 as i128) << 64u8; - let mut _mantissa = mantissa; - - // rustfmt chokes when formatting this macro. - // See: https://github.com/rust-lang/rustfmt/issues/5404 - #[rustfmt::skip] - macro_rules! add_fractional_bit { - ($bit:literal) => { - // Note squaring a number doubles its log - let mantissa_sq = - (U256::checked_mul(_mantissa.into(), _mantissa.into()).unwrap() >> 127u8); - _mantissa = if mantissa_sq.bit(128) { - // is the 129th bit set, all higher bits must be zero due to 127 right bit - // shift - log_2_q63f64 |= 1i128 << $bit; - (mantissa_sq >> 1u8).as_u128() - } else { - mantissa_sq.as_u128() - } - }; - } - - add_fractional_bit!(63u8); - add_fractional_bit!(62u8); - add_fractional_bit!(61u8); - add_fractional_bit!(60u8); - add_fractional_bit!(59u8); - add_fractional_bit!(58u8); - add_fractional_bit!(57u8); - add_fractional_bit!(56u8); - add_fractional_bit!(55u8); - add_fractional_bit!(54u8); - add_fractional_bit!(53u8); - add_fractional_bit!(52u8); - add_fractional_bit!(51u8); - add_fractional_bit!(50u8); - - // We don't need more precision than (63..50) = 14 bits - - log_2_q63f64 - }; - - // Note we don't have a I256 type so I have to handle the negative mul case manually - let log_sqrt10001_q127f128 = U256::overflowing_mul( - if log_2_q63f64 < 0 { - (U256::from(u128::MAX) << 128u8) | U256::from(log_2_q63f64 as u128) - } else { - U256::from(log_2_q63f64 as u128) - }, - U256::from(255738958999603826347141u128), - ) - .0; - - let tick_low = (U256::overflowing_sub( - log_sqrt10001_q127f128, - U256::from(3402992956809132418596140100660247210u128), - ) - .0 >> 128u8) - .as_u128() as Tick; // Add Checks - let tick_high = (U256::overflowing_add( - log_sqrt10001_q127f128, - U256::from(291339464771989622907027621153398088495u128), - ) - .0 >> 128u8) - .as_u128() as Tick; // Add Checks - - if tick_low == tick_high { - tick_low - } else if sqrt_price_at_tick(tick_high) <= sqrt_price { - tick_high - } else { - tick_low - } -} - /// Takes a Q128 fixed point number and raises it to the nth power, and returns it as a Q128 fixed /// point number. If the result is larger than the maximum U384 this function will panic. /// @@ -645,20 +296,7 @@ mod test { use rand::SeedableRng; #[cfg(feature = "slow-tests")] - use crate::test_utilities::rng_u256_inclusive_bound; - - #[cfg(feature = "slow-tests")] - #[test] - fn test_sqrt_price() { - let mut rng: rand::rngs::StdRng = rand::rngs::StdRng::from_seed([0; 32]); - - for _i in 0..10000000 { - assert!(is_sqrt_price_valid(bounded_sqrt_price( - rng_u256_inclusive_bound(&mut rng, Amount::one()..=Amount::MAX), - rng_u256_inclusive_bound(&mut rng, Amount::one()..=Amount::MAX), - ))); - } - } + use cf_amm_math::test_utilities::rng_u256_inclusive_bound; #[cfg(feature = "slow-tests")] #[test] @@ -768,221 +406,4 @@ mod test { } } } - - #[test] - fn test_mul_div_floor() { - assert_eq!(mul_div_floor(1.into(), 1.into(), 1), 1.into()); - assert_eq!(mul_div_floor(1.into(), 1.into(), 2), 0.into()); - assert_eq!(mul_div_floor(1.into(), 2.into(), 1), 2.into()); - assert_eq!(mul_div_floor(1.into(), 2.into(), 2), 1.into()); - assert_eq!(mul_div_floor(1.into(), 2.into(), 3), 0.into()); - assert_eq!(mul_div_floor(1.into(), 3.into(), 2), 1.into()); - assert_eq!(mul_div_floor(1.into(), 3.into(), 3), 1.into()); - assert_eq!(mul_div_floor(1.into(), 3.into(), 4), 0.into()); - assert_eq!(mul_div_floor(1.into(), 4.into(), 3), 1.into()); - assert_eq!(mul_div_floor(1.into(), 4.into(), 4), 1.into()); - assert_eq!(mul_div_floor(1.into(), 4.into(), 5), 0.into()); - assert_eq!(mul_div_floor(1.into(), 5.into(), 4), 1.into()); - assert_eq!(mul_div_floor(1.into(), 5.into(), 5), 1.into()); - assert_eq!(mul_div_floor(1.into(), 5.into(), 6), 0.into()); - - assert_eq!(mul_div_floor(2.into(), 1.into(), 2), 1.into()); - assert_eq!(mul_div_floor(2.into(), 1.into(), 3), 0.into()); - assert_eq!(mul_div_floor(3.into(), 1.into(), 2), 1.into()); - assert_eq!(mul_div_floor(3.into(), 1.into(), 3), 1.into()); - assert_eq!(mul_div_floor(3.into(), 1.into(), 4), 0.into()); - assert_eq!(mul_div_floor(4.into(), 1.into(), 3), 1.into()); - assert_eq!(mul_div_floor(4.into(), 1.into(), 4), 1.into()); - assert_eq!(mul_div_floor(4.into(), 1.into(), 5), 0.into()); - assert_eq!(mul_div_floor(5.into(), 1.into(), 4), 1.into()); - assert_eq!(mul_div_floor(5.into(), 1.into(), 5), 1.into()); - assert_eq!(mul_div_floor(5.into(), 1.into(), 6), 0.into()); - - assert_eq!(mul_div_floor(2.into(), 1.into(), 1), 2.into()); - assert_eq!(mul_div_floor(2.into(), 1.into(), 2), 1.into()); - - assert_eq!(mul_div_floor(U256::MAX, U256::MAX, U256::MAX), U256::MAX); - assert_eq!(mul_div_floor(U256::MAX, U256::MAX - 1, U256::MAX), U256::MAX - 1); - } - - #[test] - fn test_mul_div() { - assert_eq!(mul_div(U256::MAX, U256::MAX, U256::MAX), (U256::MAX, U256::MAX)); - assert_eq!(mul_div(U256::MAX, U256::MAX - 1, U256::MAX), (U256::MAX - 1, U256::MAX - 1)); - assert_eq!(mul_div(2.into(), 2.into(), 3), (1.into(), 2.into())); - assert_eq!(mul_div(2.into(), 2.into(), 4), (1.into(), 1.into())); - assert_eq!(mul_div(2.into(), 2.into(), 5), (0.into(), 1.into())); - assert_eq!(mul_div(2.into(), 2.into(), 6), (0.into(), 1.into())); - } - - #[cfg(feature = "slow-tests")] - #[test] - fn test_conversion_sqrt_price_back_and_forth() { - for tick in MIN_TICK..=MAX_TICK { - assert_eq!(tick, tick_at_sqrt_price(sqrt_price_at_tick(tick))); - } - } - - #[test] - fn test_sqrt_price_at_tick() { - assert_eq!(sqrt_price_at_tick(MIN_TICK), MIN_SQRT_PRICE); - assert_eq!(sqrt_price_at_tick(-738203), U256::from_dec_str("7409801140451").unwrap()); - assert_eq!(sqrt_price_at_tick(-500000), U256::from_dec_str("1101692437043807371").unwrap()); - assert_eq!( - sqrt_price_at_tick(-250000), - U256::from_dec_str("295440463448801648376846").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(-150000), - U256::from_dec_str("43836292794701720435367485").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(-50000), - U256::from_dec_str("6504256538020985011912221507").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(-5000), - U256::from_dec_str("61703726247759831737814779831").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(-4000), - U256::from_dec_str("64867181785621769311890333195").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(-3000), - U256::from_dec_str("68192822843687888778582228483").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(-2500), - U256::from_dec_str("69919044979842180277688105136").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(-1000), - U256::from_dec_str("75364347830767020784054125655").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(-500), - U256::from_dec_str("77272108795590369356373805297").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(-250), - U256::from_dec_str("78244023372248365697264290337").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(-100), - U256::from_dec_str("78833030112140176575862854579").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(-50), - U256::from_dec_str("79030349367926598376800521322").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(50), - U256::from_dec_str("79426470787362580746886972461").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(100), - U256::from_dec_str("79625275426524748796330556128").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(250), - U256::from_dec_str("80224679980005306637834519095").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(500), - U256::from_dec_str("81233731461783161732293370115").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(1000), - U256::from_dec_str("83290069058676223003182343270").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(2500), - U256::from_dec_str("89776708723587163891445672585").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(3000), - U256::from_dec_str("92049301871182272007977902845").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(4000), - U256::from_dec_str("96768528593268422080558758223").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(5000), - U256::from_dec_str("101729702841318637793976746270").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(50000), - U256::from_dec_str("965075977353221155028623082916").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(150000), - U256::from_dec_str("143194173941309278083010301478497").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(250000), - U256::from_dec_str("21246587762933397357449903968194344").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(500000), - U256::from_dec_str("5697689776495288729098254600827762987878").unwrap() - ); - assert_eq!( - sqrt_price_at_tick(738203), - U256::from_dec_str("847134979253254120489401328389043031315994541").unwrap() - ); - assert_eq!(sqrt_price_at_tick(MAX_TICK), MAX_SQRT_PRICE); - } - - #[test] - fn test_tick_at_sqrt_price() { - assert_eq!(tick_at_sqrt_price(MIN_SQRT_PRICE), MIN_TICK); - assert_eq!( - tick_at_sqrt_price(U256::from_dec_str("79228162514264337593543").unwrap()), - -276325 - ); - assert_eq!( - tick_at_sqrt_price(U256::from_dec_str("79228162514264337593543950").unwrap()), - -138163 - ); - assert_eq!( - tick_at_sqrt_price(U256::from_dec_str("9903520314283042199192993792").unwrap()), - -41591 - ); - assert_eq!( - tick_at_sqrt_price(U256::from_dec_str("28011385487393069959365969113").unwrap()), - -20796 - ); - assert_eq!( - tick_at_sqrt_price(U256::from_dec_str("56022770974786139918731938227").unwrap()), - -6932 - ); - assert_eq!( - tick_at_sqrt_price(U256::from_dec_str("79228162514264337593543950336").unwrap()), - 0 - ); - assert_eq!( - tick_at_sqrt_price(U256::from_dec_str("112045541949572279837463876454").unwrap()), - 6931 - ); - assert_eq!( - tick_at_sqrt_price(U256::from_dec_str("224091083899144559674927752909").unwrap()), - 20795 - ); - assert_eq!( - tick_at_sqrt_price(U256::from_dec_str("633825300114114700748351602688").unwrap()), - 41590 - ); - assert_eq!( - tick_at_sqrt_price(U256::from_dec_str("79228162514264337593543950336000").unwrap()), - 138162 - ); - assert_eq!( - tick_at_sqrt_price(U256::from_dec_str("79228162514264337593543950336000000").unwrap()), - 276324 - ); - assert_eq!(tick_at_sqrt_price(MAX_SQRT_PRICE - 1), MAX_TICK - 1); - assert_eq!(tick_at_sqrt_price(MAX_SQRT_PRICE), MAX_TICK); - } } diff --git a/state-chain/amm/src/lib.rs b/state-chain/amm/src/lib.rs index 6191e6b79c..61e4d62c00 100644 --- a/state-chain/amm/src/lib.rs +++ b/state-chain/amm/src/lib.rs @@ -1,26 +1,27 @@ #![cfg_attr(not(feature = "std"), no_std)] -pub mod test_utilities; mod tests; use core::convert::Infallible; +use cf_amm_math::{ + bounded_sqrt_price, is_sqrt_price_valid, mul_div_floor, price_to_sqrt_price, + sqrt_price_to_price, tick_at_sqrt_price, Amount, Price, SqrtPriceQ64F96, Tick, +}; use codec::{Decode, Encode}; use common::{ - is_sqrt_price_valid, price_to_sqrt_price, sqrt_price_to_price, tick_at_sqrt_price, Amount, - BaseToQuote, Pairs, PoolPairsMap, Price, QuoteToBase, SetFeesError, Side, SqrtPriceQ64F96, - SwapDirection, Tick, + nth_root_of_integer_as_fixed_point, BaseToQuote, Pairs, PoolPairsMap, QuoteToBase, + SetFeesError, Side, SwapDirection, }; use limit_orders::{Collected, PositionInfo}; use range_orders::Liquidity; use scale_info::TypeInfo; use sp_std::vec::Vec; -use crate::common::{mul_div_floor, nth_root_of_integer_as_fixed_point}; - pub mod common; pub mod limit_orders; pub mod range_orders; +pub use cf_amm_math as math; #[derive( Clone, Debug, TypeInfo, Encode, Decode, serde::Serialize, serde::Deserialize, PartialEq, @@ -82,8 +83,8 @@ impl PoolState { output_amount: Amount, ) -> SqrtPriceQ64F96 { match order.to_sold_pair() { - Pairs::Base => common::bounded_sqrt_price(output_amount, input_amount), - Pairs::Quote => common::bounded_sqrt_price(input_amount, output_amount), + Pairs::Base => bounded_sqrt_price(output_amount, input_amount), + Pairs::Quote => bounded_sqrt_price(input_amount, output_amount), } } diff --git a/state-chain/amm/src/limit_orders.rs b/state-chain/amm/src/limit_orders.rs index f63b25acb2..b429f48afb 100644 --- a/state-chain/amm/src/limit_orders.rs +++ b/state-chain/amm/src/limit_orders.rs @@ -33,9 +33,12 @@ use sp_core::{U256, U512}; use sp_std::vec::Vec; use crate::common::{ - is_tick_valid, mul_div_ceil, mul_div_floor, sqrt_price_at_tick, sqrt_price_to_price, - tick_at_sqrt_price, Amount, BaseToQuote, PoolPairsMap, Price, QuoteToBase, SetFeesError, - SqrtPriceQ64F96, Tick, MAX_LP_FEE, ONE_IN_HUNDREDTH_PIPS, PRICE_FRACTIONAL_BITS, + BaseToQuote, PoolPairsMap, QuoteToBase, SetFeesError, MAX_LP_FEE, ONE_IN_HUNDREDTH_PIPS, +}; +use cf_amm_math::{ + is_tick_valid, mul_div_ceil, mul_div_floor, output_amount_floor, sqrt_price_at_tick, + sqrt_price_to_price, tick_at_sqrt_price, Amount, Price, SqrtPriceQ64F96, Tick, + PRICE_FRACTIONAL_BITS, }; // This is the maximum liquidity/amount of an asset that can be sold at a single tick/price. If an @@ -217,7 +220,7 @@ impl SwapDirection for BaseToQuote { } fn output_amount_floor(input: Amount, price: Price) -> Amount { - crate::common::output_amount_floor(input, price) + output_amount_floor(input, price) } fn best_priced_fixed_pool( diff --git a/state-chain/amm/src/limit_orders/tests.rs b/state-chain/amm/src/limit_orders/tests.rs index e7d8cacf89..80fe21f797 100644 --- a/state-chain/amm/src/limit_orders/tests.rs +++ b/state-chain/amm/src/limit_orders/tests.rs @@ -1,10 +1,7 @@ -use crate::{ - common::{ - mul_div, sqrt_price_at_tick, tick_at_sqrt_price, MAX_SQRT_PRICE, MAX_TICK, MIN_SQRT_PRICE, - MIN_TICK, - }, - limit_orders, range_orders, - test_utilities::rng_u256_inclusive_bound, +use crate::{limit_orders, range_orders}; +use cf_amm_math::{ + mul_div, sqrt_price_at_tick, test_utilities::rng_u256_inclusive_bound, tick_at_sqrt_price, + MAX_SQRT_PRICE, MAX_TICK, MIN_SQRT_PRICE, MIN_TICK, }; use super::*; diff --git a/state-chain/amm/src/range_orders.rs b/state-chain/amm/src/range_orders.rs index 590b1efc78..8e4abeacdd 100644 --- a/state-chain/amm/src/range_orders.rs +++ b/state-chain/amm/src/range_orders.rs @@ -30,9 +30,11 @@ use scale_info::TypeInfo; use sp_core::{U256, U512}; use crate::common::{ + BaseToQuote, Pairs, PoolPairsMap, QuoteToBase, SetFeesError, MAX_LP_FEE, ONE_IN_HUNDREDTH_PIPS, +}; +use cf_amm_math::{ is_sqrt_price_valid, is_tick_valid, mul_div_ceil, mul_div_floor, sqrt_price_at_tick, - tick_at_sqrt_price, Amount, BaseToQuote, Pairs, PoolPairsMap, QuoteToBase, SetFeesError, - SqrtPriceQ64F96, Tick, MAX_LP_FEE, MAX_TICK, MIN_TICK, ONE_IN_HUNDREDTH_PIPS, + tick_at_sqrt_price, Amount, SqrtPriceQ64F96, Tick, MAX_TICK, MIN_TICK, SQRT_PRICE_FRACTIONAL_BITS, }; diff --git a/state-chain/amm/src/range_orders/tests.rs b/state-chain/amm/src/range_orders/tests.rs index b189e10bd7..074be3bc11 100644 --- a/state-chain/amm/src/range_orders/tests.rs +++ b/state-chain/amm/src/range_orders/tests.rs @@ -1,8 +1,9 @@ use rand::{prelude::Distribution, Rng, SeedableRng}; +use crate::common::Pairs; +use cf_amm_math::test_utilities::rng_u256_inclusive_bound; #[cfg(feature = "slow-tests")] -use crate::common::MIN_SQRT_PRICE; -use crate::{common::Pairs, test_utilities::rng_u256_inclusive_bound}; +use cf_amm_math::MIN_SQRT_PRICE; use super::*; diff --git a/state-chain/amm/src/tests.rs b/state-chain/amm/src/tests.rs index a0d1c9110a..16395d2699 100644 --- a/state-chain/amm/src/tests.rs +++ b/state-chain/amm/src/tests.rs @@ -3,9 +3,9 @@ use cf_utilities::assert_ok; use core::convert::Infallible; -use crate::{ - common::{sqrt_price_to_price, Price, MAX_SQRT_PRICE, MIN_SQRT_PRICE, PRICE_FRACTIONAL_BITS}, - range_orders::Liquidity, +use crate::range_orders::Liquidity; +use cf_amm_math::{ + sqrt_price_to_price, Price, MAX_SQRT_PRICE, MIN_SQRT_PRICE, PRICE_FRACTIONAL_BITS, }; use super::*; diff --git a/state-chain/cf-integration-tests/src/swapping.rs b/state-chain/cf-integration-tests/src/swapping.rs index 5127c58d97..d09ea5b8ee 100644 --- a/state-chain/cf-integration-tests/src/swapping.rs +++ b/state-chain/cf-integration-tests/src/swapping.rs @@ -10,7 +10,7 @@ use crate::{ witness_call, witness_ethereum_rotation_broadcast, witness_rotation_broadcasts, }; use cf_amm::{ - common::{price_at_tick, Price, Tick}, + math::{price_at_tick, Price, Tick}, range_orders::Liquidity, }; use cf_chains::{ diff --git a/state-chain/chains/Cargo.toml b/state-chain/chains/Cargo.toml index d3d7c37aaf..c0cfdc9fc5 100644 --- a/state-chain/chains/Cargo.toml +++ b/state-chain/chains/Cargo.toml @@ -9,6 +9,7 @@ description = "Shared Chain-specific functionality for use in the substrate runt workspace = true [dependencies] +cf-amm-math = { workspace = true } cf-primitives = { workspace = true } cf-utilities = { workspace = true } cf-runtime-utilities = { workspace = true } @@ -83,6 +84,7 @@ ed25519-dalek = { workspace = true, features = ["rand_core"] } default = ["std"] std = [ "bech32/std", + "cf-amm-math/std", "cf-primitives/std", "cf-utilities/std", "scale-info/std", diff --git a/state-chain/chains/src/lib.rs b/state-chain/chains/src/lib.rs index 3ded62ee9f..f721f14221 100644 --- a/state-chain/chains/src/lib.rs +++ b/state-chain/chains/src/lib.rs @@ -18,7 +18,8 @@ use address::{ AddressConverter, AddressDerivationApi, AddressDerivationError, EncodedAddress, IntoForeignChainAddress, ToHumanreadableAddress, }; -use cf_primitives::{Asset, AssetAmount, BroadcastId, ChannelId, EgressId, EthAmount, Price, TxId}; +use cf_amm_math::Price; +use cf_primitives::{Asset, AssetAmount, BroadcastId, ChannelId, EgressId, EthAmount, TxId}; use codec::{Decode, Encode, FullCodec, MaxEncodedLen}; use frame_support::{ pallet_prelude::{MaybeSerializeDeserialize, Member, RuntimeDebug}, @@ -887,6 +888,10 @@ impl ChannelRefundParametersGeneric { min_price: self.min_price, }) } + pub fn min_output_amount(&self, input_amount: AssetAmount) -> AssetAmount { + use sp_runtime::traits::UniqueSaturatedInto; + cf_amm_math::output_amount_ceil(input_amount.into(), self.min_price).unique_saturated_into() + } } pub enum RequiresSignatureRefresh> { diff --git a/state-chain/custom-rpc/src/lib.rs b/state-chain/custom-rpc/src/lib.rs index a8bc0cb48c..770d740086 100644 --- a/state-chain/custom-rpc/src/lib.rs +++ b/state-chain/custom-rpc/src/lib.rs @@ -1,7 +1,8 @@ use crate::boost_pool_rpc::BoostPoolFeesRpc; use boost_pool_rpc::BoostPoolDetailsRpc; use cf_amm::{ - common::{Amount as AmmAmount, PoolPairsMap, Side, Tick}, + common::{PoolPairsMap, Side}, + math::{Amount as AmmAmount, Tick}, range_orders::Liquidity, }; use cf_chains::{ @@ -764,7 +765,7 @@ pub trait CustomApi { &self, base_asset: Asset, quote_asset: Asset, - tick_range: Range, + tick_range: Range, at: Option, ) -> RpcResult>; #[method(name = "pool_orderbook")] @@ -787,7 +788,7 @@ pub trait CustomApi { &self, base_asset: Asset, quote_asset: Asset, - tick_range: Range, + tick_range: Range, at: Option, ) -> RpcResult>; #[method(name = "pool_liquidity")] diff --git a/state-chain/pallets/cf-pools/src/benchmarking.rs b/state-chain/pallets/cf-pools/src/benchmarking.rs index 739e4a5c9b..9f65be6035 100644 --- a/state-chain/pallets/cf-pools/src/benchmarking.rs +++ b/state-chain/pallets/cf-pools/src/benchmarking.rs @@ -1,7 +1,7 @@ #![cfg(feature = "runtime-benchmarks")] use super::*; -use cf_amm::common::price_at_tick; +use cf_amm::math::price_at_tick; use cf_chains::ForeignChainAddress; use cf_primitives::{AccountRole, Asset}; use cf_traits::AccountRoleRegistry; diff --git a/state-chain/pallets/cf-pools/src/lib.rs b/state-chain/pallets/cf-pools/src/lib.rs index b804a77d7f..37c6724672 100644 --- a/state-chain/pallets/cf-pools/src/lib.rs +++ b/state-chain/pallets/cf-pools/src/lib.rs @@ -1,7 +1,8 @@ #![cfg_attr(not(feature = "std"), no_std)] use cf_amm::{ - common::{self, Amount, PoolPairsMap, Price, Side, SqrtPriceQ64F96, Tick}, + common::{PoolPairsMap, Side}, limit_orders::{self, Collected, PositionInfo}, + math::{bounded_sqrt_price, Amount, Price, SqrtPriceQ64F96, Tick}, range_orders::{self, Liquidity}, PoolState, }; @@ -159,8 +160,8 @@ pub const PALLET_VERSION: StorageVersion = StorageVersion::new(5); #[frame_support::pallet] pub mod pallet { use cf_amm::{ - common::Tick, limit_orders, + math::Tick, range_orders::{self, Liquidity}, NewError, }; @@ -752,7 +753,7 @@ pub mod pallet { side, id, previous_tick, - IncreaseOrDecrease::Decrease(cf_amm::common::Amount::MAX), + IncreaseOrDecrease::Decrease(Amount::MAX), NoOpStatus::Error, )?; Self::inner_update_limit_order( @@ -827,7 +828,7 @@ pub mod pallet { side, id, previous_tick, - IncreaseOrDecrease::Decrease(cf_amm::common::Amount::MAX), + IncreaseOrDecrease::Decrease(Amount::MAX), NoOpStatus::Error, )?; @@ -1024,9 +1025,7 @@ pub mod pallet { side, id, previous_tick, - crate::pallet::IncreaseOrDecrease::Decrease( - cf_amm::common::Amount::MAX, - ), + crate::pallet::IncreaseOrDecrease::Decrease(Amount::MAX), crate::NoOpStatus::Allow, )?; Ok(()) @@ -1080,6 +1079,8 @@ impl SwappingApi for Pallet { to: any::Asset, input_amount: AssetAmount, ) -> Result { + use cf_amm::math::tick_at_sqrt_price; + let (asset_pair, order) = AssetPair::from_swap(from, to).ok_or(Error::::PoolDoesNotExist)?; Self::try_mutate_pool(asset_pair, |_asset_pair, pool| { @@ -1101,13 +1102,12 @@ impl SwappingApi for Pallet { .ok_or(Error::::InsufficientLiquidity)? .2; - let swap_tick = common::tick_at_sqrt_price( - PoolState::<(T::AccountId, OrderId)>::swap_sqrt_price( + let swap_tick = + tick_at_sqrt_price(PoolState::<(T::AccountId, OrderId)>::swap_sqrt_price( order, input_amount, output_amount, - ), - ); + )); let bounded_swap_tick = if tick_after < tick_before { core::cmp::min(core::cmp::max(tick_after, swap_tick), tick_before) } else { @@ -1311,13 +1311,21 @@ pub struct PoolPriceV1 { pub tick: Tick, } -#[derive(Serialize, Deserialize, Clone, Encode, Decode, TypeInfo, PartialEq, Eq, Debug)] -pub struct PoolPriceV2 { - pub sell: Option, - pub buy: Option, +pub type PoolPriceV2 = PoolPrice; + +#[derive(Clone, Debug, Encode, Decode, TypeInfo, PartialEq, Eq, Serialize, Deserialize)] +pub struct PoolPrice

{ + pub sell: Option

, + pub buy: Option

, pub range_order: SqrtPriceQ64F96, } +impl

PoolPrice

{ + pub fn map_sell_and_buy_prices(self, f: impl Fn(P) -> R) -> PoolPrice { + PoolPrice { sell: self.sell.map(&f), buy: self.buy.map(&f), range_order: self.range_order } + } +} + #[derive(PartialEq, Eq)] enum NoOpStatus { Allow, @@ -1383,8 +1391,8 @@ impl Pallet { lp: &T::AccountId, side: Side, id: OrderId, - tick: cf_amm::common::Tick, - sold_amount: cf_amm::common::Amount, + tick: Tick, + sold_amount: Amount, noop_status: NoOpStatus, ) -> Result<(Collected, PositionInfo), DispatchError> { let (collected, position_info) = match pool.pool_state.collect_and_mint_limit_order( @@ -1419,8 +1427,8 @@ impl Pallet { asset_pair: &AssetPair, side: Side, id: OrderId, - tick: cf_amm::common::Tick, - sold_amount_change: IncreaseOrDecrease, + tick: Tick, + sold_amount_change: IncreaseOrDecrease, noop_status: NoOpStatus, ) -> Result { let (sold_amount_change, position_info, collected) = match sold_amount_change { @@ -1495,7 +1503,7 @@ impl Pallet { lp: &T::AccountId, asset_pair: &AssetPair, id: OrderId, - tick_range: Range, + tick_range: Range, size_change: IncreaseOrDecrease, noop_status: NoOpStatus, ) -> Result<(AssetAmounts, Liquidity), DispatchError> { @@ -1704,12 +1712,21 @@ impl Pallet { }) } - pub fn pool_price(base_asset: Asset, quote_asset: Asset) -> Result { + pub fn pool_price( + base_asset: Asset, + quote_asset: Asset, + ) -> Result, DispatchError> { let asset_pair = AssetPair::try_new::(base_asset, quote_asset)?; let mut pool = Pools::::get(asset_pair).ok_or(Error::::PoolDoesNotExist)?; - Ok(PoolPriceV2 { - sell: pool.pool_state.current_price(Side::Sell).map(|(_, sqrt_price, _)| sqrt_price), - buy: pool.pool_state.current_price(Side::Buy).map(|(_, sqrt_price, _)| sqrt_price), + Ok(PoolPrice { + sell: pool + .pool_state + .current_price(Side::Sell) + .map(|(price, sqrt_price, tick)| PoolPriceV1 { price, sqrt_price, tick }), + buy: pool + .pool_state + .current_price(Side::Buy) + .map(|(price, sqrt_price, tick)| PoolPriceV1 { price, sqrt_price, tick }), range_order: pool.pool_state.current_range_order_pool_price(), }) } @@ -1717,7 +1734,7 @@ impl Pallet { pub fn required_asset_ratio_for_range_order( base_asset: any::Asset, quote_asset: any::Asset, - tick_range: Range, + tick_range: Range, ) -> Result, DispatchError> { let pool_state = Pools::::get(AssetPair::try_new::(base_asset, quote_asset)?) .ok_or(Error::::PoolDoesNotExist)? @@ -1768,7 +1785,7 @@ impl Pallet { } else { Some(PoolOrder { amount: sold_base_amount, - sqrt_price: cf_amm::common::bounded_sqrt_price( + sqrt_price: bounded_sqrt_price( bought_quote_amount, sold_base_amount, ), @@ -1794,7 +1811,7 @@ impl Pallet { } else { Some(PoolOrder { amount: bought_base_amount, - sqrt_price: cf_amm::common::bounded_sqrt_price( + sqrt_price: bounded_sqrt_price( sold_quote_amount, bought_base_amount, ), @@ -1809,7 +1826,7 @@ impl Pallet { pub fn pool_depth( base_asset: any::Asset, quote_asset: any::Asset, - tick_range: Range, + tick_range: Range, ) -> Result, DispatchError> { let asset_pair = AssetPair::try_new::(base_asset, quote_asset)?; let mut pool = Pools::::get(asset_pair).ok_or(Error::::PoolDoesNotExist)?; diff --git a/state-chain/pallets/cf-pools/src/tests.rs b/state-chain/pallets/cf-pools/src/tests.rs index 22235afa8c..b79bd5a5f4 100644 --- a/state-chain/pallets/cf-pools/src/tests.rs +++ b/state-chain/pallets/cf-pools/src/tests.rs @@ -3,7 +3,10 @@ use crate::{ HistoricalEarnedFees, LimitOrder, PoolInfo, PoolOrders, PoolPairsMap, Pools, RangeOrder, RangeOrderSize, ScheduledLimitOrderUpdates, STABLE_ASSET, }; -use cf_amm::common::{price_at_tick, Side, Tick}; +use cf_amm::{ + common::Side, + math::{price_at_tick, Tick}, +}; use cf_primitives::{chains::assets::any::Asset, AssetAmount}; use cf_test_utilities::{assert_events_match, assert_has_event, last_event}; use cf_traits::{PoolApi, SwappingApi}; diff --git a/state-chain/pallets/cf-swapping/src/lib.rs b/state-chain/pallets/cf-swapping/src/lib.rs index 4ee96555e6..ecc5a1224e 100644 --- a/state-chain/pallets/cf-swapping/src/lib.rs +++ b/state-chain/pallets/cf-swapping/src/lib.rs @@ -384,7 +384,7 @@ where pub mod pallet { use core::cmp::max; - use cf_amm::common::{output_amount_ceil, sqrt_price_to_price, SqrtPriceQ64F96}; + use cf_amm::math::{output_amount_ceil, sqrt_price_to_price, SqrtPriceQ64F96}; use cf_chains::{address::EncodedAddress, AnyChain, Chain}; use cf_primitives::{ AffiliateShortId, Asset, AssetAmount, BasisPoints, BlockNumber, DcaParameters, EgressId, @@ -1218,7 +1218,7 @@ pub mod pallet { // amount via pool price. Some( output_amount_ceil( - cf_amm::common::Amount::from(swap.input_amount()), + cf_amm::math::Amount::from(swap.input_amount()), sqrt_price_to_price(pool_sell_price?), ) .saturated_into(), @@ -2508,11 +2508,7 @@ pub(crate) mod utilities { ) -> SwapRefundParameters { SwapRefundParameters { refund_block: execute_at_block.saturating_add(params.retry_duration), - min_output: u128::try_from(cf_amm::common::output_amount_ceil( - input_amount.into(), - params.min_price, - )) - .unwrap_or(u128::MAX), + min_output: params.min_output_amount(input_amount), } } diff --git a/state-chain/pallets/cf-swapping/src/tests.rs b/state-chain/pallets/cf-swapping/src/tests.rs index abd7dbbdca..42d34c66dc 100644 --- a/state-chain/pallets/cf-swapping/src/tests.rs +++ b/state-chain/pallets/cf-swapping/src/tests.rs @@ -12,7 +12,7 @@ use crate::{ CollectedRejectedFunds, Error, Event, MaximumSwapAmount, Pallet, Swap, SwapOrigin, SwapQueue, SwapType, }; -use cf_amm::common::{price_to_sqrt_price, PRICE_FRACTIONAL_BITS}; +use cf_amm::math::{price_to_sqrt_price, PRICE_FRACTIONAL_BITS}; use cf_chains::{ self, address::{AddressConverter, EncodedAddress, ForeignChainAddress}, @@ -99,7 +99,7 @@ struct TestRefundParams { impl TestRefundParams { fn into_channel_params(self, input_amount: AssetAmount) -> ChannelRefundParameters { - use cf_amm::common::{bounded_sqrt_price, sqrt_price_to_price}; + use cf_amm::math::{bounded_sqrt_price, sqrt_price_to_price}; ChannelRefundParameters { retry_duration: self.retry_duration, diff --git a/state-chain/primitives/src/lib.rs b/state-chain/primitives/src/lib.rs index f372a6f4a7..bf82a9dc29 100644 --- a/state-chain/primitives/src/lib.rs +++ b/state-chain/primitives/src/lib.rs @@ -10,7 +10,7 @@ use frame_support::sp_runtime::{ }; use scale_info::TypeInfo; use serde::{Deserialize, Serialize}; -use sp_core::{ConstU32, U256}; +use sp_core::ConstU32; use sp_std::{ cmp::{Ord, PartialOrd}, fmt, @@ -103,20 +103,6 @@ pub type BoostPoolTier = u16; define_wrapper_type!(AffiliateShortId, u8, extra_derives: Serialize, Deserialize, PartialOrd, Ord); -// TODO: Consider alternative representation for Price: -// -// increasing Price to U512 or switch to a f64 (f64 would only be for the external -// price representation), as at low ticks the precision in the price is VERY LOW, but this does not -// cause any problems for the AMM code in terms of correctness - -/// This is the ratio of equivalently valued amounts of asset One and asset Zero. -/// -/// The price is always measured in amount of asset One per unit of asset Zero. Therefore as asset -/// zero becomes more valuable relative to asset one the price's literal value goes up, and vice -/// versa. This ratio is represented as a fixed point number with `PRICE_FRACTIONAL_BITS` fractional -/// bits. -pub type Price = U256; - /// The type of the Id given to threshold signature requests. Note a single request may /// result in multiple ceremonies, but only one ceremony should succeed. pub type ThresholdSignatureRequestId = u32; diff --git a/state-chain/runtime/src/lib.rs b/state-chain/runtime/src/lib.rs index 31e9c19400..67a41ccd7f 100644 --- a/state-chain/runtime/src/lib.rs +++ b/state-chain/runtime/src/lib.rs @@ -32,7 +32,8 @@ use crate::{ }, }; use cf_amm::{ - common::{Amount, PoolPairsMap, Side, Tick}, + common::{PoolPairsMap, Side}, + math::{Amount, Tick}, range_orders::Liquidity, }; pub use cf_chains::instances::{ @@ -1497,7 +1498,10 @@ impl_runtime_apis! { } fn cf_pool_price_v2(base_asset: Asset, quote_asset: Asset) -> Result { - LiquidityPools::pool_price(base_asset, quote_asset).map_err(Into::into) + Ok( + LiquidityPools::pool_price(base_asset, quote_asset)? + .map_sell_and_buy_prices(|price| price.sqrt_price) + ) } /// Simulates a swap and return the intermediate (if any) and final output. @@ -1663,7 +1667,7 @@ impl_runtime_apis! { } - fn cf_pool_depth(base_asset: Asset, quote_asset: Asset, tick_range: Range) -> Result, DispatchErrorWithMessage> { + fn cf_pool_depth(base_asset: Asset, quote_asset: Asset, tick_range: Range) -> Result, DispatchErrorWithMessage> { LiquidityPools::pool_depth(base_asset, quote_asset, tick_range).map_err(Into::into) } @@ -1674,7 +1678,7 @@ impl_runtime_apis! { fn cf_required_asset_ratio_for_range_order( base_asset: Asset, quote_asset: Asset, - tick_range: Range, + tick_range: Range, ) -> Result, DispatchErrorWithMessage> { LiquidityPools::required_asset_ratio_for_range_order(base_asset, quote_asset, tick_range).map_err(Into::into) } @@ -1965,14 +1969,23 @@ impl_runtime_apis! { let current_block = System::block_number(); pallet_cf_swapping::SwapQueue::::iter().flat_map(|(block, swaps_for_block)| { - // In case `block` has already passed, the swaps will be re-tried at the next block: let execute_at = core::cmp::max(block, current_block.saturating_add(1)); - let swaps: Vec<_> = swaps_for_block.iter().filter(|swap| swap.from == base_asset || swap.to == base_asset).cloned().collect(); + let swaps: Vec<_> = swaps_for_block + .iter() + .filter(|swap| swap.from == base_asset || swap.to == base_asset) + .cloned() + .collect(); + + let pool_sell_price = LiquidityPools::pool_price(base_asset, quote_asset). + expect("Pool should exist") + .sell + .map(|price| price.sqrt_price); - let pool_sell_price = LiquidityPools::pool_price(base_asset, quote_asset).expect("Pool should exist").sell; - Swapping::get_scheduled_swap_legs(swaps, base_asset, pool_sell_price).into_iter().map(move |swap| (swap, execute_at)) + Swapping::get_scheduled_swap_legs(swaps, base_asset, pool_sell_price) + .into_iter() + .map(move |swap| (swap, execute_at)) }).collect() } diff --git a/state-chain/runtime/src/runtime_apis.rs b/state-chain/runtime/src/runtime_apis.rs index b92a4638e1..fb8c4e5a0b 100644 --- a/state-chain/runtime/src/runtime_apis.rs +++ b/state-chain/runtime/src/runtime_apis.rs @@ -1,6 +1,7 @@ use crate::{chainflip::Offence, Runtime, RuntimeSafeMode}; use cf_amm::{ - common::{Amount, PoolPairsMap, Side, Tick}, + common::{PoolPairsMap, Side}, + math::{Amount, Tick}, range_orders::Liquidity, }; use cf_chains::{ @@ -315,7 +316,7 @@ decl_runtime_apis!( fn cf_pool_depth( base_asset: Asset, quote_asset: Asset, - tick_range: Range, + tick_range: Range, ) -> Result, DispatchErrorWithMessage>; fn cf_pool_liquidity( base_asset: Asset, @@ -324,7 +325,7 @@ decl_runtime_apis!( fn cf_required_asset_ratio_for_range_order( base_asset: Asset, quote_asset: Asset, - tick_range: Range, + tick_range: Range, ) -> Result, DispatchErrorWithMessage>; fn cf_pool_orderbook( base_asset: Asset,