Skip to content

Commit

Permalink
refactor prices+fees
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
henopied committed Jan 20, 2025
1 parent 4d321a5 commit 9899f1e
Show file tree
Hide file tree
Showing 17 changed files with 514 additions and 686 deletions.
13 changes: 2 additions & 11 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ alloy = { version = "0.9", features = [
alloy-json-rpc = { version = "0.5" }

# tracing
eyre = "0.6"
tracing = "0.1"

# async
Expand All @@ -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"
Expand All @@ -57,29 +55,22 @@ async-stream = "0.3.6"


[dev-dependencies]
float-cmp = "0.10.0"
rand = "0.8.5"
tracing-subscriber = "0.3"
criterion = "0.5"
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
lto = true
codegen-units = 1
panic = "abort"

[profile.dev]
opt-level = 3
lto = true
codegen-units = 1
debug = "full"


[[bench]]
name = "uniswap_v2"
Expand Down
2 changes: 1 addition & 1 deletion benches/uniswap_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
};

Expand Down
4 changes: 2 additions & 2 deletions contracts/src/Balancer/GetBalancerPoolDataBatchRequest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ contract GetBalancerPoolDataBatchRequest {
uint8[] decimals;
uint256[] liquidity;
uint256[] weights;
uint32 fee;
uint64 fee;
}

constructor(address[] memory pools) {
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion examples/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ async fn main() -> eyre::Result<()> {
// UniswapV2
UniswapV2Factory::new(
address!("5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"),
300,
3000,
10000835,
)
.into(),
Expand Down
5 changes: 2 additions & 3 deletions examples/state_space_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ async fn main() -> eyre::Result<()> {
// UniswapV2
UniswapV2Factory::new(
address!("5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"),
300,
3000,
10000835,
)
.into(),
Expand All @@ -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(),
];

Expand All @@ -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?;
Expand Down
2 changes: 1 addition & 1 deletion examples/swap_calldata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?;

Expand Down
2 changes: 1 addition & 1 deletion examples/sync_macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ async fn main() -> eyre::Result<()> {
// UniswapV2
UniswapV2Factory::new(
address!("5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"),
300,
3000,
10000835,
)
.into(),
Expand Down
4 changes: 2 additions & 2 deletions src/amms/amm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use alloy::{
rpc::types::Log,
transports::Transport,
};
use eyre::Result;
use serde::{Deserialize, Serialize};
use std::{
hash::{Hash, Hasher},
Expand All @@ -31,7 +30,8 @@ pub trait AutomatedMarketMaker {
/// Returns a list of token addresses used in the AMM
fn tokens(&self) -> Vec<Address>;

/// 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<f64, AMMError>;

/// Simulate a swap
Expand Down
113 changes: 52 additions & 61 deletions src/amms/balancer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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")]
Expand All @@ -99,7 +99,7 @@ pub struct BalancerPool {
// TODO:
state: HashMap<Address, TokenPoolState>,
/// The Swap Fee on the Pool.
fee: u32,
fee: u64,
}

#[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -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(&quote_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.
Expand All @@ -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(&quote_token)
.ok_or(BalancerError::TokenOutDoesNotExist)?;
.ok_or(AMMError::IncompatibleToken)?;

Ok(bmath::calculate_out_given_in(
token_in.liquidity,
Expand All @@ -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(&quote_token)
.ok_or(BalancerError::TokenOutDoesNotExist)?;
.ok_or(AMMError::IncompatibleToken)?;

let out = bmath::calculate_out_given_in(
token_in.liquidity,
Expand Down Expand Up @@ -284,7 +272,7 @@ impl AutomatedMarketMaker for BalancerPool {
let res = deployer.block(block_number).call_raw().await?;

let mut data =
<Vec<(Vec<Address>, Vec<u16>, Vec<U256>, Vec<U256>, u32)> as SolValue>::abi_decode(
<Vec<(Vec<Address>, Vec<u16>, Vec<U256>, Vec<U256>, u64)> as SolValue>::abi_decode(
&res, false,
)?;
let (tokens, decimals, liquidity, weights, fee) = if !data.is_empty() {
Expand Down Expand Up @@ -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<Address>, Vec<u16>, Vec<U256>, Vec<U256>, u32)> as SolValue>::abi_decode(
let return_data = <Vec<(Vec<Address>, Vec<u16>, Vec<U256>, Vec<U256>, u64)> as SolValue>::abi_decode(
&res, false,
)?;

Ok::<(Vec<Address>, Vec<(Vec<Address>, Vec<u16>, Vec<U256>, Vec<U256>, u32)>), AMMError>((
Ok::<(Vec<Address>, Vec<(Vec<Address>, Vec<u16>, Vec<U256>, Vec<U256>, u64)>), AMMError>((
group,
return_data,
))
Expand Down Expand Up @@ -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())
Expand All @@ -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"),
Expand All @@ -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"),
Expand All @@ -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"
Expand All @@ -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())
Expand All @@ -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())
Expand Down
Loading

0 comments on commit 9899f1e

Please sign in to comment.