use std::fmt;
use num_bigint::BigUint;
use num_traits::ToPrimitive;
use rustc_hash::FxHashMap;
use tycho_simulation::tycho_common::models::{token::Token, Address};
use crate::types::{quote::branch_collections, ComponentId, Route, Swap};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AmountSource {
RouteInput,
RouteOutput,
SwapInput,
}
impl fmt::Display for AmountSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AmountSource::RouteInput => f.write_str("route input"),
AmountSource::RouteOutput => f.write_str("route output"),
AmountSource::SwapInput => f.write_str("swap input"),
}
}
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum PriceImpactError {
#[error("route has no swaps")]
EmptyRoute,
#[error("the route's token map has no entry for {0}")]
UnknownToken(Address),
#[error(
"component {component_id} could not report spot_price for ({token_in}, {token_out}): \
{reason}"
)]
SpotPriceQueryFailed {
component_id: ComponentId,
token_in: Address,
token_out: Address,
reason: String,
},
#[error("{kind} amount {amount} cannot be represented as a finite f64")]
AmountOutOfRange { kind: AmountSource, amount: BigUint },
#[error("a swap consumes {0} before any swap produced it")]
UnfedToken(Address),
#[error("swaps from token {0} consume zero input")]
ZeroConsumedInput(Address),
#[error(
"spot_price for ({token_in}, {token_out}) returned {price}, which is not a positive \
finite number"
)]
InvalidSpotPrice { token_in: Address, token_out: Address, price: f64 },
#[error("the route's output at spot prices is not a positive finite number")]
NoReferenceOutput,
#[error("price impact {impact} cannot be represented as basis points in an i32")]
BasisPointsOutOfRange { impact: f64 },
}
impl PriceImpactError {
pub(crate) fn outcome(&self) -> &'static str {
match self {
PriceImpactError::EmptyRoute => "empty_route",
PriceImpactError::UnknownToken(_) => "unknown_token",
PriceImpactError::SpotPriceQueryFailed { .. } => "spot_price_query_failed",
PriceImpactError::AmountOutOfRange { .. } => "amount_out_of_range",
PriceImpactError::UnfedToken(_) => "unfed_token",
PriceImpactError::ZeroConsumedInput(_) => "zero_consumed_input",
PriceImpactError::InvalidSpotPrice { .. } => "invalid_spot_price",
PriceImpactError::NoReferenceOutput => "no_reference_output",
PriceImpactError::BasisPointsOutOfRange { .. } => "basis_points_out_of_range",
}
}
}
pub(crate) struct SpotLeg<'a> {
pub token_in: &'a Address,
pub token_out: &'a Address,
pub amount_in_raw: f64,
pub reported_spot_price: f64,
}
pub(crate) struct PriceImpactInputs<'a> {
pub token_in: &'a Address,
pub token_out: &'a Address,
pub amount_in_raw: &'a BigUint,
pub amount_out_raw: &'a BigUint,
pub input_decimals: u32,
pub output_decimals: u32,
}
pub(crate) fn price_impact_from_spot_legs(
legs: &[SpotLeg<'_>],
inputs: &PriceImpactInputs<'_>,
) -> Result<f64, PriceImpactError> {
if legs.is_empty() {
return Err(PriceImpactError::EmptyRoute);
}
let spot_reference_output_human = spot_reference_output(legs, inputs)?;
let executed_output_human = raw_to_human_units(
inputs.amount_out_raw,
inputs.output_decimals,
AmountSource::RouteOutput,
)?;
Ok(1.0 - executed_output_human / spot_reference_output_human)
}
pub(crate) fn price_impact_to_basis_points(impact: f64) -> Result<i32, PriceImpactError> {
let rounded_basis_points = (impact * 10_000.0).round();
if !rounded_basis_points.is_finite() ||
rounded_basis_points < f64::from(i32::MIN) ||
rounded_basis_points > f64::from(i32::MAX)
{
return Err(PriceImpactError::BasisPointsOutOfRange { impact });
}
Ok(rounded_basis_points as i32)
}
fn spot_reference_output(
legs: &[SpotLeg<'_>],
inputs: &PriceImpactInputs<'_>,
) -> Result<f64, PriceImpactError> {
let mut reference_human_by_token: FxHashMap<&Address, f64> = FxHashMap::default();
reference_human_by_token.insert(
inputs.token_in,
raw_to_human_units(inputs.amount_in_raw, inputs.input_decimals, AmountSource::RouteInput)?,
);
let mut reference_output_human = 0.0_f64;
for (token_in, collection) in branch_collections(legs, |leg| leg.token_in) {
let available_reference_input_human = reference_human_by_token
.get(&token_in)
.copied()
.ok_or_else(|| PriceImpactError::UnfedToken(token_in.clone()))?;
let consumed_input_raw_total: f64 = collection
.iter()
.map(|leg| leg.amount_in_raw)
.sum();
if consumed_input_raw_total <= 0.0 {
return Err(PriceImpactError::ZeroConsumedInput(token_in));
}
for leg in collection {
if !(leg.reported_spot_price.is_finite() && leg.reported_spot_price > 0.0) {
return Err(PriceImpactError::InvalidSpotPrice {
token_in: leg.token_in.clone(),
token_out: leg.token_out.clone(),
price: leg.reported_spot_price,
});
}
let share = leg.amount_in_raw / consumed_input_raw_total;
let reference_output_contribution_human =
available_reference_input_human * share * leg.reported_spot_price;
if leg.token_out == inputs.token_out {
reference_output_human += reference_output_contribution_human;
} else {
*reference_human_by_token
.entry(leg.token_out)
.or_insert(0.0) += reference_output_contribution_human;
}
}
}
if reference_output_human.is_finite() && reference_output_human > 0.0 {
Ok(reference_output_human)
} else {
Err(PriceImpactError::NoReferenceOutput)
}
}
fn raw_to_human_units(
raw: &BigUint,
decimals: u32,
kind: AmountSource,
) -> Result<f64, PriceImpactError> {
let raw_f64 = raw
.to_f64()
.filter(|value| value.is_finite())
.ok_or_else(|| PriceImpactError::AmountOutOfRange { kind, amount: raw.clone() })?;
Ok(raw_f64 / 10f64.powi(decimals as i32))
}
fn reported_spot_price(
swap: &Swap,
token_in: &Token,
token_out: &Token,
) -> Result<f64, PriceImpactError> {
swap.protocol_state()
.spot_price(token_in, token_out)
.map_err(|err| PriceImpactError::SpotPriceQueryFailed {
component_id: swap.component_id().to_string(),
token_in: token_in.address.clone(),
token_out: token_out.address.clone(),
reason: err.to_string(),
})
}
pub(crate) fn route_price_impact(
route: &Route,
amount_in_raw: &BigUint,
amount_out_raw: &BigUint,
) -> Result<f64, PriceImpactError> {
let swaps = route.swaps();
let (Some(first), Some(last)) = (swaps.first(), swaps.last()) else {
return Err(PriceImpactError::EmptyRoute);
};
let token_of = |address: &Address| {
route
.tokens()
.get(address)
.ok_or_else(|| PriceImpactError::UnknownToken(address.clone()))
};
let mut legs = Vec::with_capacity(swaps.len());
for swap in swaps {
let reported_spot_price =
reported_spot_price(swap, token_of(swap.token_in())?, token_of(swap.token_out())?)?;
let swap_amount_in_raw = swap
.amount_in()
.to_f64()
.filter(|value| value.is_finite())
.ok_or_else(|| PriceImpactError::AmountOutOfRange {
kind: AmountSource::SwapInput,
amount: swap.amount_in().clone(),
})?;
legs.push(SpotLeg {
token_in: swap.token_in(),
token_out: swap.token_out(),
amount_in_raw: swap_amount_in_raw,
reported_spot_price,
});
}
let inputs = PriceImpactInputs {
token_in: first.token_in(),
token_out: last.token_out(),
amount_in_raw,
amount_out_raw,
input_decimals: token_of(first.token_in())?.decimals,
output_decimals: token_of(last.token_out())?.decimals,
};
price_impact_from_spot_legs(&legs, &inputs)
}
#[cfg(test)]
mod tests {
use num_bigint::BigUint;
use tycho_simulation::tycho_core::{
dto::ProtocolStateDelta,
models::token::Token,
simulation::{
errors::{SimulationError, TransitionError},
protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
},
Bytes,
};
use super::*;
use crate::algorithm::test_utils::{
addr, component, token, token_with_decimals, MockProtocolSim,
};
fn parse_biguint(s: &str) -> BigUint {
s.parse().unwrap()
}
fn inputs<'a>(
token_in: &'a Address,
token_out: &'a Address,
amount_in_raw: &'a BigUint,
amount_out_raw: &'a BigUint,
) -> PriceImpactInputs<'a> {
PriceImpactInputs {
token_in,
token_out,
amount_in_raw,
amount_out_raw,
input_decimals: 0,
output_decimals: 0,
}
}
#[test]
fn test_single_hop_near_one_to_one() {
let dai = addr(0x01);
let usdc = addr(0x02);
let legs = [SpotLeg {
token_in: &dai,
token_out: &usdc,
amount_in_raw: 1e24,
reported_spot_price: 0.9998,
}];
let (amount_in, amount_out) =
(parse_biguint("1000000000000000000000000"), parse_biguint("999843730000"));
let impact_inputs = PriceImpactInputs {
input_decimals: 18,
output_decimals: 6,
..inputs(&dai, &usdc, &amount_in, &amount_out)
};
let impact = price_impact_from_spot_legs(&legs, &impact_inputs).unwrap();
assert!(impact.abs() < 0.001, "expected ~0 impact, got {impact}");
}
#[test]
fn test_linear_route_multiplies_reported_spot_prices() {
let a = addr(0x01);
let b = addr(0x02);
let c = addr(0x03);
let legs = [
SpotLeg {
token_in: &a,
token_out: &b,
amount_in_raw: 1000.0,
reported_spot_price: 2.0,
},
SpotLeg {
token_in: &b,
token_out: &c,
amount_in_raw: 1990.0,
reported_spot_price: 0.5,
},
];
let (amount_in, amount_out) = (parse_biguint("1000"), parse_biguint("980"));
let impact_inputs = inputs(&a, &c, &amount_in, &amount_out);
let impact = price_impact_from_spot_legs(&legs, &impact_inputs).unwrap();
assert!((impact - 0.02).abs() < 1e-9, "got {impact}");
}
#[test]
fn test_favorable_execution_is_negative() {
let a = addr(0x01);
let b = addr(0x02);
let legs = [SpotLeg {
token_in: &a,
token_out: &b,
amount_in_raw: 100.0,
reported_spot_price: 1.0,
}];
let (amount_in, amount_out) = (parse_biguint("100"), parse_biguint("101"));
let impact_inputs = inputs(&a, &b, &amount_in, &amount_out);
let impact = price_impact_from_spot_legs(&legs, &impact_inputs).unwrap();
assert!(impact < 0.0, "got {impact}");
}
#[test]
fn test_basis_points_conversion_rejects_unrepresentable_impact() {
assert_eq!(price_impact_to_basis_points(-1.5).unwrap(), -15_000);
let a = addr(0x01);
let b = addr(0x02);
let legs = [SpotLeg {
token_in: &a,
token_out: &b,
amount_in_raw: 1.0,
reported_spot_price: 1e-300,
}];
let (amount_in, amount_out) = (parse_biguint("1"), parse_biguint("1"));
let impact_inputs = inputs(&a, &b, &amount_in, &amount_out);
let impact = price_impact_from_spot_legs(&legs, &impact_inputs).unwrap();
let err = price_impact_to_basis_points(impact).unwrap_err();
assert_eq!(err.outcome(), "basis_points_out_of_range");
let PriceImpactError::BasisPointsOutOfRange { impact: rejected } = err else {
panic!("expected BasisPointsOutOfRange, got {err}");
};
assert_eq!(rejected, impact);
}
#[test]
fn test_zero_reported_spot_price() {
let a = addr(0x01);
let b = addr(0x02);
let legs = [SpotLeg {
token_in: &a,
token_out: &b,
amount_in_raw: 100.0,
reported_spot_price: 0.0,
}];
let (amount_in, amount_out) = (parse_biguint("100"), parse_biguint("100"));
let impact_inputs = inputs(&a, &b, &amount_in, &amount_out);
let err = price_impact_from_spot_legs(&legs, &impact_inputs).unwrap_err();
let PriceImpactError::InvalidSpotPrice { price, .. } = err else {
panic!("expected InvalidSpotPrice, got {err}");
};
assert_eq!(price, 0.0);
}
#[test]
fn test_endpoint_decimals() {
let weth = addr(0x01);
let usdc = addr(0x02);
let legs = [SpotLeg {
token_in: &weth,
token_out: &usdc,
amount_in_raw: 1e18,
reported_spot_price: 2000.0,
}];
let (amount_in, amount_out) =
(parse_biguint("1000000000000000000"), parse_biguint("2000000000"));
let impact_inputs = PriceImpactInputs {
input_decimals: 18,
output_decimals: 6,
..inputs(&weth, &usdc, &amount_in, &amount_out)
};
let impact = price_impact_from_spot_legs(&legs, &impact_inputs).unwrap();
assert!(impact.abs() < 1e-6, "got {impact}");
}
#[test]
fn test_amount_beyond_f64_names_its_source() {
let a = addr(0x01);
let b = addr(0x02);
let huge = "1".to_string() + &"0".repeat(400);
let legs =
[SpotLeg { token_in: &a, token_out: &b, amount_in_raw: 1.0, reported_spot_price: 1.0 }];
let (amount_in, amount_out) = (parse_biguint(&huge), parse_biguint("1"));
let impact_inputs = inputs(&a, &b, &amount_in, &amount_out);
let err = price_impact_from_spot_legs(&legs, &impact_inputs).unwrap_err();
let PriceImpactError::AmountOutOfRange { kind, .. } = err else {
panic!("expected AmountOutOfRange, got {err}");
};
assert_eq!(kind, AmountSource::RouteInput);
}
#[test]
fn test_parallel_split_weights_branches_by_amount() {
let a = addr(0x01);
let b = addr(0x02);
let legs = [
SpotLeg { token_in: &a, token_out: &b, amount_in_raw: 60.0, reported_spot_price: 2.0 },
SpotLeg { token_in: &a, token_out: &b, amount_in_raw: 40.0, reported_spot_price: 3.0 },
];
let (amount_in, amount_out) = (parse_biguint("100"), parse_biguint("228"));
let impact_inputs = inputs(&a, &b, &amount_in, &amount_out);
let impact = price_impact_from_spot_legs(&legs, &impact_inputs).unwrap();
assert!((impact - 0.05).abs() < 1e-9, "got {impact}");
}
#[test]
fn test_tree_split_compounds_intermediate_deviation() {
let a = addr(0x01);
let b = addr(0x02);
let c = addr(0x03);
let legs = [
SpotLeg { token_in: &a, token_out: &c, amount_in_raw: 50.0, reported_spot_price: 2.0 },
SpotLeg { token_in: &a, token_out: &b, amount_in_raw: 50.0, reported_spot_price: 1.0 },
SpotLeg { token_in: &b, token_out: &c, amount_in_raw: 30.0, reported_spot_price: 2.0 },
SpotLeg { token_in: &b, token_out: &c, amount_in_raw: 19.0, reported_spot_price: 2.0 },
];
let (amount_in, amount_out) = (parse_biguint("100"), parse_biguint("186"));
let impact_inputs = inputs(&a, &c, &amount_in, &amount_out);
let impact = price_impact_from_spot_legs(&legs, &impact_inputs).unwrap();
assert!((impact - 0.07).abs() < 1e-9, "got {impact}");
}
#[test]
fn test_ungrouped_swap_order() {
let a = addr(0x01);
let b = addr(0x02);
let c = addr(0x03);
let d = addr(0x04);
let legs = [
SpotLeg { token_in: &a, token_out: &b, amount_in_raw: 50.0, reported_spot_price: 1.0 },
SpotLeg { token_in: &b, token_out: &c, amount_in_raw: 49.0, reported_spot_price: 1.0 },
SpotLeg { token_in: &c, token_out: &d, amount_in_raw: 97.0, reported_spot_price: 2.0 },
SpotLeg { token_in: &a, token_out: &c, amount_in_raw: 50.0, reported_spot_price: 1.0 },
];
let (amount_in, amount_out) = (parse_biguint("100"), parse_biguint("190"));
let impact_inputs = inputs(&a, &d, &amount_in, &amount_out);
let impact = price_impact_from_spot_legs(&legs, &impact_inputs).unwrap();
assert!((impact - 0.05).abs() < 1e-9, "got {impact}");
}
#[test]
fn test_consumer_before_producer() {
let a = addr(0x01);
let b = addr(0x02);
let c = addr(0x03);
let legs = [SpotLeg {
token_in: &b,
token_out: &c,
amount_in_raw: 10.0,
reported_spot_price: 1.0,
}];
let (amount_in, amount_out) = (parse_biguint("10"), parse_biguint("10"));
let impact_inputs = inputs(&a, &c, &amount_in, &amount_out);
let err = price_impact_from_spot_legs(&legs, &impact_inputs).unwrap_err();
let PriceImpactError::UnfedToken(unfed) = err else {
panic!("expected UnfedToken, got {err}");
};
assert_eq!(unfed, b);
}
#[test]
fn test_zero_consumed_input() {
let a = addr(0x01);
let b = addr(0x02);
let legs =
[SpotLeg { token_in: &a, token_out: &b, amount_in_raw: 0.0, reported_spot_price: 1.0 }];
let (amount_in, amount_out) = (parse_biguint("100"), parse_biguint("100"));
let impact_inputs = inputs(&a, &b, &amount_in, &amount_out);
let err = price_impact_from_spot_legs(&legs, &impact_inputs).unwrap_err();
let PriceImpactError::ZeroConsumedInput(token) = err else {
panic!("expected ZeroConsumedInput, got {err}");
};
assert_eq!(token, a);
}
#[test]
fn test_flow_that_never_reaches_output_token() {
let a = addr(0x01);
let b = addr(0x02);
let c = addr(0x03);
let legs = [SpotLeg {
token_in: &a,
token_out: &b,
amount_in_raw: 100.0,
reported_spot_price: 1.0,
}];
let (amount_in, amount_out) = (parse_biguint("100"), parse_biguint("100"));
let impact_inputs = inputs(&a, &c, &amount_in, &amount_out);
let err = price_impact_from_spot_legs(&legs, &impact_inputs).unwrap_err();
let PriceImpactError::NoReferenceOutput = err else {
panic!("expected NoReferenceOutput, got {err}");
};
}
fn route_swap(
pool_id: &str,
token_in: &Token,
token_out: &Token,
amount_in: u64,
amount_out: u64,
state: Box<dyn ProtocolSim>,
split: f64,
) -> Swap {
Swap::new(
pool_id.to_string(),
"mock".to_string(),
token_in.address.clone(),
token_out.address.clone(),
BigUint::from(amount_in),
BigUint::from(amount_out),
BigUint::ZERO,
component(pool_id, &[token_in.clone(), token_out.clone()]),
state,
)
.with_split(split)
}
fn token_map(tokens: &[&Token]) -> Vec<(Bytes, Token)> {
tokens
.iter()
.map(|t| (t.address.clone(), (*t).clone()))
.collect()
}
#[test]
fn test_route_price_impact_for_split_route() {
let a = token(0x01, "A");
let b = token(0x02, "B");
let route = Route::new(
vec![
route_swap("p1", &a, &b, 60, 114, Box::new(MockProtocolSim::new(2.0)), 0.6),
route_swap("p2", &a, &b, 40, 78, Box::new(MockProtocolSim::new(2.0)), 0.0),
],
token_map(&[&a, &b]),
)
.unwrap();
let impact =
route_price_impact(&route, &parse_biguint("100"), &parse_biguint("192")).unwrap();
assert!((impact - 0.04).abs() < 1e-9, "got {impact}");
}
#[test]
fn test_route_price_impact_reads_decimals_from_route_tokens() {
let a = token_with_decimals(0x01, "A", 18);
let b = token_with_decimals(0x02, "B", 6);
let route = Route::new(
vec![route_swap(
"p1",
&a,
&b,
1_000_000_000_000_000_000,
2_000_000,
Box::new(MockProtocolSim::new(2.0)),
0.0,
)],
token_map(&[&a, &b]),
)
.unwrap();
let impact = route_price_impact(
&route,
&parse_biguint("1000000000000000000"),
&parse_biguint("2000000"),
)
.unwrap();
assert!(impact.abs() < 1e-9, "got {impact}");
}
#[test]
fn test_route_price_impact_without_token_map() {
let a = token(0x01, "A");
let b = token(0x02, "B");
let route = Route::new(
vec![route_swap("p1", &a, &b, 100, 200, Box::new(MockProtocolSim::new(2.0)), 0.0)],
Vec::new(),
)
.unwrap();
let err =
route_price_impact(&route, &parse_biguint("100"), &parse_biguint("200")).unwrap_err();
let PriceImpactError::UnknownToken(unknown) = err else {
panic!("expected UnknownToken, got {err}");
};
assert_eq!(unknown, a.address);
}
#[test]
fn test_spot_price_failure_names_the_leg() {
let a = token(0x01, "A");
let b = token(0x02, "B");
let route = Route::new(
vec![route_swap("p1", &a, &b, 100, 200, Box::new(SellRateSim::unpriced()), 0.0)],
token_map(&[&a, &b]),
)
.unwrap();
let err =
route_price_impact(&route, &parse_biguint("100"), &parse_biguint("200")).unwrap_err();
let PriceImpactError::SpotPriceQueryFailed { component_id, token_in, token_out, .. } = err
else {
panic!("expected SpotPriceQueryFailed, got {err}");
};
assert_eq!(component_id, "p1");
assert_eq!(token_in, a.address);
assert_eq!(token_out, b.address);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
enum SpotPriceBehavior {
SellRate,
Unavailable,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct SellRateSim {
mid: f64,
fee: f64,
spot: SpotPriceBehavior,
}
impl SellRateSim {
fn new(mid: f64, fee: f64) -> Self {
Self { mid, fee, spot: SpotPriceBehavior::SellRate }
}
fn unpriced() -> Self {
Self { mid: 1.0, fee: 0.0, spot: SpotPriceBehavior::Unavailable }
}
fn rate_for(&self, token_in: &Token, token_out: &Token) -> f64 {
let mid = if token_in.address < token_out.address { self.mid } else { 1.0 / self.mid };
mid * (1.0 - self.fee)
}
}
#[typetag::serde]
impl ProtocolSim for SellRateSim {
fn fee(&self) -> f64 {
self.fee
}
fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
match self.spot {
SpotPriceBehavior::SellRate => Ok(self.rate_for(base, quote)),
SpotPriceBehavior::Unavailable => {
Err(SimulationError::RecoverableError("spot price is unavailable".to_string()))
}
}
}
fn get_amount_out(
&self,
amount_in: BigUint,
token_in: &Token,
token_out: &Token,
) -> Result<GetAmountOutResult, SimulationError> {
let rate = self.rate_for(token_in, token_out);
let amount_out = (amount_in.to_f64().unwrap_or(0.0) * rate).round();
Ok(GetAmountOutResult::new(
BigUint::from(amount_out as u64),
BigUint::ZERO,
self.clone_box(),
))
}
fn get_limits(
&self,
_sell_token: Bytes,
_buy_token: Bytes,
) -> Result<(BigUint, BigUint), SimulationError> {
Ok((BigUint::from(u64::MAX), BigUint::from(u64::MAX)))
}
fn delta_transition(
&mut self,
_delta: ProtocolStateDelta,
_tokens: &std::collections::HashMap<Bytes, Token>,
_balances: &Balances,
) -> Result<(), TransitionError> {
unimplemented!("delta_transition is not needed by SellRateSim")
}
fn clone_box(&self) -> Box<dyn ProtocolSim> {
Box::new(self.clone())
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn eq(&self, other: &dyn ProtocolSim) -> bool {
other
.as_any()
.downcast_ref::<Self>()
.is_some_and(|o| o.mid == self.mid && o.fee == self.fee)
}
}
#[test]
fn test_no_slippage_sell_rate_pool_reports_zero_impact() {
let a = token(0x01, "A");
let b = token(0x02, "B");
let state = SellRateSim::new(1.0, 0.003);
let paid = state
.get_amount_out(BigUint::from(1_000_000u64), &a, &b)
.unwrap()
.amount;
assert_eq!(paid, BigUint::from(997_000u64));
let route = Route::new(
vec![route_swap("p1", &a, &b, 1_000_000, 997_000, Box::new(state), 0.0)],
token_map(&[&a, &b]),
)
.unwrap();
let impact =
route_price_impact(&route, &parse_biguint("1000000"), &parse_biguint("997000"))
.unwrap();
assert!(impact.abs() < 1e-9, "expected zero impact, got {impact}");
}
}