diff --git a/pallets/swap/src/lib.rs b/pallets/swap/src/lib.rs index ab98205b44..6526aa5937 100644 --- a/pallets/swap/src/lib.rs +++ b/pallets/swap/src/lib.rs @@ -9,7 +9,7 @@ use safe_math::*; use sp_arithmetic::helpers_128bit::sqrt; use substrate_fixed::types::U64F64; -use self::tick::{Tick, TickIndex}; +use self::tick::{Layer, Tick, TickIndex, TickIndexBitmap}; pub mod pallet; mod tick; @@ -1040,7 +1040,7 @@ where /// Get fees above a tick /// fn get_fees_above(&mut self, tick_index: TickIndex, quote: bool) -> U64F64 { - let maybe_tick_index = tick_index.find_closest_lower_active(&self.state_ops); + let maybe_tick_index = self.find_closest_lower_active_tick_index(tick_index); let current_tick = self.get_current_tick_index(); if let Some(tick_index) = maybe_tick_index { @@ -1072,7 +1072,7 @@ where /// Get fees below a tick fn get_fees_below(&mut self, tick_index: TickIndex, quote: bool) -> U64F64 { - let maybe_tick_index = tick_index.find_closest_lower_active(&self.state_ops); + let maybe_tick_index = self.find_closest_lower_active_tick_index(tick_index); let current_tick = self.get_current_tick_index(); if let Some(tick_index) = maybe_tick_index { @@ -1115,17 +1115,7 @@ where /// Deletion: 2 reads, 1-3 writes /// - // Addresses an index as (word, bit) - fn index_to_address(index: u32) -> (u32, u32) { - let word: u32 = index.safe_div(128); - let bit: u32 = index.checked_rem(128).unwrap_or_default(); - (word, bit) - } - - // Reconstructs an index from address in lower level - fn address_to_index(word: u32, bit: u32) -> u32 { - word.saturating_mul(128).saturating_add(bit) - } + // Use TickIndexBitmap::layer_to_index instead pub fn insert_active_tick(&mut self, index: TickIndex) { // Check the range @@ -1133,28 +1123,30 @@ where return; } - // Convert the tick index value to an offset_index for the tree representation - // to avoid working with the sign bit. - let offset_index = (index.get() + TickIndex::OFFSET.get()) as u32; - - // Calculate index in each layer - let (layer2_word, layer2_bit) = Self::index_to_address(offset_index); - let (layer1_word, layer1_bit) = Self::index_to_address(layer2_word); - let (layer0_word, layer0_bit) = Self::index_to_address(layer1_word); + // Convert to bitmap representation + let bitmap = TickIndexBitmap::from(index); // Update layer words - let mut word0_value = self.state_ops.get_layer0_word(layer0_word); - let mut word1_value = self.state_ops.get_layer1_word(layer1_word); - let mut word2_value = self.state_ops.get_layer2_word(layer2_word); - - let bit: u128 = 1; - word0_value = word0_value | bit.wrapping_shl(layer0_bit); - word1_value = word1_value | bit.wrapping_shl(layer1_bit); - word2_value = word2_value | bit.wrapping_shl(layer2_bit); - - self.state_ops.set_layer0_word(layer0_word, word0_value); - self.state_ops.set_layer1_word(layer1_word, word1_value); - self.state_ops.set_layer2_word(layer2_word, word2_value); + let mut word0_value = self.state_ops.get_layer0_word(bitmap.word_at(Layer::Top)); + let mut word1_value = self + .state_ops + .get_layer1_word(bitmap.word_at(Layer::Middle)); + let mut word2_value = self + .state_ops + .get_layer2_word(bitmap.word_at(Layer::Bottom)); + + // Set bits in each layer + word0_value |= bitmap.bit_mask(Layer::Top); + word1_value |= bitmap.bit_mask(Layer::Middle); + word2_value |= bitmap.bit_mask(Layer::Bottom); + + // Update the storage + self.state_ops + .set_layer0_word(bitmap.word_at(Layer::Top), word0_value); + self.state_ops + .set_layer1_word(bitmap.word_at(Layer::Middle), word1_value); + self.state_ops + .set_layer2_word(bitmap.word_at(Layer::Bottom), word2_value); } pub fn remove_active_tick(&mut self, index: TickIndex) { @@ -1163,58 +1155,34 @@ where return; } - // Convert the tick index value to an offset_index for the tree representation - // to avoid working with the sign bit. - let offset_index = (index.get() + TickIndex::OFFSET.get()) as u32; - - // Calculate index in each layer - let (layer2_word, layer2_bit) = Self::index_to_address(offset_index); - let (layer1_word, layer1_bit) = Self::index_to_address(layer2_word); - let (layer0_word, layer0_bit) = Self::index_to_address(layer1_word); + // Convert to bitmap representation + let bitmap = TickIndexBitmap::from(index); // Update layer words - let mut word0_value = self.state_ops.get_layer0_word(layer0_word); - let mut word1_value = self.state_ops.get_layer1_word(layer1_word); - let mut word2_value = self.state_ops.get_layer2_word(layer2_word); + let mut word0_value = self.state_ops.get_layer0_word(bitmap.word_at(Layer::Top)); + let mut word1_value = self + .state_ops + .get_layer1_word(bitmap.word_at(Layer::Middle)); + let mut word2_value = self + .state_ops + .get_layer2_word(bitmap.word_at(Layer::Bottom)); // Turn the bit off (& !bit) and save as needed - let bit: u128 = 1; - word2_value = word2_value & !bit.wrapping_shl(layer2_bit); - self.state_ops.set_layer2_word(layer2_word, word2_value); + word2_value &= !bitmap.bit_mask(Layer::Bottom); + self.state_ops + .set_layer2_word(bitmap.word_at(Layer::Bottom), word2_value); + if word2_value == 0 { - word1_value = word1_value & !bit.wrapping_shl(layer1_bit); - self.state_ops.set_layer1_word(layer1_word, word1_value); - } - if word1_value == 0 { - word0_value = word0_value & !bit.wrapping_shl(layer0_bit); - self.state_ops.set_layer0_word(layer0_word, word0_value); + word1_value &= !bitmap.bit_mask(Layer::Middle); + self.state_ops + .set_layer1_word(bitmap.word_at(Layer::Middle), word1_value); } - } - // Finds the closest active bit and, if active bit exactly matches bit, then the next one - // Exact match: return Some([next, bit]) - // Non-exact match: return Some([next]) - // No match: return None - fn find_closest_active_bit_candidates(&self, word: u128, bit: u32, lower: bool) -> Vec { - let mut result = vec![]; - let mut mask: u128 = 1_u128.wrapping_shl(bit); - let mut layer0_active_bit: u32 = bit; - while mask > 0 { - if mask & word != 0 { - result.push(layer0_active_bit); - if layer0_active_bit != bit { - break; - } - } - mask = if lower { - layer0_active_bit = layer0_active_bit.saturating_sub(1); - mask.wrapping_shr(1) - } else { - layer0_active_bit = layer0_active_bit.saturating_add(1); - mask.wrapping_shl(1) - }; + if word1_value == 0 { + word0_value &= !bitmap.bit_mask(Layer::Top); + self.state_ops + .set_layer0_word(bitmap.word_at(Layer::Top), word0_value); } - result } pub fn find_closest_active_tick_index( @@ -1227,28 +1195,31 @@ where return None; } - // Convert the tick index value to an offset_index for the tree representation - // to avoid working with the sign bit. - let offset_index = (index.get() + TickIndex::OFFSET.get()) as u32; + // Convert to bitmap representation + let bitmap = TickIndexBitmap::from(index); let mut found = false; let mut result: u32 = 0; - // Calculate index in each layer - let (layer2_word, layer2_bit) = Self::index_to_address(offset_index); - let (layer1_word, layer1_bit) = Self::index_to_address(layer2_word); - let (layer0_word, layer0_bit) = Self::index_to_address(layer1_word); + // Layer positions from bitmap + let layer0_word = bitmap.word_at(Layer::Top); + let layer0_bit = bitmap.bit_at(Layer::Top); + let layer1_word = bitmap.word_at(Layer::Middle); + let layer1_bit = bitmap.bit_at(Layer::Middle); + let layer2_word = bitmap.word_at(Layer::Bottom); + let layer2_bit = bitmap.bit_at(Layer::Bottom); // Find the closest active bits in layer 0, then 1, then 2 /////////////// // Level 0 let word0 = self.state_ops.get_layer0_word(layer0_word); - let closest_bits_l0 = self.find_closest_active_bit_candidates(word0, layer0_bit, lower); + let closest_bits_l0 = + TickIndexBitmap::find_closest_active_bit_candidates(word0, layer0_bit, lower); closest_bits_l0.iter().for_each(|&closest_bit_l0| { /////////////// // Level 1 - let word1_index = Self::address_to_index(0, closest_bit_l0); + let word1_index = TickIndexBitmap::layer_to_index(0, closest_bit_l0); // Layer 1 words are different, shift the bit to the word edge let start_from_l1_bit = if word1_index < layer1_word { @@ -1260,12 +1231,15 @@ where }; let word1_value = self.state_ops.get_layer1_word(word1_index); - let closest_bits_l1 = - self.find_closest_active_bit_candidates(word1_value, start_from_l1_bit, lower); + let closest_bits_l1 = TickIndexBitmap::find_closest_active_bit_candidates( + word1_value, + start_from_l1_bit, + lower, + ); closest_bits_l1.iter().for_each(|&closest_bit_l1| { /////////////// // Level 2 - let word2_index = Self::address_to_index(word1_index, closest_bit_l1); + let word2_index = TickIndexBitmap::layer_to_index(word1_index, closest_bit_l1); // Layer 2 words are different, shift the bit to the word edge let start_from_l2_bit = if word2_index < layer2_word { @@ -1277,13 +1251,16 @@ where }; let word2_value = self.state_ops.get_layer2_word(word2_index); - let closest_bits_l2 = - self.find_closest_active_bit_candidates(word2_value, start_from_l2_bit, lower); + let closest_bits_l2 = TickIndexBitmap::find_closest_active_bit_candidates( + word2_value, + start_from_l2_bit, + lower, + ); if closest_bits_l2.len() > 0 { // The active tick is found, restore its full index and return let offset_found_index = - Self::address_to_index(word2_index, closest_bits_l2[0]); + TickIndexBitmap::layer_to_index(word2_index, closest_bits_l2[0]); if lower { if (offset_found_index > result) || (!found) { @@ -1300,13 +1277,12 @@ where }); }); - if found { - // Convert the tree offset_index back to a tick index value - let tick_value = (result as i32).saturating_sub(TickIndex::OFFSET.get()); - Some(TickIndex::new_unchecked(tick_value)) - } else { - None + if !found { + return None; } + + // Convert the result offset_index back to a tick index + TickIndex::from_offset_index(result).ok() } pub fn find_closest_lower_active_tick_index(&self, index: TickIndex) -> Option { diff --git a/pallets/swap/src/tick.rs b/pallets/swap/src/tick.rs index 809b261702..9a21dd70d4 100644 --- a/pallets/swap/src/tick.rs +++ b/pallets/swap/src/tick.rs @@ -7,9 +7,10 @@ use core::ops::{Add, AddAssign, BitOr, Deref, Neg, Shl, Shr, Sub, SubAssign}; use alloy_primitives::{I256, U256}; use frame_support::pallet_prelude::*; +use safe_math::*; use substrate_fixed::types::U64F64; -use crate::{SqrtPrice, SwapDataOperations}; +use crate::SqrtPrice; const U256_1: U256 = U256::from_limbs([1, 0, 0, 0]); const U256_2: U256 = U256::from_limbs([2, 0, 0, 0]); @@ -159,17 +160,19 @@ impl TryIntoTickIndex for i32 { } impl TickIndex { + /// Minimum value of the tick index + /// The tick_math library uses different bitness, so we have to divide by 2. + /// It's unsafe to change this value to something else. + pub const MIN: Self = Self(MIN_TICK / 2); + /// Maximum value of the tick index /// The tick_math library uses different bitness, so we have to divide by 2. + /// It's unsafe to change this value to something else. pub const MAX: Self = Self(MAX_TICK / 2); - /// Minimum value of the tick index - /// The tick_math library uses different bitness, so we have to divide by 2. - pub const MIN: Self = Self(MIN_TICK / 2); - /// All tick indexes are offset by this value for storage needs /// so that tick indexes are positive, which simplifies bit logic - pub const OFFSET: Self = Self(MAX_TICK); + const OFFSET: Self = Self(MAX_TICK); /// Converts a sqrt price to a tick index, ensuring it's within valid bounds /// @@ -235,6 +238,18 @@ impl TickIndex { self.0 } + /// Creates a TickIndex from an offset representation (u32) + /// + /// # Arguments + /// * `offset_index` - An offset index (u32 value) representing a tick index + /// + /// # Returns + /// * `Result` - The corresponding TickIndex if within valid bounds + pub fn from_offset_index(offset_index: u32) -> Result { + let signed_index = (offset_index as i64 - Self::OFFSET.get() as i64) as i32; + Self::new(signed_index) + } + /// Get the next tick index (incrementing by 1) pub fn next(&self) -> Result { Self::new(self.0 + 1) @@ -358,52 +373,128 @@ impl TickIndex { } } } +} - /// Find the closest lower active tick index - pub fn find_closest_lower_active(&self, ops: &Ops) -> Option - where - AccountIdType: Eq, - Ops: SwapDataOperations, - { - // TODO: Implement without iteration - let mut current_index = *self; - loop { - if current_index.get() < Self::MIN.get() { - return None; - } - if ops.get_tick_by_index(current_index).is_some() { - return Some(current_index); - } +/// Represents the three layers in the Uniswap V3 bitmap structure +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Layer { + /// Top layer (highest level of the hierarchy) + Top = 0, + /// Middle layer + Middle = 1, + /// Bottom layer (contains the actual ticks) + Bottom = 2, +} - // Create a new index with value one less - match current_index.prev() { - Ok(next_index) => current_index = next_index, - Err(_) => return None, // Return None if we go out of bounds - } +/// A bitmap representation of a tick index position across the three-layer structure +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TickIndexBitmap { + /// The position in layer 0 (top layer) + layer0: (u32, u32), // (word, bit) + /// The position in layer 1 (middle layer) + layer1: (u32, u32), // (word, bit) + /// The position in layer 2 (bottom layer) + layer2: (u32, u32), // (word, bit) +} + +impl TickIndexBitmap { + /// Helper function to convert a bitmap index to a (word, bit) tuple in a bitmap layer using + /// safe methods + /// + /// Note: This function operates on bitmap navigation indices, NOT tick indices. + /// It converts a flat index within the bitmap structure to a (word, bit) position. + fn index_to_layer(index: u32) -> (u32, u32) { + let word = index.safe_div(128); + let bit = index.checked_rem(128).unwrap_or_default(); + (word, bit) + } + + /// Converts a position (word, bit) within a layer to a word index in the next layer down + /// Note: This returns a bitmap navigation index, NOT a tick index + pub fn layer_to_index(word: u32, bit: u32) -> u32 { + word.saturating_mul(128).saturating_add(bit) + } + + /// Get the mask for a bit in the specified layer + pub fn bit_mask(&self, layer: Layer) -> u128 { + match layer { + Layer::Top => 1u128 << self.layer0.1, + Layer::Middle => 1u128 << self.layer1.1, + Layer::Bottom => 1u128 << self.layer2.1, } } - /// Find the closest higher active tick index - pub fn find_closest_higher_active(&self, ops: &Ops) -> Option - where - AccountIdType: Eq, - Ops: SwapDataOperations, - { - // TODO: Implement without iteration - let mut current_index = *self; - loop { - if current_index.get() > Self::MAX.get() { - return None; - } - if ops.get_tick_by_index(current_index).is_some() { - return Some(current_index); - } + /// Get the word for the specified layer + pub fn word_at(&self, layer: Layer) -> u32 { + match layer { + Layer::Top => self.layer0.0, + Layer::Middle => self.layer1.0, + Layer::Bottom => self.layer2.0, + } + } + + /// Get the bit for the specified layer + pub fn bit_at(&self, layer: Layer) -> u32 { + match layer { + Layer::Top => self.layer0.1, + Layer::Middle => self.layer1.1, + Layer::Bottom => self.layer2.1, + } + } - // Create a new index with value one more - match current_index.next() { - Ok(next_index) => current_index = next_index, - Err(_) => return None, // Return None if we go out of bounds + /// Finds the closest active bit in a bitmap word, and if the active bit exactly matches the + /// requested bit, then it finds the next one as well + /// + /// # Arguments + /// * `word` - The bitmap word to search within + /// * `bit` - The bit position to start searching from + /// * `lower` - If true, search for lower bits (decreasing bit position), + /// if false, search for higher bits (increasing bit position) + /// + /// # Returns + /// * Exact match: Vec with [next_bit, bit] + /// * Non-exact match: Vec with [closest_bit] + /// * No match: Empty Vec + pub fn find_closest_active_bit_candidates(word: u128, bit: u32, lower: bool) -> Vec { + let mut result = vec![]; + let mut mask: u128 = 1_u128.wrapping_shl(bit); + let mut active_bit: u32 = bit; + + while mask > 0 { + if mask & word != 0 { + result.push(active_bit); + if active_bit != bit { + break; + } } + + mask = if lower { + active_bit = active_bit.saturating_sub(1); + mask.wrapping_shr(1) + } else { + active_bit = active_bit.saturating_add(1); + mask.wrapping_shl(1) + }; + } + + result + } +} + +impl From for TickIndexBitmap { + fn from(tick_index: TickIndex) -> Self { + // Convert to offset index (internal operation only) + let offset_index = (tick_index.get().saturating_add(TickIndex::OFFSET.get())) as u32; + + // Calculate layer positions + let layer2 = Self::index_to_layer(offset_index); + let layer1 = Self::index_to_layer(layer2.0); + let layer0 = Self::index_to_layer(layer1.0); + + Self { + layer0, + layer1, + layer2, } } } @@ -972,4 +1063,35 @@ mod tests { assert_eq!(round_trip_tick_index, tick_index); } } + + #[test] + fn test_from_offset_index() { + // Test various tick indices + for i32_value in [ + MIN_TICK / 2, + -1000, + -100, + -10, + 0, + 10, + 100, + 1000, + MAX_TICK / 2, + ] { + let original_tick = TickIndex::new_unchecked(i32_value); + + // Calculate the offset index (adding OFFSET) + let offset_index = (i32_value + TickIndex::OFFSET.get()) as u32; + + // Convert back from offset index to tick index + let roundtrip_tick = TickIndex::from_offset_index(offset_index).unwrap(); + + // Check that we get the same tick index back + assert_eq!(original_tick, roundtrip_tick); + } + + // Test out of bounds values + let too_large = (TickIndex::MAX.get() + TickIndex::OFFSET.get() + 1) as u32; + assert!(TickIndex::from_offset_index(too_large).is_err()); + } }