Skip to main content

nautilus_model/defi/tick_map/
mod.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 ahash::AHashMap;
17use alloy_primitives::U256;
18
19use crate::defi::tick_map::{
20    liquidity_math::tick_spacing_to_max_liquidity_per_tick, tick::PoolTick, tick_bitmap::TickBitmap,
21};
22
23pub mod bit_math;
24pub mod full_math;
25pub mod liquidity_math;
26pub mod sqrt_price_math;
27pub mod tick;
28pub mod tick_bitmap;
29pub mod tick_math;
30
31/// A tick map implementation for managing liquidity distribution in an AMM (Automated Market Maker).
32///
33/// This structure maintains a mapping of price ticks to liquidity data, allowing efficient
34/// navigation and manipulation of concentrated liquidity positions. It tracks active liquidity,
35/// global fee growth, and uses a bitmap for efficient tick traversal during swaps.
36#[derive(Debug, Clone)]
37pub struct TickMap {
38    /// Mapping of tick indices to tick data
39    ticks: AHashMap<i32, PoolTick>,
40    /// Tick bitmap for efficient tick navigation
41    tick_bitmap: TickBitmap,
42    /// Current active liquidity
43    pub liquidity: u128,
44    /// Maximum liquidity that can be concentrated in a single tick based on tick spacing.
45    pub max_liquidity_per_tick: u128,
46}
47
48impl Default for TickMap {
49    fn default() -> Self {
50        Self::new(1)
51    }
52}
53
54impl TickMap {
55    /// Creates a new [`TickMap`] with the specified tick spacing.
56    ///
57    /// # Panics
58    ///
59    /// Panics if `tick_spacing` is zero.
60    #[must_use]
61    pub fn new(tick_spacing: u32) -> Self {
62        assert!(tick_spacing > 0, "Tick spacing must be greater than zero");
63        Self {
64            ticks: AHashMap::new(),
65            tick_bitmap: TickBitmap::new(tick_spacing),
66            liquidity: 0,
67            max_liquidity_per_tick: tick_spacing_to_max_liquidity_per_tick(tick_spacing as i32),
68        }
69    }
70
71    /// Retrieves a reference to the tick data at the specified tick index.
72    #[must_use]
73    pub fn get_tick(&self, tick: i32) -> Option<&PoolTick> {
74        self.ticks.get(&tick)
75    }
76
77    /// Gets a mutable reference to the tick data, initializing it if it doesn't exist.
78    pub fn get_tick_or_init(&mut self, tick: i32) -> &mut PoolTick {
79        self.ticks
80            .entry(tick)
81            .or_insert_with(|| PoolTick::from_tick(tick))
82    }
83
84    /// Calculates the fee growth inside a price range defined by lower and upper ticks.
85    pub fn get_fee_growth_inside(
86        &mut self,
87        lower_tick: i32,
88        upper_tick: i32,
89        current_tick: i32,
90        fee_growth_global_0: U256,
91        fee_growth_global_1: U256,
92    ) -> (U256, U256) {
93        // Ensure both ticks exist by initializing them first
94        self.ticks
95            .entry(lower_tick)
96            .or_insert_with(|| PoolTick::from_tick(lower_tick));
97        self.ticks
98            .entry(upper_tick)
99            .or_insert_with(|| PoolTick::from_tick(upper_tick));
100
101        // Now safely access both ticks (they're guaranteed to exist)
102        let lower_tick = &self.ticks[&lower_tick];
103        let upper_tick = &self.ticks[&upper_tick];
104
105        // Calculate the fee growth below
106        let fee_growth_below_0 = if current_tick >= lower_tick.value {
107            lower_tick.fee_growth_outside_0
108        } else {
109            fee_growth_global_0 - lower_tick.fee_growth_outside_0
110        };
111        let fee_growth_below_1 = if current_tick >= lower_tick.value {
112            lower_tick.fee_growth_outside_1
113        } else {
114            fee_growth_global_1 - lower_tick.fee_growth_outside_1
115        };
116
117        // Calculate the fee growth above
118        let fee_growth_above_0 = if current_tick < upper_tick.value {
119            upper_tick.fee_growth_outside_0
120        } else {
121            fee_growth_global_0 - upper_tick.fee_growth_outside_0
122        };
123        let fee_growth_above_1 = if current_tick < upper_tick.value {
124            upper_tick.fee_growth_outside_1
125        } else {
126            fee_growth_global_1 - upper_tick.fee_growth_outside_1
127        };
128
129        // Calculate the fee growth inside
130        let fee_growth_inside_0 = fee_growth_global_0 - fee_growth_below_0 - fee_growth_above_0;
131        let fee_growth_inside_1 = fee_growth_global_1 - fee_growth_below_1 - fee_growth_above_1;
132
133        (fee_growth_inside_0, fee_growth_inside_1)
134    }
135
136    /// Updates tick data and returns its flip status.
137    fn update_tick_data(
138        &mut self,
139        tick: i32,
140        tick_current: i32,
141        liquidity_delta: i128,
142        upper: bool,
143        fee_growth_global_0: U256,
144        fee_growth_global_1: U256,
145    ) -> bool {
146        let max_liquidity_per_tick = self.max_liquidity_per_tick;
147        let tick = self.get_tick_or_init(tick);
148
149        let liquidity_gross_before = tick.update_liquidity(liquidity_delta, upper);
150        let liquidity_gross_after = tick.liquidity_gross;
151        assert!(
152            liquidity_gross_after <= max_liquidity_per_tick,
153            "Liquidity exceeds maximum per tick"
154        );
155
156        if liquidity_gross_before == 0 {
157            // By convention, we assume that all growth before a tick was initialized happened _below_ the tick
158            if tick.value <= tick_current {
159                tick.fee_growth_outside_0 = fee_growth_global_0;
160                tick.fee_growth_outside_1 = fee_growth_global_1;
161            }
162            tick.initialized = true;
163        }
164
165        // Check if tick was flipped from inactive to active or vice versa
166        (liquidity_gross_after == 0) != (liquidity_gross_before == 0)
167    }
168
169    /// Updates liquidity at a specific tick and manages the tick bitmap.
170    pub fn update(
171        &mut self,
172        tick: i32,
173        tick_current: i32,
174        liquidity_delta: i128,
175        upper: bool,
176        fee_growth_global_0: U256,
177        fee_growth_global_1: U256,
178    ) -> bool {
179        let flipped = self.update_tick_data(
180            tick,
181            tick_current,
182            liquidity_delta,
183            upper,
184            fee_growth_global_0,
185            fee_growth_global_1,
186        );
187
188        // Only flip the bitmap if the tick actually flipped state
189        if flipped {
190            self.tick_bitmap.flip_tick(tick);
191        }
192
193        flipped
194    }
195
196    /// Crosses a tick during a swap, updating fee growth tracking.
197    pub fn cross_tick(
198        &mut self,
199        tick: i32,
200        fee_growth_global_0: U256,
201        fee_growth_global_1: U256,
202    ) -> i128 {
203        let tick = self.get_tick_or_init(tick);
204        tick.update_fee_growth(fee_growth_global_0, fee_growth_global_1);
205
206        tick.liquidity_net
207    }
208
209    /// Returns the number of currently active (initialized) ticks.
210    #[must_use]
211    pub fn active_tick_count(&self) -> usize {
212        self.ticks
213            .iter()
214            .filter(|(_, tick)| self.is_tick_initialized(tick.value))
215            .count()
216    }
217
218    /// Returns the total number of ticks stored in the map.
219    #[must_use]
220    pub fn total_tick_count(&self) -> usize {
221        self.ticks.len()
222    }
223
224    /// Returns a reference to all ticks in the map for debugging/analysis purposes.
225    #[must_use]
226    pub fn get_all_ticks(&self) -> &AHashMap<i32, PoolTick> {
227        &self.ticks
228    }
229
230    /// Sets the tick data for a specific tick index.
231    pub fn set_tick(&mut self, tick_data: PoolTick) {
232        let tick = tick_data.value;
233        self.ticks.insert(tick, tick_data);
234    }
235
236    /// Restores a tick from a snapshot, updating both tick data and bitmap.
237    ///
238    /// This method is used when restoring pool state from a saved snapshot.
239    /// It sets the tick data and updates the bitmap if the tick is initialized.
240    pub fn restore_tick(&mut self, tick_data: PoolTick) {
241        let is_initialized = tick_data.initialized;
242        let tick_value = tick_data.value;
243
244        self.set_tick(tick_data);
245
246        // Update bitmap if the tick is initialized
247        if is_initialized {
248            self.tick_bitmap.flip_tick(tick_value);
249        }
250    }
251
252    /// Clears all data in a tick by removing it from the tick map.
253    pub fn clear(&mut self, tick: i32) {
254        self.ticks.remove(&tick);
255    }
256
257    /// Finds the next initialized tick after the given tick.
258    #[must_use]
259    pub fn next_initialized_tick(&self, tick: i32, lte: bool) -> (i32, bool) {
260        self.tick_bitmap
261            .next_initialized_tick_within_one_word(tick, lte)
262    }
263
264    /// Checks if a tick is initialized in the bitmap.
265    #[must_use]
266    pub fn is_tick_initialized(&self, tick: i32) -> bool {
267        self.tick_bitmap.is_initialized(tick)
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use std::str::FromStr;
274
275    use rstest::{fixture, rstest};
276
277    use super::*;
278
279    #[fixture]
280    fn tick_map() -> TickMap {
281        TickMap::new(1)
282    }
283
284    #[rstest]
285    fn test_new_tick_maps(tick_map: TickMap) {
286        assert_eq!(tick_map.active_tick_count(), 0);
287        assert_eq!(tick_map.liquidity, 0);
288    }
289
290    #[rstest]
291    fn test_get_fee_growth_inside_uninitialized_ticks(mut tick_map: TickMap) {
292        let fee_growth_global_0 = U256::from(15);
293        let fee_growth_global_1 = U256::from(15);
294
295        // If tick is inside: Tick 0 is inside -2 and 2
296        let (fee_growth_inside_0, fee_growth_inside_1) =
297            tick_map.get_fee_growth_inside(-2, 2, 0, fee_growth_global_0, fee_growth_global_1);
298        assert_eq!(fee_growth_inside_0, U256::from_str("15").unwrap());
299        assert_eq!(fee_growth_inside_1, U256::from_str("15").unwrap());
300
301        // If tick is above: Tick 4 is not in [-2,2] so above 2
302        let (fee_growth_inside_0, fee_growth_inside_1) =
303            tick_map.get_fee_growth_inside(-2, 2, 4, fee_growth_global_0, fee_growth_global_1);
304        assert_eq!(fee_growth_inside_0, U256::ZERO);
305        assert_eq!(fee_growth_inside_1, U256::ZERO);
306
307        // If tick is below: Tick -4 is not in [-2,2] so below -2
308        let (fee_growth_inside_0, fee_growth_inside_1) =
309            tick_map.get_fee_growth_inside(-2, 2, -4, fee_growth_global_0, fee_growth_global_1);
310        assert_eq!(fee_growth_inside_0, U256::ZERO);
311        assert_eq!(fee_growth_inside_1, U256::ZERO);
312    }
313
314    #[rstest]
315    fn test_get_fee_growth_inside_if_upper_tick_is_below(mut tick_map: TickMap) {
316        tick_map.set_tick(PoolTick::new(
317            2, // Set 2 at the upper range boundary
318            0,
319            0,
320            U256::from(2),
321            U256::from(3),
322            true,
323            0,
324        ));
325        let fee_growth_global_0 = U256::from(15);
326        let fee_growth_global_1 = U256::from(15);
327        let (fee_growth_inside_0, fee_growth_inside_1) =
328            tick_map.get_fee_growth_inside(-2, 2, 0, fee_growth_global_0, fee_growth_global_1);
329        assert_eq!(fee_growth_inside_0, U256::from(13));
330        assert_eq!(fee_growth_inside_1, U256::from(12));
331    }
332
333    #[rstest]
334    fn test_get_fee_growth_inside_if_lower_tick_is_above(mut tick_map: TickMap) {
335        tick_map.set_tick(PoolTick::new(
336            -2, // Set -2 at the lower range boundary
337            0,
338            0,
339            U256::from(2),
340            U256::from(3),
341            true,
342            0,
343        ));
344        let fee_growth_global_0 = U256::from(15);
345        let fee_growth_global_1 = U256::from(15);
346        let (fee_growth_inside_0, fee_growth_inside_1) =
347            tick_map.get_fee_growth_inside(-2, 2, 0, fee_growth_global_0, fee_growth_global_1);
348        assert_eq!(fee_growth_inside_0, U256::from(13));
349        assert_eq!(fee_growth_inside_1, U256::from(12));
350    }
351
352    #[rstest]
353    fn test_get_fee_growth_inside_if_upper_and_lower_tick_are_initialized(mut tick_map: TickMap) {
354        tick_map.set_tick(PoolTick::new(
355            -2, // Set -2 at the lower range boundary
356            0,
357            0,
358            U256::from(2),
359            U256::from(3),
360            true,
361            0,
362        ));
363        tick_map.set_tick(PoolTick::new(
364            2, // Set -2 at the lower range boundary
365            0,
366            0,
367            U256::from(4),
368            U256::from(1),
369            true,
370            0,
371        ));
372        let fee_growth_global_0 = U256::from(15);
373        let fee_growth_global_1 = U256::from(15);
374        let (fee_growth_inside_0, fee_growth_inside_1) =
375            tick_map.get_fee_growth_inside(-2, 2, 0, fee_growth_global_0, fee_growth_global_1);
376        assert_eq!(fee_growth_inside_0, U256::from(9));
377        assert_eq!(fee_growth_inside_1, U256::from(11));
378    }
379
380    #[rstest]
381    #[case::current_below_range(-4, (10, 12), (4, 1), (6, 11))]
382    #[case::current_at_upper_tick(2, (2, 3), (10, 12), (8, 9))]
383    fn test_get_fee_growth_inside_outside_range(
384        mut tick_map: TickMap,
385        #[case] current_tick: i32,
386        #[case] lower_outside: (u32, u32),
387        #[case] upper_outside: (u32, u32),
388        #[case] expected_inside: (u32, u32),
389    ) {
390        tick_map.set_tick(PoolTick::new(
391            -2,
392            0,
393            0,
394            U256::from(lower_outside.0),
395            U256::from(lower_outside.1),
396            true,
397            0,
398        ));
399        tick_map.set_tick(PoolTick::new(
400            2,
401            0,
402            0,
403            U256::from(upper_outside.0),
404            U256::from(upper_outside.1),
405            true,
406            0,
407        ));
408
409        let fee_growth_inside =
410            tick_map.get_fee_growth_inside(-2, 2, current_tick, U256::from(15), U256::from(15));
411
412        assert_eq!(
413            fee_growth_inside,
414            (U256::from(expected_inside.0), U256::from(expected_inside.1))
415        );
416    }
417
418    #[rstest]
419    fn test_get_fee_growth_inside_with_overflow(mut tick_map: TickMap) {
420        tick_map.set_tick(PoolTick::new(
421            -2,
422            0,
423            0,
424            U256::MAX - U256::from(3u32), // MaxUint256 - 3
425            U256::MAX - U256::from(2u32), // MaxUint256 - 2
426            true,
427            0,
428        ));
429        tick_map.set_tick(PoolTick::new(
430            2,
431            0,
432            0,
433            U256::from(3u32),
434            U256::from(5u32),
435            true,
436            0,
437        ));
438        let fee_growth_global_0 = U256::from(15);
439        let fee_growth_global_1 = U256::from(15);
440        let (fee_growth_inside_0, fee_growth_inside_1) =
441            tick_map.get_fee_growth_inside(-2, 2, 0, fee_growth_global_0, fee_growth_global_1);
442        assert_eq!(fee_growth_inside_0, U256::from(16u32));
443        assert_eq!(fee_growth_inside_1, U256::from(13u32));
444    }
445
446    #[rstest]
447    fn test_update_flips_from_zero_to_nonzero(mut tick_map: TickMap) {
448        // Initially tick should not be initialized in bitmap
449        assert!(!tick_map.is_tick_initialized(0));
450
451        let flipped = tick_map.update(0, 0, 1, false, U256::ZERO, U256::ZERO);
452        assert!(flipped);
453
454        // After flipping from zero to nonzero, tick should be initialized in bitmap
455        assert!(tick_map.is_tick_initialized(0));
456    }
457
458    #[rstest]
459    fn test_update_does_not_flip_from_nonzero_to_greater_nonzero(mut tick_map: TickMap) {
460        // First update: flip from 0 to 1
461        tick_map.update(0, 0, 1, false, U256::ZERO, U256::ZERO);
462        assert!(tick_map.is_tick_initialized(0));
463
464        // Second update: should not flip from 1 to 2
465        let flipped = tick_map.update(0, 0, 1, false, U256::ZERO, U256::ZERO);
466        assert!(!flipped);
467
468        // Bitmap should remain unchanged (still initialized)
469        assert!(tick_map.is_tick_initialized(0));
470    }
471
472    #[rstest]
473    fn test_update_flips_from_nonzero_to_zero(mut tick_map: TickMap) {
474        // First update: flip from 0 to 1
475        let flipped_first = tick_map.update(0, 0, 1, false, U256::ZERO, U256::ZERO);
476        assert!(flipped_first);
477        assert!(tick_map.is_tick_initialized(0));
478
479        // Second update: flip from 1 to 0 (remove all liquidity)
480        let flipped_second = tick_map.update(0, 0, -1, false, U256::ZERO, U256::ZERO);
481        assert!(flipped_second);
482
483        // After flipping back to zero, tick should not be initialized in bitmap
484        assert!(!tick_map.is_tick_initialized(0));
485    }
486
487    #[rstest]
488    fn test_update_does_not_flip_from_nonzero_to_lesser_nonzero(mut tick_map: TickMap) {
489        // First update: flip from 0 to 2
490        tick_map.update(0, 0, 2, false, U256::ZERO, U256::ZERO);
491        assert!(tick_map.is_tick_initialized(0));
492
493        // Second update: should not flip from 2 to 1 (remove some but not all liquidity)
494        let flipped = tick_map.update(0, 0, -1, false, U256::ZERO, U256::ZERO);
495        assert!(!flipped);
496
497        // Bitmap should remain unchanged (still initialized)
498        assert!(tick_map.is_tick_initialized(0));
499    }
500
501    #[rstest]
502    #[should_panic(expected = "Liquidity exceeds maximum per tick")]
503    fn test_update_reverts_if_total_liquidity_gross_exceeds_max() {
504        let mut tick_map = TickMap::new(200); // Higher tick spacing = lower max liquidity per tick
505
506        // Add liquidity close to max
507        let max_liquidity = tick_map.max_liquidity_per_tick;
508        tick_map.update(
509            0,
510            0,
511            (max_liquidity / 2) as i128,
512            false,
513            U256::ZERO,
514            U256::ZERO,
515        );
516        tick_map.update(
517            0,
518            0,
519            (max_liquidity / 2) as i128,
520            true,
521            U256::ZERO,
522            U256::ZERO,
523        );
524
525        // This should panic as it exceeds max liquidity per tick
526        tick_map.update(0, 0, 1, false, U256::ZERO, U256::ZERO);
527    }
528
529    #[rstest]
530    fn test_update_nets_liquidity_based_on_upper_flag(mut tick_map: TickMap) {
531        // Update with upper=false: liquidity_net += delta
532        tick_map.update(0, 0, 2, false, U256::ZERO, U256::ZERO);
533        // Update with upper=true: liquidity_net -= delta
534        tick_map.update(0, 0, 1, true, U256::ZERO, U256::ZERO);
535        // Update with upper=true: liquidity_net -= delta
536        tick_map.update(0, 0, 3, true, U256::ZERO, U256::ZERO);
537        // Update with upper=false: liquidity_net += delta
538        tick_map.update(0, 0, 1, false, U256::ZERO, U256::ZERO);
539
540        let tick = tick_map.get_tick(0).unwrap();
541
542        // liquidity_gross should be the sum of all absolute deltas: 2 + 1 + 3 + 1 = 7
543        assert_eq!(tick.liquidity_gross, 7);
544
545        // liquidity_net should be: 2 - 1 - 3 + 1 = -1
546        assert_eq!(tick.liquidity_net, -1);
547    }
548
549    #[rstest]
550    fn test_update_assumes_all_growth_happens_below_ticks_lte_current_tick() {
551        let mut tick_map = TickMap::new(1);
552        let fee_growth_global_0 = U256::from(15);
553        let fee_growth_global_1 = U256::from(2);
554        // Update tick 1 when current tick is 1 (tick <= current_tick)
555        tick_map.update(1, 1, 1, false, fee_growth_global_0, fee_growth_global_1);
556
557        let tick = tick_map.get_tick(1).unwrap();
558
559        // Since tick <= current_tick, fee growth outside should be set to global values
560        assert_eq!(tick.fee_growth_outside_0, U256::from(15u32));
561        assert_eq!(tick.fee_growth_outside_1, U256::from(2u32));
562        assert!(tick.initialized);
563        assert_eq!(tick.liquidity_gross, 1);
564        assert_eq!(tick.liquidity_net, 1);
565    }
566
567    #[rstest]
568    fn test_update_does_not_set_growth_fields_if_tick_already_initialized() {
569        let mut tick_map = TickMap::new(1);
570        let fee_growth_0_initial = U256::from(1);
571        let fee_growth_1_initial = U256::from(2);
572        // First update: Initialize the tick
573        tick_map.update(1, 1, 1, false, fee_growth_0_initial, fee_growth_1_initial);
574
575        // Second update: Different fee growth values, but tick is already initialized
576        let fee_growth_0_second = U256::from(6);
577        let fee_growth_1_second = U256::from(7);
578        tick_map.update(1, 1, 1, false, fee_growth_0_second, fee_growth_1_second);
579
580        let tick = tick_map.get_tick(1).unwrap();
581
582        // Fee growth outside should still be the original values from first initialization
583        assert_eq!(tick.fee_growth_outside_0, U256::from(1u32)); // Still 1, not 6
584        assert_eq!(tick.fee_growth_outside_1, U256::from(2u32)); // Still 2, not 7
585        assert!(tick.initialized);
586        assert_eq!(tick.liquidity_gross, 2); // Should be 1 + 1 = 2
587        assert_eq!(tick.liquidity_net, 2); // Should be 1 + 1 = 2
588    }
589
590    #[rstest]
591    fn test_update_does_not_set_growth_fields_for_ticks_gt_current_tick() {
592        let mut tick_map = TickMap::new(1);
593        let fee_growth_global_0 = U256::from(1);
594        let fee_growth_global_1 = U256::from(2u32);
595        // Update tick 2 when current tick is 1 (tick > current_tick)
596        tick_map.update(2, 1, 1, false, fee_growth_global_0, fee_growth_global_1);
597
598        let tick = tick_map.get_tick(2).unwrap();
599
600        // Since tick > current_tick, fee growth outside should remain 0 (not set to global values)
601        assert_eq!(tick.fee_growth_outside_0, U256::ZERO); // Should be 0, not 1
602        assert_eq!(tick.fee_growth_outside_1, U256::ZERO); // Should be 0, not 2
603        assert!(tick.initialized);
604        assert_eq!(tick.liquidity_gross, 1);
605        assert_eq!(tick.liquidity_net, 1);
606    }
607
608    #[rstest]
609    fn test_clear_deletes_all_data_in_tick(mut tick_map: TickMap) {
610        // Set a tick with various data
611        tick_map.set_tick(PoolTick::new(
612            2,
613            3,
614            4,
615            U256::from(1u32),
616            U256::from(2u32),
617            true,
618            0,
619        ));
620
621        // Verify the tick exists with the set data
622        let tick_before = tick_map.get_tick(2).unwrap();
623        assert_eq!(tick_before.fee_growth_outside_0, U256::from(1u32));
624        assert_eq!(tick_before.fee_growth_outside_1, U256::from(2u32));
625        assert_eq!(tick_before.liquidity_gross, 3);
626        assert_eq!(tick_before.liquidity_net, 4);
627        assert!(tick_before.initialized);
628
629        // Clear the tick
630        tick_map.clear(2);
631
632        // Verify the tick no longer exists
633        assert_eq!(tick_map.get_tick(2), None);
634    }
635
636    #[rstest]
637    fn test_cross_tick_flips_growth_variables(mut tick_map: TickMap) {
638        // Set a tick with initial values
639        tick_map.set_tick(PoolTick::new(
640            2,
641            3,
642            4,
643            U256::from(1u32),
644            U256::from(2u32),
645            true,
646            7,
647        ));
648
649        // Cross the tick with global values: (7, 9)
650        // Expected results: fee_growth_outside = global - current
651        // fee_growth_outside_0: 7 - 1 = 6
652        // fee_growth_outside_1: 9 - 2 = 7
653        let liquidity_net = tick_map.cross_tick(
654            2,
655            U256::from(7u32), // fee_growth_global_0
656            U256::from(9u32), // fee_growth_global_1
657        );
658
659        let tick = tick_map.get_tick(2).unwrap();
660
661        // Verify fee growth variables were flipped (global - current)
662        assert_eq!(tick.fee_growth_outside_0, U256::from(6u32)); // 7 - 1 = 6
663        assert_eq!(tick.fee_growth_outside_1, U256::from(7u32)); // 9 - 2 = 7
664
665        // Verify liquidity_net is returned
666        assert_eq!(liquidity_net, 4);
667
668        // Other fields should remain unchanged
669        assert_eq!(tick.liquidity_gross, 3);
670        assert_eq!(tick.liquidity_net, 4);
671        assert!(tick.initialized);
672    }
673}