diff --git a/programs/whirlpool/src/math/tick_math.rs b/programs/whirlpool/src/math/tick_math.rs index 6b41cbf9f..f432282da 100644 --- a/programs/whirlpool/src/math/tick_math.rs +++ b/programs/whirlpool/src/math/tick_math.rs @@ -341,22 +341,22 @@ mod test_tick_index_from_sqrt_price { use crate::state::{MAX_TICK_INDEX, MIN_TICK_INDEX}; #[test] - fn test_sqrt_price_from_tick_index_at_max() { + fn test_tick_index_from_sqrt_price_at_max() { let r = tick_index_from_sqrt_price(&MAX_SQRT_PRICE_X64); assert_eq!(&r, &MAX_TICK_INDEX); } #[test] - fn test_sqrt_price_from_tick_index_at_min() { + fn test_tick_index_from_sqrt_price_at_min() { let r = tick_index_from_sqrt_price(&MIN_SQRT_PRICE_X64); assert_eq!(&r, &MIN_TICK_INDEX); } #[test] - fn test_sqrt_price_from_tick_index_at_max_add_one() { + fn test_tick_index_from_sqrt_price_at_max_add_one() { let sqrt_price_x64_max_add_one = MAX_SQRT_PRICE_X64 + 1; let tick_from_max_add_one = tick_index_from_sqrt_price(&sqrt_price_x64_max_add_one); - let sqrt_price_x64_max = MAX_SQRT_PRICE_X64 + 1; + let sqrt_price_x64_max = MAX_SQRT_PRICE_X64; let tick_from_max = tick_index_from_sqrt_price(&sqrt_price_x64_max); // We don't care about accuracy over the limit. We just care about it's equality properties. @@ -364,21 +364,21 @@ mod test_tick_index_from_sqrt_price { } #[test] - fn test_sqrt_price_from_tick_index_at_min_add_one() { + fn test_tick_index_from_sqrt_price_at_min_add_one() { let sqrt_price_x64 = MIN_SQRT_PRICE_X64 + 1; let r = tick_index_from_sqrt_price(&sqrt_price_x64); assert_eq!(&r, &(MIN_TICK_INDEX)); } #[test] - fn test_sqrt_price_from_tick_index_at_max_sub_one() { + fn test_tick_index_from_sqrt_price_at_max_sub_one() { let sqrt_price_x64 = MAX_SQRT_PRICE_X64 - 1; let r = tick_index_from_sqrt_price(&sqrt_price_x64); assert_eq!(&r, &(MAX_TICK_INDEX - 1)); } #[test] - fn test_sqrt_price_from_tick_index_at_min_sub_one() { + fn test_tick_index_from_sqrt_price_at_min_sub_one() { let sqrt_price_x64_min_sub_one = MIN_SQRT_PRICE_X64 - 1; let tick_from_min_sub_one = tick_index_from_sqrt_price(&sqrt_price_x64_min_sub_one); let sqrt_price_x64_min = MIN_SQRT_PRICE_X64 + 1; @@ -389,21 +389,21 @@ mod test_tick_index_from_sqrt_price { } #[test] - fn test_sqrt_price_from_tick_index_at_one() { + fn test_tick_index_from_sqrt_price_at_one() { let sqrt_price_x64: u128 = u64::MAX as u128 + 1; let r = tick_index_from_sqrt_price(&sqrt_price_x64); assert_eq!(r, 0); } #[test] - fn test_sqrt_price_from_tick_index_at_one_add_one() { + fn test_tick_index_from_sqrt_price_at_one_add_one() { let sqrt_price_x64: u128 = u64::MAX as u128 + 2; let r = tick_index_from_sqrt_price(&sqrt_price_x64); assert_eq!(r, 0); } #[test] - fn test_sqrt_price_from_tick_index_at_one_sub_one() { + fn test_tick_index_from_sqrt_price_at_one_sub_one() { let sqrt_price_x64: u128 = u64::MAX.into(); let r = tick_index_from_sqrt_price(&sqrt_price_x64); assert_eq!(r, -1); diff --git a/rust-sdk/core/Cargo.lock b/rust-sdk/core/Cargo.lock index ca6aa64f0..b6bc08901 100644 --- a/rust-sdk/core/Cargo.lock +++ b/rust-sdk/core/Cargo.lock @@ -17,6 +17,27 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + [[package]] name = "bumpalo" version = "3.17.0" @@ -29,12 +50,46 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "ethnum" version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca81e6b4777c89fd810c25a4be2b1bd93ea034fbe58e6a75216a34c6b82c539b" +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "gloo-utils" version = "0.1.7" @@ -64,12 +119,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + [[package]] name = "libm" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "log" version = "0.4.26" @@ -106,6 +173,7 @@ dependencies = [ "js-sys", "libm", "orca_whirlpools_macros", + "proptest", "serde", "serde-big-array", "serde-wasm-bindgen 0.6.5", @@ -122,6 +190,15 @@ dependencies = [ "syn", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.94" @@ -131,6 +208,31 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.39" @@ -140,12 +242,87 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustversion" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.20" @@ -248,6 +425,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "tsify" version = "0.4.5" @@ -274,12 +464,36 @@ dependencies = [ "syn", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -347,3 +561,44 @@ dependencies = [ "js-sys", "wasm-bindgen", ] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/rust-sdk/core/Cargo.toml b/rust-sdk/core/Cargo.toml index 064189475..18ceb7df7 100644 --- a/rust-sdk/core/Cargo.toml +++ b/rust-sdk/core/Cargo.toml @@ -13,7 +13,14 @@ edition = "2021" [features] default = ["floats", "swap"] -wasm = ["dep:wasm-bindgen", "dep:serde", "dep:serde-big-array", "dep:serde-wasm-bindgen", "dep:js-sys", "dep:tsify"] +wasm = [ + "dep:wasm-bindgen", + "dep:serde", + "dep:serde-big-array", + "dep:serde-wasm-bindgen", + "dep:js-sys", + "dep:tsify", +] floats = ["dep:libm"] swap = [] @@ -30,3 +37,4 @@ tsify = { version = "^0.4", features = ["js"], optional = true } [dev-dependencies] approx = { version = "^0" } +proptest = { version = "^1" } diff --git a/rust-sdk/core/src/constants/error.rs b/rust-sdk/core/src/constants/error.rs index 6f217b673..ad433ea54 100644 --- a/rust-sdk/core/src/constants/error.rs +++ b/rust-sdk/core/src/constants/error.rs @@ -52,3 +52,6 @@ pub const INVALID_TICK_ARRAY_SEQUENCE: CoreError = "Invalid tick array sequence" #[cfg_attr(feature = "wasm", wasm_expose)] pub const INVALID_ADAPTIVE_FEE_INFO: CoreError = "Invalid adaptive fee info"; + +#[cfg_attr(feature = "wasm", wasm_expose)] +pub const NUMBER_DOWN_CAST_ERROR: CoreError = "Unable to down cast number"; diff --git a/rust-sdk/core/src/math/tick.rs b/rust-sdk/core/src/math/tick.rs index f0e2250f4..4c25178e6 100644 --- a/rust-sdk/core/src/math/tick.rs +++ b/rust-sdk/core/src/math/tick.rs @@ -5,7 +5,7 @@ use orca_whirlpools_macros::wasm_expose; use crate::{ CoreError, TickRange, FULL_RANGE_ONLY_TICK_SPACING_THRESHOLD, MAX_TICK_INDEX, MIN_TICK_INDEX, - TICK_ARRAY_SIZE, TICK_INDEX_NOT_IN_ARRAY, U128, + NUMBER_DOWN_CAST_ERROR, TICK_ARRAY_SIZE, TICK_INDEX_NOT_IN_ARRAY, U128, }; const LOG_B_2_X32: i128 = 59543866431248i128; @@ -123,7 +123,7 @@ pub fn sqrt_price_to_tick_index(sqrt_price: U128) -> i32 { /// - `round_up` - A boolean value indicating if the supplied tick index should be rounded up. None will round to the nearest. /// /// # Returns -/// - A i32 integer representing the previous initializable tick index +/// - A i32 representing the initializable tick index (rounded per round_up or nearest). #[cfg_attr(feature = "wasm", wasm_expose)] pub fn get_initializable_tick_index( tick_index: i32, @@ -137,7 +137,7 @@ pub fn get_initializable_tick_index( let should_round_up = if let Some(round_up) = round_up { round_up && remainder > 0 } else { - remainder >= tick_spacing_i32 / 2 + remainder >= tick_spacing_i32 / 2 && remainder > 0 }; if should_round_up { @@ -321,9 +321,17 @@ pub fn get_tick_index_in_array( // Private functions +fn u256_to_u128_checked(value: &U256) -> Result { + if *value.high() != U256::ZERO { + Err(NUMBER_DOWN_CAST_ERROR) + } else { + Ok(value.as_u128()) + } +} + fn mul_shift_96(n0: u128, n1: u128) -> u128 { let mul: U256 = (::from(n0) * ::from(n1)) >> 96; - mul.as_u128() + u256_to_u128_checked(&mul).unwrap() } fn get_sqrt_price_positive_tick(tick: i32) -> u128 { @@ -458,161 +466,715 @@ fn get_sqrt_price_negative_tick(tick: i32) -> u128 { ratio } -// Tests +#[cfg(all(test, not(feature = "wasm")))] +mod test_get_tick_array_start_tick_index { + use super::*; + const TS_8: u16 = 8; + const TS_128: u16 = 128; + + #[test] + fn test_start_tick_ts8_0() { + assert_eq!(get_tick_array_start_tick_index(0, TS_8), 0); + } + + #[test] + fn test_start_tick_ts8_740() { + assert_eq!(get_tick_array_start_tick_index(740, TS_8), 704); + } + + #[test] + fn test_start_tick_ts128_337920() { + assert_eq!(get_tick_array_start_tick_index(338433, TS_128), 337920); + } + + #[test] + fn test_start_tick_ts8_negative_704() { + assert_eq!(get_tick_array_start_tick_index(-624, TS_8), -704); + } + + #[test] + fn test_start_tick_ts128_negative_337920() { + assert_eq!(get_tick_array_start_tick_index(-337409, TS_128), -337920); + } + + #[test] + fn test_start_tick_ts8_not_2353573() { + assert_ne!(get_tick_array_start_tick_index(2354285, TS_8), 2353573); + } + + #[test] + fn test_start_tick_ts128_not_negative_2353573() { + assert_ne!(get_tick_array_start_tick_index(-2342181, TS_128), -2353573); + } + + #[test] + fn test_min_tick_array_start_tick_is_valid_ts8() { + let expected_array_index: i32 = (MIN_TICK_INDEX / TICK_ARRAY_SIZE as i32 / TS_8 as i32) - 1; + let expected_start_index_for_last_array: i32 = + expected_array_index * TICK_ARRAY_SIZE as i32 * TS_8 as i32; + assert_eq!( + get_tick_array_start_tick_index(MIN_TICK_INDEX, TS_8), + expected_start_index_for_last_array + ); + } + + #[test] + fn test_min_tick_array_start_tick_is_valid_ts128() { + let expected_array_index: i32 = + (MIN_TICK_INDEX / TICK_ARRAY_SIZE as i32 / TS_128 as i32) - 1; + let expected_start_index_for_last_array: i32 = + expected_array_index * TICK_ARRAY_SIZE as i32 * TS_128 as i32; + assert_eq!( + get_tick_array_start_tick_index(MIN_TICK_INDEX, TS_128), + expected_start_index_for_last_array + ); + } +} #[cfg(all(test, not(feature = "wasm")))] -mod tests { +mod test_tick_index_to_sqrt_price { use super::*; - use crate::{MAX_SQRT_PRICE, MIN_SQRT_PRICE}; + use crate::{MAX_SQRT_PRICE, MAX_TICK_INDEX, MIN_SQRT_PRICE, MIN_TICK_INDEX}; #[test] - fn test_get_tick_array_start_tick_index() { - assert_eq!(get_tick_array_start_tick_index(1000, 10), 880); - assert_eq!(get_tick_array_start_tick_index(100, 10), 0); - assert_eq!(get_tick_array_start_tick_index(0, 10), 0); - assert_eq!(get_tick_array_start_tick_index(-100, 10), -880); - assert_eq!(get_tick_array_start_tick_index(-1000, 10), -1760); + #[should_panic(expected = "Unable to down cast number")] + fn test_tick_exceed_max() { + let sqrt_price_from_max_tick_add_one = tick_index_to_sqrt_price(MAX_TICK_INDEX + 1); + let sqrt_price_from_max_tick = tick_index_to_sqrt_price(MAX_TICK_INDEX); + assert!(sqrt_price_from_max_tick_add_one > sqrt_price_from_max_tick); } #[test] - fn test_tick_index_to_sqrt_price() { - assert_eq!(tick_index_to_sqrt_price(MAX_TICK_INDEX), MAX_SQRT_PRICE); - assert_eq!(tick_index_to_sqrt_price(100), 18539204128674405812); - assert_eq!(tick_index_to_sqrt_price(1), 18447666387855959850); - assert_eq!(tick_index_to_sqrt_price(0), 18446744073709551616); - assert_eq!(tick_index_to_sqrt_price(-1), 18445821805675392311); - assert_eq!(tick_index_to_sqrt_price(-100), 18354745142194483561); - assert_eq!(tick_index_to_sqrt_price(MIN_TICK_INDEX), MIN_SQRT_PRICE); + fn test_tick_below_min() { + let sqrt_price_from_min_tick_sub_one = tick_index_to_sqrt_price(MIN_TICK_INDEX - 1); + let sqrt_price_from_min_tick = tick_index_to_sqrt_price(MIN_TICK_INDEX); + assert!(sqrt_price_from_min_tick_sub_one < sqrt_price_from_min_tick); } #[test] - fn test_sqrt_price_to_tick_index() { - assert_eq!(sqrt_price_to_tick_index(MAX_SQRT_PRICE), MAX_TICK_INDEX); - assert_eq!(sqrt_price_to_tick_index(18539204128674405812), 100); - assert_eq!(sqrt_price_to_tick_index(18447666387855959850), 1); - assert_eq!(sqrt_price_to_tick_index(18446744073709551616), 0); - assert_eq!(sqrt_price_to_tick_index(18445821805675392311), -1); - assert_eq!(sqrt_price_to_tick_index(18354745142194483561), -100); - assert_eq!(sqrt_price_to_tick_index(MIN_SQRT_PRICE), MIN_TICK_INDEX); + fn test_tick_at_max() { + let max_tick = MAX_TICK_INDEX; + let r = tick_index_to_sqrt_price(max_tick); + assert_eq!(r, MAX_SQRT_PRICE); } #[test] - fn test_get_initializable_tick_index() { - assert_eq!(get_initializable_tick_index(-100, 10, Some(true)), -100); - assert_eq!(get_initializable_tick_index(-100, 10, Some(false)), -100); - assert_eq!(get_initializable_tick_index(-100, 10, None), -100); + fn test_tick_at_min() { + let min_tick = MIN_TICK_INDEX; + let r = tick_index_to_sqrt_price(min_tick); + assert_eq!(r, MIN_SQRT_PRICE); + } - assert_eq!(get_initializable_tick_index(-101, 10, Some(true)), -100); - assert_eq!(get_initializable_tick_index(-101, 10, Some(false)), -110); - assert_eq!(get_initializable_tick_index(-101, 10, None), -100); + #[test] + fn test_exact_bit_values() { + let conditions = &[ + ( + 0i32, + 18446744073709551616u128, + 18446744073709551616u128, + "0x0", + ), + ( + 1i32, + 18447666387855959850u128, + 18445821805675392311u128, + "0x1", + ), + ( + 2i32, + 18448588748116922571u128, + 18444899583751176498u128, + "0x2", + ), + ( + 4i32, + 18450433606991734263u128, + 18443055278223354162u128, + "0x4", + ), + ( + 8i32, + 18454123878217468680u128, + 18439367220385604838u128, + "0x8", + ), + ( + 16i32, + 18461506635090006701u128, + 18431993317065449817u128, + "0x10", + ), + ( + 32i32, + 18476281010653910144u128, + 18417254355718160513u128, + "0x20", + ), + ( + 64i32, + 18505865242158250041u128, + 18387811781193591352u128, + "0x40", + ), + ( + 128i32, + 18565175891880433522u128, + 18329067761203520168u128, + "0x80", + ), + ( + 256i32, + 18684368066214940582u128, + 18212142134806087854u128, + "0x100", + ), + ( + 512i32, + 18925053041275764671u128, + 17980523815641551639u128, + "0x200", + ), + ( + 1024i32, + 19415764168677886926u128, + 17526086738831147013u128, + "0x400", + ), + ( + 2048i32, + 20435687552633177494u128, + 16651378430235024244u128, + "0x800", + ), + ( + 4096i32, + 22639080592224303007u128, + 15030750278693429944u128, + "0x1000", + ), + ( + 8192i32, + 27784196929998399742u128, + 12247334978882834399u128, + "0x2000", + ), + ( + 16384i32, + 41848122137994986128u128, + 8131365268884726200u128, + "0x4000", + ), + ( + 32768i32, + 94936283578220370716u128, + 3584323654723342297u128, + "0x8000", + ), + ( + 65536i32, + 488590176327622479860u128, + 696457651847595233u128, + "0x10000", + ), + ( + 131072i32, + 12941056668319229769860u128, + 26294789957452057u128, + "0x20000", + ), + ( + 262144i32, + 9078618265828848800676189u128, + 37481735321082u128, + "0x40000", + ), + ]; - assert_eq!(get_initializable_tick_index(-105, 10, Some(true)), -100); - assert_eq!(get_initializable_tick_index(-105, 10, Some(false)), -110); - assert_eq!(get_initializable_tick_index(-105, 10, None), -100); + for (p_tick, expected, neg_expected, _desc) in conditions { + let p_result = tick_index_to_sqrt_price(*p_tick); + let n_tick = -*p_tick; + let n_result = tick_index_to_sqrt_price(n_tick); + assert_eq!(p_result, *expected); + assert_eq!(n_result, *neg_expected); + } + } +} - assert_eq!(get_initializable_tick_index(-109, 10, Some(true)), -100); - assert_eq!(get_initializable_tick_index(-109, 10, Some(false)), -110); - assert_eq!(get_initializable_tick_index(-109, 10, None), -110); +#[cfg(all(test, not(feature = "wasm")))] +mod test_sqrt_price_to_tick_index { + use super::*; + use crate::{MAX_SQRT_PRICE, MAX_TICK_INDEX, MIN_SQRT_PRICE, MIN_TICK_INDEX}; - assert_eq!(get_initializable_tick_index(-100, 10, Some(true)), -100); - assert_eq!(get_initializable_tick_index(-100, 10, Some(false)), -100); - assert_eq!(get_initializable_tick_index(-100, 10, None), -100); + #[test] + fn test_sqrt_price_to_tick_index_at_max() { + let r = sqrt_price_to_tick_index(MAX_SQRT_PRICE); + assert_eq!(&r, &MAX_TICK_INDEX); + } - assert_eq!(get_initializable_tick_index(101, 10, Some(true)), 110); - assert_eq!(get_initializable_tick_index(101, 10, Some(false)), 100); - assert_eq!(get_initializable_tick_index(101, 10, None), 100); + #[test] + fn test_sqrt_price_to_tick_index_at_min() { + let r = sqrt_price_to_tick_index(MIN_SQRT_PRICE); + assert_eq!(&r, &MIN_TICK_INDEX); + } - assert_eq!(get_initializable_tick_index(105, 10, Some(true)), 110); - assert_eq!(get_initializable_tick_index(105, 10, Some(false)), 100); - assert_eq!(get_initializable_tick_index(105, 10, None), 110); + #[test] + fn test_sqrt_price_to_tick_index_at_max_add_one() { + let sqrt_price_x64_max_add_one = MAX_SQRT_PRICE + 1; + let tick_from_max_add_one = sqrt_price_to_tick_index(sqrt_price_x64_max_add_one); + let sqrt_price_x64_max = MAX_SQRT_PRICE; + let tick_from_max = sqrt_price_to_tick_index(sqrt_price_x64_max); + // We don't care about accuracy over the limit. We just care about it's equality properties. + assert!(tick_from_max_add_one >= tick_from_max); + } - assert_eq!(get_initializable_tick_index(109, 10, Some(true)), 110); - assert_eq!(get_initializable_tick_index(109, 10, Some(false)), 100); - assert_eq!(get_initializable_tick_index(109, 10, None), 110); + #[test] + fn test_sqrt_price_to_tick_index_at_min_add_one() { + let sqrt_price_x64 = MIN_SQRT_PRICE + 1; + let r = sqrt_price_to_tick_index(sqrt_price_x64); + assert_eq!(&r, &(MIN_TICK_INDEX)); } #[test] - fn test_get_prev_initializable_tick_index() { - assert_eq!(get_prev_initializable_tick_index(10, 10), 0); - assert_eq!(get_prev_initializable_tick_index(5, 10), 0); - assert_eq!(get_prev_initializable_tick_index(0, 10), -10); - assert_eq!(get_prev_initializable_tick_index(-5, 10), -10); - assert_eq!(get_prev_initializable_tick_index(-10, 10), -20); + fn test_sqrt_price_to_tick_index_at_max_sub_one() { + let sqrt_price_x64 = MAX_SQRT_PRICE - 1; + let r = sqrt_price_to_tick_index(sqrt_price_x64); + assert_eq!(&r, &(MAX_TICK_INDEX - 1)); } #[test] - fn test_get_next_initializable_tick_index() { - assert_eq!(get_next_initializable_tick_index(10, 10), 20); - assert_eq!(get_next_initializable_tick_index(5, 10), 10); - assert_eq!(get_next_initializable_tick_index(0, 10), 10); - assert_eq!(get_next_initializable_tick_index(-5, 10), 0); - assert_eq!(get_next_initializable_tick_index(-10, 10), 0); + fn test_sqrt_price_to_tick_index_at_min_sub_one() { + let sqrt_price_x64_min_sub_one = MIN_SQRT_PRICE - 1; + let tick_from_min_sub_one = sqrt_price_to_tick_index(sqrt_price_x64_min_sub_one); + let sqrt_price_x64_min = MIN_SQRT_PRICE + 1; + let tick_from_min = sqrt_price_to_tick_index(sqrt_price_x64_min); + assert!(tick_from_min_sub_one < tick_from_min); } #[test] - fn test_is_tick_index_in_bounds() { - assert!(is_tick_index_in_bounds(MAX_TICK_INDEX)); + fn test_sqrt_price_to_tick_index_at_one() { + let sqrt_price_x64: u128 = u64::MAX as u128 + 1; + let r = sqrt_price_to_tick_index(sqrt_price_x64); + assert_eq!(r, 0); + } + + #[test] + fn test_sqrt_price_to_tick_index_at_one_add_one() { + let sqrt_price_x64: u128 = u64::MAX as u128 + 2; + let r = sqrt_price_to_tick_index(sqrt_price_x64); + assert_eq!(r, 0); + } + + #[test] + fn test_sqrt_price_to_tick_index_at_one_sub_one() { + let sqrt_price_x64: u128 = u64::MAX.into(); + let r = sqrt_price_to_tick_index(sqrt_price_x64); + assert_eq!(r, -1); + } +} + +#[cfg(all(test, not(feature = "wasm")))] +mod fuzz_tests { + use super::*; + use crate::{MAX_SQRT_PRICE, MAX_TICK_INDEX, MIN_SQRT_PRICE, MIN_TICK_INDEX}; + use ethnum::U256; + use proptest::prelude::*; + + fn within_price_approximation(lower: u128, upper: u128) -> bool { + let precision = 96u32; + // We increase the resolution of upper to find ratio_x96 + let x = U256::from(upper) << precision; + let y = U256::from(lower); + + // (1.0001 ^ 0.5) << 96 (precision) + let sqrt_10001_x96 = 79232123823359799118286999567u128; + + // This ratio should be as close to sqrt_10001_x96 as possible + let ratio_x96 = (x / y).as_u128(); + + // Find absolute error in ratio in x96 + let error = if sqrt_10001_x96 > ratio_x96 { + sqrt_10001_x96 - ratio_x96 + } else { + ratio_x96 - sqrt_10001_x96 + }; + + // Calculate number of error bits + let error_bits = 128 - error.leading_zeros(); + precision - error_bits >= 32 + } + + proptest! { + #[test] + fn test_tick_index_to_sqrt_price ( + tick in MIN_TICK_INDEX..MAX_TICK_INDEX, + ) { + let sqrt_price: u128 = tick_index_to_sqrt_price(tick).into(); + + // Check bounds + assert!(sqrt_price >= MIN_SQRT_PRICE); + assert!(sqrt_price <= MAX_SQRT_PRICE); + + // Check the inverted tick has unique price and within bounds + let minus_tick_price: u128 = tick_index_to_sqrt_price(tick - 1).into(); + let plus_tick_price: u128 = tick_index_to_sqrt_price(tick + 1).into(); + assert!(minus_tick_price < sqrt_price && sqrt_price < plus_tick_price); + + // Check that sqrt_price_from_tick_index(tick + 1) approximates sqrt(1.0001) * sqrt_price_from_tick_index(tick) + assert!(within_price_approximation(minus_tick_price, sqrt_price)); + assert!(within_price_approximation(sqrt_price, plus_tick_price)); + } + + #[test] + fn test_tick_index_from_sqrt_price ( + sqrt_price in MIN_SQRT_PRICE..MAX_SQRT_PRICE + ) { + let tick = sqrt_price_to_tick_index(sqrt_price); + + assert!(tick >= MIN_TICK_INDEX); + assert!(tick < MAX_TICK_INDEX); + + // Check the inverted price from the calculated tick is within tick boundaries + assert!(sqrt_price >= tick_index_to_sqrt_price(tick) && sqrt_price < tick_index_to_sqrt_price(tick + 1)) + } + + #[test] + // Verify that both conversion functions are symmetrical. + fn test_tick_index_and_sqrt_price_symmetry ( + tick in MIN_TICK_INDEX..MAX_TICK_INDEX + ) { + let sqrt_price_x64: u128 = tick_index_to_sqrt_price(tick).into(); + let resolved_tick = sqrt_price_to_tick_index(sqrt_price_x64); + assert!(resolved_tick == tick); + } + + #[test] + fn test_sqrt_price_from_tick_index_is_sequence ( + tick in (MIN_TICK_INDEX - 1)..MAX_TICK_INDEX + ) { + let sqrt_price_x64: u128 = tick_index_to_sqrt_price(tick).into(); + let last_sqrt_price_x64: u128 = tick_index_to_sqrt_price(tick - 1).into(); + assert!(last_sqrt_price_x64 < sqrt_price_x64); + } + + #[test] + fn test_tick_index_from_sqrt_price_is_sequence ( + sqrt_price in (MIN_SQRT_PRICE + 10)..MAX_SQRT_PRICE + ) { + let tick = sqrt_price_to_tick_index(sqrt_price); + let last_tick = sqrt_price_to_tick_index(sqrt_price - 10); + assert!(last_tick <= tick); + } + } +} + +#[cfg(all(test, not(feature = "wasm")))] +mod test_get_initializable_tick_index { + use super::*; + + fn nearest_expected(tick: i32, spacing: i32) -> i32 { + let base = tick.div_euclid(spacing) * spacing; + let rem = tick.rem_euclid(spacing); + if rem > 0 && rem >= spacing / 2 { + base + spacing + } else { + base + } + } + + #[test] + fn test_nearest_rounding_multiple_spacings() { + let spacings = [1, 2, 4, 8, 16, 64, 96, 128, 256]; + for &s in &spacings { + let si = s as i32; + for t in (-1000i32)..=1000i32 { + let got = get_initializable_tick_index(t, s, None); + let exp = nearest_expected(t, si); + assert_eq!(got, exp); + } + } + } + + #[test] + fn test_round_up_true_behavior() { + let spacings = [1, 2, 4, 8, 16, 64, 96, 128, 256]; + for &s in &spacings { + let si = s as i32; + for t in (-500i32)..=500i32 { + let rem = t.rem_euclid(si); + let got = get_initializable_tick_index(t, s, Some(true)); + let exp = if rem > 0 { + t.div_euclid(si) * si + si + } else { + t + }; + assert_eq!(got, exp); + } + } + } + + #[test] + fn test_round_up_false_behavior() { + let spacings = [1, 2, 4, 8, 16, 64, 96, 128, 256]; + for &s in &spacings { + let si = s as i32; + for t in (-500i32)..=500i32 { + let got = get_initializable_tick_index(t, s, Some(false)); + let exp = t.div_euclid(si) * si; + assert_eq!(got, exp); + } + } + } + + #[test] + fn test_exact_multiples_remain_stable() { + let spacings = [1, 2, 4, 8, 16, 64, 96, 128, 256]; + for &s in &spacings { + let si = s as i32; + for k in -20..=20 { + let t = k * si; + assert_eq!(get_initializable_tick_index(t, s, None), t); + assert_eq!(get_initializable_tick_index(t, s, Some(true)), t); + assert_eq!(get_initializable_tick_index(t, s, Some(false)), t); + } + } + } + + #[test] + fn prev_initializable_matches_euclidean_rule() { + let spacings = [1, 3, 10, 16, 128]; + for &s in &spacings { + let si = s as i32; + for t in (-1000i32)..=1000i32 { + let rem = t.rem_euclid(si); + let expected = if rem == 0 { t - si } else { t - rem }; + let got = get_prev_initializable_tick_index(t, s); + assert_eq!(got, expected, "t={} s={}", t, s); + } + } + } + + #[test] + fn next_initializable_matches_euclidean_rule() { + let spacings = [1, 3, 10, 16, 128]; + for &s in &spacings { + let si = s as i32; + for t in (-1000i32)..=1000i32 { + let rem = t.rem_euclid(si); + let expected = t - rem + si; + let got = get_next_initializable_tick_index(t, s); + assert_eq!(got, expected, "t={} s={}", t, s); + } + } + } +} + +#[cfg(all(test, not(feature = "wasm")))] +mod test_is_tick_index_in_bounds { + use super::*; + + #[test] + fn test_min_tick_index() { assert!(is_tick_index_in_bounds(MIN_TICK_INDEX)); - assert!(!is_tick_index_in_bounds(MAX_TICK_INDEX + 1)); + } + + #[test] + fn test_max_tick_index() { + assert!(is_tick_index_in_bounds(MAX_TICK_INDEX)); + } + + #[test] + fn test_min_tick_index_sub_1() { assert!(!is_tick_index_in_bounds(MIN_TICK_INDEX - 1)); } #[test] - fn test_is_tick_initializable() { - assert!(is_tick_initializable(100, 10)); - assert!(!is_tick_initializable(105, 10)); + fn test_max_tick_index_add_1() { + assert!(!is_tick_index_in_bounds(MAX_TICK_INDEX + 1)); } +} + +#[cfg(all(test, not(feature = "wasm")))] +mod test_is_tick_initializable {} + +#[cfg(all(test, not(feature = "wasm")))] +mod test_invert_tick_index { + use super::*; #[test] - fn test_invert_tick_index() { + fn test_invert_positive_tick_index() { assert_eq!(invert_tick_index(100), -100); + } + + #[test] + fn test_invert_negative_tick_index() { assert_eq!(invert_tick_index(-100), 100); } +} + +#[cfg(all(test, not(feature = "wasm")))] +mod test_get_full_range_tick_indexes { + use super::*; + + #[test] + fn test_min_tick_spacing() { + let range = get_full_range_tick_indexes(1); + assert_eq!(range.tick_lower_index, MIN_TICK_INDEX); + assert_eq!(range.tick_upper_index, MAX_TICK_INDEX); + } + + #[test] + fn test_standard_tick_spacing() { + let spacing: u16 = 128; + let expected_lower = (MIN_TICK_INDEX / spacing as i32) * spacing as i32; + let expected_upper = (MAX_TICK_INDEX / spacing as i32) * spacing as i32; + let range = get_full_range_tick_indexes(spacing); + assert_eq!(range.tick_lower_index, expected_lower); + assert_eq!(range.tick_upper_index, expected_upper); + } + + #[test] + fn test_full_range_only_tick_spacing() { + let spacing = FULL_RANGE_ONLY_TICK_SPACING_THRESHOLD; + let expected_lower = (MIN_TICK_INDEX / spacing as i32) * spacing as i32; + let expected_upper = (MAX_TICK_INDEX / spacing as i32) * spacing as i32; + let range = get_full_range_tick_indexes(spacing); + assert_eq!(range.tick_lower_index, expected_lower); + assert_eq!(range.tick_upper_index, expected_upper); + } + + #[test] + fn test_max_tick_spacing() { + let spacing = u16::MAX; + let expected_lower = (MIN_TICK_INDEX / spacing as i32) * spacing as i32; + let expected_upper = (MAX_TICK_INDEX / spacing as i32) * spacing as i32; + let range = get_full_range_tick_indexes(spacing); + assert_eq!(range.tick_lower_index, expected_lower); + assert_eq!(range.tick_upper_index, expected_upper); + } +} + +#[cfg(all(test, not(feature = "wasm")))] +mod test_order_tick_indexes { + use super::*; + use proptest::prelude::*; + + #[test] + fn test_order_ascending() { + let r = order_tick_indexes(100, 200); + assert_eq!(r.tick_lower_index, 100); + assert_eq!(r.tick_upper_index, 200); + } + + #[test] + fn test_order_descending() { + let r = order_tick_indexes(200, 100); + assert_eq!(r.tick_lower_index, 100); + assert_eq!(r.tick_upper_index, 200); + } + + #[test] + fn test_order_positive_negative() { + let r = order_tick_indexes(100, -200); + assert_eq!(r.tick_lower_index, -200); + assert_eq!(r.tick_upper_index, 100); + } #[test] - fn test_get_full_range_tick_indexes() { - let range = get_full_range_tick_indexes(10); - assert_eq!(range.tick_lower_index, (MIN_TICK_INDEX / 10) * 10); - assert_eq!(range.tick_upper_index, (MAX_TICK_INDEX / 10) * 10); + fn test_order_negative_positive() { + let r = order_tick_indexes(-100, 200); + assert_eq!(r.tick_lower_index, -100); + assert_eq!(r.tick_upper_index, 200); } #[test] - fn test_order_tick_indexes() { - let range_1 = order_tick_indexes(100, 200); - assert_eq!(range_1.tick_lower_index, 100); - assert_eq!(range_1.tick_upper_index, 200); + fn test_order_both_negative_ascending() { + let r = order_tick_indexes(-200, -100); + assert_eq!(r.tick_lower_index, -200); + assert_eq!(r.tick_upper_index, -100); + } - let range_2 = order_tick_indexes(200, 100); - assert_eq!(range_2.tick_lower_index, 100); - assert_eq!(range_2.tick_upper_index, 200); + #[test] + fn test_order_both_negative_descending() { + let r = order_tick_indexes(-100, -200); + assert_eq!(r.tick_lower_index, -200); + assert_eq!(r.tick_upper_index, -100); + } + + #[test] + fn test_order_equal_negative() { + let r = order_tick_indexes(-100, -100); + assert_eq!(r.tick_lower_index, -100); + assert_eq!(r.tick_upper_index, -100); + } - let range_3 = order_tick_indexes(100, 100); - assert_eq!(range_3.tick_lower_index, 100); - assert_eq!(range_3.tick_upper_index, 100); + #[test] + fn test_order_equal_positive() { + let r = order_tick_indexes(100, 100); + assert_eq!(r.tick_lower_index, 100); + assert_eq!(r.tick_upper_index, 100); } #[test] - fn test_is_full_range_only() { + fn test_order_equal_zero() { + let r = order_tick_indexes(0, 0); + assert_eq!(r.tick_lower_index, 0); + assert_eq!(r.tick_upper_index, 0); + } + + proptest! { + #[test] + fn prop_min_max(a in i32::MIN..i32::MAX, b in i32::MIN..i32::MAX) { + let r = order_tick_indexes(a, b); + let lo = a.min(b); + let hi = a.max(b); + prop_assert_eq!(r.tick_lower_index, lo); + prop_assert_eq!(r.tick_upper_index, hi); + } + } +} + +#[cfg(all(test, not(feature = "wasm")))] +mod test_is_full_range_only_flag { + use super::*; + + #[test] + fn at_threshold_is_true() { assert!(is_full_range_only(FULL_RANGE_ONLY_TICK_SPACING_THRESHOLD)); + } + + #[test] + fn below_threshold_is_false() { assert!(!is_full_range_only( FULL_RANGE_ONLY_TICK_SPACING_THRESHOLD - 1 )); } + #[test] + fn above_threshold_is_true() { + assert!(is_full_range_only( + FULL_RANGE_ONLY_TICK_SPACING_THRESHOLD + 1 + )); + } +} + +#[cfg(all(test, not(feature = "wasm")))] +mod tests { + use super::*; + use crate::{MAX_SQRT_PRICE, MIN_SQRT_PRICE}; + + #[test] + fn test_is_tick_initializable() { + assert!(is_tick_initializable(100, 10)); + assert!(!is_tick_initializable(105, 10)); + } + #[test] fn test_get_tick_index_in_array() { - // Zero tick index assert_eq!(get_tick_index_in_array(0, 0, 10), Ok(0)); - - // Positive tick index assert_eq!(get_tick_index_in_array(100, 0, 10), Ok(10)); assert_eq!(get_tick_index_in_array(50, 0, 10), Ok(5)); - - // Negative tick index assert_eq!(get_tick_index_in_array(-830, -880, 10), Ok(5)); assert_eq!(get_tick_index_in_array(-780, -880, 10), Ok(10)); - - // Outside of the tick array assert_eq!( get_tick_index_in_array(880, 0, 10), Err(TICK_INDEX_NOT_IN_ARRAY) @@ -625,8 +1187,6 @@ mod tests { get_tick_index_in_array(-881, -880, 10), Err(TICK_INDEX_NOT_IN_ARRAY) ); - - // Splash pool tick spacing assert_eq!(get_tick_index_in_array(2861952, 0, 32896), Ok(87)); assert_eq!(get_tick_index_in_array(-32896, -2894848, 32896), Ok(87)); }