From 9899f1ede3a0c0ff8d5100c7b57456f34e230ecf Mon Sep 17 00:00:00 2001 From: henopied Date: Mon, 20 Jan 2025 00:15:45 +0000 Subject: [PATCH] refactor prices+fees - All fees (except balancer) are specified to `1e6` (Uniswap v3 standard) as base - Fees are now included in costs - Removed fixed point calculations and replaced with f64 directly - Balancer fee must be u64 (bug fix) - Consistent error type for unsupported tokens - Uniswap v3 had the spot pricing backwards --- Cargo.toml | 13 +- benches/uniswap_v2.rs | 2 +- .../GetBalancerPoolDataBatchRequest.sol | 4 +- examples/filters.rs | 2 +- examples/state_space_builder.rs | 5 +- examples/swap_calldata.rs | 2 +- examples/sync_macro.rs | 2 +- src/amms/amm.rs | 4 +- src/amms/balancer/mod.rs | 113 ++--- src/amms/consts.rs | 20 +- src/amms/erc_4626/mod.rs | 187 +++++-- src/amms/error.rs | 4 +- src/amms/factory.rs | 6 +- src/amms/float.rs | 59 ++- src/amms/mod.rs | 33 ++ src/amms/uniswap_v2/mod.rs | 276 +++-------- src/amms/uniswap_v3/mod.rs | 468 ++++++------------ 17 files changed, 514 insertions(+), 686 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index af1d72d1..b478ee1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,6 @@ alloy = { version = "0.9", features = [ alloy-json-rpc = { version = "0.5" } # tracing -eyre = "0.6" tracing = "0.1" # async @@ -47,7 +46,6 @@ lazy_static = "1.5" num-bigfloat = "1.7" regex = "1.10" thiserror = "1.0" -rug = "1.24.1" derive_more = "1.0.0" governor = "0.7.0" itertools = "0.13.0" @@ -57,6 +55,7 @@ async-stream = "0.3.6" [dev-dependencies] +float-cmp = "0.10.0" rand = "0.8.5" tracing-subscriber = "0.3" criterion = "0.5" @@ -64,9 +63,7 @@ tokio = { version = "1.42", default-features = false, features = [ "rt-multi-thread", ] } alloy = { version = "0.9", features = ["rpc-client"] } - -[build-dependencies] - +eyre = "0.6" [profile.release] opt-level = 3 @@ -74,12 +71,6 @@ lto = true codegen-units = 1 panic = "abort" -[profile.dev] -opt-level = 3 -lto = true -codegen-units = 1 -debug = "full" - [[bench]] name = "uniswap_v2" diff --git a/benches/uniswap_v2.rs b/benches/uniswap_v2.rs index b4e21e4a..a914ddc3 100644 --- a/benches/uniswap_v2.rs +++ b/benches/uniswap_v2.rs @@ -14,7 +14,7 @@ fn simulate_swap(c: &mut Criterion) { token_b_decimals: 18, reserve_0: 20_000_000_u128, reserve_1: 20_000_000_u128, - fee: 300, + fee: 3000, ..Default::default() }; diff --git a/contracts/src/Balancer/GetBalancerPoolDataBatchRequest.sol b/contracts/src/Balancer/GetBalancerPoolDataBatchRequest.sol index 6df7b511..4f0d2982 100644 --- a/contracts/src/Balancer/GetBalancerPoolDataBatchRequest.sol +++ b/contracts/src/Balancer/GetBalancerPoolDataBatchRequest.sol @@ -22,7 +22,7 @@ contract GetBalancerPoolDataBatchRequest { uint8[] decimals; uint256[] liquidity; uint256[] weights; - uint32 fee; + uint64 fee; } constructor(address[] memory pools) { @@ -62,7 +62,7 @@ contract GetBalancerPoolDataBatchRequest { } // Grab the swap fee - poolData.fee = uint32(IBPool(poolAddress).getSwapFee()); + poolData.fee = uint64(IBPool(poolAddress).getSwapFee()); poolData.tokens = tokens; poolData.decimals = decimals; poolData.liquidity = liquidity; diff --git a/examples/filters.rs b/examples/filters.rs index ccf5ec26..469250d1 100644 --- a/examples/filters.rs +++ b/examples/filters.rs @@ -28,7 +28,7 @@ async fn main() -> eyre::Result<()> { // UniswapV2 UniswapV2Factory::new( address!("5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"), - 300, + 3000, 10000835, ) .into(), diff --git a/examples/state_space_builder.rs b/examples/state_space_builder.rs index 9931b5cc..a94a4dfc 100644 --- a/examples/state_space_builder.rs +++ b/examples/state_space_builder.rs @@ -38,7 +38,7 @@ async fn main() -> eyre::Result<()> { // UniswapV2 UniswapV2Factory::new( address!("5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"), - 300, + 3000, 10000835, ) .into(), @@ -62,7 +62,7 @@ async fn main() -> eyre::Result<()> { need to track a handful of specific pools. */ let amms = vec![ - UniswapV2Pool::new(address!("B4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"), 300).into(), + UniswapV2Pool::new(address!("B4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"), 3000).into(), UniswapV3Pool::new(address!("88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")).into(), ]; @@ -81,7 +81,6 @@ async fn main() -> eyre::Result<()> { let amms = vec![ERC4626Vault::new(address!("163538E22F4d38c1eb21B79939f3d2ee274198Ff")).into()]; let _state_space_manager = StateSpaceBuilder::new(provider.clone()) - .with_factories(factories) .with_amms(amms) .sync() .await?; diff --git a/examples/swap_calldata.rs b/examples/swap_calldata.rs index 956744cf..990c19b0 100644 --- a/examples/swap_calldata.rs +++ b/examples/swap_calldata.rs @@ -18,7 +18,7 @@ async fn main() -> eyre::Result<()> { let provider = Arc::new(ProviderBuilder::new().on_client(client)); - let pool = UniswapV2Pool::new(address!("B4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"), 300) + let pool = UniswapV2Pool::new(address!("B4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"), 3000) .init(BlockId::latest(), provider) .await?; diff --git a/examples/sync_macro.rs b/examples/sync_macro.rs index fdaa4f2f..db513cf5 100644 --- a/examples/sync_macro.rs +++ b/examples/sync_macro.rs @@ -33,7 +33,7 @@ async fn main() -> eyre::Result<()> { // UniswapV2 UniswapV2Factory::new( address!("5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"), - 300, + 3000, 10000835, ) .into(), diff --git a/src/amms/amm.rs b/src/amms/amm.rs index bc448f91..3c279e2d 100644 --- a/src/amms/amm.rs +++ b/src/amms/amm.rs @@ -10,7 +10,6 @@ use alloy::{ rpc::types::Log, transports::Transport, }; -use eyre::Result; use serde::{Deserialize, Serialize}; use std::{ hash::{Hash, Hasher}, @@ -31,7 +30,8 @@ pub trait AutomatedMarketMaker { /// Returns a list of token addresses used in the AMM fn tokens(&self) -> Vec
; - /// Calculates the price of `base_token` in terms of `quote_token` + /// Calculates the price of `base_token` in terms of `quote_token` with fee included + /// The result should be precise down to a few bits fn calculate_price(&self, base_token: Address, quote_token: Address) -> Result; /// Simulate a swap diff --git a/src/amms/balancer/mod.rs b/src/amms/balancer/mod.rs index 77f82387..f677cdb2 100644 --- a/src/amms/balancer/mod.rs +++ b/src/amms/balancer/mod.rs @@ -15,17 +15,16 @@ use alloy::{ use async_trait::async_trait; use futures::{stream::FuturesUnordered, StreamExt}; use itertools::Itertools; -use rug::{float::Round, Float}; use serde::{Deserialize, Serialize}; use thiserror::Error; use tracing::info; use super::{ amm::{AutomatedMarketMaker, AMM}, - consts::{BONE, MPFR_T_PRECISION}, + consts::{F64_BONE, U64_BONE}, error::AMMError, factory::{AutomatedMarketMakerFactory, DiscoverySync}, - float::u256_to_float, + float::u256_to_f64, Token, }; @@ -75,6 +74,7 @@ sol!( pub enum BalancerError { #[error("Error initializing Balancer Pool")] InitializationError, + // TODO: remove? #[error("Token in does not exist")] TokenInDoesNotExist, #[error("Token out does not exist")] @@ -99,7 +99,7 @@ pub struct BalancerPool { // TODO: state: HashMap, /// The Swap Fee on the Pool. - fee: u32, + fee: u64, } #[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] @@ -166,41 +166,29 @@ impl AutomatedMarketMaker for BalancerPool { let token_in = self .state .get(&base_token) - .ok_or(BalancerError::TokenInDoesNotExist)?; + .ok_or(AMMError::IncompatibleToken)?; let token_out = self .state .get("e_token) - .ok_or(BalancerError::TokenOutDoesNotExist)?; + .ok_or(AMMError::IncompatibleToken)?; - let bone = u256_to_float(BONE)?; - let norm_base = if token_in.token.decimals < 18 { - Float::with_val( - MPFR_T_PRECISION, - 10_u64.pow(18 - token_in.token.decimals as u32), - ) - } else { - Float::with_val(MPFR_T_PRECISION, 1) - }; - let norm_quote = if token_out.token.decimals < 18 { - Float::with_val( - MPFR_T_PRECISION, - 10_u64.pow(18 - token_out.token.decimals as u32), - ) - } else { - Float::with_val(MPFR_T_PRECISION, 1) - }; + if token_out.liquidity == U256::ZERO { + return Err(BalancerError::DivZero.into()); + } - let norm_weight_base = u256_to_float(token_in.weight)? / norm_base; - let norm_weight_quote = u256_to_float(token_out.weight)? / norm_quote; - let balance_base = u256_to_float(token_in.liquidity)?; - let balance_quote = u256_to_float(token_out.liquidity)?; + // Should be exact through 10**24 + let inv_norm_base = 10f64.powi(token_in.token.decimals as i32); + let inv_norm_quote = 10f64.powi(token_out.token.decimals as i32); - let dividend = (balance_quote / norm_weight_quote) * bone.clone(); - let divisor = (balance_base / norm_weight_base) - * (bone - Float::with_val(MPFR_T_PRECISION, self.fee)); - let ratio = dividend / divisor; - Ok(ratio.to_f64_round(Round::Nearest)) + let norm_weight_base = u256_to_f64(token_in.weight) * inv_norm_base; + let norm_weight_quote = u256_to_f64(token_out.weight) * inv_norm_quote; + let balance_base = u256_to_f64(token_in.liquidity); + let balance_quote = u256_to_f64(token_out.liquidity); + + let numerator = balance_quote * norm_weight_base * F64_BONE; + let denominator = balance_base * norm_weight_quote * (U64_BONE - self.fee) as f64; + Ok(numerator / denominator) } /// Locally simulates a swap in the AMM. @@ -216,12 +204,12 @@ impl AutomatedMarketMaker for BalancerPool { let token_in = self .state .get(&base_token) - .ok_or(BalancerError::TokenInDoesNotExist)?; + .ok_or(AMMError::IncompatibleToken)?; let token_out = self .state .get("e_token) - .ok_or(BalancerError::TokenOutDoesNotExist)?; + .ok_or(AMMError::IncompatibleToken)?; Ok(bmath::calculate_out_given_in( token_in.liquidity, @@ -247,12 +235,12 @@ impl AutomatedMarketMaker for BalancerPool { let token_in = self .state .get(&base_token) - .ok_or(BalancerError::TokenInDoesNotExist)?; + .ok_or(AMMError::IncompatibleToken)?; let token_out = self .state .get("e_token) - .ok_or(BalancerError::TokenOutDoesNotExist)?; + .ok_or(AMMError::IncompatibleToken)?; let out = bmath::calculate_out_given_in( token_in.liquidity, @@ -284,7 +272,7 @@ impl AutomatedMarketMaker for BalancerPool { let res = deployer.block(block_number).call_raw().await?; let mut data = - , Vec, Vec, Vec, u32)> as SolValue>::abi_decode( + , Vec, Vec, Vec, u64)> as SolValue>::abi_decode( &res, false, )?; let (tokens, decimals, liquidity, weights, fee) = if !data.is_empty() { @@ -483,11 +471,11 @@ impl BalancerFactory { futures_unordered.push(async move { let res = deployer.call_raw().block(block_number).await?; - let return_data = , Vec, Vec, Vec, u32)> as SolValue>::abi_decode( + let return_data = , Vec, Vec, Vec, u64)> as SolValue>::abi_decode( &res, false, )?; - Ok::<(Vec
, Vec<(Vec
, Vec, Vec, Vec, u32)>), AMMError>(( + Ok::<(Vec
, Vec<(Vec
, Vec, Vec, Vec, u64)>), AMMError>(( group, return_data, )) @@ -552,25 +540,25 @@ impl BalancerFactory { #[cfg(test)] mod tests { - use std::{collections::HashMap, sync::Arc}; + use std::collections::HashMap; - use alloy::{ - primitives::{address, Address, U256}, - providers::ProviderBuilder, - }; + use alloy::primitives::{address, Address, U256}; use eyre::Ok; + use float_cmp::assert_approx_eq; - use crate::amms::{ - amm::AutomatedMarketMaker, - balancer::{BalancerPool, IBPool::IBPoolInstance}, - }; use crate::amms::{balancer::TokenPoolState, Token}; + use crate::{ + amms::{ + amm::AutomatedMarketMaker, + balancer::{BalancerPool, IBPool::IBPoolInstance}, + error::AMMError, + }, + test_provider, + }; #[tokio::test] pub async fn test_populate_data() -> eyre::Result<()> { - let provider = Arc::new( - ProviderBuilder::new().on_http(std::env::var("ETHEREUM_PROVIDER")?.parse().unwrap()), - ); + let provider = test_provider!()?; let balancer_pool = BalancerPool::new(address!("8a649274E4d777FFC6851F13d23A86BBFA2f2Fbf")) .init(20487793.into(), provider.clone()) @@ -581,7 +569,7 @@ mod tests { ( address!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"), TokenPoolState { - liquidity: U256::from(1234567890000000000_u128), + liquidity: U256::from(9244284612208827034_u128), weight: U256::from(25000000000000000000_u128), token: Token::new_with_decimals( address!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"), @@ -592,7 +580,7 @@ mod tests { ( address!("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), TokenPoolState { - liquidity: U256::from(987654321000000_u128), + liquidity: U256::from(24609707945_u128), weight: U256::from(25000000000000000000_u128), token: Token::new_with_decimals( address!("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), @@ -611,7 +599,7 @@ mod tests { ); // Validate the fee - let expected_fee = 640942080; + let expected_fee = 8000000000000000; assert_eq!( balancer_pool.fee, expected_fee, "Fee does not match expected value" @@ -622,9 +610,7 @@ mod tests { #[tokio::test] pub async fn test_calculate_price() -> eyre::Result<()> { - let provider = Arc::new( - ProviderBuilder::new().on_http(std::env::var("ETHEREUM_PROVIDER")?.parse().unwrap()), - ); + let provider = test_provider!()?; let balancer_pool = BalancerPool::new(address!("8a649274E4d777FFC6851F13d23A86BBFA2f2Fbf")) .init(20487793.into(), provider.clone()) @@ -637,15 +623,20 @@ mod tests { ) .unwrap(); - assert_eq!(calculated, 2662.153859723404_f64); + assert_approx_eq!(f64, calculated, 2683.622840743061482241114050, ulps = 4); + + let incompatible = balancer_pool.calculate_price( + address!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"), + Address::default(), + ); + assert!(matches!(incompatible, Err(AMMError::IncompatibleToken))); + Ok(()) } #[tokio::test] pub async fn test_simulate_swap() -> eyre::Result<()> { - let provider = Arc::new( - ProviderBuilder::new().on_http(std::env::var("ETHEREUM_PROVIDER")?.parse().unwrap()), - ); + let provider = test_provider!()?; let balancer_pool = BalancerPool::new(address!("8a649274E4d777FFC6851F13d23A86BBFA2f2Fbf")) .init(20487793.into(), provider.clone()) diff --git a/src/amms/consts.rs b/src/amms/consts.rs index 95ec4548..41bc068f 100644 --- a/src/amms/consts.rs +++ b/src/amms/consts.rs @@ -20,6 +20,10 @@ pub const U256_4: U256 = U256::from_limbs([4, 0, 0, 0]); pub const U256_2: U256 = U256::from_limbs([2, 0, 0, 0]); pub const U256_1: U256 = U256::from_limbs([1, 0, 0, 0]); +pub const U256_FEE_ONE: U256 = U256::from_limbs([1_000_000, 0, 0, 0]); +pub const U32_FEE_ONE: u32 = 1_000_000; +pub const F64_FEE_ONE: f64 = 1e6; + // Uniswap V3 specific pub const POPULATE_TICK_DATA_STEP: u64 = 100000; pub const Q128: U256 = U256::from_limbs([0, 0, 1, 0]); @@ -27,6 +31,8 @@ pub const Q224: U256 = U256::from_limbs([0, 0, 0, 4294967296]); // Balancer V2 specific pub const BONE: U256 = U256::from_limbs([0xDE0B6B3A7640000, 0, 0, 0]); +pub const F64_BONE: f64 = 1e18; +pub const U64_BONE: u64 = 0xDE0B6B3A7640000; // Others pub const U128_0X10000000000000000: u128 = 18446744073709551616; @@ -38,6 +44,16 @@ pub const U256_0XFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF: U256 = U256:: ]); pub const U256_0XFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF: U256 = U256::from_limbs([18446744073709551615, 18446744073709551615, 0, 0]); +pub const U256_0X1FFFFFFFFFFFFF: U256 = + U256::from_limbs([9007199254740991, 0, 0, 0]); // 2^53 - 1 +pub const U256_0X3FFFFFFFFFFFFF: U256 = + U256::from_limbs([18014398509481983, 0, 0, 0]); // 2^54 - 1 -pub const DECIMAL_RADIX: i32 = 10; -pub const MPFR_T_PRECISION: u32 = 70; +pub const MANTISSA_BITS_F64: i32 = 53; +pub const F64_MAX_SAFE_INTEGER: f64 = 9007199254740991.0; // 2^53 - 1 +pub const F64_2P53: f64 = 9007199254740992.0; // 2^53 +pub const F64_2P54: f64 = 18014398509481984.0; // 2^54 +pub const F64_2P64: f64 = 18446744073709551616.0; // 2^64 +pub const F64_2P96: f64 = 79228162514264337593543950336.0; // 2^96 +pub const F64_2P128: f64 = 340282366920938463463374607431768211456.0; // 2^128 +pub const F64_2P192: f64 = 6277101735386680763835789423207666416102355444464034512896.0; // 2^192 diff --git a/src/amms/erc_4626/mod.rs b/src/amms/erc_4626/mod.rs index 674ada4a..75f4e388 100644 --- a/src/amms/erc_4626/mod.rs +++ b/src/amms/erc_4626/mod.rs @@ -1,8 +1,9 @@ use super::{ amm::AutomatedMarketMaker, - consts::{U128_0X10000000000000000, U256_10000, U256_2}, + consts::{F64_FEE_ONE, U256_2, U256_FEE_ONE, U32_FEE_ONE}, error::AMMError, - uniswap_v2::{div_uu, q64_to_float}, + float::u256_to_f64, + Token, }; use alloy::{ eips::BlockId, @@ -15,7 +16,7 @@ use alloy::{ transports::Transport, }; use serde::{Deserialize, Serialize}; -use std::{cmp::Ordering, sync::Arc}; +use std::sync::Arc; use thiserror::Error; use tracing::info; @@ -49,11 +50,9 @@ pub enum ERC4626VaultError { #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct ERC4626Vault { /// Token received from depositing, i.e. shares token - pub vault_token: Address, - pub vault_token_decimals: u8, + pub vault_token: Token, /// Token received from withdrawing, i.e. underlying token - pub asset_token: Address, - pub asset_token_decimals: u8, + pub asset_token: Token, /// Total supply of vault tokens pub vault_reserve: U256, /// Total balance of asset tokens held by vault @@ -66,7 +65,7 @@ pub struct ERC4626Vault { impl AutomatedMarketMaker for ERC4626Vault { fn address(&self) -> Address { - self.vault_token + self.vault_token.address } fn sync_events(&self) -> Vec { @@ -116,11 +115,36 @@ impl AutomatedMarketMaker for ERC4626Vault { } fn tokens(&self) -> Vec
{ - vec![self.vault_token, self.asset_token] + vec![self.vault_token.address, self.asset_token.address] } fn calculate_price(&self, base_token: Address, _quote_token: Address) -> Result { - q64_to_float(self.calculate_price_64_x_64(base_token)?) + // TODO: this is the same behavior as before, but I'm not sure it's correct + if base_token == self.vault_token { + if self.vault_reserve == U256::ZERO { + return Ok(1.0); + } + } else { + if self.asset_reserve == U256::ZERO { + return Ok(1.0); + } + } + + // Decimals are intentionally swapped as we are multiplying rather than dividing + let (r_a, r_v) = ( + u256_to_f64(self.asset_reserve) * (10f64).powi(self.vault_token.decimals as i32), + u256_to_f64(self.vault_reserve) * (10f64).powi(self.asset_token.decimals as i32), + ); + let (reserve_in, reserve_out, fee) = if base_token == self.asset_token { + Ok((r_a, r_v, self.deposit_fee)) + } else if base_token == self.vault_token { + Ok((r_v, r_a, self.withdraw_fee)) + } else { + Err(AMMError::IncompatibleToken) + }?; + let numerator = reserve_out * F64_FEE_ONE; + let denominator = reserve_in * (U32_FEE_ONE - fee) as f64; + Ok(numerator / denominator) } fn simulate_swap( @@ -172,8 +196,10 @@ impl AutomatedMarketMaker for ERC4626Vault { N: Network, P: Provider, { - let deployer = - IGetERC4626VaultDataBatchRequest::deploy_builder(provider, vec![self.vault_token]); + let deployer = IGetERC4626VaultDataBatchRequest::deploy_builder( + provider, + vec![self.vault_token.address], + ); let res = deployer.call_raw().block(block_number).await?; let data = populate the vault - self.vault_token = vault_token; - self.vault_token_decimals = vault_token_dec as u8; - self.asset_token = asset_token; - self.asset_token_decimals = asset_token_dec as u8; + self.vault_token = Token::new_with_decimals(vault_token, vault_token_dec as u8); + self.asset_token = Token::new_with_decimals(asset_token, asset_token_dec as u8); self.vault_reserve = vault_reserve; self.asset_reserve = asset_reserve; @@ -251,7 +275,7 @@ impl ERC4626Vault { // Returns a new, unsynced ERC4626 vault pub fn new(address: Address) -> Self { Self { - vault_token: address, + vault_token: address.into(), ..Default::default() } } @@ -276,44 +300,16 @@ impl ERC4626Vault { self.deposit_fee }; - if reserve_in.is_zero() || 10000 - fee == 0 { + if reserve_in.is_zero() || U32_FEE_ONE - fee == 0 { return Err(ERC4626VaultError::DivisionByZero.into()); } - Ok(amount_in * reserve_out / reserve_in * U256::from(10000 - fee) / U256_10000) - } - - // TODO: Right now this will return a uv2 error, fix this - pub fn calculate_price_64_x_64(&self, base_token: Address) -> Result { - let decimal_shift = self.vault_token_decimals as i8 - self.asset_token_decimals as i8; - - // Normalize reserves by decimal shift - let (r_v, r_a) = match decimal_shift.cmp(&0) { - Ordering::Less => ( - self.vault_reserve * U256::from(10u128.pow(decimal_shift.unsigned_abs() as u32)), - self.asset_reserve, - ), - _ => ( - self.vault_reserve, - self.asset_reserve * U256::from(10u128.pow(decimal_shift as u32)), - ), - }; - - // Withdraw - if base_token == self.vault_token { - if r_v.is_zero() { - // Return 1 in Q64 - Ok(U128_0X10000000000000000) - } else { - Ok(div_uu(r_a, r_v)?) - } - // Deposit - } else if r_a.is_zero() { - // Return 1 in Q64 - Ok(U128_0X10000000000000000) - } else { - Ok(div_uu(r_v, r_a)?) - } + // TODO: support virtual offset? + // TODO: guessing this new fee calculation is more accurate but not sure + let fee_num = U32_FEE_ONE - fee; + let numerator = amount_in * reserve_out * U256::from(fee_num); + let denominator = reserve_in * U256_FEE_ONE; + Ok(numerator / denominator) } pub async fn get_reserves( @@ -326,7 +322,7 @@ impl ERC4626Vault { N: Network, P: Provider + Clone, { - let vault = IERC4626Vault::new(self.vault_token, provider); + let vault = IERC4626Vault::new(self.vault_token.address, provider); let total_assets = vault.totalAssets().block(block_number).call().await?._0; @@ -335,3 +331,84 @@ impl ERC4626Vault { Ok((total_supply, total_assets)) } } + +#[cfg(test)] +mod tests { + use alloy::primitives::{address, Address, U256}; + use float_cmp::assert_approx_eq; + + use crate::amms::{amm::AutomatedMarketMaker, Token}; + + use super::ERC4626Vault; + + fn get_test_vault(vault_reserve: u128, asset_reserve: u128) -> ERC4626Vault { + ERC4626Vault { + vault_token: Token { + address: address!("163538E22F4d38c1eb21B79939f3d2ee274198Ff"), + decimals: 18, + }, + asset_token: Token { + address: address!("6B175474E89094C44Da98b954EedeAC495271d0F"), + decimals: 6, + }, + vault_reserve: U256::from(vault_reserve), + asset_reserve: U256::from(asset_reserve), + // ficticious fees + deposit_fee: 1000, + withdraw_fee: 5000, + } + } + + #[test] + fn test_calculate_price_varying_decimals() { + let vault = get_test_vault(501910315708981197269904, 505434849031); + + let price_v_for_a = vault + .calculate_price(vault.vault_token.address, Address::default()) + .unwrap(); + let price_a_for_v = vault + .calculate_price(vault.asset_token.address, Address::default()) + .unwrap(); + + assert_approx_eq!(f64, price_v_for_a, 1.012082650516304962229139433, ulps = 4); + assert_approx_eq!(f64, price_a_for_v, 0.9940207514393293696121269615, ulps = 4); + } + + #[test] + fn test_calculate_price_zero_reserve() { + let vault = get_test_vault(0, 0); + + let price_v_for_a = vault + .calculate_price(vault.vault_token.address, Address::default()) + .unwrap(); + let price_a_for_v = vault + .calculate_price(vault.asset_token.address, Address::default()) + .unwrap(); + + assert_eq!(price_v_for_a, 1.0); + assert_eq!(price_a_for_v, 1.0); + } + + #[test] + fn test_simulate_swap() { + let vault = get_test_vault(501910315708981197269904, 505434849031054568651911); + + let assets_out = vault + .simulate_swap( + vault.vault_token.address, + vault.asset_token.address, + U256::from(3000000000000000000_u128), + ) + .unwrap(); + let shares_out = vault + .simulate_swap( + vault.asset_token.address, + vault.vault_token.address, + U256::from(3000000000000000000_u128), + ) + .unwrap(); + + assert_eq!(assets_out, U256::from(3005961378232538995_u128)); + assert_eq!(shares_out, U256::from(2976101111871285139_u128)); + } +} diff --git a/src/amms/error.rs b/src/amms/error.rs index 7c32e067..a7d462dd 100644 --- a/src/amms/error.rs +++ b/src/amms/error.rs @@ -24,10 +24,10 @@ pub enum AMMError { BalancerError(#[from] BalancerError), #[error(transparent)] ERC4626VaultError(#[from] ERC4626VaultError), - #[error(transparent)] - ParseFloatError(#[from] rug::float::ParseFloatError), #[error("Unrecognized Event Signature {0}")] UnrecognizedEventSignature(FixedBytes<32>), #[error(transparent)] JoinError(#[from] tokio::task::JoinError), + #[error("Specified tokens are incompatible")] + IncompatibleToken, } diff --git a/src/amms/factory.rs b/src/amms/factory.rs index 26f6fbaa..9c2405bc 100644 --- a/src/amms/factory.rs +++ b/src/amms/factory.rs @@ -1,8 +1,9 @@ -use super::{amm::Variant, uniswap_v2::UniswapV2Factory, uniswap_v3::UniswapV3Factory}; use super::{ - amm::{AutomatedMarketMaker, AMM}, + amm::{AutomatedMarketMaker, Variant, AMM}, balancer::BalancerFactory, error::AMMError, + uniswap_v2::UniswapV2Factory, + uniswap_v3::UniswapV3Factory, }; use alloy::{ eips::BlockId, @@ -12,7 +13,6 @@ use alloy::{ rpc::types::eth::Log, transports::Transport, }; -use eyre::Result; use serde::{Deserialize, Serialize}; use std::{ future::Future, diff --git a/src/amms/float.rs b/src/amms/float.rs index 0637aa16..4dfa388e 100644 --- a/src/amms/float.rs +++ b/src/amms/float.rs @@ -1,25 +1,48 @@ use alloy::primitives::U256; -use rug::Float; -use super::{ - consts::{MPFR_T_PRECISION, U128_0X10000000000000000}, - error::AMMError, -}; +use super::consts::{F64_2P128, F64_2P192, F64_2P64}; -pub fn q64_to_float(num: u128) -> Result { - let float_num = u128_to_float(num)?; - let divisor = u128_to_float(U128_0X10000000000000000)?; - Ok((float_num / divisor).to_f64()) +/// Converts an alloy U256 to f64 with nearest rounding +pub fn u256_to_f64(num: U256) -> f64 { + let [l0, l1, l2, l3] = num.into_limbs(); + let (l0f, l1f, l2f, l3f) = (l0 as f64, l1 as f64, l2 as f64, l3 as f64); + return l0f + l1f * F64_2P64 + l2f * F64_2P128 + l3f * F64_2P192; } -pub fn u128_to_float(num: u128) -> Result { - let value_string = num.to_string(); - let parsed_value = Float::parse_radix(value_string, 10)?; - Ok(Float::with_val(MPFR_T_PRECISION, parsed_value)) -} +#[cfg(test)] +mod test { + use alloy::primitives::U256; + + use crate::amms::{consts::{ + F64_2P54, F64_MAX_SAFE_INTEGER, MANTISSA_BITS_F64, U256_0X10000, U256_0X1FFFFFFFFFFFFF, + U256_0X3FFFFFFFFFFFFF, U256_1, + }, float::u256_to_f64}; + + #[test] + fn test_u256_to_f64_simple() { + assert_eq!(u256_to_f64(U256::ZERO), 0.0); + assert_eq!(u256_to_f64(U256_1), 1.0); + assert_eq!(u256_to_f64(U256_0X10000), 65536.0); + } + + // Make sure that all bits in the input are not lost in the output + #[test] + fn test_u256_to_f64_all_bits() { + for i in 0..256 - MANTISSA_BITS_F64 { + let actual = u256_to_f64(U256_0X1FFFFFFFFFFFFF << i); + let expected = F64_MAX_SAFE_INTEGER * (2.0_f64.powi(i)); + assert_eq!(actual, expected, "incorrect bits produced at shift {}", i); + } + } -pub fn u256_to_float(num: U256) -> Result { - let value_string = num.to_string(); - let parsed_value = Float::parse_radix(value_string, 10)?; - Ok(Float::with_val(MPFR_T_PRECISION, parsed_value)) + // Ensures the correct rounding behavior on all positions (should round up) + #[test] + fn test_u256_to_f64_rounding() { + for i in 0..256 - (MANTISSA_BITS_F64 + 1) { + // Should round 2^54 - 1 up to 2^54 due to 53 bit mantissa + let actual = u256_to_f64(U256_0X3FFFFFFFFFFFFF << i); + let expected = F64_2P54 * (2.0_f64.powi(i)); + assert_eq!(actual, expected, "incorrect rounding at shift {}", i); + } + } } diff --git a/src/amms/mod.rs b/src/amms/mod.rs index 41e1b917..ce77fe75 100644 --- a/src/amms/mod.rs +++ b/src/amms/mod.rs @@ -76,6 +76,18 @@ impl From
for Token { } } +impl PartialEq
for Token { + fn eq(&self, other: &Address) -> bool { + self.address() == *other + } +} + +impl PartialEq for Address { + fn eq(&self, other: &Token) -> bool { + other == self + } +} + impl Hash for Token { fn hash(&self, state: &mut H) { self.address.hash(state); @@ -133,3 +145,24 @@ where } token_decimals } + +#[cfg(test)] +mod test { + #[macro_export] + macro_rules! test_provider { + () => {{ + let rpc_endpoint = ::std::env::var("ETHEREUM_PROVIDER")?; + + let client = ::alloy::rpc::client::ClientBuilder::default() + .layer(::alloy_throttle::ThrottleLayer::new(250, None)?) + .layer(::alloy::transports::layers::RetryBackoffLayer::new( + 5, 200, 330, + )) + .http(rpc_endpoint.parse()?); + + ::eyre::Ok(::std::sync::Arc::new( + ::alloy::providers::ProviderBuilder::new().on_client(client), + )) + }}; + } +} diff --git a/src/amms/uniswap_v2/mod.rs b/src/amms/uniswap_v2/mod.rs index 58d59364..9d2e5d93 100644 --- a/src/amms/uniswap_v2/mod.rs +++ b/src/amms/uniswap_v2/mod.rs @@ -1,11 +1,6 @@ use super::{ amm::{AutomatedMarketMaker, AMM}, - consts::{ - MPFR_T_PRECISION, U128_0X10000000000000000, U256_0X100, U256_0X10000, U256_0X100000000, - U256_0XFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, - U256_0XFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, U256_1, U256_1000, U256_128, - U256_16, U256_191, U256_192, U256_2, U256_255, U256_32, U256_4, U256_64, U256_8, - }, + consts::{F64_FEE_ONE, U256_FEE_ONE, U32_FEE_ONE}, error::AMMError, factory::{AutomatedMarketMakerFactory, DiscoverySync}, Token, @@ -23,7 +18,6 @@ use alloy::{ }; use futures::{stream::FuturesUnordered, StreamExt}; use itertools::Itertools; -use rug::Float; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, future::Future, hash::Hash, sync::Arc}; use thiserror::Error; @@ -68,12 +62,7 @@ sol!( ); #[derive(Error, Debug)] -pub enum UniswapV2Error { - #[error("Division by zero")] - DivisionByZero, - #[error("Rounding Error")] - RoundingError, -} +pub enum UniswapV2Error {} #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct UniswapV2Pool { @@ -82,7 +71,7 @@ pub struct UniswapV2Pool { pub token_b: Token, pub reserve_0: u128, pub reserve_1: u128, - pub fee: usize, + pub fee: u32, } impl AutomatedMarketMaker for UniswapV2Pool { @@ -170,8 +159,21 @@ impl AutomatedMarketMaker for UniswapV2Pool { } fn calculate_price(&self, base_token: Address, _quote_token: Address) -> Result { - let price = self.calculate_price_64_x_64(base_token)?; - q64_to_float(price) + // Decimals are intentionally swapped as we are multiplying rather than dividing + let (r_0, r_1) = ( + self.reserve_0 as f64 * (10f64).powi(self.token_b.decimals as i32), + self.reserve_1 as f64 * (10f64).powi(self.token_a.decimals as i32), + ); + let (reserve_in, reserve_out) = if base_token == self.token_a { + Ok((r_0, r_1)) + } else if base_token == self.token_b { + Ok((r_1, r_0)) + } else { + Err(AMMError::IncompatibleToken) + }?; + let numerator = reserve_out * F64_FEE_ONE; + let denominator = reserve_in * (U32_FEE_ONE - self.fee) as f64; + Ok(numerator / denominator) } async fn init( @@ -210,22 +212,10 @@ impl AutomatedMarketMaker for UniswapV2Pool { } } -pub fn q64_to_float(num: u128) -> Result { - let float_num = u128_to_float(num)?; - let divisor = u128_to_float(U128_0X10000000000000000)?; - Ok((float_num / divisor).to_f64()) -} - -pub fn u128_to_float(num: u128) -> Result { - let value_string = num.to_string(); - let parsed_value = Float::parse_radix(value_string, 10)?; - Ok(Float::with_val(MPFR_T_PRECISION, parsed_value)) -} - impl UniswapV2Pool { // Create a new, unsynced UniswapV2 pool // TODO: update the init function to derive the fee - pub fn new(address: Address, fee: usize) -> Self { + pub fn new(address: Address, fee: u32) -> Self { Self { address, fee, @@ -240,46 +230,14 @@ impl UniswapV2Pool { } // TODO: we could set this as the fee on the pool instead of calculating this - let fee = (10000 - (self.fee / 10)) / 10; // Fee of 300 => (10,000 - 30) / 10 = 997 + let fee = U32_FEE_ONE - self.fee; let amount_in_with_fee = amount_in * U256::from(fee); let numerator = amount_in_with_fee * reserve_out; - let denominator = reserve_in * U256_1000 + amount_in_with_fee; + let denominator = reserve_in * U256_FEE_ONE + amount_in_with_fee; numerator / denominator } - /// Calculates the price of the base token in terms of the quote token. - /// - /// Returned as a Q64 fixed point number. - pub fn calculate_price_64_x_64(&self, base_token: Address) -> Result { - let decimal_shift = self.token_a.decimals as i8 - self.token_b.decimals as i8; - - let (r_0, r_1) = if decimal_shift < 0 { - ( - U256::from(self.reserve_0) - * U256::from(10u128.pow(decimal_shift.unsigned_abs() as u32)), - U256::from(self.reserve_1), - ) - } else { - ( - U256::from(self.reserve_0), - U256::from(self.reserve_1) * U256::from(10u128.pow(decimal_shift as u32)), - ) - }; - - if base_token == self.token_a.address { - if r_0.is_zero() { - Ok(U128_0X10000000000000000) - } else { - div_uu(r_1, r_0) - } - } else if r_1.is_zero() { - Ok(U128_0X10000000000000000) - } else { - div_uu(r_0, r_1) - } - } - pub fn swap_calldata( &self, amount_0_out: U256, @@ -298,96 +256,15 @@ impl UniswapV2Pool { } } -pub fn div_uu(x: U256, y: U256) -> Result { - if !y.is_zero() { - let mut answer; - - if x <= U256_0XFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF { - answer = (x << U256_64) / y; - } else { - let mut msb = U256_192; - let mut xc = x >> U256_192; - - if xc >= U256_0X100000000 { - xc >>= U256_32; - msb += U256_32; - } - - if xc >= U256_0X10000 { - xc >>= U256_16; - msb += U256_16; - } - - if xc >= U256_0X100 { - xc >>= U256_8; - msb += U256_8; - } - - if xc >= U256_16 { - xc >>= U256_4; - msb += U256_4; - } - - if xc >= U256_4 { - xc >>= U256_2; - msb += U256_2; - } - - if xc >= U256_2 { - msb += U256_1; - } - - answer = (x << (U256_255 - msb)) / (((y - U256_1) >> (msb - U256_191)) + U256_1); - } - - if answer > U256_0XFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF { - return Ok(0); - } - - let hi = answer * (y >> U256_128); - let mut lo = answer * (y & U256_0XFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF); - - let mut xh = x >> U256_192; - let mut xl = x << U256_64; - - if xl < lo { - xh -= U256_1; - } - - xl = xl.overflowing_sub(lo).0; - lo = hi << U256_128; - - if xl < lo { - xh -= U256_1; - } - - xl = xl.overflowing_sub(lo).0; - - if xh != hi >> U256_128 { - return Err(UniswapV2Error::RoundingError.into()); - } - - answer += xl / y; - - if answer > U256_0XFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF { - return Ok(0_u128); - } - - Ok(answer.to::()) - } else { - Err(UniswapV2Error::DivisionByZero.into()) - } -} - #[derive(Debug, Clone, Serialize, Deserialize, Hash, PartialEq, Eq)] pub struct UniswapV2Factory { pub address: Address, - pub fee: usize, + pub fee: u32, pub creation_block: u64, } impl UniswapV2Factory { - pub fn new(address: Address, fee: usize, creation_block: u64) -> Self { + pub fn new(address: Address, fee: u32, creation_block: u64) -> Self { Self { address, creation_block, @@ -619,14 +496,14 @@ impl DiscoverySync for UniswapV2Factory { #[cfg(test)] mod tests { - use crate::amms::{amm::AutomatedMarketMaker, uniswap_v2::UniswapV2Pool, Token}; + use crate::amms::{ + amm::AutomatedMarketMaker, error::AMMError, uniswap_v2::UniswapV2Pool, Token, + }; use alloy::primitives::{address, Address}; + use float_cmp::assert_approx_eq; - #[test] - fn test_calculate_price_edge_case() { - let token_a = address!("0d500b1d8e8ef31e21c99d1db9a6444d3adf1270"); - let token_b = address!("8f18dc399594b451eda8c5da02d0563c0b2d0f16"); - let pool = UniswapV2Pool { + fn get_test_pool(reserve_0: u128, reserve_1: u128) -> UniswapV2Pool { + UniswapV2Pool { address: address!("B4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"), token_a: Token::new_with_decimals( address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), @@ -636,66 +513,65 @@ mod tests { address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), 18, ), - reserve_0: 23595096345912178729927, - reserve_1: 154664232014390554564, - fee: 300, - }; + reserve_0: reserve_0, + reserve_1: reserve_1, + fee: 3000, + } + } - assert!(pool.calculate_price(token_a, Address::default()).unwrap() != 0.0); - assert!(pool.calculate_price(token_b, Address::default()).unwrap() != 0.0); + #[test] + fn test_calculate_price_edge_case() { + let pool = get_test_pool(23595096345912178729927, 154664232014390554564); + assert_ne!( + pool.calculate_price(pool.token_a.address, Address::default()) + .unwrap(), + 0.0 + ); + assert_ne!( + pool.calculate_price(pool.token_b.address, Address::default()) + .unwrap(), + 0.0 + ); } - #[tokio::test] - async fn test_calculate_price() { - let pool = UniswapV2Pool { - address: address!("B4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"), - token_a: Token::new_with_decimals( - address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), - 6, - ), - token_b: Token::new_with_decimals( - address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), - 18, - ), - reserve_0: 47092140895915, - reserve_1: 28396598565590008529300, - fee: 300, - }; + #[test] + fn test_calculate_price() { + let pool = get_test_pool(47092140895915, 28396598565590008529300); - let price_a_64_x = pool + let price_a_for_b = pool .calculate_price(pool.token_a.address, Address::default()) .unwrap(); - let price_b_64_x = pool + let price_b_for_a = pool .calculate_price(pool.token_b.address, Address::default()) .unwrap(); - // No precision loss: 30591574867092394336528 / 2**64 - assert_eq!(1658.3725965327264, price_b_64_x); - // Precision loss: 11123401407064628 / 2**64 - assert_eq!(0.0006030007985483893, price_a_64_x); + // FWIW, the representation is accurate to 0 and 1 ULPs on this example, but we don't want a change detector + assert_approx_eq!(f64, 1663.362684586485983229152871, price_b_for_a, ulps = 4); + assert_approx_eq!( + f64, + 0.0006048152442812330502786409979, + price_a_for_b, + ulps = 4 + ); } - #[tokio::test] - async fn test_calculate_price_64_x_64() { - let pool = UniswapV2Pool { - address: address!("B4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"), - token_a: Token::new_with_decimals( - address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), - 6, - ), - token_b: Token::new_with_decimals( - address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), - 18, - ), - reserve_0: 47092140895915, - reserve_1: 28396598565590008529300, - fee: 300, - }; + #[test] + fn test_incompatible_token() { + let pool = get_test_pool(47092140895915, 28396598565590008529300); + + let invalid_price = pool.calculate_price(Address::default(), Address::default()); + + assert!(matches!(invalid_price, Err(AMMError::IncompatibleToken))); + } + + #[test] + fn test_zero_reserve() { + let pool = get_test_pool(0, 28396598565590008529300); - let price_a_64_x = pool.calculate_price_64_x_64(pool.token_a.address).unwrap(); - let price_b_64_x = pool.calculate_price_64_x_64(pool.token_b.address).unwrap(); + let infinite_price = pool.calculate_price(pool.token_a.address, Address::default()); + let zero_price = pool.calculate_price(pool.token_b.address, Address::default()); - assert_eq!(30591574867092394336528, price_b_64_x); - assert_eq!(11123401407064628, price_a_64_x); + assert_eq!(infinite_price.unwrap(), f64::INFINITY); + assert_eq!(zero_price.unwrap(), 0.0); } } diff --git a/src/amms/uniswap_v3/mod.rs b/src/amms/uniswap_v3/mod.rs index 35497a52..c498533b 100644 --- a/src/amms/uniswap_v3/mod.rs +++ b/src/amms/uniswap_v3/mod.rs @@ -1,11 +1,14 @@ use super::{ amm::{AutomatedMarketMaker, AMM}, + consts::{F64_FEE_ONE, U32_FEE_ONE}, error::AMMError, factory::{AutomatedMarketMakerFactory, DiscoverySync}, + float::u256_to_f64, get_token_decimals, Token, }; use crate::amms::{ - consts::U256_1, uniswap_v3::GetUniswapV3PoolTickBitmapBatchRequest::TickBitmapInfo, + consts::{F64_2P192, U256_1}, + uniswap_v3::GetUniswapV3PoolTickBitmapBatchRequest::TickBitmapInfo, }; use alloy::{ eips::BlockId, @@ -273,7 +276,7 @@ impl AutomatedMarketMaker for UniswapV3Pool { return Ok(U256::ZERO); } - let zero_for_one = base_token == self.token_a.address; + let zero_for_one = base_token == self.token_a; // Set sqrt_price_limit_x_96 to the max or min sqrt price in the pool depending on zero_for_one let sqrt_price_limit_x_96 = if zero_for_one { @@ -417,7 +420,7 @@ impl AutomatedMarketMaker for UniswapV3Pool { return Ok(U256::ZERO); } - let zero_for_one = base_token == self.token_a.address; + let zero_for_one = base_token == self.token_a; // Set sqrt_price_limit_x_96 to the max or min sqrt price in the pool depending on zero_for_one let sqrt_price_limit_x_96 = if zero_for_one { @@ -564,20 +567,21 @@ impl AutomatedMarketMaker for UniswapV3Pool { } fn calculate_price(&self, base_token: Address, _quote_token: Address) -> Result { - let tick = uniswap_v3_math::tick_math::get_tick_at_sqrt_ratio(self.sqrt_price) - .map_err(UniswapV3Error::from)?; - let shift = self.token_a.decimals as i8 - self.token_b.decimals as i8; - - let price = match shift.cmp(&0) { - Ordering::Less => 1.0001_f64.powi(tick) / 10_f64.powi(-shift as i32), - Ordering::Greater => 1.0001_f64.powi(tick) * 10_f64.powi(shift as i32), - Ordering::Equal => 1.0001_f64.powi(tick), - }; - - if base_token == self.token_a.address { - Ok(price) + let fsqrt_price = u256_to_f64(self.sqrt_price); + let price = fsqrt_price * fsqrt_price; + let inv_norm_a = 10f64.powi(self.token_a.decimals as i32); + let inv_norm_b = 10f64.powi(self.token_b.decimals as i32); + + if base_token == self.token_a { + let numerator = F64_2P192 * inv_norm_b * F64_FEE_ONE; + let denominator = price * inv_norm_a * (U32_FEE_ONE - self.fee) as f64; + Ok(numerator / denominator) + } else if base_token == self.token_b { + let numerator = price * inv_norm_a * F64_FEE_ONE; + let denominator = F64_2P192 * inv_norm_b * (U32_FEE_ONE - self.fee) as f64; + Ok(numerator / denominator) } else { - Ok(1.0 / price) + Err(AMMError::IncompatibleToken) } } @@ -1256,16 +1260,12 @@ impl DiscoverySync for UniswapV3Factory { #[cfg(test)] mod test { + use alloy::primitives::{address, aliases::U24, U160, U256}; + use float_cmp::assert_approx_eq; - use super::*; + use crate::test_provider; - use alloy::{ - primitives::{address, aliases::U24, U160, U256}, - providers::ProviderBuilder, - rpc::client::ClientBuilder, - transports::layers::RetryBackoffLayer, - }; - use alloy_throttle::ThrottleLayer; + use super::*; sol! { /// Interface of the Quoter @@ -1278,17 +1278,12 @@ mod test { #[tokio::test] async fn test_simulate_swap_usdc_weth() -> eyre::Result<()> { - let rpc_endpoint = std::env::var("ETHEREUM_PROVIDER")?; - - let client = ClientBuilder::default() - .layer(ThrottleLayer::new(250, None)?) - .layer(RetryBackoffLayer::new(5, 200, 330)) - .http(rpc_endpoint.parse()?); + let provider = test_provider!()?; - let provider = Arc::new(ProviderBuilder::new().on_client(client)); + let current_block = BlockId::from(provider.get_block_number().await?); let pool = UniswapV3Pool::new(address!("88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")) - .init(BlockId::latest(), provider.clone()) + .init(current_block, provider.clone()) .await?; let quoter = IQuoter::new( @@ -1296,163 +1291,79 @@ mod test { provider.clone(), ); - // Test swap from USDC to WETH - let amount_in = U256::from(100000000); // 100 USDC - let amount_out = pool.simulate_swap(pool.token_a.address, Address::default(), amount_in)?; - + dbg!(current_block); dbg!(pool.token_a.address); dbg!(pool.token_b.address); - dbg!(amount_in); - dbg!(amount_out); dbg!(pool.fee); - let expected_amount_out = quoter - .quoteExactInputSingle( - pool.token_a.address, - pool.token_b.address, - U24::from(pool.fee), - amount_in, - U160::ZERO, - ) - .block(BlockId::latest()) - .call() - .await?; - - assert_eq!(amount_out, expected_amount_out.amountOut); - - let amount_in_1 = U256::from(10000000000_u64); // 10_000 USDC - let amount_out_1 = - pool.simulate_swap(pool.token_a.address, Address::default(), amount_in_1)?; - - let expected_amount_out_1 = quoter - .quoteExactInputSingle( - pool.token_a.address, - pool.token_b.address, - U24::from(pool.fee), - amount_in_1, - U160::ZERO, - ) - .block(BlockId::latest()) - .call() - .await?; - - assert_eq!(amount_out_1, expected_amount_out_1.amountOut); - - let amount_in_2 = U256::from(10000000000000_u128); // 10_000_000 USDC - let amount_out_2 = - pool.simulate_swap(pool.token_a.address, Address::default(), amount_in_2)?; - - let expected_amount_out_2 = quoter - .quoteExactInputSingle( - pool.token_a.address, - pool.token_b.address, - U24::from(pool.fee), - amount_in_2, - U160::ZERO, - ) - .block(BlockId::latest()) - .call() - .await?; - - assert_eq!(amount_out_2, expected_amount_out_2.amountOut); - - let amount_in_3 = U256::from(100000000000000_u128); // 100_000_000 USDC - let amount_out_3 = - pool.simulate_swap(pool.token_a.address, Address::default(), amount_in_3)?; - - let expected_amount_out_3 = quoter - .quoteExactInputSingle( - pool.token_a.address, - pool.token_b.address, - U24::from(pool.fee), - amount_in_3, - U160::ZERO, - ) - .block(BlockId::latest()) - .call() - .await?; + // Test swap from USDC to WETH + // 100, 10_000, 10_000_000, 100_000_000 USDC + let usdc_vals = vec![ + 100_000000_u128, + 10_000_000000u128, + 10_000_000_000000_u128, + 100_000_000_000000_u128, + ]; + for usdc_in in usdc_vals { + let amount_in = U256::from(usdc_in); + let amount_out = + pool.simulate_swap(pool.token_a.address, Address::default(), amount_in)?; + + dbg!(amount_in); + dbg!(amount_out); + + let expected_amount_out = quoter + .quoteExactInputSingle( + pool.token_a.address, + pool.token_b.address, + U24::from(pool.fee), + amount_in, + U160::ZERO, + ) + .block(current_block) + .call() + .await?; - assert_eq!(amount_out_3, expected_amount_out_3.amountOut); + assert_eq!(amount_out, expected_amount_out.amountOut); + } // Test swap from WETH to USDC + // 1, 10, 100, 100_000 WETH + let weth_vals = vec![ + 1_000000000000000000_u128, + 10_000000000000000000_u128, + 100_000000000000000000_u128, + 100_000_000000000000000000_u128, + ]; + for weth_in in weth_vals { + let amount_in = U256::from(weth_in); + let amount_out = + pool.simulate_swap(pool.token_b.address, Address::default(), amount_in)?; + + dbg!(amount_in); + dbg!(amount_out); + + let expected_amount_out = quoter + .quoteExactInputSingle( + pool.token_b.address, + pool.token_a.address, + U24::from(pool.fee), + amount_in, + U160::ZERO, + ) + .block(current_block) + .call() + .await?; - let amount_in = U256::from(1000000000000000000_u128); // 1 ETH - let amount_out = pool.simulate_swap(pool.token_b.address, Address::default(), amount_in)?; - let expected_amount_out = quoter - .quoteExactInputSingle( - pool.token_b.address, - pool.token_a.address, - U24::from(pool.fee), - amount_in, - U160::ZERO, - ) - .block(BlockId::latest()) - .call() - .await?; - assert_eq!(amount_out, expected_amount_out.amountOut); - - let amount_in_1 = U256::from(10000000000000000000_u128); // 10 ETH - let amount_out_1 = - pool.simulate_swap(pool.token_b.address, Address::default(), amount_in_1)?; - let expected_amount_out_1 = quoter - .quoteExactInputSingle( - pool.token_b.address, - pool.token_a.address, - U24::from(pool.fee), - amount_in_1, - U160::ZERO, - ) - .block(BlockId::latest()) - .call() - .await?; - assert_eq!(amount_out_1, expected_amount_out_1.amountOut); - - let amount_in_2 = U256::from(100000000000000000000_u128); // 100 ETH - let amount_out_2 = - pool.simulate_swap(pool.token_b.address, Address::default(), amount_in_2)?; - let expected_amount_out_2 = quoter - .quoteExactInputSingle( - pool.token_b.address, - pool.token_a.address, - U24::from(pool.fee), - amount_in_2, - U160::ZERO, - ) - .block(BlockId::latest()) - .call() - .await?; - assert_eq!(amount_out_2, expected_amount_out_2.amountOut); - - let amount_in_3 = U256::from(100000000000000000000_u128); // 100_000 ETH - let amount_out_3 = - pool.simulate_swap(pool.token_b.address, Address::default(), amount_in_3)?; - let expected_amount_out_3 = quoter - .quoteExactInputSingle( - pool.token_b.address, - pool.token_a.address, - U24::from(pool.fee), - amount_in_3, - U160::ZERO, - ) - .block(BlockId::latest()) - .call() - .await?; - - assert_eq!(amount_out_3, expected_amount_out_3.amountOut); + assert_eq!(amount_out, expected_amount_out.amountOut); + } Ok(()) } #[tokio::test] async fn test_simulate_swap_link_weth() -> eyre::Result<()> { - let rpc_endpoint = std::env::var("ETHEREUM_PROVIDER")?; - - let client = ClientBuilder::default() - .layer(ThrottleLayer::new(250, None)?) - .layer(RetryBackoffLayer::new(5, 200, 330)) - .http(rpc_endpoint.parse()?); - - let provider = Arc::new(ProviderBuilder::new().on_client(client)); + let provider = test_provider!()?; let current_block = BlockId::from(provider.get_block_number().await?); @@ -1466,168 +1377,79 @@ mod test { ); // Test swap LINK to WETH - let amount_in = U256::from(1000000000000000000_u128); // 1 LINK - let amount_out = pool.simulate_swap(pool.token_a.address, Address::default(), amount_in)?; - let expected_amount_out = quoter - .quoteExactInputSingle( - pool.token_a.address, - pool.token_b.address, - U24::from(pool.fee), - amount_in, - U160::ZERO, - ) - .block(current_block.into()) - .call() - .await?; - - assert_eq!(amount_out, expected_amount_out.amountOut); - - let amount_in_1 = U256::from(100000000000000000000_u128); // 100 LINK - let amount_out_1 = pool - .simulate_swap(pool.token_a.address, Address::default(), amount_in_1) - .unwrap(); - let expected_amount_out_1 = quoter - .quoteExactInputSingle( - pool.token_a.address, - pool.token_b.address, - U24::from(pool.fee), - amount_in_1, - U160::ZERO, - ) - .block(current_block.into()) - .call() - .await?; - - assert_eq!(amount_out_1, expected_amount_out_1.amountOut); - - let amount_in_2 = U256::from(10000000000000000000000_u128); // 10_000 LINK - let amount_out_2 = pool - .simulate_swap(pool.token_a.address, Address::default(), amount_in_2) - .unwrap(); - let expected_amount_out_2 = quoter - .quoteExactInputSingle( - pool.token_a.address, - pool.token_b.address, - U24::from(pool.fee), - amount_in_2, - U160::ZERO, - ) - .block(current_block.into()) - .call() - .await?; - - assert_eq!(amount_out_2, expected_amount_out_2.amountOut); - - let amount_in_3 = U256::from(10000000000000000000000_u128); // 1_000_000 LINK - let amount_out_3 = pool - .simulate_swap(pool.token_a.address, Address::default(), amount_in_3) - .unwrap(); - let expected_amount_out_3 = quoter - .quoteExactInputSingle( - pool.token_a.address, - pool.token_b.address, - U24::from(pool.fee), - amount_in_3, - U160::ZERO, - ) - .block(current_block.into()) - .call() - .await?; + // 1, 100, 10_000, 1_000_000 LINK + let link_vals = vec![ + 1_000000000000000000_u128, + 100_000000000000000000_u128, + 10_000_000000000000000000_u128, + 1_000_000_000000000000000000_u128, + ]; + for link_in in link_vals { + let amount_in = U256::from(link_in); + let amount_out = + pool.simulate_swap(pool.token_a.address, Address::default(), amount_in)?; + let expected_amount_out = quoter + .quoteExactInputSingle( + pool.token_a.address, + pool.token_b.address, + U24::from(pool.fee), + amount_in, + U160::ZERO, + ) + .block(current_block.into()) + .call() + .await?; - assert_eq!(amount_out_3, expected_amount_out_3.amountOut); + assert_eq!(amount_out, expected_amount_out.amountOut); + } // Test swap WETH to LINK + // 1, 10, 100, 100_000 WETH + let weth_vals = vec![ + 1_000000000000000000_u128, + 10_000000000000000000_u128, + 100_000000000000000000_u128, + 100_000_000000000000000000_u128, + ]; + for weth_in in weth_vals { + let amount_in = U256::from(weth_in); // 1 ETH + let amount_out = + pool.simulate_swap(pool.token_b.address, Address::default(), amount_in)?; + let expected_amount_out = quoter + .quoteExactInputSingle( + pool.token_b.address, + pool.token_a.address, + U24::from(pool.fee), + amount_in, + U160::ZERO, + ) + .block(current_block.into()) + .call() + .await?; - let amount_in = U256::from(1000000000000000000_u128); // 1 ETH - let amount_out = pool.simulate_swap(pool.token_b.address, Address::default(), amount_in)?; - let expected_amount_out = quoter - .quoteExactInputSingle( - pool.token_b.address, - pool.token_a.address, - U24::from(pool.fee), - amount_in, - U160::ZERO, - ) - .block(current_block.into()) - .call() - .await?; - - assert_eq!(amount_out, expected_amount_out.amountOut); - - let amount_in_1 = U256::from(10000000000000000000_u128); // 10 ETH - let amount_out_1 = - pool.simulate_swap(pool.token_b.address, Address::default(), amount_in_1)?; - let expected_amount_out_1 = quoter - .quoteExactInputSingle( - pool.token_b.address, - pool.token_a.address, - U24::from(pool.fee), - amount_in_1, - U160::ZERO, - ) - .block(current_block.into()) - .call() - .await?; - - assert_eq!(amount_out_1, expected_amount_out_1.amountOut); - - let amount_in_2 = U256::from(100000000000000000000_u128); // 100 ETH - let amount_out_2 = - pool.simulate_swap(pool.token_b.address, Address::default(), amount_in_2)?; - let expected_amount_out_2 = quoter - .quoteExactInputSingle( - pool.token_b.address, - pool.token_a.address, - U24::from(pool.fee), - amount_in_2, - U160::ZERO, - ) - .block(current_block.into()) - .call() - .await?; - assert_eq!(amount_out_2, expected_amount_out_2.amountOut); - - let amount_in_3 = U256::from(100000000000000000000_u128); // 100_000 ETH - let amount_out_3 = - pool.simulate_swap(pool.token_b.address, Address::default(), amount_in_3)?; - let expected_amount_out_3 = quoter - .quoteExactInputSingle( - pool.token_b.address, - pool.token_a.address, - U24::from(pool.fee), - amount_in_3, - U160::ZERO, - ) - .block(current_block.into()) - .call() - .await?; - - assert_eq!(amount_out_3, expected_amount_out_3.amountOut); - + assert_eq!(amount_out, expected_amount_out.amountOut); + } Ok(()) } - // NOTE: test is failing due to invalid push0 opcode, update this test to use a block post push0 #[tokio::test] async fn test_calculate_price() -> eyre::Result<()> { - let rpc_endpoint = std::env::var("ETHEREUM_PROVIDER")?; - - let client = ClientBuilder::default() - .layer(ThrottleLayer::new(250, None)?) - .layer(RetryBackoffLayer::new(5, 200, 330)) - .http(rpc_endpoint.parse()?); + let provider = test_provider!()?; - let provider = Arc::new(ProviderBuilder::new().on_client(client)); - - let block_number = BlockId::from(16515398); + let block_number = BlockId::from(21591876); let pool = UniswapV3Pool::new(address!("88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")) .init(block_number, provider.clone()) .await?; - let float_price_a = pool.calculate_price(pool.token_a.address, Address::default())?; - let float_price_b = pool.calculate_price(pool.token_b.address, Address::default())?; - assert_eq!(float_price_a, 0.0006081236083117488); - assert_eq!(float_price_b, 1644.4025299004006); + let price_a_for_b = pool.calculate_price(pool.token_a.address, Address::default())?; + let price_b_for_a = pool.calculate_price(pool.token_b.address, Address::default())?; + assert_approx_eq!(f64, price_a_for_b, 3254.224139938114022340147834, ulps = 4); + assert_approx_eq!( + f64, + price_b_for_a, + 0.000307600431763544363928698876, + ulps = 4 + ); Ok(()) }