Skip to content

Commit

Permalink
feat(tickmap): add TickMap and EphemeralTickMapDataProvider (#79)
Browse files Browse the repository at this point in the history
* feat(tickmap): add `TickMap` and `EphemeralTickMapDataProvider`

Introduce `TickMap` to efficiently handle tick data with specified spacing. Additionally, implement `EphemeralTickMapDataProvider` for fetching ticks using ephemeral contracts, enhancing tick data management and retrieval.

* Rename `TickMapTrait` to `TickMap` in documentation

`TickMapTrait` was incorrectly named in the documentation header. This change corrects the name to `TickMap` to align with the actual implementation and improve clarity.

* Validate ticks and fix liquidity net calculation

Add validation for ticks in `TickMap::new` to ensure list consistency. Also, improve liquidity net calculation using `checked_add_signed` for better safety and correctness.

* Fix `tick_list` tests and improve error handling

Updated `TickListDataProvider` to correct liquidity_net values in the test setup. Revised error handling in `tick_list.rs` to use expect instead of unwrap to provide a more descriptive error message.
  • Loading branch information
shuhuiluo committed Sep 14, 2024
1 parent 6e694e0 commit 20cd90e
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 49 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "uniswap-v3-sdk"
version = "0.39.0"
version = "0.40.0"
edition = "2021"
authors = ["Shuhui Luo <twitter.com/aureliano_law>"]
description = "Uniswap V3 SDK for Rust"
Expand Down
13 changes: 12 additions & 1 deletion src/entities/tick.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ use crate::prelude::*;
use alloy_primitives::{aliases::I24, Signed};
use core::{
fmt::Debug,
ops::{Add, Div, Mul, Rem, Shl, Shr, Sub},
hash::Hash,
ops::{Add, BitAnd, Div, Mul, Rem, Shl, Shr, Sub},
};

#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
Expand Down Expand Up @@ -34,7 +35,9 @@ pub trait TickIndex:
Copy
+ Debug
+ Default
+ Hash
+ Ord
+ BitAnd<Output = Self>
+ Add<Output = Self>
+ Div<Output = Self>
+ Mul<Output = Self>
Expand Down Expand Up @@ -66,6 +69,14 @@ pub trait TickIndex:
self / tick_spacing
}
}

#[inline]
fn position(self) -> (Self, u8) {
(
self >> 8,
(self & Self::try_from(0xff).unwrap()).try_into().unwrap() as u8,
)
}
}

impl TickIndex for i32 {
Expand Down
6 changes: 3 additions & 3 deletions src/entities/tick_list_data_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ mod tests {
use once_cell::sync::Lazy;

static PROVIDER: Lazy<TickListDataProvider> =
Lazy::new(|| TickListDataProvider::new(vec![Tick::new(-1, 1, -1), Tick::new(1, 1, 1)], 1));
Lazy::new(|| TickListDataProvider::new(vec![Tick::new(-1, 1, 1), Tick::new(1, 1, -1)], 1));

#[test]
fn can_take_an_empty_list_of_ticks() {
Expand Down Expand Up @@ -67,14 +67,14 @@ mod tests {
#[test]
fn gets_the_smallest_tick_from_the_list() {
let tick = PROVIDER.get_tick(-1).unwrap();
assert_eq!(tick.liquidity_net, -1);
assert_eq!(tick.liquidity_net, 1);
assert_eq!(tick.liquidity_gross, 1);
}

#[test]
fn gets_the_largest_tick_from_the_list() {
let tick = PROVIDER.get_tick(1).unwrap();
assert_eq!(tick.liquidity_net, 1);
assert_eq!(tick.liquidity_net, -1);
assert_eq!(tick.liquidity_gross, 1);
}
}
15 changes: 7 additions & 8 deletions src/extensions/ephemeral_tick_data_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ pub struct EphemeralTickDataProvider<I = I24> {
pub pool: Address,
pub tick_lower: I,
pub tick_upper: I,
pub tick_spacing: I,
pub block_id: Option<BlockId>,
pub ticks: Vec<Tick<I>>,
/// the minimum distance between two ticks in the list
pub tick_spacing: I,
}

impl<I: TickIndex> EphemeralTickDataProvider<I> {
Expand Down Expand Up @@ -52,9 +51,9 @@ impl<I: TickIndex> EphemeralTickDataProvider<I> {
pool,
tick_lower: I::from_i24(tick_lower),
tick_upper: I::from_i24(tick_upper),
tick_spacing: I::from_i24(tick_spacing),
block_id,
ticks,
tick_spacing: I::from_i24(tick_spacing),
})
}
}
Expand Down Expand Up @@ -111,16 +110,16 @@ mod tests {
let tick = provider.get_tick(-92110)?;
assert_eq!(tick.liquidity_gross, 398290794261);
assert_eq!(tick.liquidity_net, 398290794261);
let (tick, success) = provider.next_initialized_tick_within_one_word(
MIN_TICK.as_i32() + TICK_SPACING,
let (tick, initialized) = provider.next_initialized_tick_within_one_word(
MIN_TICK_I32 + TICK_SPACING,
true,
TICK_SPACING,
)?;
assert!(success);
assert!(initialized);
assert_eq!(tick, -887270);
let (tick, success) =
let (tick, initialized) =
provider.next_initialized_tick_within_one_word(0, false, TICK_SPACING)?;
assert!(success);
assert!(initialized);
assert_eq!(tick, 100);
let provider: TickListDataProvider = provider.into();
let tick = provider.get_tick(-92110)?;
Expand Down
69 changes: 53 additions & 16 deletions src/extensions/ephemeral_tick_map_data_provider.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
//! ## Ephemeral Tick Map Data Provider
//! A data provider that fetches ticks using an [ephemeral contract](https://github.com/Aperture-Finance/Aperture-Lens/blob/904101e4daed59e02fd4b758b98b0749e70b583b/contracts/EphemeralGetPopulatedTicksInRange.sol) in a single `eth_call`.

#![allow(unused_variables)]
use crate::prelude::*;
use alloy::{eips::BlockId, providers::Provider, transports::Transport};
use alloy_primitives::{aliases::I24, Address};
use uniswap_lens::pool_lens;

/// A data provider that fetches ticks using an ephemeral contract in a single `eth_call`.
#[derive(Clone, Debug, PartialEq)]
pub struct EphemeralTickMapDataProvider<I = I24> {
pub struct EphemeralTickMapDataProvider<I: TickIndex = I24> {
pub pool: Address,
pub tick_lower: I,
pub tick_upper: I,
pub block_id: Option<BlockId>,
pub tick_map: TickMap,
/// the minimum distance between two ticks in the list
pub tick_spacing: I,
pub block_id: Option<BlockId>,
pub tick_map: TickMap<I>,
}

impl<I: TickIndex> EphemeralTickMapDataProvider<I> {
Expand All @@ -32,14 +29,17 @@ impl<I: TickIndex> EphemeralTickMapDataProvider<I> {
T: Transport + Clone,
P: Provider<T>,
{
let tick_lower = tick_lower.map_or(MIN_TICK, I::to_i24);
let tick_upper = tick_upper.map_or(MAX_TICK, I::to_i24);
let (ticks, tick_spacing) = pool_lens::get_populated_ticks_in_range(
pool, tick_lower, tick_upper, provider, block_id,
)
.await
.map_err(Error::LensError)?;
unimplemented!()
let provider =
EphemeralTickDataProvider::new(pool, provider, tick_lower, tick_upper, block_id)
.await?;
Ok(Self {
pool,
tick_lower: provider.tick_lower,
tick_upper: provider.tick_upper,
tick_spacing: provider.tick_spacing,
block_id,
tick_map: TickMap::new(provider.ticks, provider.tick_spacing),
})
}
}

Expand All @@ -48,7 +48,7 @@ impl<I: TickIndex> TickDataProvider for EphemeralTickMapDataProvider<I> {

#[inline]
fn get_tick(&self, tick: I) -> Result<&Tick<I>, Error> {
unimplemented!()
self.tick_map.get_tick(tick)
}

#[inline]
Expand All @@ -58,6 +58,43 @@ impl<I: TickIndex> TickDataProvider for EphemeralTickMapDataProvider<I> {
lte: bool,
tick_spacing: I,
) -> Result<(I, bool), Error> {
unimplemented!()
self.tick_map
.next_initialized_tick_within_one_word(tick, lte, tick_spacing)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::tests::*;
use alloy_primitives::address;

const TICK_SPACING: i32 = 10;

#[tokio::test]
async fn test_ephemeral_tick_map_data_provider() -> Result<(), Error> {
let provider = EphemeralTickMapDataProvider::new(
address!("88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"),
PROVIDER.clone(),
None,
None,
*BLOCK_ID,
)
.await?;
let tick = provider.get_tick(-92110)?;
assert_eq!(tick.liquidity_gross, 398290794261);
assert_eq!(tick.liquidity_net, 398290794261);
let (tick, initialized) = provider.next_initialized_tick_within_one_word(
MIN_TICK_I32 + TICK_SPACING,
true,
TICK_SPACING,
)?;
assert_eq!(tick, -887270);
assert!(initialized);
let (tick, initialized) =
provider.next_initialized_tick_within_one_word(0, false, TICK_SPACING)?;
assert!(initialized);
assert_eq!(tick, 100);
Ok(())
}
}
1 change: 0 additions & 1 deletion src/extensions/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use alloy::{
transports::Transport,
};
use alloy_primitives::{Address, ChainId, B256};
use anyhow::Result;
use uniswap_lens::{
bindings::{
ierc20metadata::IERC20Metadata, iuniswapv3pool::IUniswapV3Pool::IUniswapV3PoolInstance,
Expand Down
92 changes: 74 additions & 18 deletions src/extensions/tick_map.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,88 @@
//! ## Tick Map
//! [`TickMapTrait`] is a trait that provides a way to access tick data directly from a hashmap,
//! supposedly more efficient than [`TickList`].
//! [`TickMap`] provides a way to access tick data directly from a hashmap, supposedly more
//! efficient than [`TickList`].

use crate::prelude::*;
use alloy_primitives::U256;
use anyhow::Result;
use alloy::uint;
use alloy_primitives::{aliases::I24, U256};
use rustc_hash::FxHashMap;

#[derive(Clone, Debug, PartialEq)]
pub struct TickMap {
pub bitmap: FxHashMap<i16, U256>,
pub ticks: FxHashMap<i32, Tick>,
pub struct TickMap<I: TickIndex = I24> {
pub bitmap: FxHashMap<I, U256>,
pub inner: FxHashMap<I, Tick<I>>,
pub tick_spacing: I,
}

pub trait TickMapTrait {
type Tick;

impl<I: TickIndex> TickMap<I> {
#[inline]
#[must_use]
fn position(tick: i32) -> (i16, u8) {
((tick >> 8) as i16, (tick & 0xff) as u8)
pub fn new(ticks: Vec<Tick<I>>, tick_spacing: I) -> Self {
ticks.validate_list(tick_spacing);
let mut bitmap = FxHashMap::<I, U256>::default();
for tick in &ticks {
let compressed = tick.index.compress(tick_spacing);
let (word_pos, bit_pos) = compressed.position();
let word = bitmap.get(&word_pos).unwrap_or(&U256::ZERO);
bitmap.insert(word_pos, word | uint!(1_U256) << bit_pos);
}
Self {
bitmap,
inner: FxHashMap::from_iter(ticks.into_iter().map(|tick| (tick.index, tick))),
tick_spacing,
}
}

fn get_bitmap(&self, tick: i32) -> Result<U256>;

fn get_tick(&self, index: i32) -> &Self::Tick;
}

// impl TickMapTrait for TickMap {}
impl<I: TickIndex> TickDataProvider for TickMap<I> {
type Index = I;

pub trait TickMapDataProvider: TickMapTrait + TickDataProvider {}
#[inline]
fn get_tick(&self, tick: Self::Index) -> Result<&Tick<Self::Index>, Error> {
self.inner
.get(&tick)
.ok_or(Error::InvalidTick(tick.to_i24()))
}

#[inline]
fn next_initialized_tick_within_one_word(
&self,
tick: Self::Index,
lte: bool,
tick_spacing: Self::Index,
) -> Result<(Self::Index, bool), Error> {
let compressed = tick.compress(tick_spacing);
if lte {
let (word_pos, bit_pos) = compressed.position();
// all the 1s at or to the right of the current `bit_pos`
// (2 << bitPos) may overflow but fine since 2 << 255 = 0
let mask = (TWO << bit_pos) - uint!(1_U256);
let word = self.bitmap.get(&word_pos).unwrap_or(&U256::ZERO);
let masked = word & mask;
let initialized = masked != U256::ZERO;
let bit_pos = if initialized {
let msb = masked.most_significant_bit() as u8;
(bit_pos - msb) as i32
} else {
bit_pos as i32
};
let next = (compressed - Self::Index::try_from(bit_pos).unwrap()) * tick_spacing;
Ok((next, initialized))
} else {
let (word_pos, bit_pos) = compressed.position();
// all the 1s at or to the left of the `bit_pos`
let mask = U256::ZERO - (uint!(1_U256) << bit_pos);
let word = self.bitmap.get(&word_pos).unwrap_or(&U256::ZERO);
let masked = word & mask;
let initialized = masked != U256::ZERO;
let bit_pos = if initialized {
let lsb = masked.least_significant_bit() as u8;
(lsb - bit_pos) as i32
} else {
(255 - bit_pos) as i32
};
let next = (compressed + Self::Index::try_from(bit_pos).unwrap()) * tick_spacing;
Ok((next, initialized))
}
}
}
4 changes: 3 additions & 1 deletion src/utils/tick_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ impl<I: TickIndex> TickList for [Tick<I>] {
assert!(self[i] >= self[i - 1], "SORTED");
}
assert_eq!(
self.iter().fold(0, |acc, x| acc + x.liquidity_net),
self.iter().fold(0_u128, |acc, x| acc
.checked_add_signed(x.liquidity_net)
.expect("ZERO_NET")),
0,
"ZERO_NET"
);
Expand Down

0 comments on commit 20cd90e

Please sign in to comment.