pub mod fee_tier_fetcher;
use std::sync::{Arc, RwLock};
use num_bigint::BigUint;
use rustc_hash::{FxHashMap, FxHashSet};
use tycho_simulation::tycho_common::models::Address;
use crate::{
feed::{events::MarketEvent, market_data::MarketDataView},
replay::replay_route,
types::{ComponentId, Route, Swap},
};
pub fn has_pamm_leg(route: &Route) -> bool {
route.swaps().iter().any(|swap| {
swap.protocol()
.starts_with(PROPAMM_FALLBACK_PREFIX)
})
}
pub fn fallback_amount_out(
route: &Route,
market: &MarketDataView<'_>,
fee_tiers: &FeeTiers,
index: &FallbackPoolIndex,
) -> FallbackAmountOut {
let mut substituted = Vec::with_capacity(route.swaps().len());
for swap in route.swaps() {
if !swap
.protocol()
.starts_with(PROPAMM_FALLBACK_PREFIX)
{
substituted.push(swap.clone());
continue;
}
let fee_tier = fee_tiers.resolved_tier(swap.token_in(), swap.token_out());
let Some(pool) = index.pool_for(swap.token_in(), swap.token_out(), fee_tier) else {
return FallbackAmountOut::NoFallbackPool {
component_id: swap.component_id().to_string(),
fee_tier,
};
};
let (Some(component), Some(state)) =
(market.get_component(pool), market.get_simulation_state(pool))
else {
return FallbackAmountOut::NoFallbackPool {
component_id: swap.component_id().to_string(),
fee_tier,
};
};
substituted.push(
Swap::new(
pool.clone(),
component.protocol_system.clone(),
swap.token_in().clone(),
swap.token_out().clone(),
swap.amount_in().clone(),
swap.amount_out().clone(),
swap.gas_estimate().clone(),
component.clone(),
state.clone_box(),
)
.with_split(*swap.split()),
);
}
let replayed_components: Vec<ComponentId> = substituted
.iter()
.map(|swap| swap.component_id().to_string())
.collect();
let market_state = market.extract_subset_with_overlay(
&replayed_components
.iter()
.collect::<FxHashSet<_>>(),
);
let substituted = match Route::new(substituted, FxHashMap::default()) {
Ok(route) => route,
Err(e) => return FallbackAmountOut::NotPriceable { reason: e.to_string() },
};
match replay_route(&substituted, &market_state) {
Ok(replay) => FallbackAmountOut::AmountOut(replay.amount_out),
Err(e) => FallbackAmountOut::NotPriceable { reason: e.to_string() },
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FallbackAmountOut {
AmountOut(BigUint),
NoFallbackPool {
component_id: ComponentId,
fee_tier: u32,
},
NotPriceable {
reason: String,
},
}
#[derive(Debug, Clone)]
pub struct FeeTiers {
default_tier: u32,
per_pair: FxHashMap<(Address, Address), u32>,
}
impl FeeTiers {
pub fn new(default_tier: u32) -> Self {
Self { default_tier, per_pair: FxHashMap::default() }
}
pub fn resolved_tier(&self, token_a: &Address, token_b: &Address) -> u32 {
self.per_pair
.get(&sorted_pair(token_a, token_b))
.copied()
.unwrap_or(self.default_tier)
}
pub fn set_pair_tier(&mut self, token_a: &Address, token_b: &Address, fee_tier: u32) {
let key = sorted_pair(token_a, token_b);
if fee_tier == 0 {
self.per_pair.remove(&key);
} else {
self.per_pair.insert(key, fee_tier);
}
}
pub fn pair_override_count(&self) -> usize {
self.per_pair.len()
}
}
#[derive(Debug, Clone, Default)]
pub struct SharedFeeTiers(Arc<RwLock<Option<FeeTiers>>>);
impl SharedFeeTiers {
pub fn snapshot(&self) -> Option<FeeTiers> {
self.0
.read()
.expect("fallback fee tier lock poisoned")
.clone()
}
pub fn set(&self, fee_tiers: FeeTiers) {
*self
.0
.write()
.expect("fallback fee tier lock poisoned") = Some(fee_tiers);
}
}
#[derive(Debug, Default, Clone)]
pub struct FallbackPoolIndex {
pools: FxHashMap<(Address, Address, u32), ComponentId>,
keys: FxHashMap<ComponentId, (Address, Address, u32)>,
}
impl FallbackPoolIndex {
pub fn build(market: &MarketDataView<'_>) -> Self {
let mut index = Self::default();
for component_id in market.component_topology().into_keys() {
index.insert(market, component_id);
}
index
}
pub fn apply_event(&mut self, market: &MarketDataView<'_>, event: &MarketEvent) {
let MarketEvent::MarketUpdated { added_components, removed_components, .. } = event;
for component_id in removed_components {
self.remove(component_id);
}
for component_id in added_components.keys() {
self.insert(market, component_id.clone());
}
}
fn insert(&mut self, market: &MarketDataView<'_>, component_id: ComponentId) {
let Some(component) = market.get_component(&component_id) else { return };
if component.protocol_system != FALLBACK_PROTOCOL_SYSTEM {
return;
}
let [token_a, token_b] = component.tokens.as_slice() else { return };
let Some(fee_tier) = component
.static_attributes
.get(FEE_ATTRIBUTE)
.and_then(parse_fee_tier)
else {
return;
};
let (low, high) = sorted_pair(token_a, token_b);
self.keys
.insert(component_id.clone(), (low.clone(), high.clone(), fee_tier));
self.pools
.insert((low, high, fee_tier), component_id);
}
fn remove(&mut self, component_id: &ComponentId) {
let Some(key) = self.keys.remove(component_id) else { return };
if self.pools.get(&key) == Some(component_id) {
self.pools.remove(&key);
}
}
pub fn pool_for(
&self,
token_a: &Address,
token_b: &Address,
fee_tier: u32,
) -> Option<&ComponentId> {
let (low, high) = sorted_pair(token_a, token_b);
self.pools.get(&(low, high, fee_tier))
}
}
pub const PROPAMM_FALLBACK_PREFIX: &str = "propammfallback:";
pub const PROPAMM_ROUTER_ADDRESS: &str = "0x4DdF368080CD7946db5b459aD591c350158175e1";
pub const PROPAMM_VENUES: &[&str] = &[
"0x5979458912F80B96d30D4220af8E2e4925A33320",
"0x71e790dd841c8A9061487cb3E78C288E75cE0B3d",
];
const FALLBACK_PROTOCOL_SYSTEM: &str = "uniswap_v3";
const FEE_ATTRIBUTE: &str = "fee";
fn sorted_pair(token_a: &Address, token_b: &Address) -> (Address, Address) {
if token_a <= token_b {
(token_a.clone(), token_b.clone())
} else {
(token_b.clone(), token_a.clone())
}
}
fn parse_fee_tier(raw: &tycho_simulation::tycho_common::Bytes) -> Option<u32> {
let bytes = raw.as_ref();
if bytes.is_empty() || bytes.len() > 4 {
return None;
}
let mut padded = [0u8; 4];
padded[4 - bytes.len()..].copy_from_slice(bytes);
Some(u32::from_be_bytes(padded))
}
#[cfg(test)]
mod tests {
use tycho_simulation::{
tycho_common::Bytes, tycho_core::simulation::protocol_sim::ProtocolSim,
};
use super::*;
use crate::algorithm::test_utils::{self as util, addr};
const DEFAULT_TIER: u32 = 3000;
#[test]
fn test_resolved_tier_defaults_and_overrides() {
let mut fee_tiers = FeeTiers::new(DEFAULT_TIER);
assert_eq!(fee_tiers.resolved_tier(&addr(1), &addr(2)), DEFAULT_TIER);
fee_tiers.set_pair_tier(&addr(2), &addr(1), 500);
assert_eq!(fee_tiers.resolved_tier(&addr(1), &addr(2)), 500);
assert_eq!(fee_tiers.resolved_tier(&addr(2), &addr(1)), 500);
fee_tiers.set_pair_tier(&addr(1), &addr(2), 0);
assert_eq!(fee_tiers.resolved_tier(&addr(1), &addr(2)), DEFAULT_TIER);
}
#[test]
fn test_shared_fee_tiers_empty_until_set() {
let shared = SharedFeeTiers::default();
assert!(shared.snapshot().is_none());
shared.set(FeeTiers::new(500));
assert_eq!(
shared
.snapshot()
.expect("tiers were set")
.resolved_tier(&addr(1), &addr(2)),
500
);
}
#[test]
fn test_parse_fee_tier() {
assert_eq!(parse_fee_tier(&Bytes::from(3000_i32.to_be_bytes().to_vec())), Some(3000));
assert_eq!(parse_fee_tier(&Bytes::from(vec![0x01, 0xf4])), Some(500));
assert_eq!(parse_fee_tier(&Bytes::from(Vec::new())), None);
assert_eq!(parse_fee_tier(&Bytes::from(vec![0u8; 5])), None);
}
#[test]
fn test_pool_for_token_order() {
let mut index = FallbackPoolIndex::default();
let (low, high) = sorted_pair(&addr(9), &addr(3));
index
.pools
.insert((low, high, 500), "pool".to_string());
assert_eq!(
index
.pool_for(&addr(3), &addr(9), 500)
.map(String::as_str),
Some("pool")
);
assert_eq!(
index
.pool_for(&addr(9), &addr(3), 500)
.map(String::as_str),
Some("pool")
);
assert!(index
.pool_for(&addr(3), &addr(9), 3000)
.is_none());
}
#[test]
fn test_apply_event_added_and_removed_components() {
let market = market_with_fallback_pool(500);
let view = market
.try_read_blocking()
.expect("uncontended");
let mut index = FallbackPoolIndex::build(&view);
assert_eq!(index.pools.len(), 1);
index.apply_event(
&view,
&MarketEvent::MarketUpdated {
added_components: FxHashMap::default(),
removed_components: vec![FALLBACK_POOL.to_string()],
updated_components: Vec::new(),
},
);
assert!(index.pools.is_empty());
assert!(index
.pool_for(&addr(1), &addr(2), 500)
.is_none());
index.apply_event(
&view,
&MarketEvent::MarketUpdated {
added_components: FxHashMap::from_iter([(FALLBACK_POOL.to_string(), Vec::new())]),
removed_components: Vec::new(),
updated_components: Vec::new(),
},
);
assert_eq!(
index
.pool_for(&addr(1), &addr(2), 500)
.map(String::as_str),
Some(FALLBACK_POOL)
);
}
#[test]
fn test_apply_event_state_updates() {
let market = market_with_fallback_pool(500);
let view = market
.try_read_blocking()
.expect("uncontended");
let mut index = FallbackPoolIndex::build(&view);
index.apply_event(
&view,
&MarketEvent::MarketUpdated {
added_components: FxHashMap::default(),
removed_components: Vec::new(),
updated_components: vec![FALLBACK_POOL.to_string()],
},
);
assert_eq!(index.pools.len(), 1);
}
const PAMM_PRICE: f64 = 2.0;
const FALLBACK_PRICE: f64 = 1.0;
const FALLBACK_POOL: &str = "0xretry";
const PAMM_COMPONENT: &str = "0xpamm";
const PAMM_PROTOCOL: &str = "propammfallback:fermiswap";
fn market_with_fallback_pool(fee_tier: u32) -> crate::feed::market_data::MarketData {
let (token_in, token_out) = (util::token(1, "WETH"), util::token(2, "USDC"));
let mut component = util::component_with_protocol(
FALLBACK_POOL,
FALLBACK_PROTOCOL_SYSTEM,
&[token_in.clone(), token_out.clone()],
);
component
.static_attributes
.insert(FEE_ATTRIBUTE.to_string(), Bytes::from(fee_tier.to_be_bytes().to_vec()));
let market = crate::feed::market_data::MarketData::new_shared();
{
let mut state = market.try_write().expect("uncontended");
state.upsert_tokens([token_in, token_out]);
state.upsert_components([component]);
state.update_states([(
FALLBACK_POOL.to_string(),
Box::new(util::MockProtocolSim::new(FALLBACK_PRICE)) as Box<dyn ProtocolSim>,
)]);
}
market
}
fn pamm_swap() -> Swap {
let (token_in, token_out) = (util::token(1, "WETH"), util::token(2, "USDC"));
Swap::new(
PAMM_COMPONENT.to_string(),
PAMM_PROTOCOL.to_string(),
token_in.address.clone(),
token_out.address.clone(),
BigUint::from(1_000u32),
BigUint::from(2_000u32),
BigUint::from(100_000u32),
util::component_with_protocol(PAMM_COMPONENT, PAMM_PROTOCOL, &[token_in, token_out]),
Box::new(util::MockProtocolSim::new(PAMM_PRICE)),
)
}
fn uniswap_swap() -> Swap {
let (token_in, token_out) = (util::token(1, "WETH"), util::token(2, "USDC"));
Swap::new(
FALLBACK_POOL.to_string(),
FALLBACK_PROTOCOL_SYSTEM.to_string(),
token_in.address.clone(),
token_out.address.clone(),
BigUint::from(1_000u32),
BigUint::from(1_000u32),
BigUint::from(100_000u32),
util::component_with_protocol(
FALLBACK_POOL,
FALLBACK_PROTOCOL_SYSTEM,
&[token_in, token_out],
),
Box::new(util::MockProtocolSim::new(FALLBACK_PRICE)),
)
}
#[test]
fn test_has_pamm_leg() {
let non_pamm =
Route::new(vec![uniswap_swap()], FxHashMap::default()).expect("non-empty route");
assert!(!has_pamm_leg(&non_pamm));
let pamm = Route::new(vec![pamm_swap()], FxHashMap::default()).expect("non-empty route");
assert!(has_pamm_leg(&pamm));
}
#[test]
fn test_fallback_amount_out_with_fallback_pool() {
let market = market_with_fallback_pool(500);
let view = market
.try_read_blocking()
.expect("uncontended");
let index = FallbackPoolIndex::build(&view);
let mut fee_tiers = FeeTiers::new(DEFAULT_TIER);
fee_tiers.set_pair_tier(&addr(1), &addr(2), 500);
let swap = pamm_swap();
let route = Route::new(vec![swap.clone()], FxHashMap::default()).expect("non-empty route");
let amount_out = fallback_amount_out(&route, &view, &fee_tiers, &index);
assert_eq!(amount_out, FallbackAmountOut::AmountOut(BigUint::from(1_000u32)));
assert_eq!(*swap.amount_out(), BigUint::from(2_000u32));
}
#[tokio::test]
async fn test_fallback_amount_out_uses_the_overlay_state() {
let market = market_with_fallback_pool(500);
let label = "test_overlay".to_string();
let mut overlay: rustc_hash::FxHashMap<ComponentId, Box<dyn ProtocolSim>> =
FxHashMap::default();
overlay.insert(
FALLBACK_POOL.to_string(),
Box::new(util::MockProtocolSim::new(FALLBACK_PRICE / 2.0)),
);
market
.register_labeled_state(label.clone(), overlay, u64::MAX)
.await;
let view = market
.read_labeled(&label)
.await
.expect("registered overlay");
let index = FallbackPoolIndex::build(&view);
let mut fee_tiers = FeeTiers::new(DEFAULT_TIER);
fee_tiers.set_pair_tier(&addr(1), &addr(2), 500);
let route = Route::new(vec![pamm_swap()], FxHashMap::default()).expect("non-empty route");
let amount_out = fallback_amount_out(&route, &view, &fee_tiers, &index);
assert_eq!(amount_out, FallbackAmountOut::AmountOut(BigUint::from(500u32)));
}
#[test]
fn test_fallback_amount_out_without_pool_at_resolved_tier() {
let market = market_with_fallback_pool(500);
let view = market
.try_read_blocking()
.expect("uncontended");
let index = FallbackPoolIndex::build(&view);
let route = Route::new(vec![pamm_swap()], FxHashMap::default()).expect("non-empty route");
let amount_out = fallback_amount_out(&route, &view, &FeeTiers::new(DEFAULT_TIER), &index);
assert_eq!(
amount_out,
FallbackAmountOut::NoFallbackPool {
component_id: PAMM_COMPONENT.to_string(),
fee_tier: DEFAULT_TIER,
}
);
}
#[test]
fn test_fallback_amount_out_split_route() {
let market = market_with_fallback_pool(500);
let view = market
.try_read_blocking()
.expect("uncontended");
let index = FallbackPoolIndex::build(&view);
let mut fee_tiers = FeeTiers::new(DEFAULT_TIER);
fee_tiers.set_pair_tier(&addr(1), &addr(2), 500);
let route =
Route::new(vec![pamm_swap().with_split(0.6), pamm_swap()], FxHashMap::default())
.expect("non-empty route");
let amount_out = fallback_amount_out(&route, &view, &fee_tiers, &index);
assert!(
matches!(&amount_out, FallbackAmountOut::AmountOut(amount) if *amount > BigUint::ZERO),
"expected a priced split route, got {amount_out:?}"
);
}
#[test]
fn test_pool_index_non_uniswap_v3_components() {
let market = market_with_fallback_pool(500);
{
let mut state = market.try_write().expect("uncontended");
state.upsert_components([util::component_with_protocol(
"0xv2",
"uniswap_v2",
&[util::token(1, "WETH"), util::token(2, "USDC")],
)]);
}
let view = market
.try_read_blocking()
.expect("uncontended");
let index = FallbackPoolIndex::build(&view);
assert_eq!(index.pools.len(), 1);
assert_eq!(
index
.pool_for(&addr(1), &addr(2), 500)
.map(String::as_str),
Some(FALLBACK_POOL)
);
}
}