Skip to main content

nautilus_model/defi/tick_map/
liquidity_math.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use crate::defi::{pool_analysis::error::LiquidityMathError, tick_map::tick::PoolTick};
17
18/// Adds a signed liquidity delta to liquidity, returning a structured error on
19/// overflow or underflow.
20///
21/// # Errors
22///
23/// Returns [`LiquidityMathError::Overflow`] when adding a positive delta wraps past
24/// `u128::MAX`, or [`LiquidityMathError::Underflow`] when subtracting wraps below zero.
25pub fn try_liquidity_math_add(x: u128, y: i128) -> Result<u128, LiquidityMathError> {
26    if y < 0 {
27        let delta = y.unsigned_abs();
28        let z = x.wrapping_sub(delta);
29        if z >= x {
30            return Err(LiquidityMathError::Underflow { current: x, delta });
31        }
32        Ok(z)
33    } else {
34        let delta = y as u128;
35        let z = x.wrapping_add(delta);
36        if z < x {
37            return Err(LiquidityMathError::Overflow { current: x, delta });
38        }
39        Ok(z)
40    }
41}
42
43/// Adds a signed liquidity delta to liquidity, panicking on overflow or underflow.
44///
45/// Prefer [`try_liquidity_math_add`] in event-replay paths where a structured error
46/// with surrounding context is preferred. This panic-style variant is kept for
47/// in-pool invariants where overflow is treated as a contract bug rather than an
48/// expected runtime error.
49///
50/// # Returns
51///
52/// The resulting liquidity after applying the delta.
53///
54/// # Panics
55///
56/// This function panics if:
57/// - Adding positive delta causes overflow.
58/// - Subtracting causes underflow.
59#[must_use]
60pub fn liquidity_math_add(x: u128, y: i128) -> u128 {
61    match try_liquidity_math_add(x, y) {
62        Ok(value) => value,
63        Err(LiquidityMathError::Overflow { current, delta }) => {
64            panic!("Liquidity addition overflow: x={current}, y={y}, delta={delta}")
65        }
66        Err(LiquidityMathError::Underflow { current, delta }) => {
67            panic!("Liquidity subtraction underflow: x={current}, y={y}, delta={delta}")
68        }
69    }
70}
71
72/// Derives max liquidity per tick from a given tick spacing.
73///
74/// # Panics
75///
76/// Panics if `tick_spacing` is zero.
77#[must_use]
78pub fn tick_spacing_to_max_liquidity_per_tick(tick_spacing: i32) -> u128 {
79    assert!(tick_spacing != 0, "Tick spacing must be non-zero");
80
81    // Calculate min and max tick aligned to tick spacing
82    let min_tick = (PoolTick::MIN_TICK / tick_spacing) * tick_spacing;
83    let max_tick = (PoolTick::MAX_TICK / tick_spacing) * tick_spacing;
84
85    // Calculate total number of ticks, cast to i64 to avoid potential overflow in subtraction
86    let num_ticks = ((i64::from(max_tick) - i64::from(min_tick)) / i64::from(tick_spacing)) + 1;
87
88    u128::MAX / num_ticks as u128
89}
90
91#[cfg(test)]
92mod tests {
93    use rstest::rstest;
94
95    use super::*;
96
97    #[rstest]
98    fn test_add() {
99        assert_eq!(liquidity_math_add(1, 0), 1);
100        assert_eq!(liquidity_math_add(1, 1), 2);
101    }
102
103    #[rstest]
104    fn test_subtract_one() {
105        assert_eq!(liquidity_math_add(1, -1), 0);
106        assert_eq!(liquidity_math_add(3, -2), 1);
107    }
108
109    #[rstest]
110    #[should_panic(expected = "Liquidity addition overflow")]
111    fn test_addition_overflow() {
112        let x = u128::MAX - 14; // Close to max so adding 15 will overflow
113        let _ = liquidity_math_add(x, 15);
114    }
115
116    #[rstest]
117    #[should_panic(expected = "Liquidity subtraction underflow")]
118    fn test_subtraction_underflow_zero() {
119        let _ = liquidity_math_add(0, -1);
120    }
121
122    #[rstest]
123    #[should_panic(expected = "Liquidity subtraction underflow")]
124    fn test_subtraction_underflow() {
125        let _ = liquidity_math_add(3, -4);
126    }
127
128    #[rstest]
129    fn test_try_add_returns_overflow_error() {
130        let x = u128::MAX - 14;
131        let err = try_liquidity_math_add(x, 15).unwrap_err();
132        assert_eq!(
133            err,
134            LiquidityMathError::Overflow {
135                current: x,
136                delta: 15
137            }
138        );
139    }
140
141    #[rstest]
142    fn test_try_add_returns_underflow_error() {
143        let err = try_liquidity_math_add(3, -4).unwrap_err();
144        assert_eq!(
145            err,
146            LiquidityMathError::Underflow {
147                current: 3,
148                delta: 4
149            }
150        );
151    }
152
153    #[rstest]
154    fn test_tick_spacing_to_max_liquidity() {
155        // 0.01 tier ot 1 tick spacing
156        assert_eq!(
157            tick_spacing_to_max_liquidity_per_tick(1),
158            191_757_530_477_355_301_479_181_766_273_477
159        );
160        // 0.05 % tier or 10 tick spacing
161        assert_eq!(
162            tick_spacing_to_max_liquidity_per_tick(10),
163            1_917_569_901_783_203_986_719_870_431_555_990
164        );
165        // 0.3 % tier or 60 tick spacing
166        assert_eq!(
167            tick_spacing_to_max_liquidity_per_tick(60),
168            11_505_743_598_341_114_571_880_798_222_544_994
169        );
170        // 1.00% tier or 200 tick spacing
171        assert_eq!(
172            tick_spacing_to_max_liquidity_per_tick(200),
173            38_350_317_471_085_141_830_651_933_667_504_588
174        );
175    }
176}