Skip to content

Commit

Permalink
Merge pull request #115 from propeller-heads/tl/uniswap_v4/implement-…
Browse files Browse the repository at this point in the history
…try-from

feat: TryFromWithBlock impl for UniV4State
  • Loading branch information
zizou0x authored Dec 23, 2024
2 parents 294b786 + ca28bf5 commit a5b735b
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 41 deletions.
4 changes: 2 additions & 2 deletions src/evm/protocol/uniswap_v3/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ use num_bigint::BigUint;
use tracing::trace;
use tycho_core::{dto::ProtocolStateDelta, Bytes};

use super::{enums::FeeAmount, tycho_decoder::i24_be_bytes_to_i32};
use super::enums::FeeAmount;
use crate::{
evm::protocol::{
safe_math::{safe_add_u256, safe_sub_u256},
u256_num::u256_to_biguint,
utils::uniswap::{
liquidity_math,
i24_be_bytes_to_i32, liquidity_math,
sqrt_price_math::sqrt_price_q96_to_f64,
swap_math,
tick_list::{TickInfo, TickList, TickListErrorKind},
Expand Down
40 changes: 1 addition & 39 deletions src/evm/protocol/uniswap_v3/tycho_decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use tycho_core::Bytes;

use super::{enums::FeeAmount, state::UniswapV3State};
use crate::{
evm::protocol::utils::uniswap::tick_list::TickInfo,
evm::protocol::utils::uniswap::{i24_be_bytes_to_i32, tick_list::TickInfo},
models::Token,
protocol::{errors::InvalidSnapshotError, models::TryFromWithBlock},
};
Expand Down Expand Up @@ -124,31 +124,6 @@ impl TryFromWithBlock<ComponentWithState> for UniswapV3State {
}
}

/// Converts a slice of bytes representing a big-endian 24-bit signed integer
/// to a 32-bit signed integer.
///
/// # Arguments
/// * `val` - A reference to a `Bytes` type, which should contain at most three bytes.
///
/// # Returns
/// * The 32-bit signed integer representation of the input bytes.
pub(crate) fn i24_be_bytes_to_i32(val: &Bytes) -> i32 {
let bytes_slice = val.as_ref();
let bytes_len = bytes_slice.len();
let mut result = 0i32;

for (i, &byte) in bytes_slice.iter().enumerate() {
result |= (byte as i32) << (8 * (bytes_len - 1 - i));
}

// If the first byte (most significant byte) has its most significant bit set (0x80),
// perform sign extension for negative numbers.
if bytes_len > 0 && bytes_slice[0] & 0x80 != 0 {
result |= -1i32 << (8 * bytes_len);
}
result
}

#[cfg(test)]
mod tests {
use std::{collections::HashMap, str::FromStr};
Expand Down Expand Up @@ -299,17 +274,4 @@ mod tests {
InvalidSnapshotError::ValueError(err) if err == *"Unsupported fee amount"
));
}

#[test]
fn test_i24_be_bytes_to_i32() {
let val = Bytes::from_str("0xfeafc6").unwrap();
let converted = i24_be_bytes_to_i32(&val);
assert_eq!(converted, -86074);
let val = Bytes::from_str("0x02dd").unwrap();
let converted = i24_be_bytes_to_i32(&val);
assert_eq!(converted, 733);
let val = Bytes::from_str("0xe2bb").unwrap();
let converted = i24_be_bytes_to_i32(&val);
assert_eq!(converted, -7493);
}
}
1 change: 1 addition & 0 deletions src/evm/protocol/uniswap_v4/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod state;
mod tycho_decoder;
263 changes: 263 additions & 0 deletions src/evm/protocol/uniswap_v4/tycho_decoder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
use std::collections::HashMap;

use alloy_primitives::U256;
use tycho_client::feed::{synchronizer::ComponentWithState, Header};
use tycho_core::Bytes;

use super::state::UniswapV4State;
use crate::{
evm::protocol::{
uniswap_v4::state::UniswapV4Fees,
utils::uniswap::{i24_be_bytes_to_i32, tick_list::TickInfo},
},
models::Token,
protocol::{errors::InvalidSnapshotError, models::TryFromWithBlock},
};

impl TryFromWithBlock<ComponentWithState> for UniswapV4State {
type Error = InvalidSnapshotError;

/// Decodes a `ComponentWithState` into a `UniswapV4State`. Errors with a `InvalidSnapshotError`
/// if the snapshot is missing any required attributes.
async fn try_from_with_block(
snapshot: ComponentWithState,
_block: Header,
_all_tokens: &HashMap<Bytes, Token>,
) -> Result<Self, Self::Error> {
let liq = snapshot
.state
.attributes
.get("liquidity")
.ok_or_else(|| InvalidSnapshotError::MissingAttribute("liquidity".to_string()))?
.clone();

let liquidity = u128::from(liq);

let sqrt_price = U256::from_be_slice(
snapshot
.state
.attributes
.get("sqrt_price_x96")
.ok_or_else(|| InvalidSnapshotError::MissingAttribute("sqrt_price".to_string()))?,
);

let lp_fee = u32::from(
snapshot
.state
.attributes
.get("fee")
.ok_or_else(|| InvalidSnapshotError::MissingAttribute("fee".to_string()))?
.clone(),
);

let zero2one_protocol_fee = u32::from(
snapshot
.state
.attributes
.get("protocol_fees/zero2one")
.ok_or_else(|| {
InvalidSnapshotError::MissingAttribute("protocol_fees/zero2one".to_string())
})?
.clone(),
);
let one2zero_protocol_fee = u32::from(
snapshot
.state
.attributes
.get("protocol_fees/one2zero")
.ok_or_else(|| {
InvalidSnapshotError::MissingAttribute("protocol_fees/one2zero".to_string())
})?
.clone(),
);

let fees: UniswapV4Fees =
UniswapV4Fees::new(zero2one_protocol_fee, one2zero_protocol_fee, lp_fee);

let tick_spacing: i32 = i32::from(
snapshot
.component
.static_attributes
.get("tick_spacing")
.ok_or_else(|| InvalidSnapshotError::MissingAttribute("tick_spacing".to_string()))?
.clone(),
);

let tick = i24_be_bytes_to_i32(
snapshot
.state
.attributes
.get("tick")
.ok_or_else(|| InvalidSnapshotError::MissingAttribute("tick".to_string()))?,
);

let ticks: Result<Vec<_>, _> = snapshot
.state
.attributes
.iter()
.filter_map(|(key, value)| {
if key.starts_with("ticks/") {
Some(
key.split('/')
.nth(1)?
.parse::<i32>()
.map(|tick_index| TickInfo::new(tick_index, i128::from(value.clone())))
.map_err(|err| InvalidSnapshotError::ValueError(err.to_string())),
)
} else {
None
}
})
.collect();

let mut ticks = match ticks {
Ok(ticks) if !ticks.is_empty() => ticks
.into_iter()
.filter(|t| t.net_liquidity != 0)
.collect::<Vec<_>>(),
_ => return Err(InvalidSnapshotError::MissingAttribute("tick_liquidities".to_string())),
};

ticks.sort_by_key(|tick| tick.index);

Ok(UniswapV4State::new(liquidity, sqrt_price, fees, tick, tick_spacing, ticks))
}
}

#[cfg(test)]
mod tests {
use std::{collections::HashMap, str::FromStr};

use chrono::DateTime;
use rstest::rstest;
use tycho_core::{
dto::{Chain, ChangeType, ProtocolComponent, ResponseProtocolState},
hex_bytes::Bytes,
};

use super::*;

fn usv4_component() -> ProtocolComponent {
let creation_time = DateTime::from_timestamp(1622526000, 0)
.unwrap()
.naive_utc();

// Add a static attribute "tick_spacing"
let mut static_attributes: HashMap<String, Bytes> = HashMap::new();
static_attributes
.insert("tick_spacing".to_string(), Bytes::from(60_i32.to_be_bytes().to_vec()));

ProtocolComponent {
id: "State1".to_string(),
protocol_system: "system1".to_string(),
protocol_type_name: "typename1".to_string(),
chain: Chain::Ethereum,
tokens: Vec::new(),
contract_ids: Vec::new(),
static_attributes,
change: ChangeType::Creation,
creation_tx: Bytes::from_str("0x0000").unwrap(),
created_at: creation_time,
}
}

fn usv4_attributes() -> HashMap<String, Bytes> {
vec![
("fee".to_string(), Bytes::from(500_i32.to_be_bytes().to_vec())),
("liquidity".to_string(), Bytes::from(100_u64.to_be_bytes().to_vec())),
("tick".to_string(), Bytes::from(300_i32.to_be_bytes().to_vec())),
(
"sqrt_price_x96".to_string(),
Bytes::from(
79228162514264337593543950336_u128
.to_be_bytes()
.to_vec(),
),
),
("protocol_fees/zero2one".to_string(), Bytes::from(0_u32.to_be_bytes().to_vec())),
("protocol_fees/one2zero".to_string(), Bytes::from(0_u32.to_be_bytes().to_vec())),
("ticks/60/net_liquidity".to_string(), Bytes::from(400_i128.to_be_bytes().to_vec())),
]
.into_iter()
.collect::<HashMap<String, Bytes>>()
}
fn header() -> Header {
Header {
number: 1,
hash: Bytes::from(vec![0; 32]),
parent_hash: Bytes::from(vec![0; 32]),
revert: false,
}
}

#[tokio::test]
async fn test_usv4_try_from() {
let snapshot = ComponentWithState {
state: ResponseProtocolState {
component_id: "State1".to_owned(),
attributes: usv4_attributes(),
balances: HashMap::new(),
},
component: usv4_component(),
};

let result = UniswapV4State::try_from_with_block(snapshot, header(), &HashMap::new())
.await
.unwrap();

let fees = UniswapV4Fees::new(0, 0, 500);
let expected = UniswapV4State::new(
100,
U256::from(79228162514264337593543950336_u128),
fees,
300,
60,
vec![TickInfo::new(60, 400)],
);
assert_eq!(result, expected);
}

#[tokio::test]
#[rstest]
#[case::missing_liquidity("liquidity")]
#[case::missing_sqrt_price("sqrt_price")]
#[case::missing_tick("tick")]
#[case::missing_tick_liquidity("tick_liquidities")]
#[case::missing_fee("fee")]
#[case::missing_fee("protocol_fees/one2zero")]
#[case::missing_fee("protocol_fees/zero2one")]
async fn test_usv4_try_from_invalid(#[case] missing_attribute: String) {
// remove missing attribute
let mut attributes = usv4_attributes();
attributes.remove(&missing_attribute);

if missing_attribute == "tick_liquidities" {
attributes.remove("ticks/60/net_liquidity");
}

if missing_attribute == "sqrt_price" {
attributes.remove("sqrt_price_x96");
}

if missing_attribute == "fee" {
attributes.remove("fee");
}

let snapshot = ComponentWithState {
state: ResponseProtocolState {
component_id: "State1".to_owned(),
attributes,
balances: HashMap::new(),
},
component: usv4_component(),
};

let result = UniswapV4State::try_from_with_block(snapshot, header(), &HashMap::new()).await;

assert!(result.is_err());
assert!(matches!(
result.err().unwrap(),
InvalidSnapshotError::MissingAttribute(attr) if attr == missing_attribute
));
}
}
48 changes: 48 additions & 0 deletions src/evm/protocol/utils/uniswap/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use alloy_primitives::{I256, U256};
use tycho_core::Bytes;

pub(crate) mod liquidity_math;
mod solidity_math;
Expand Down Expand Up @@ -35,3 +36,50 @@ pub(crate) struct SwapResults {
pub(crate) tick: i32,
pub(crate) gas_used: U256,
}

/// Converts a slice of bytes representing a big-endian 24-bit signed integer
/// to a 32-bit signed integer.
///
/// # Arguments
/// * `val` - A reference to a `Bytes` type, which should contain at most three bytes.
///
/// # Returns
/// * The 32-bit signed integer representation of the input bytes.
pub(crate) fn i24_be_bytes_to_i32(val: &Bytes) -> i32 {
let bytes_slice = val.as_ref();
let bytes_len = bytes_slice.len();
let mut result = 0i32;

for (i, &byte) in bytes_slice.iter().enumerate() {
result |= (byte as i32) << (8 * (bytes_len - 1 - i));
}

// If the first byte (most significant byte) has its most significant bit set (0x80),
// perform sign extension for negative numbers.
if bytes_len > 0 && bytes_slice[0] & 0x80 != 0 {
result |= -1i32 << (8 * bytes_len);
}
result
}

#[cfg(test)]
mod test {
use std::str::FromStr;

use tycho_core::Bytes;

use crate::evm::protocol::utils::uniswap::i24_be_bytes_to_i32;

#[test]
fn test_i24_be_bytes_to_i32() {
let val = Bytes::from_str("0xfeafc6").unwrap();
let converted = i24_be_bytes_to_i32(&val);
assert_eq!(converted, -86074);
let val = Bytes::from_str("0x02dd").unwrap();
let converted = i24_be_bytes_to_i32(&val);
assert_eq!(converted, 733);
let val = Bytes::from_str("0xe2bb").unwrap();
let converted = i24_be_bytes_to_i32(&val);
assert_eq!(converted, -7493);
}
}

0 comments on commit a5b735b

Please sign in to comment.