From 765bf7d439e458de8ed429d3d311b27ebde0b52b Mon Sep 17 00:00:00 2001 From: Rohan Date: Fri, 17 Jul 2026 11:45:29 -0400 Subject: [PATCH 1/2] Fix canonical tick bounds and maker validation --- src/client/trades.rs | 72 ++++++++++++++++++++++++++++++++++++-------- src/constants.rs | 20 ++++++------ src/convert.rs | 30 ++++++------------ src/math/tick.rs | 15 ++++++--- 4 files changed, 90 insertions(+), 47 deletions(-) diff --git a/src/client/trades.rs b/src/client/trades.rs index d7eaf2f..4d17ca1 100644 --- a/src/client/trades.rs +++ b/src/client/trades.rs @@ -3,7 +3,7 @@ use alloy::primitives::{Address, B256, Bytes, I256, U256}; use alloy::sol_types::SolEvent; -use crate::constants::{MIN_OPENING_MARGIN, TICK_SPACING}; +use crate::constants::{MAX_TICK, MIN_OPENING_MARGIN, MIN_TICK, TICK_SPACING}; use crate::contracts::{IERC20, Perp}; use crate::convert::{scale_from_6dec, scale_to_6dec}; use crate::errors::{ContractError, Result, ValidationError}; @@ -91,6 +91,24 @@ fn scale_opening_margin(margin: f64) -> std::result::Result std::result::Result<(i32, i32), ValidationError> { + let tick_lower = align_tick_down(price_to_tick(price_lower)?, TICK_SPACING); + let tick_upper = align_tick_up(price_to_tick(price_upper)?, TICK_SPACING); + + if tick_lower < MIN_TICK || tick_upper > MAX_TICK || tick_lower >= tick_upper { + return Err(ValidationError::InvalidTickRange { + lower: tick_lower, + upper: tick_upper, + }); + } + + Ok((tick_lower, tick_upper)) +} + impl PerpClient { // ── Position operations ────────────────────────────────────────── @@ -158,16 +176,8 @@ impl PerpClient { ) -> Result { let margin_scaled = scale_opening_margin(params.margin)?; - let tick_lower = align_tick_down(price_to_tick(params.price_lower)?, TICK_SPACING); - let tick_upper = align_tick_up(price_to_tick(params.price_upper)?, TICK_SPACING); - - if tick_lower >= tick_upper { - return Err(ValidationError::InvalidTickRange { - lower: tick_lower, - upper: tick_upper, - } - .into()); - } + let (tick_lower, tick_upper) = + maker_ticks_from_prices(params.price_lower, params.price_upper)?; let wire_params = crate::contracts::OpenMakerParams { holder: self.address, @@ -437,7 +447,9 @@ impl PerpClient { #[cfg(test)] mod tests { - use super::scale_opening_margin; + use super::{maker_ticks_from_prices, scale_opening_margin}; + use crate::constants::{MAX_TICK, MIN_TICK, TICK_SPACING}; + use crate::math::tick::tick_to_price; #[test] fn opening_margin_enforces_protocol_minimum() { @@ -445,4 +457,40 @@ mod tests { assert_eq!(scale_opening_margin(5.0).unwrap(), 5_000_000); assert_eq!(scale_opening_margin(5.000_001).unwrap(), 5_000_001); } + + #[test] + fn maker_tick_range_accepts_protocol_bounds() { + let ticks = maker_ticks_from_prices( + tick_to_price(MIN_TICK).unwrap(), + tick_to_price(MAX_TICK).unwrap(), + ) + .unwrap(); + assert_eq!(ticks, (MIN_TICK, MAX_TICK)); + } + + #[test] + fn maker_starting_price_bounds_align_to_protocol_ticks() { + assert_eq!( + maker_ticks_from_prices(1e-6, 1e6).unwrap(), + (MIN_TICK, MAX_TICK) + ); + } + + #[test] + fn maker_tick_range_rejects_lower_bound_violation() { + let result = maker_ticks_from_prices( + tick_to_price(MIN_TICK - TICK_SPACING).unwrap(), + tick_to_price(0).unwrap(), + ); + assert!(result.is_err()); + } + + #[test] + fn maker_tick_range_rejects_upper_bound_violation() { + let result = maker_ticks_from_prices( + tick_to_price(0).unwrap(), + tick_to_price(MAX_TICK + TICK_SPACING).unwrap(), + ); + assert!(result.is_err()); + } } diff --git a/src/constants.rs b/src/constants.rs index 89c271b..e779de4 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -40,17 +40,17 @@ pub const MIN_OPENING_MARGIN: u32 = 5_000_000; /// Funding interval in seconds (1 day). pub const INTERVAL: u64 = 86_400; -/// sqrt(0.001) * 2^96 — the minimum allowed sqrtPriceX96. -pub const MIN_SQRT_PRICE_X96: U256 = uint!(2505414483750479311864138016_U256); +/// sqrt(1e-6) * 2^96 — the minimum starting sqrtPriceX96 accepted by the factory. +pub const MIN_SQRT_PRICE_X96: U256 = uint!(79228162514264337593543951_U256); -/// sqrt(1000) * 2^96 — the maximum allowed sqrtPriceX96. -pub const MAX_SQRT_PRICE_X96: U256 = uint!(2505414483750479311864138015696_U256); +/// sqrt(1e6) * 2^96 — the maximum starting sqrtPriceX96 accepted by the factory. +pub const MAX_SQRT_PRICE_X96: U256 = uint!(79228162514264337593543950336000_U256); -/// Minimum tick (~= TickMath.getTickAtSqrtPrice(MIN_SQRT_PRICE_X96)). -pub const MIN_TICK: i32 = -69_090; +/// Minimum allowed maker tick, slightly below the factory's minimum starting price. +pub const MIN_TICK: i32 = -138_180; -/// Maximum tick (~= TickMath.getTickAtSqrtPrice(MAX_SQRT_PRICE_X96)). -pub const MAX_TICK: i32 = 69_090; +/// Maximum allowed maker tick, slightly above the factory's maximum starting price. +pub const MAX_TICK: i32 = 138_180; /// Total supply of the internal accounting token: type(uint120).max. pub const ACCOUNTING_TOKEN_SUPPLY: U256 = U256::from_limbs([u64::MAX, u64::MAX >> 8, 0, 0]); // 2^120 - 1 @@ -125,13 +125,13 @@ mod tests { #[test] fn min_sqrt_price_x96_matches_contract() { - let expected = U256::from_str_radix("2505414483750479311864138016", 10).unwrap(); + let expected = U256::from_str_radix("79228162514264337593543951", 10).unwrap(); assert_eq!(MIN_SQRT_PRICE_X96, expected); } #[test] fn max_sqrt_price_x96_matches_contract() { - let expected = U256::from_str_radix("2505414483750479311864138015696", 10).unwrap(); + let expected = U256::from_str_radix("79228162514264337593543950336000", 10).unwrap(); assert_eq!(MAX_SQRT_PRICE_X96, expected); } } diff --git a/src/convert.rs b/src/convert.rs index 7b1166b..85a7eda 100644 --- a/src/convert.rs +++ b/src/convert.rs @@ -286,7 +286,7 @@ pub fn sqrt_price_x96_to_price(sqrt_price_x96: U256) -> Result 0.0); assert!(max_price > min_price); - // MIN_TICK ≈ -69090 → price ≈ 0.001 - assert!((min_price - 0.001).abs() < 0.0005, "min_price={min_price}"); - // MAX_TICK ≈ 69090 → price ≈ 1000 - assert!((max_price - 1000.0).abs() < 1.0, "max_price={max_price}"); + assert!( + (min_price - 1e-6).abs() / 1e-6 < 0.01, + "min_price={min_price}" + ); + assert!( + (max_price - 1e6).abs() / 1e6 < 0.01, + "max_price={max_price}" + ); } } From c08fb748ba46c6fb3a093dc112bd83011bf7eb01 Mon Sep 17 00:00:00 2001 From: Rohan Date: Fri, 17 Jul 2026 12:10:22 -0400 Subject: [PATCH 2/2] rm unneeded tick conversion abstraction --- src/client/trades.rs | 70 +++++++------------------------------------- 1 file changed, 11 insertions(+), 59 deletions(-) diff --git a/src/client/trades.rs b/src/client/trades.rs index 4d17ca1..2ebe11c 100644 --- a/src/client/trades.rs +++ b/src/client/trades.rs @@ -91,24 +91,6 @@ fn scale_opening_margin(margin: f64) -> std::result::Result std::result::Result<(i32, i32), ValidationError> { - let tick_lower = align_tick_down(price_to_tick(price_lower)?, TICK_SPACING); - let tick_upper = align_tick_up(price_to_tick(price_upper)?, TICK_SPACING); - - if tick_lower < MIN_TICK || tick_upper > MAX_TICK || tick_lower >= tick_upper { - return Err(ValidationError::InvalidTickRange { - lower: tick_lower, - upper: tick_upper, - }); - } - - Ok((tick_lower, tick_upper)) -} - impl PerpClient { // ── Position operations ────────────────────────────────────────── @@ -176,8 +158,16 @@ impl PerpClient { ) -> Result { let margin_scaled = scale_opening_margin(params.margin)?; - let (tick_lower, tick_upper) = - maker_ticks_from_prices(params.price_lower, params.price_upper)?; + let tick_lower = align_tick_down(price_to_tick(params.price_lower)?, TICK_SPACING); + let tick_upper = align_tick_up(price_to_tick(params.price_upper)?, TICK_SPACING); + + if tick_lower < MIN_TICK || tick_upper > MAX_TICK || tick_lower >= tick_upper { + return Err(ValidationError::InvalidTickRange { + lower: tick_lower, + upper: tick_upper, + } + .into()); + } let wire_params = crate::contracts::OpenMakerParams { holder: self.address, @@ -447,9 +437,7 @@ impl PerpClient { #[cfg(test)] mod tests { - use super::{maker_ticks_from_prices, scale_opening_margin}; - use crate::constants::{MAX_TICK, MIN_TICK, TICK_SPACING}; - use crate::math::tick::tick_to_price; + use super::scale_opening_margin; #[test] fn opening_margin_enforces_protocol_minimum() { @@ -457,40 +445,4 @@ mod tests { assert_eq!(scale_opening_margin(5.0).unwrap(), 5_000_000); assert_eq!(scale_opening_margin(5.000_001).unwrap(), 5_000_001); } - - #[test] - fn maker_tick_range_accepts_protocol_bounds() { - let ticks = maker_ticks_from_prices( - tick_to_price(MIN_TICK).unwrap(), - tick_to_price(MAX_TICK).unwrap(), - ) - .unwrap(); - assert_eq!(ticks, (MIN_TICK, MAX_TICK)); - } - - #[test] - fn maker_starting_price_bounds_align_to_protocol_ticks() { - assert_eq!( - maker_ticks_from_prices(1e-6, 1e6).unwrap(), - (MIN_TICK, MAX_TICK) - ); - } - - #[test] - fn maker_tick_range_rejects_lower_bound_violation() { - let result = maker_ticks_from_prices( - tick_to_price(MIN_TICK - TICK_SPACING).unwrap(), - tick_to_price(0).unwrap(), - ); - assert!(result.is_err()); - } - - #[test] - fn maker_tick_range_rejects_upper_bound_violation() { - let result = maker_ticks_from_prices( - tick_to_price(0).unwrap(), - tick_to_price(MAX_TICK + TICK_SPACING).unwrap(), - ); - assert!(result.is_err()); - } }