use std::{
sync::{Arc, Mutex, MutexGuard},
time::{Duration, Instant},
};
use async_trait::async_trait;
use num_bigint::BigUint;
use num_traits::Zero;
use petgraph::graph::NodeIndex;
use rustc_hash::{FxHashMap, FxHashSet};
use tracing::{debug, instrument, trace, warn, Span};
use tycho_simulation::{
tycho_common::models::Address, tycho_core::simulation::protocol_sim::Price,
};
use crate::{
algorithm::{
bellman_ford::{BellmanFordContext, FindRouteOptions, ReachOutcome, ReachedToken},
Algorithm, AlgorithmConfig, BellmanFordAlgorithm,
},
derived::{
computation::{
ComputationId, ComputationOutput, ComputationRequirements, DerivedComputation,
FailedItem, FailedItemError,
},
error::ComputationError,
manager::{ChangedComponents, SharedDerivedDataRef},
store::DerivedData,
types::{TokenGasPrices, TokenPriceEntry, TokenPricesWithDeps},
},
feed::market_data::MarketData,
graph::{GraphManager, PetgraphStableDiGraphManager},
types::{ComponentId, Order, OrderSide, RouteExclusions},
};
struct PricingPass<'a> {
algorithm: &'a BellmanFordAlgorithm,
graph: &'a <BellmanFordAlgorithm as Algorithm>::GraphType,
ctx: BellmanFordContext,
computation: &'a TokenGasPriceComputation,
buys: ReachOutcome,
gas_node: NodeIndex,
hops_to_gas: FxHashMap<NodeIndex, usize>,
token_nodes: FxHashMap<Address, NodeIndex>,
}
struct SellLeg {
amount_out: BigUint,
components: FxHashSet<ComponentId>,
}
impl<'a> PricingPass<'a> {
fn new(
algorithm: &'a BellmanFordAlgorithm,
graph: &'a <BellmanFordAlgorithm as Algorithm>::GraphType,
ctx: BellmanFordContext,
computation: &'a TokenGasPriceComputation,
) -> Self {
let buys = algorithm.reach_from_source_token(&ctx, &computation.probe_amount);
let gas_node = ctx.token_in_node;
let token_nodes = ctx
.node_address
.iter()
.map(|(&node, address)| (address.clone(), node))
.collect();
let hops_to_gas = BellmanFordAlgorithm::get_hops_to_reach(
graph,
gas_node,
gas_node,
algorithm.max_hops(),
&RouteExclusions::default(),
);
Self { algorithm, graph, ctx, computation, buys, gas_node, hops_to_gas, token_nodes }
}
fn sell_loop(&mut self, tokens_to_price: Vec<Address>, block: u64) -> PricingPassOutcome {
let deadline = Instant::now() + self.computation.pass_budget;
let mut prices = FxHashMap::default();
let mut failed_items = Vec::new();
let mut unattempted = FxHashSet::default();
let mut unreachable_tokens = 0usize;
let mut remaining = tokens_to_price.into_iter();
for token in &mut remaining {
if Instant::now() >= deadline {
unattempted.insert(token);
break;
}
let Some(buy_leg) = self.buys.reached.remove(&token) else {
if self.buys.timed_out {
unattempted.insert(token);
} else {
unreachable_tokens += 1;
}
continue;
};
match self.price_token(&token, &buy_leg) {
Ok(priced) => {
prices.insert(token, priced);
}
Err(error) => failed_items.push(FailedItem { key: token.to_string(), error }),
}
}
unattempted.extend(remaining);
if unattempted.is_empty() {
debug!(
priced = prices.len(),
failed = failed_items.len(),
unreachable = unreachable_tokens,
block,
"token pricing pass complete"
);
} else {
warn!(
priced = prices.len(),
failed = failed_items.len(),
unreachable = unreachable_tokens,
unattempted = unattempted.len(),
buy_pass_timed_out = self.buys.timed_out,
block,
"token pricing pass cut short; unattempted tokens keep previous prices"
);
}
PricingPassOutcome { prices, block, failed_items, unattempted }
}
fn price_token(
&mut self,
token: &Address,
buy_leg: &ReachedToken,
) -> Result<TokenPriceEntry, FailedItemError> {
let SellLeg { amount_out: sell_out, mut components } =
self.solve_sell_leg(token, buy_leg.amount_out.clone())?;
trace!(%token, buy_out = %buy_leg.amount_out, sell_out = %sell_out, "token priced");
components.extend(buy_leg.components.iter().cloned());
let mid_price = Price {
numerator: &buy_leg.amount_out * (&self.computation.probe_amount + &sell_out),
denominator: BigUint::from(2u8) * &self.computation.probe_amount * sell_out,
};
Ok(TokenPriceEntry { price: mid_price, path_components: components })
}
fn solve_sell_leg(
&mut self,
token: &Address,
amount: BigUint,
) -> Result<SellLeg, FailedItemError> {
let token_node = *self
.token_nodes
.get(token)
.ok_or_else(|| {
FailedItemError::MissingSellRoute("token is not in the pass subgraph".into())
})?;
let candidate_components = self
.ctx
.reroot_toward(
self.graph,
token_node,
self.gas_node,
&self.hops_to_gas,
self.algorithm.max_hops(),
)
.ok_or_else(|| {
FailedItemError::MissingSellRoute("no pruned subgraph toward the gas token".into())
})?;
let mut components: FxHashSet<ComponentId> = candidate_components
.into_iter()
.cloned()
.collect();
let order = Order::new(
token.clone(),
self.computation.gas_token.clone(),
amount,
OrderSide::Sell,
Address::zero(20),
);
let result = self
.algorithm
.find_single_route(&self.ctx, &order, FindRouteOptions::default())
.map_err(|error| FailedItemError::MissingSellRoute(error.to_string()))?;
let route = result.route();
let amount_out = route.amount_out(&self.computation.gas_token);
if amount_out.is_zero() {
return Err(FailedItemError::MissingSellRoute("the sell route returns zero".into()));
}
components.extend(
route
.swaps()
.iter()
.map(|swap| swap.component_id().to_string()),
);
Ok(SellLeg { amount_out, components })
}
}
struct PricingPassOutcome {
prices: FxHashMap<Address, TokenPriceEntry>,
block: u64,
failed_items: Vec<FailedItem>,
unattempted: FxHashSet<Address>,
}
#[derive(Debug, Clone)]
pub struct TokenGasPriceComputation {
gas_token: Address,
max_hops: usize,
probe_amount: BigUint,
pass_budget: Duration,
max_tokens_per_pass: usize,
min_pass_interval: Duration,
pass_state: Arc<Mutex<PassState>>,
}
#[derive(Debug, Default)]
struct PassState {
passes: u64,
last_pass_started: Option<Instant>,
last_attempted: FxHashMap<Address, u64>,
pending_arrivals: FxHashSet<Address>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PassSlot {
Due,
ArrivalsOnly,
Deferred,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PassScope {
Whole,
ArrivalsOnly,
}
#[derive(Debug, Default)]
pub(crate) struct PassPriority {
arrived: FxHashSet<Address>,
priced: FxHashSet<Address>,
last_attempted: FxHashMap<Address, u64>,
}
impl PassPriority {
fn keep_unpriced_arrivals(&mut self) {
let priced = &self.priced;
self.arrived
.retain(|token| !priced.contains(token));
}
fn stamp(&self, token: &Address) -> u64 {
self.last_attempted
.get(token)
.copied()
.unwrap_or(0)
}
}
fn select_pass_tokens(
universe: &FxHashSet<Address>,
changed: Option<&FxHashSet<Address>>,
priority: &PassPriority,
scope: PassScope,
max_tokens: usize,
) -> Vec<Address> {
const ARRIVED: u8 = 0;
const ROTATING: u8 = 1;
let mut ranked: Vec<(u8, u64, Address)> = Vec::with_capacity(universe.len());
for token in universe {
let stamp = priority.stamp(token);
if priority.arrived.contains(token) {
ranked.push((ARRIVED, stamp, token.clone()));
continue;
}
if scope == PassScope::ArrivalsOnly {
continue;
}
if !priority.priced.contains(token) || changed.is_none_or(|changed| changed.contains(token))
{
ranked.push((ROTATING, stamp, token.clone()));
}
}
ranked.sort_unstable_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
ranked.truncate(max_tokens);
ranked
.into_iter()
.map(|(_, _, token)| token)
.collect()
}
const DEFAULT_PASS_BUDGET: Duration = Duration::from_secs(30);
const DEFAULT_MIN_PASS_INTERVAL: Duration = Duration::from_secs(2);
const DEFAULT_MAX_TOKENS_PER_PASS: usize = 100;
impl Default for TokenGasPriceComputation {
fn default() -> Self {
Self {
gas_token: Address::zero(20), max_hops: crate::solver::defaults::POOL_MAX_HOPS,
probe_amount: BigUint::from(10u64).pow(18), pass_budget: DEFAULT_PASS_BUDGET,
max_tokens_per_pass: DEFAULT_MAX_TOKENS_PER_PASS,
min_pass_interval: DEFAULT_MIN_PASS_INTERVAL,
pass_state: Arc::new(Mutex::new(PassState::default())),
}
}
}
impl TokenGasPriceComputation {
#[cfg(test)]
pub fn new(gas_token: Address, max_hops: usize, probe_amount: BigUint) -> Self {
Self {
gas_token,
max_hops,
probe_amount,
min_pass_interval: Duration::ZERO,
..Self::default()
}
}
pub fn with_pass_budget(self, pass_budget: Duration) -> Self {
Self { pass_budget, ..self }
}
pub fn with_max_tokens_per_pass(self, max_tokens_per_pass: usize) -> Self {
Self { max_tokens_per_pass, ..self }
}
pub fn with_min_pass_interval(self, min_pass_interval: Duration) -> Self {
Self { min_pass_interval, ..self }
}
fn lock_pass_state(&self) -> MutexGuard<'_, PassState> {
match self.pass_state.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
}
}
fn start_pass(&self, arrivals: bool, must_solve: bool) -> PassSlot {
let mut state = self.lock_pass_state();
let now = Instant::now();
let due = state
.last_pass_started
.is_none_or(|started| now.duration_since(started) >= self.min_pass_interval);
if due || must_solve {
state.last_pass_started = Some(now);
return PassSlot::Due;
}
if arrivals {
return PassSlot::ArrivalsOnly;
}
PassSlot::Deferred
}
fn pass_priority(
&self,
arrived: FxHashSet<Address>,
priced: FxHashSet<Address>,
) -> PassPriority {
let mut state = self.lock_pass_state();
state.pending_arrivals.extend(arrived);
PassPriority {
arrived: state.pending_arrivals.clone(),
priced,
last_attempted: state.last_attempted.clone(),
}
}
fn record_attempts(
&self,
selected: &FxHashSet<Address>,
outcome: &PricingPassOutcome,
universe: &FxHashSet<Address>,
) {
let mut state = self.lock_pass_state();
let pass = state.passes.wrapping_add(1);
state.passes = pass;
for token in selected {
if outcome.unattempted.contains(token) {
continue;
}
state
.last_attempted
.insert(token.clone(), pass);
state.pending_arrivals.remove(token);
}
state
.last_attempted
.retain(|token, _| universe.contains(token));
state
.pending_arrivals
.retain(|token| universe.contains(token));
}
pub fn with_max_hops(self, max_hops: usize) -> Self {
Self { max_hops, ..self }
}
pub fn with_gas_token(self, gas_token: Address) -> Self {
Self { gas_token, ..self }
}
async fn solve_token_prices(
&self,
market: &MarketData,
changed: Option<&FxHashSet<Address>>,
priority: &PassPriority,
scope: PassScope,
max_tokens: usize,
) -> Result<PricingPassOutcome, ComputationError> {
let (topology, block) = {
let guard = market.read().await;
let block = guard
.last_updated()
.map(|b| b.number())
.unwrap_or(0);
(guard.component_topology(), block)
};
let universe = self.tokens_to_price(&topology);
let ordered = select_pass_tokens(&universe, changed, priority, scope, max_tokens);
if ordered.is_empty() {
return Ok(PricingPassOutcome {
prices: FxHashMap::default(),
block,
failed_items: Vec::new(),
unattempted: universe,
});
}
let mut graph_manager = PetgraphStableDiGraphManager::new();
graph_manager.initialize_graph(&topology);
let config = AlgorithmConfig::new(1, self.max_hops, Duration::from_secs(1), None)
.map_err(|error| ComputationError::InvalidConfiguration(error.to_string()))?
.with_gas_aware(false);
let algorithm = BellmanFordAlgorithm::with_config(config);
let selected: FxHashSet<Address> = ordered.iter().cloned().collect();
let mut capped_out: FxHashSet<Address> = FxHashSet::default();
for token in &universe {
if !selected.contains(token) {
capped_out.insert(token.clone());
}
}
let graph = graph_manager.graph();
let Some(ctx) = algorithm
.build_context_from_source_token(
graph,
market.clone(),
&self.gas_token,
self.max_hops + 1,
Some(&selected),
)
.await
else {
warn!(unattempted = universe.len(), "no subgraph around the gas token");
return Ok(PricingPassOutcome {
prices: FxHashMap::default(),
block,
failed_items: Vec::new(),
unattempted: universe,
});
};
let block = ctx
.market_data
.last_updated()
.map_or(block, |b| b.number());
let computation = self.clone();
let span = Span::current();
let mut outcome = tokio::task::spawn_blocking(move || {
let _entered = span.enter();
let mut sell = PricingPass::new(&algorithm, graph_manager.graph(), ctx, &computation);
sell.sell_loop(ordered, block)
})
.await
.map_err(|join_error| {
ComputationError::Internal(format!("token pricing pass did not complete: {join_error}"))
})?;
outcome.unattempted.extend(capped_out);
self.record_attempts(&selected, &outcome, &universe);
Ok(outcome)
}
fn tokens_to_price(
&self,
topology: &FxHashMap<ComponentId, Vec<Address>>,
) -> FxHashSet<Address> {
topology
.values()
.flatten()
.filter(|token| *token != &self.gas_token)
.cloned()
.collect()
}
async fn update_prices(
&self,
market: &MarketData,
store: &SharedDerivedDataRef,
changed: &ChangedComponents,
scope: PassScope,
) -> Result<Option<ComputationOutput<TokenGasPrices>>, ComputationError> {
let (tokens_to_recompute, new_tokens, mut priority, existing_prices) = {
let store_guard = store.read().await;
let Some(existing_deps) = store_guard.token_prices_deps() else {
return Ok(None);
};
let Some(existing_prices) = store_guard.token_prices().cloned() else {
return Ok(None);
};
let changed_components = changed.all_changed_ids();
let mut tokens_to_recompute: FxHashSet<Address> = existing_deps
.iter()
.filter(|(_, entry)| {
!entry
.path_components
.is_disjoint(&changed_components)
})
.map(|(addr, _)| addr.clone())
.collect();
let mut arrived: FxHashSet<Address> = FxHashSet::default();
let mut new_tokens = 0usize;
for token in changed.added.values().flatten() {
if *token == self.gas_token || !arrived.insert(token.clone()) {
continue;
}
if !existing_deps.contains_key(token) {
new_tokens += 1;
}
tokens_to_recompute.insert(token.clone());
}
let priced = existing_deps.keys().cloned().collect();
let priority = self.pass_priority(arrived, priced);
(tokens_to_recompute, new_tokens, priority, existing_prices)
};
if scope == PassScope::ArrivalsOnly {
priority.keep_unpriced_arrivals();
if priority.arrived.is_empty() {
Span::current().record("updated_token_prices", existing_prices.len());
return Ok(Some(ComputationOutput::with_failures(existing_prices, Vec::new())));
}
}
debug!(
affected_tokens = tokens_to_recompute.len(),
new_tokens,
total_tokens = existing_prices.len(),
"incremental token price recomputation"
);
let solved = self
.solve_token_prices(
market,
Some(&tokens_to_recompute),
&priority,
scope,
self.max_tokens_per_pass,
)
.await?;
let mut result = existing_prices;
let edited = store
.write()
.await
.edit_token_prices_deps(solved.block, |deps| {
for (token, entry) in &solved.prices {
result.insert(token.clone(), entry.price.clone());
deps.insert(token.clone(), entry.clone());
}
let dropped: Vec<Address> = deps
.keys()
.filter(|token| {
**token != self.gas_token &&
!solved.prices.contains_key(*token) &&
!solved.unattempted.contains(*token)
})
.cloned()
.collect();
for token in dropped {
result.remove(&token);
deps.remove(&token);
}
});
if !edited {
warn!("token price dependencies vanished between the read and the write");
return Ok(None);
}
Span::current().record("updated_token_prices", result.len());
Ok(Some(ComputationOutput::with_failures(result, solved.failed_items)))
}
async fn seed_all_prices(
&self,
market: &MarketData,
store: &SharedDerivedDataRef,
) -> Result<ComputationOutput<TokenGasPrices>, ComputationError> {
let priority = {
let store_guard = store.read().await;
let priced = store_guard
.token_prices_deps()
.map(|deps| deps.keys().cloned().collect())
.unwrap_or_default();
self.pass_priority(FxHashSet::default(), priced)
};
let solved = self
.solve_token_prices(market, None, &priority, PassScope::Whole, usize::MAX)
.await?;
let mut token_prices_with_deps = TokenPricesWithDeps::default();
let mut token_prices = TokenGasPrices::default();
for (token, entry) in solved.prices {
token_prices.insert(token.clone(), entry.price.clone());
token_prices_with_deps.insert(token, entry);
}
if !solved.unattempted.is_empty() {
let store_guard = store.read().await;
if let Some(previous) = store_guard.token_prices_deps() {
for token in &solved.unattempted {
let Some(entry) = previous.get(token) else {
continue;
};
token_prices_with_deps.insert(token.clone(), entry.clone());
token_prices.insert(token.clone(), entry.price.clone());
}
}
}
let gas_token_price =
Price { numerator: self.probe_amount.clone(), denominator: self.probe_amount.clone() };
token_prices_with_deps.insert(
self.gas_token.clone(),
TokenPriceEntry {
price: gas_token_price.clone(),
path_components: FxHashSet::default(),
},
);
token_prices.insert(self.gas_token.clone(), gas_token_price);
store
.write()
.await
.set_token_prices_deps(token_prices_with_deps, solved.block);
debug!(priced = token_prices.len() - 1, "token price computation complete");
Span::current().record("updated_token_prices", token_prices.len());
Ok(ComputationOutput::with_failures(token_prices, solved.failed_items))
}
}
#[async_trait]
impl DerivedComputation for TokenGasPriceComputation {
type Output = TokenGasPrices;
const ID: ComputationId = "token_prices";
fn requirements(&self) -> ComputationRequirements {
ComputationRequirements::none()
}
fn persist(
store: &mut DerivedData,
output: ComputationOutput<Self::Output>,
block: u64,
is_full_recompute: bool,
) {
store.set_token_prices(output.data, output.failed_items, block, is_full_recompute);
}
#[instrument(
level = "debug",
skip_all,
fields(computation_id = Self::ID, updated_token_prices)
)]
async fn compute(
&self,
market: &MarketData,
store: &SharedDerivedDataRef,
changed: &ChangedComponents,
) -> Result<ComputationOutput<Self::Output>, ComputationError> {
let scope = match self.start_pass(!changed.added.is_empty(), changed.is_full_recompute) {
PassSlot::Due => PassScope::Whole,
PassSlot::ArrivalsOnly => PassScope::ArrivalsOnly,
PassSlot::Deferred => {
let stored = {
let store_guard = store.read().await;
store_guard.token_prices().cloned()
};
let Some(prices) = stored else {
return self
.seed_all_prices(market, store)
.await;
};
Span::current().record("updated_token_prices", prices.len());
return Ok(ComputationOutput::with_failures(prices, Vec::new()));
}
};
if !changed.is_full_recompute {
if let Some(result) = self
.update_prices(market, store, changed, scope)
.await?
{
return Ok(result);
}
}
self.seed_all_prices(market, store)
.await
}
}
#[cfg(test)]
mod tests {
use num_traits::ToPrimitive;
use tycho_simulation::tycho_core::models::token::Token;
use super::*;
use crate::{
algorithm::test_utils::{component, setup_market_weighted, token, MockProtocolSim},
derived::store::DerivedData,
};
const PROBE_AMOUNT: u128 = 1_000_000_000_000_000_000;
fn computation_for(gas_token: &Address) -> TokenGasPriceComputation {
TokenGasPriceComputation::new(gas_token.clone(), 3, BigUint::from(PROBE_AMOUNT))
}
fn ratio(price: &Price) -> f64 {
let numerator = price
.numerator
.to_f64()
.expect("price numerator fits in f64");
let denominator = price
.denominator
.to_f64()
.expect("price denominator fits in f64");
numerator / denominator
}
async fn prices_for(
gas_token: &Token,
pools: Vec<(&str, &Token, &Token, MockProtocolSim)>,
) -> TokenGasPrices {
let (market, _) = setup_market_weighted(pools);
let store = DerivedData::new_shared();
computation_for(&gas_token.address)
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail")
.data
}
#[tokio::test]
async fn test_price_via_direct_pool() {
let eth = token(0, "ETH");
let usdc = token(1, "USDC");
let prices =
prices_for(ð, vec![("eth_usdc", ð, &usdc, MockProtocolSim::new(2000.0))]).await;
assert!((ratio(&prices[&usdc.address]) - 2000.0).abs() < 1e-6);
}
#[tokio::test]
async fn test_gas_token_price() {
let eth = token(0, "ETH");
let usdc = token(1, "USDC");
let prices =
prices_for(ð, vec![("eth_usdc", ð, &usdc, MockProtocolSim::new(2000.0))]).await;
let eth_price = prices
.get(ð.address)
.expect("gas token should be priced");
assert_eq!(eth_price.numerator, BigUint::from(PROBE_AMOUNT));
assert_eq!(eth_price.denominator, BigUint::from(PROBE_AMOUNT));
}
#[tokio::test]
async fn test_price_with_pool_fee() {
let eth = token(0, "ETH");
let usdc = token(1, "USDC");
let prices = prices_for(
ð,
vec![("eth_usdc", ð, &usdc, MockProtocolSim::new(2000.0).with_fee(0.01))],
)
.await;
let expected_mean = (1980.0 + 2000.0 / 0.99) / 2.0;
assert!((ratio(&prices[&usdc.address]) - expected_mean).abs() < 1e-6);
}
#[tokio::test]
async fn test_parallel_pools_price_via_best_output() {
let eth = token(0, "ETH");
let usdc = token(1, "USDC");
let prices = prices_for(
ð,
vec![
("tight", ð, &usdc, MockProtocolSim::new(2000.0)),
("wide", ð, &usdc, MockProtocolSim::new(2500.0).with_fee(0.01)),
],
)
.await;
assert!((ratio(&prices[&usdc.address]) - 2237.5).abs() < 1e-6);
}
#[tokio::test]
async fn test_price_via_multi_hop_route() {
let eth = token(0, "ETH");
let mid = token(2, "MID");
let target = token(3, "TARGET");
let prices = prices_for(
ð,
vec![
("eth_mid", ð, &mid, MockProtocolSim::new(2.0)),
("mid_target", &mid, &target, MockProtocolSim::new(3.0)),
],
)
.await;
assert!((ratio(&prices[&target.address]) - 6.0).abs() < 1e-6);
}
#[tokio::test]
async fn test_price_at_exactly_max_hops() {
let eth = token(0, "ETH");
let mid = token(2, "MID");
let next = token(3, "NEXT");
let far = token(4, "FAR");
let prices = prices_for(
ð,
vec![
("eth_mid", ð, &mid, MockProtocolSim::new(2.0)),
("mid_next", &mid, &next, MockProtocolSim::new(2.0)),
("next_far", &next, &far, MockProtocolSim::new(2.0)),
],
)
.await;
assert!((ratio(&prices[&far.address]) - 8.0).abs() < 1e-6);
}
#[tokio::test]
async fn test_seeding_pass_past_deadline() {
let eth = token(0, "ETH");
let usdc = token(1, "USDC");
let (market, _) =
setup_market_weighted(vec![("eth_usdc", ð, &usdc, MockProtocolSim::new(2000.0))]);
let store = DerivedData::new_shared();
computation_for(ð.address)
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
let output = computation_for(ð.address)
.with_pass_budget(Duration::ZERO)
.compute(
&market,
&store,
&ChangedComponents { is_full_recompute: true, ..ChangedComponents::default() },
)
.await
.expect("pricing must not fail");
assert!((ratio(&output.data[&usdc.address]) - 2000.0).abs() < 1e-6);
assert!(output.failed_items.is_empty(), "an unattempted token is not a failure");
let guard = store.read().await;
assert!(
guard
.token_prices_deps()
.expect("deps are stored")
.contains_key(&usdc.address),
"carried tokens must stay visible to incremental invalidation"
);
}
#[tokio::test]
async fn test_vanished_gas_subgraph() {
let eth = token(0, "ETH");
let usdc = token(1, "USDC");
let aaa = token(2, "AAA");
let (market, _) = setup_market_weighted(vec![
("eth_usdc", ð, &usdc, MockProtocolSim::new(2000.0)),
("usdc_aaa", &usdc, &aaa, MockProtocolSim::new(1.0)),
]);
let store = DerivedData::new_shared();
computation_for(ð.address)
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
market
.write()
.await
.remove_components(["eth_usdc".to_string()].iter());
let output = computation_for(ð.address)
.compute(
&market,
&store,
&ChangedComponents { is_full_recompute: true, ..ChangedComponents::default() },
)
.await
.expect("pricing must not fail");
assert!((ratio(&output.data[&usdc.address]) - 2000.0).abs() < 1e-6);
assert!((ratio(&output.data[&aaa.address]) - 2000.0).abs() < 1e-6);
}
#[tokio::test]
async fn test_incremental_solve_past_deadline() {
let eth = token(0, "ETH");
let usdc = token(1, "USDC");
let (market, _) =
setup_market_weighted(vec![("eth_usdc", ð, &usdc, MockProtocolSim::new(2000.0))]);
let store = DerivedData::new_shared();
let full = computation_for(ð.address)
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
TokenGasPriceComputation::persist(&mut *store.write().await, full, 1, true);
let output = computation_for(ð.address)
.with_pass_budget(Duration::ZERO)
.compute(
&market,
&store,
&ChangedComponents {
updated: vec!["eth_usdc".to_string()],
..ChangedComponents::default()
},
)
.await
.expect("pricing must not fail");
assert!((ratio(&output.data[&usdc.address]) - 2000.0).abs() < 1e-6);
let guard = store.read().await;
assert!(
guard
.token_prices_deps()
.expect("deps are stored")
.contains_key(&usdc.address),
"a carried token must stay visible to incremental invalidation"
);
}
#[tokio::test]
async fn test_incremental_resolves_only_affected_tokens() {
use tycho_simulation::tycho_common::simulation::protocol_sim::ProtocolSim;
let eth = token(0, "ETH");
let aaa = token(1, "AAA");
let bbb = token(2, "BBB");
let (market, _) = setup_market_weighted(vec![
("eth_aaa", ð, &aaa, MockProtocolSim::new(2000.0)),
("eth_bbb", ð, &bbb, MockProtocolSim::new(2500.0)),
]);
let computation =
TokenGasPriceComputation::new(eth.address.clone(), 1, BigUint::from(PROBE_AMOUNT));
let store = DerivedData::new_shared();
let full = computation
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
TokenGasPriceComputation::persist(&mut *store.write().await, full, 1, true);
market.write().await.update_states([
("eth_aaa".to_string(), Box::new(MockProtocolSim::new(4000.0)) as Box<dyn ProtocolSim>),
("eth_bbb".to_string(), Box::new(MockProtocolSim::new(5000.0)) as Box<dyn ProtocolSim>),
]);
let output = computation
.compute(
&market,
&store,
&ChangedComponents {
updated: vec!["eth_aaa".to_string()],
..ChangedComponents::default()
},
)
.await
.expect("pricing must not fail");
assert!((ratio(&output.data[&aaa.address]) - 4000.0).abs() < 1e-6, "AAA re-solved");
assert!((ratio(&output.data[&bbb.address]) - 2500.0).abs() < 1e-6, "BBB untouched");
}
#[tokio::test]
async fn test_arrived_component_reprices_only_its_own_tokens() {
use tycho_simulation::tycho_common::simulation::protocol_sim::ProtocolSim;
let eth = token(0, "ETH");
let aaa = token(1, "AAA");
let bbb = token(2, "BBB");
let (market, _) = setup_market_weighted(vec![
("eth_aaa", ð, &aaa, MockProtocolSim::new(2000.0)),
("eth_bbb", ð, &bbb, MockProtocolSim::new(2500.0)),
]);
let computation =
TokenGasPriceComputation::new(eth.address.clone(), 1, BigUint::from(PROBE_AMOUNT));
let store = DerivedData::new_shared();
let full = computation
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
TokenGasPriceComputation::persist(&mut *store.write().await, full, 1, true);
market.write().await.update_states([
("eth_aaa".to_string(), Box::new(MockProtocolSim::new(4000.0)) as Box<dyn ProtocolSim>),
("eth_bbb".to_string(), Box::new(MockProtocolSim::new(5000.0)) as Box<dyn ProtocolSim>),
]);
let mut added = FxHashMap::default();
added.insert("eth_aaa_v2".to_string(), vec![eth.address.clone(), aaa.address.clone()]);
let output = computation
.compute(&market, &store, &ChangedComponents { added, ..ChangedComponents::default() })
.await
.expect("pricing must not fail");
assert!(
(ratio(&output.data[&aaa.address]) - 4000.0).abs() < 1e-6,
"the arrived component's token is re-priced"
);
assert!(
(ratio(&output.data[&bbb.address]) - 2500.0).abs() < 1e-6,
"a token no change points at keeps its stored price"
);
}
#[tokio::test]
async fn test_added_token_is_priced_immediately() {
use tycho_simulation::tycho_common::simulation::protocol_sim::ProtocolSim;
let eth = token(0, "ETH");
let aaa = token(1, "AAA");
let ccc = token(2, "CCC");
let (market, _) =
setup_market_weighted(vec![("eth_aaa", ð, &aaa, MockProtocolSim::new(2000.0))]);
let computation =
TokenGasPriceComputation::new(eth.address.clone(), 1, BigUint::from(PROBE_AMOUNT));
let store = DerivedData::new_shared();
let full = computation
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
TokenGasPriceComputation::persist(&mut *store.write().await, full, 1, true);
assert!(!store
.read()
.await
.token_prices()
.expect("priced")
.contains_key(&ccc.address));
{
let mut guard = market.write().await;
guard.upsert_tokens([ccc.clone()]);
guard.upsert_components([component("eth_ccc", &[eth.clone(), ccc.clone()])]);
guard.update_states([(
"eth_ccc".to_string(),
Box::new(MockProtocolSim::new(5000.0)) as Box<dyn ProtocolSim>,
)]);
}
let mut added = FxHashMap::default();
added.insert("eth_ccc".to_string(), vec![eth.address.clone(), ccc.address.clone()]);
let output = computation
.compute(&market, &store, &ChangedComponents { added, ..ChangedComponents::default() })
.await
.expect("pricing must not fail");
assert!(
(ratio(&output.data[&ccc.address]) - 5000.0).abs() < 1e-6,
"a token arriving with a new component is priced on the block it arrives"
);
assert!(
(ratio(&output.data[&aaa.address]) - 2000.0).abs() < 1e-6,
"an unrelated token is left alone"
);
}
#[tokio::test]
async fn test_removed_component_unprices_its_token_incrementally() {
let eth = token(0, "ETH");
let aaa = token(1, "AAA");
let bbb = token(2, "BBB");
let (market, _) = setup_market_weighted(vec![
("eth_aaa", ð, &aaa, MockProtocolSim::new(2000.0)),
("eth_bbb", ð, &bbb, MockProtocolSim::new(2500.0)),
]);
let computation =
TokenGasPriceComputation::new(eth.address.clone(), 1, BigUint::from(PROBE_AMOUNT));
let store = DerivedData::new_shared();
let full = computation
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
TokenGasPriceComputation::persist(&mut *store.write().await, full, 1, true);
market
.write()
.await
.remove_components([&"eth_aaa".to_string()]);
let output = computation
.compute(
&market,
&store,
&ChangedComponents {
removed: vec!["eth_aaa".to_string()],
..ChangedComponents::default()
},
)
.await
.expect("pricing must not fail");
assert!(!output.data.contains_key(&aaa.address), "AAA is unpriced, not stale");
assert!(
!store
.read()
.await
.token_prices_deps()
.expect("deps stored")
.contains_key(&aaa.address),
"AAA's dependencies are dropped with its price"
);
assert!((ratio(&output.data[&bbb.address]) - 2500.0).abs() < 1e-6, "BBB untouched");
}
#[test]
fn test_select_pass_tokens_ranks_arrived_unpriced_then_changed() {
let arrived_token = token(1, "AAA").address;
let unpriced = token(2, "BBB").address;
let changed_token = token(3, "CCC").address;
let untouched = token(4, "DDD").address;
let universe: FxHashSet<Address> =
[arrived_token.clone(), unpriced.clone(), changed_token.clone(), untouched.clone()]
.into_iter()
.collect();
let priority = PassPriority {
arrived: [arrived_token.clone()]
.into_iter()
.collect(),
priced: [arrived_token.clone(), changed_token.clone(), untouched.clone()]
.into_iter()
.collect(),
last_attempted: [
(arrived_token.clone(), 9),
(changed_token.clone(), 5),
(untouched.clone(), 1),
]
.into_iter()
.collect(),
};
let changed: FxHashSet<Address> = [changed_token.clone()]
.into_iter()
.collect();
let ordered =
select_pass_tokens(&universe, Some(&changed), &priority, PassScope::Whole, usize::MAX);
assert_eq!(
ordered,
vec![arrived_token, unpriced, changed_token],
"arrived first, then the unpriced, then what the change points at; a priced token \
no change points at is not a candidate however stale it is"
);
}
#[test]
fn test_select_pass_tokens_ranks_unpriced_arrivals_first() {
let with_price = token(1, "AAA").address;
let without_price = token(2, "BBB").address;
let universe: FxHashSet<Address> = [with_price.clone(), without_price.clone()]
.into_iter()
.collect();
let priority = PassPriority {
arrived: [with_price.clone(), without_price.clone()]
.into_iter()
.collect(),
priced: [with_price.clone()]
.into_iter()
.collect(),
last_attempted: [(with_price.clone(), 7)]
.into_iter()
.collect(),
};
let ordered =
select_pass_tokens(&universe, None, &priority, PassScope::ArrivalsOnly, usize::MAX);
assert_eq!(
ordered,
vec![without_price, with_price],
"the arrival with no price is attempted first"
);
}
#[test]
fn test_select_pass_tokens_caps_the_selection() {
let unpriced = token(1, "AAA").address;
let stale = token(2, "BBB").address;
let fresh = token(3, "CCC").address;
let universe: FxHashSet<Address> = [unpriced.clone(), stale.clone(), fresh.clone()]
.into_iter()
.collect();
let priority = PassPriority {
arrived: FxHashSet::default(),
priced: [stale.clone(), fresh.clone()]
.into_iter()
.collect(),
last_attempted: [(stale.clone(), 1), (fresh.clone(), 8)]
.into_iter()
.collect(),
};
let ordered = select_pass_tokens(&universe, None, &priority, PassScope::Whole, 2);
assert_eq!(ordered, vec![unpriced, stale], "the cap keeps the two highest ranks");
}
#[tokio::test]
async fn test_a_pass_inside_the_interval_serves_the_stored_prices() {
use tycho_simulation::tycho_common::simulation::protocol_sim::ProtocolSim;
let eth = token(0, "ETH");
let aaa = token(1, "AAA");
let (market, _) =
setup_market_weighted(vec![("eth_aaa", ð, &aaa, MockProtocolSim::new(2000.0))]);
let computation =
TokenGasPriceComputation::new(eth.address.clone(), 1, BigUint::from(PROBE_AMOUNT))
.with_min_pass_interval(Duration::from_secs(3600));
let store = DerivedData::new_shared();
let full = computation
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
TokenGasPriceComputation::persist(&mut *store.write().await, full, 1, true);
market.write().await.update_states([(
"eth_aaa".to_string(),
Box::new(MockProtocolSim::new(4000.0)) as Box<dyn ProtocolSim>,
)]);
let changed = ChangedComponents {
updated: vec!["eth_aaa".to_string()],
..ChangedComponents::default()
};
let output = computation
.compute(&market, &store, &changed)
.await
.expect("pricing must not fail");
assert!(
(ratio(&output.data[&aaa.address]) - 2000.0).abs() < 1e-6,
"the block inside the interval serves the stored price, not the moved market"
);
}
#[tokio::test]
async fn test_an_arriving_component_runs_a_pass_inside_the_interval() {
use tycho_simulation::tycho_common::simulation::protocol_sim::ProtocolSim;
let eth = token(0, "ETH");
let aaa = token(1, "AAA");
let ccc = token(2, "CCC");
let (market, _) =
setup_market_weighted(vec![("eth_aaa", ð, &aaa, MockProtocolSim::new(2000.0))]);
let computation =
TokenGasPriceComputation::new(eth.address.clone(), 1, BigUint::from(PROBE_AMOUNT))
.with_min_pass_interval(Duration::from_secs(3600));
let store = DerivedData::new_shared();
let full = computation
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
TokenGasPriceComputation::persist(&mut *store.write().await, full, 1, true);
{
let mut guard = market.write().await;
guard.upsert_tokens([ccc.clone()]);
guard.upsert_components([component("eth_ccc", &[eth.clone(), ccc.clone()])]);
guard.update_states([(
"eth_ccc".to_string(),
Box::new(MockProtocolSim::new(5000.0)) as Box<dyn ProtocolSim>,
)]);
}
let mut added = FxHashMap::default();
added.insert("eth_ccc".to_string(), vec![eth.address.clone(), ccc.address.clone()]);
let output = computation
.compute(&market, &store, &ChangedComponents { added, ..ChangedComponents::default() })
.await
.expect("pricing must not fail");
assert!(
(ratio(&output.data[&ccc.address]) - 5000.0).abs() < 1e-6,
"an arriving token is priced although the interval has not elapsed"
);
}
#[tokio::test]
async fn test_a_pool_for_a_priced_token_waits_for_the_interval() {
use tycho_simulation::tycho_common::simulation::protocol_sim::ProtocolSim;
let eth = token(0, "ETH");
let aaa = token(1, "AAA");
let (market, _) =
setup_market_weighted(vec![("eth_aaa", ð, &aaa, MockProtocolSim::new(2000.0))]);
let computation =
TokenGasPriceComputation::new(eth.address.clone(), 1, BigUint::from(PROBE_AMOUNT))
.with_min_pass_interval(Duration::from_secs(3600));
let store = DerivedData::new_shared();
let seeded = computation
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
TokenGasPriceComputation::persist(&mut *store.write().await, seeded, 1, true);
let mut added = FxHashMap::default();
{
let mut guard = market.write().await;
guard.upsert_components([component("eth_aaa_2", &[eth.clone(), aaa.clone()])]);
guard.update_states([(
"eth_aaa_2".to_string(),
Box::new(MockProtocolSim::new(9000.0)) as Box<dyn ProtocolSim>,
)]);
added.insert("eth_aaa_2".to_string(), vec![eth.address.clone(), aaa.address.clone()]);
}
let output = computation
.compute(&market, &store, &ChangedComponents { added, ..ChangedComponents::default() })
.await
.expect("pricing must not fail");
assert!(
(ratio(&output.data[&aaa.address]) - 2000.0).abs() < 1e-6,
"AAA has a price already, so its new pool runs no pass inside the interval"
);
}
#[tokio::test]
async fn test_a_cut_arrival_is_attempted_by_the_next_pass() {
use tycho_simulation::tycho_common::simulation::protocol_sim::ProtocolSim;
let eth = token(0, "ETH");
let aaa = token(1, "AAA");
let ccc = token(2, "CCC");
let (market, _) =
setup_market_weighted(vec![("eth_aaa", ð, &aaa, MockProtocolSim::new(2000.0))]);
let computation =
TokenGasPriceComputation::new(eth.address.clone(), 1, BigUint::from(PROBE_AMOUNT))
.with_max_tokens_per_pass(1);
let store = DerivedData::new_shared();
let seeded = computation
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
TokenGasPriceComputation::persist(&mut *store.write().await, seeded, 1, true);
let mut added = FxHashMap::default();
{
let mut guard = market.write().await;
guard.upsert_tokens([ccc.clone()]);
guard.upsert_components([
component("eth_aaa_2", &[eth.clone(), aaa.clone()]),
component("eth_ccc", &[eth.clone(), ccc.clone()]),
]);
guard.update_states([
(
"eth_aaa_2".to_string(),
Box::new(MockProtocolSim::new(9000.0)) as Box<dyn ProtocolSim>,
),
(
"eth_ccc".to_string(),
Box::new(MockProtocolSim::new(5000.0)) as Box<dyn ProtocolSim>,
),
]);
added.insert("eth_aaa_2".to_string(), vec![eth.address.clone(), aaa.address.clone()]);
added.insert("eth_ccc".to_string(), vec![eth.address.clone(), ccc.address.clone()]);
}
let cut = computation
.compute(&market, &store, &ChangedComponents { added, ..ChangedComponents::default() })
.await
.expect("pricing must not fail");
assert!(
(ratio(&cut.data[&aaa.address]) - 2000.0).abs() < 1e-6,
"the cap took the token with no price, so AAA keeps its old price"
);
TokenGasPriceComputation::persist(&mut *store.write().await, cut, 2, false);
let next = computation
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
assert!(
ratio(&next.data[&aaa.address]) > 2000.0,
"the next pass attempts the cut arrival and re-prices it through its new pool"
);
}
#[tokio::test]
async fn test_an_arrivals_pass_is_capped() {
use tycho_simulation::tycho_common::simulation::protocol_sim::ProtocolSim;
let eth = token(0, "ETH");
let aaa = token(1, "AAA");
let arriving = [token(2, "BBB"), token(3, "CCC"), token(4, "DDD")];
let (market, _) =
setup_market_weighted(vec![("eth_aaa", ð, &aaa, MockProtocolSim::new(2000.0))]);
let computation =
TokenGasPriceComputation::new(eth.address.clone(), 1, BigUint::from(PROBE_AMOUNT))
.with_min_pass_interval(Duration::from_secs(3600))
.with_max_tokens_per_pass(2);
let store = DerivedData::new_shared();
let seeded = computation
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
TokenGasPriceComputation::persist(&mut *store.write().await, seeded, 1, true);
let mut added = FxHashMap::default();
{
let mut guard = market.write().await;
for arrival in &arriving {
let id = format!("eth_{}", arrival.symbol.to_lowercase());
guard.upsert_tokens([arrival.clone()]);
guard.upsert_components([component(&id, &[eth.clone(), arrival.clone()])]);
guard.update_states([(
id.clone(),
Box::new(MockProtocolSim::new(5000.0)) as Box<dyn ProtocolSim>,
)]);
added.insert(id, vec![eth.address.clone(), arrival.address.clone()]);
}
}
let output = computation
.compute(&market, &store, &ChangedComponents { added, ..ChangedComponents::default() })
.await
.expect("pricing must not fail");
let priced = arriving
.iter()
.filter(|arrival| {
output
.data
.contains_key(&arrival.address)
})
.count();
assert_eq!(
priced, 2,
"three components arrive inside the interval and the cap of two holds; the third waits for the next pass"
);
}
#[test]
fn test_an_arrival_does_not_restart_the_interval() {
let eth = token(0, "ETH").address;
let computation = TokenGasPriceComputation::new(eth, 1, BigUint::from(PROBE_AMOUNT))
.with_min_pass_interval(Duration::from_millis(400));
assert_eq!(
computation.start_pass(false, false),
PassSlot::Due,
"the first pass is due, nothing has run"
);
assert_eq!(
computation.start_pass(false, false),
PassSlot::Deferred,
"a block straight after one inside the interval waits"
);
std::thread::sleep(Duration::from_millis(150));
assert_eq!(
computation.start_pass(true, false),
PassSlot::ArrivalsOnly,
"an arrival inside the interval earns a pass for its own tokens"
);
std::thread::sleep(Duration::from_millis(300));
assert_eq!(
computation.start_pass(false, false),
PassSlot::Due,
"450ms after the only whole pass the interval has elapsed, so the arrival in the \
middle of it did not restart it"
);
}
#[test]
fn test_a_must_solve_pass_ignores_the_interval() {
let eth = token(0, "ETH").address;
let computation = TokenGasPriceComputation::new(eth, 1, BigUint::from(PROBE_AMOUNT))
.with_min_pass_interval(Duration::from_secs(3600));
assert_eq!(computation.start_pass(false, false), PassSlot::Due);
assert_eq!(computation.start_pass(false, false), PassSlot::Deferred);
assert_eq!(
computation.start_pass(false, true),
PassSlot::Due,
"a full recompute has nothing to serve instead, so it runs a whole pass"
);
}
#[tokio::test]
async fn test_unpriceable_tokens_do_not_hold_a_slot_every_pass() {
use tycho_simulation::tycho_common::simulation::protocol_sim::ProtocolSim;
let eth = token(0, "ETH");
let aaa = token(1, "AAA");
let dead = |n: u8| {
(
format!("eth_dead{n}"),
token(10 + n, "DEAD"),
MockProtocolSim::new(0.5).with_liquidity(600_000_000_000_000_000),
)
};
let (dead0, dead1, dead2) = (dead(0), dead(1), dead(2));
let (market, _) = setup_market_weighted(vec![
("eth_aaa", ð, &aaa, MockProtocolSim::new(2000.0)),
(dead0.0.as_str(), ð, &dead0.1, dead0.2.clone()),
(dead1.0.as_str(), ð, &dead1.1, dead1.2.clone()),
(dead2.0.as_str(), ð, &dead2.1, dead2.2.clone()),
]);
let computation =
TokenGasPriceComputation::new(eth.address.clone(), 1, BigUint::from(PROBE_AMOUNT))
.with_max_tokens_per_pass(2);
let store = DerivedData::new_shared();
let seeded = computation
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
TokenGasPriceComputation::persist(&mut *store.write().await, seeded, 1, true);
market.write().await.update_states([(
"eth_aaa".to_string(),
Box::new(MockProtocolSim::new(4000.0)) as Box<dyn ProtocolSim>,
)]);
let mut repriced = false;
for block in 2..=5 {
let changed = ChangedComponents {
updated: vec!["eth_aaa".to_string()],
..ChangedComponents::default()
};
let output = computation
.compute(&market, &store, &changed)
.await
.expect("pricing must not fail");
if (ratio(&output.data[&aaa.address]) - 4000.0).abs() < 1e-6 {
repriced = true;
break;
}
TokenGasPriceComputation::persist(&mut *store.write().await, output, block, false);
}
assert!(repriced, "the priced token is refreshed rather than starved by failing tokens");
}
#[tokio::test]
async fn test_unpriced_tokens_are_reached_without_a_change_pointing_at_them() {
use tycho_simulation::tycho_common::simulation::protocol_sim::ProtocolSim;
let eth = token(0, "ETH");
let aaa = token(1, "AAA");
let oneway = token(2, "ONEWAY");
let (market, _) = setup_market_weighted(vec![
("eth_aaa", ð, &aaa, MockProtocolSim::new(2000.0)),
(
"eth_oneway",
ð,
&oneway,
MockProtocolSim::new(0.5).with_liquidity(600_000_000_000_000_000),
),
]);
let computation =
TokenGasPriceComputation::new(eth.address.clone(), 1, BigUint::from(PROBE_AMOUNT));
let store = DerivedData::new_shared();
let full = computation
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
TokenGasPriceComputation::persist(&mut *store.write().await, full, 1, true);
assert!(
!store
.read()
.await
.token_prices()
.expect("prices are stored")
.contains_key(&oneway.address),
"the unsellable token starts with no price and no dependency set"
);
market.write().await.update_states([(
"eth_oneway".to_string(),
Box::new(MockProtocolSim::new(3000.0)) as Box<dyn ProtocolSim>,
)]);
let changed = ChangedComponents {
updated: vec!["eth_oneway".to_string()],
..ChangedComponents::default()
};
let output = computation
.compute(&market, &store, &changed)
.await
.expect("pricing must not fail");
assert!(
output
.data
.contains_key(&oneway.address),
"a token that had no price is priced once it becomes sellable"
);
assert!(
(ratio(&output.data[&aaa.address]) - 2000.0).abs() < 1e-6,
"the already-priced token keeps its price"
);
}
#[tokio::test]
async fn test_the_seeding_pass_is_not_capped() {
let eth = token(0, "ETH");
let aaa = token(1, "AAA");
let bbb = token(2, "BBB");
let ccc = token(3, "CCC");
let (market, _) = setup_market_weighted(vec![
("eth_aaa", ð, &aaa, MockProtocolSim::new(2000.0)),
("eth_bbb", ð, &bbb, MockProtocolSim::new(2500.0)),
("eth_ccc", ð, &ccc, MockProtocolSim::new(3000.0)),
]);
let computation =
TokenGasPriceComputation::new(eth.address.clone(), 1, BigUint::from(PROBE_AMOUNT))
.with_max_tokens_per_pass(1);
let store = DerivedData::new_shared();
let output = computation
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
for (name, address) in [("AAA", &aaa.address), ("BBB", &bbb.address), ("CCC", &ccc.address)]
{
assert!(
output.data.contains_key(address),
"{name} is priced by the startup solve although the cap is 1"
);
}
}
#[tokio::test]
async fn test_a_pass_stamps_only_the_tokens_it_reprices() {
fn stamp(computation: &TokenGasPriceComputation, token: &Address) -> u64 {
computation
.lock_pass_state()
.last_attempted
.get(token)
.copied()
.expect("the token was attempted")
}
let eth = token(0, "ETH");
let aaa = token(1, "AAA");
let bbb = token(2, "BBB");
let (market, _) = setup_market_weighted(vec![
("eth_aaa", ð, &aaa, MockProtocolSim::new(2000.0)),
("eth_bbb", ð, &bbb, MockProtocolSim::new(2500.0)),
]);
let computation =
TokenGasPriceComputation::new(eth.address.clone(), 1, BigUint::from(PROBE_AMOUNT));
let store = DerivedData::new_shared();
let full = computation
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
TokenGasPriceComputation::persist(&mut *store.write().await, full, 1, true);
let first_pass = stamp(&computation, &aaa.address);
assert_eq!(stamp(&computation, &bbb.address), first_pass, "one pass priced both");
let changed = ChangedComponents {
updated: vec!["eth_aaa".to_string()],
..ChangedComponents::default()
};
let incremental = computation
.compute(&market, &store, &changed)
.await
.expect("pricing must not fail");
TokenGasPriceComputation::persist(&mut *store.write().await, incremental, 2, false);
assert!(
stamp(&computation, &aaa.address) > first_pass,
"the re-priced token carries the newer pass"
);
assert_eq!(
stamp(&computation, &bbb.address),
first_pass,
"a token the pass left alone keeps its stamp, so it now ranks ahead of AAA"
);
}
#[tokio::test]
async fn test_incremental_with_disjoint_change_keeps_all_prices() {
use tycho_simulation::tycho_common::simulation::protocol_sim::ProtocolSim;
let eth = token(0, "ETH");
let usdc = token(1, "USDC");
let (market, _) =
setup_market_weighted(vec![("eth_usdc", ð, &usdc, MockProtocolSim::new(2000.0))]);
let computation = computation_for(ð.address);
let store = DerivedData::new_shared();
let full = computation
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
TokenGasPriceComputation::persist(&mut *store.write().await, full, 1, true);
market.write().await.update_states([(
"eth_usdc".to_string(),
Box::new(MockProtocolSim::new(9000.0)) as Box<dyn ProtocolSim>,
)]);
let output = computation
.compute(
&market,
&store,
&ChangedComponents {
updated: vec!["unrelated_pool".to_string()],
..ChangedComponents::default()
},
)
.await
.expect("pricing must not fail");
assert!((ratio(&output.data[&usdc.address]) - 2000.0).abs() < 1e-6);
}
#[tokio::test]
async fn test_token_without_sell_route_is_a_failed_item() {
let eth = token(0, "ETH");
let oneway = token(1, "ONEWAY");
let (market, _) = setup_market_weighted(vec![(
"eth_oneway",
ð,
&oneway,
MockProtocolSim::new(0.5).with_liquidity(600_000_000_000_000_000),
)]);
let store = DerivedData::new_shared();
let output = computation_for(ð.address)
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
assert!(
!output
.data
.contains_key(&oneway.address),
"an unsellable token has no price"
);
assert_eq!(output.failed_items.len(), 1);
assert_eq!(output.failed_items[0].key, oneway.address.to_string());
let FailedItemError::MissingSellRoute(reason) = &output.failed_items[0].error else {
panic!("expected MissingSellRoute, got {:?}", output.failed_items[0].error);
};
assert!(!reason.is_empty(), "the failure carries why the sell solve failed");
}
#[tokio::test]
async fn test_incremental_removes_unsellable_token() {
use tycho_simulation::tycho_common::simulation::protocol_sim::ProtocolSim;
let eth = token(0, "ETH");
let oneway = token(1, "ONEWAY");
let (market, _) =
setup_market_weighted(vec![("eth_oneway", ð, &oneway, MockProtocolSim::new(0.5))]);
let store = DerivedData::new_shared();
let computation = computation_for(ð.address);
let full = computation
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
TokenGasPriceComputation::persist(&mut *store.write().await, full, 1, true);
market.write().await.update_states([(
"eth_oneway".to_string(),
Box::new(MockProtocolSim::new(0.5).with_liquidity(600_000_000_000_000_000))
as Box<dyn ProtocolSim>,
)]);
let output = computation
.compute(
&market,
&store,
&ChangedComponents {
updated: vec!["eth_oneway".to_string()],
..ChangedComponents::default()
},
)
.await
.expect("pricing must not fail");
assert!(
!output
.data
.contains_key(&oneway.address),
"an unsellable token has no price"
);
let guard = store.read().await;
assert!(
!guard
.token_prices_deps()
.expect("deps are stored")
.contains_key(&oneway.address),
"a dropped token must leave the dependency map too"
);
}
#[tokio::test]
async fn test_deps_cover_rival_routes() {
let eth = token(0, "ETH");
let usdc = token(1, "USDC");
let mid = token(2, "MID");
let (market, _) = setup_market_weighted(vec![
("direct", ð, &usdc, MockProtocolSim::new(2000.0)),
("eth_mid", ð, &mid, MockProtocolSim::new(1.0)),
("mid_usdc", &mid, &usdc, MockProtocolSim::new(1500.0)),
]);
let store = DerivedData::new_shared();
computation_for(ð.address)
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
let guard = store.read().await;
let deps = &guard
.token_prices_deps()
.expect("deps are stored")[&usdc.address]
.path_components;
for component in ["direct", "eth_mid", "mid_usdc"] {
assert!(deps.contains(component), "{component} must invalidate USDC's price");
}
}
#[tokio::test]
async fn test_empty_market() {
let eth = token(0, "ETH");
let prices = prices_for(ð, vec![]).await;
assert_eq!(prices.len(), 1);
assert!((ratio(&prices[ð.address]) - 1.0).abs() < 1e-9);
}
#[tokio::test]
async fn test_gas_token_outside_graph() {
let eth = token(0, "ETH");
let aaa = token(1, "AAA");
let bbb = token(2, "BBB");
let (market, _) =
setup_market_weighted(vec![("aaa_bbb", &aaa, &bbb, MockProtocolSim::new(1.0))]);
let store = DerivedData::new_shared();
let output = computation_for(ð.address)
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
assert_eq!(output.data.len(), 1);
assert!((ratio(&output.data[ð.address]) - 1.0).abs() < 1e-9);
assert!(output.failed_items.is_empty(), "unreachable tokens are not failures");
}
#[tokio::test]
async fn test_unreachable_token() {
let eth = token(0, "ETH");
let usdc = token(1, "USDC");
let island = token(4, "ISLAND");
let other = token(5, "OTHER");
let (market, _) = setup_market_weighted(vec![
("eth_usdc", ð, &usdc, MockProtocolSim::new(2000.0)),
("island_other", &island, &other, MockProtocolSim::new(1.0)),
]);
let store = DerivedData::new_shared();
let output = computation_for(ð.address)
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
assert!(output.data.contains_key(&usdc.address));
assert!(
!output
.data
.contains_key(&island.address),
"an unreachable token has no price"
);
assert!(
output.failed_items.is_empty(),
"unreachable tokens are counted, not reported as failed items"
);
}
}