1use std::{any::Any, collections::HashMap};
2
3use num_bigint::BigUint;
4use num_traits::{CheckedSub, ToPrimitive};
5use serde::{Deserialize, Serialize};
6use tycho_common::{
7 dto::ProtocolStateDelta,
8 models::token::Token,
9 simulation::{
10 errors::{SimulationError, TransitionError},
11 protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
12 },
13 Bytes,
14};
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21pub struct PriceLevelStreamQuote {
22 pub amount_in: BigUint,
23 pub amount_out: BigUint,
24}
25
26impl PriceLevelStreamQuote {
27 pub fn new(amount_in: BigUint, amount_out: BigUint) -> Self {
28 Self { amount_in, amount_out }
29 }
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct PriceLevelStreamState {
40 pub token0: Bytes,
41 pub token1: Bytes,
42 pub quotes_0_to_1: Vec<PriceLevelStreamQuote>,
43 pub quotes_1_to_0: Vec<PriceLevelStreamQuote>,
44 pub gas_cost: BigUint,
45}
46
47impl PriceLevelStreamState {
48 pub fn new(
53 token0: Bytes,
54 token1: Bytes,
55 mut quotes_0_to_1: Vec<PriceLevelStreamQuote>,
56 mut quotes_1_to_0: Vec<PriceLevelStreamQuote>,
57 gas_cost: BigUint,
58 ) -> Self {
59 for quotes in [&mut quotes_0_to_1, &mut quotes_1_to_0] {
60 quotes.sort_by(|a, b| a.amount_in.cmp(&b.amount_in));
61 quotes.dedup_by(|a, b| a.amount_in == b.amount_in);
62 }
63 Self { token0, token1, quotes_0_to_1, quotes_1_to_0, gas_cost }
64 }
65
66 fn quotes(
69 &self,
70 token_in: &Bytes,
71 token_out: &Bytes,
72 ) -> Result<&[PriceLevelStreamQuote], SimulationError> {
73 if token_in == &self.token0 && token_out == &self.token1 {
74 Ok(&self.quotes_0_to_1)
75 } else if token_in == &self.token1 && token_out == &self.token0 {
76 Ok(&self.quotes_1_to_0)
77 } else {
78 Err(SimulationError::RecoverableError(format!(
79 "Invalid token addresses for pair {}/{}: {token_in}, {token_out}",
80 self.token0, self.token1
81 )))
82 }
83 }
84
85 fn interpolate(
93 &self,
94 quotes: &[PriceLevelStreamQuote],
95 amount_in: &BigUint,
96 ) -> Result<BigUint, SimulationError> {
97 let idx = quotes.partition_point(|quote| "e.amount_in < amount_in);
100 let upper = "es[idx];
101 if &upper.amount_in == amount_in {
102 return Ok(upper.amount_out.clone());
103 }
104 let lower = "es[idx - 1];
105 let Some(out_span) = upper
106 .amount_out
107 .checked_sub(&lower.amount_out)
108 else {
109 return Err(SimulationError::RecoverableError(format!(
111 "Quote ladder {}/{} is not monotonically increasing in amount_out around the \
112 requested amount {amount_in}: {} -> {}, but {} -> {}",
113 self.token0,
114 self.token1,
115 lower.amount_in,
116 lower.amount_out,
117 upper.amount_in,
118 upper.amount_out,
119 )));
120 };
121 let in_span = &upper.amount_in - &lower.amount_in;
122 let offset = amount_in - &lower.amount_in;
123 Ok(&lower.amount_out + out_span * offset / in_span)
124 }
125
126 fn consumed(&self) -> Box<dyn ProtocolSim> {
130 Box::new(Self {
131 token0: self.token0.clone(),
132 token1: self.token1.clone(),
133 quotes_0_to_1: Vec::new(),
134 quotes_1_to_0: Vec::new(),
135 gas_cost: self.gas_cost.clone(),
136 })
137 }
138}
139
140#[typetag::serde]
141impl ProtocolSim for PriceLevelStreamState {
142 fn fee(&self) -> f64 {
143 0.0
144 }
145
146 fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
147 let quotes = self.quotes(&base.address, "e.address)?;
148 let best = quotes
149 .iter()
150 .find(|q| q.amount_in > BigUint::ZERO && q.amount_out > BigUint::ZERO)
151 .ok_or_else(|| {
152 SimulationError::RecoverableError("No liquidity available".to_string())
153 })?;
154 let amount_in = best.amount_in.to_f64().ok_or_else(|| {
155 SimulationError::RecoverableError("Can't convert amount in to f64".to_string())
156 })?;
157 let amount_out = best
158 .amount_out
159 .to_f64()
160 .ok_or_else(|| {
161 SimulationError::RecoverableError("Can't convert amount out to f64".to_string())
162 })?;
163 Ok((amount_out / 10f64.powi(quote.decimals as i32)) /
164 (amount_in / 10f64.powi(base.decimals as i32)))
165 }
166
167 fn get_amount_out(
168 &self,
169 amount_in: BigUint,
170 token_in: &Token,
171 token_out: &Token,
172 ) -> Result<GetAmountOutResult, SimulationError> {
173 let quotes = self.quotes(&token_in.address, &token_out.address)?;
174 let (Some(first), Some(last)) = (quotes.first(), quotes.last()) else {
175 return Err(SimulationError::RecoverableError("No liquidity available".to_string()));
176 };
177 if amount_in < first.amount_in {
191 return Err(SimulationError::InvalidInput(
192 format!(
193 "Input amount is below the smallest quote. input amount: {amount_in}, minimum quoted amount: {}",
194 first.amount_in
195 ),
196 None,
197 ));
198 }
199 if amount_in > last.amount_in {
202 let res = GetAmountOutResult {
203 amount: last.amount_out.clone(),
204 gas: self.gas_cost.clone(),
205 new_state: self.consumed(),
206 };
207 return Err(SimulationError::InvalidInput(
208 format!(
209 "Not enough liquidity to support complete swap. input amount: {amount_in}, maximum quoted amount: {}",
210 last.amount_in
211 ),
212 Some(res),
213 ));
214 }
215 Ok(GetAmountOutResult {
216 amount: self.interpolate(quotes, &amount_in)?,
217 gas: self.gas_cost.clone(),
218 new_state: self.consumed(),
219 })
220 }
221
222 fn get_limits(
223 &self,
224 sell_token: Bytes,
225 buy_token: Bytes,
226 ) -> Result<(BigUint, BigUint), SimulationError> {
227 let quotes = self.quotes(&sell_token, &buy_token)?;
228 match quotes.last() {
229 Some(largest) => Ok((largest.amount_in.clone(), largest.amount_out.clone())),
230 None => Ok((BigUint::ZERO, BigUint::ZERO)),
231 }
232 }
233
234 fn delta_transition(
235 &mut self,
236 _delta: ProtocolStateDelta,
237 _tokens: &HashMap<Bytes, Token>,
238 _balances: &Balances,
239 ) -> Result<(), TransitionError> {
240 Err(TransitionError::DecodeError("Not implemented".into()))
241 }
242
243 fn clone_box(&self) -> Box<dyn ProtocolSim> {
244 Box::new(self.clone())
245 }
246
247 fn as_any(&self) -> &dyn Any {
248 self
249 }
250
251 fn as_any_mut(&mut self) -> &mut dyn Any {
252 self
253 }
254
255 fn eq(&self, other: &dyn ProtocolSim) -> bool {
256 other
257 .as_any()
258 .downcast_ref::<PriceLevelStreamState>()
259 .is_some_and(|other| {
260 let Self { token0, token1, quotes_0_to_1, quotes_1_to_0, gas_cost } = other;
261 &self.token0 == token0 &&
262 &self.token1 == token1 &&
263 &self.quotes_0_to_1 == quotes_0_to_1 &&
264 &self.quotes_1_to_0 == quotes_1_to_0 &&
265 &self.gas_cost == gas_cost
266 })
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use std::str::FromStr;
273
274 use tycho_common::models::Chain;
275
276 use super::*;
277
278 fn wbtc() -> Token {
279 Token::new(
280 &Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(),
281 "WBTC",
282 8,
283 0,
284 &[Some(10_000)],
285 Chain::Ethereum,
286 100,
287 )
288 }
289
290 fn usdc() -> Token {
291 Token::new(
292 &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
293 "USDC",
294 6,
295 0,
296 &[Some(10_000)],
297 Chain::Ethereum,
298 100,
299 )
300 }
301
302 fn weth() -> Token {
303 Token::new(
304 &Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
305 "WETH",
306 18,
307 0,
308 &[Some(10_000)],
309 Chain::Ethereum,
310 100,
311 )
312 }
313
314 fn quote(amount_in: u64, amount_out: u64) -> PriceLevelStreamQuote {
315 PriceLevelStreamQuote::new(BigUint::from(amount_in), BigUint::from(amount_out))
316 }
317
318 fn state() -> PriceLevelStreamState {
321 PriceLevelStreamState::new(
322 wbtc().address,
323 usdc().address,
324 vec![quote(100_000_000, 100_000_000_000), quote(200_000_000, 190_000_000_000)],
325 vec![quote(100_000_000_000, 99_000_000), quote(200_000_000_000, 190_000_000)],
326 BigUint::from(120_000u64),
327 )
328 }
329
330 #[test]
331 fn new_sorts_and_dedups_quotes() {
332 let state = PriceLevelStreamState::new(
333 wbtc().address,
334 usdc().address,
335 vec![quote(200, 380), quote(100, 200), quote(200, 999)],
336 vec![],
337 BigUint::ZERO,
338 );
339 assert_eq!(state.quotes_0_to_1, vec![quote(100, 200), quote(200, 380)]);
340 }
341
342 #[test]
343 fn get_amount_out_exact_level() {
344 let result = state()
345 .get_amount_out(BigUint::from(100_000_000u64), &wbtc(), &usdc())
346 .unwrap();
347 assert_eq!(result.amount, BigUint::from(100_000_000_000u64));
348 assert_eq!(result.gas, BigUint::from(120_000u64));
349 }
350
351 #[test]
352 fn get_amount_out_interpolates_between_levels() {
353 let result = state()
355 .get_amount_out(BigUint::from(150_000_000u64), &wbtc(), &usdc())
356 .unwrap();
357 assert_eq!(result.amount, BigUint::from(145_000_000_000u64));
358 }
359
360 #[test]
361 fn get_amount_out_on_glitched_ladder_is_rejected() {
362 let state = PriceLevelStreamState::new(
366 wbtc().address,
367 usdc().address,
368 vec![quote(100, 200), quote(200, 150)],
369 vec![],
370 BigUint::ZERO,
371 );
372 let result = state.get_amount_out(BigUint::from(150u64), &wbtc(), &usdc());
373 assert!(matches!(result, Err(SimulationError::RecoverableError(_))));
374
375 let result = state
377 .get_amount_out(BigUint::from(100u64), &wbtc(), &usdc())
378 .unwrap();
379 assert_eq!(result.amount, BigUint::from(200u64));
380 }
381
382 #[test]
383 fn get_amount_out_below_smallest_level_is_rejected() {
384 let result = state().get_amount_out(BigUint::from(50_000_000u64), &wbtc(), &usdc());
387 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
388 }
389
390 #[test]
391 fn get_amount_out_reverse_direction() {
392 let result = state()
393 .get_amount_out(BigUint::from(100_000_000_000u64), &usdc(), &wbtc())
394 .unwrap();
395 assert_eq!(result.amount, BigUint::from(99_000_000u64));
396 }
397
398 #[test]
399 fn get_amount_out_beyond_largest_level_is_partial() {
400 let result = state().get_amount_out(BigUint::from(300_000_000u64), &wbtc(), &usdc());
401 match result {
402 Err(SimulationError::InvalidInput(_, Some(partial))) => {
403 assert_eq!(partial.amount, BigUint::from(190_000_000_000u64));
404 }
405 other => panic!("expected partial InvalidInput, got {other:?}"),
406 }
407 }
408
409 #[test]
410 fn get_amount_out_consumes_both_ladders() {
411 let result = state()
412 .get_amount_out(BigUint::from(100_000_000u64), &wbtc(), &usdc())
413 .unwrap();
414 let new_state = result
415 .new_state
416 .as_any()
417 .downcast_ref::<PriceLevelStreamState>()
418 .expect("price level state");
419 assert!(new_state.quotes_0_to_1.is_empty());
420 assert!(new_state.quotes_1_to_0.is_empty());
421 }
422
423 #[test]
424 fn get_amount_out_rejects_unknown_tokens() {
425 let result = state().get_amount_out(BigUint::from(1u64), &weth(), &usdc());
426 assert!(matches!(result, Err(SimulationError::RecoverableError(_))));
427 }
428
429 #[test]
430 fn get_amount_out_without_liquidity() {
431 let state = PriceLevelStreamState::new(
432 wbtc().address,
433 usdc().address,
434 vec![],
435 vec![],
436 BigUint::ZERO,
437 );
438 let result = state.get_amount_out(BigUint::from(1u64), &wbtc(), &usdc());
439 assert!(matches!(result, Err(SimulationError::RecoverableError(_))));
440 }
441
442 #[test]
443 fn spot_price_uses_smallest_quote() {
444 let price = state()
446 .spot_price(&wbtc(), &usdc())
447 .unwrap();
448 assert!((price - 100_000.0).abs() < 1e-9);
449
450 let inverse = state()
451 .spot_price(&usdc(), &wbtc())
452 .unwrap();
453 assert!((inverse - 9.9e-6).abs() < 1e-15);
455 }
456
457 #[test]
458 fn spot_price_skips_zero_amount_out_quotes() {
459 let state = PriceLevelStreamState::new(
462 wbtc().address,
463 usdc().address,
464 vec![quote(1, 0), quote(100_000_000, 100_000_000_000)],
465 vec![],
466 BigUint::ZERO,
467 );
468 let price = state
469 .spot_price(&wbtc(), &usdc())
470 .unwrap();
471 assert!((price - 100_000.0).abs() < 1e-9);
472 }
473
474 #[test]
475 fn get_limits_returns_largest_quote() {
476 let (max_in, max_out) = state()
477 .get_limits(wbtc().address, usdc().address)
478 .unwrap();
479 assert_eq!(max_in, BigUint::from(200_000_000u64));
480 assert_eq!(max_out, BigUint::from(190_000_000_000u64));
481 }
482
483 #[test]
484 fn get_limits_without_liquidity() {
485 let state = PriceLevelStreamState::new(
486 wbtc().address,
487 usdc().address,
488 vec![],
489 vec![],
490 BigUint::ZERO,
491 );
492 let (max_in, max_out) = state
493 .get_limits(wbtc().address, usdc().address)
494 .unwrap();
495 assert_eq!(max_in, BigUint::ZERO);
496 assert_eq!(max_out, BigUint::ZERO);
497 }
498
499 #[test]
500 fn eq_compares_quotes() {
501 let a = state();
502 let mut b = state();
503 assert!(a.eq(&b as &dyn ProtocolSim));
504 b.quotes_0_to_1[0].amount_out += 1u32;
505 assert!(!a.eq(&b as &dyn ProtocolSim));
506 }
507}