1use crate::{
10 data::{ExactInParams, ExactOutParams, PoolState, Quote},
11 swap,
12};
13use alloy_primitives::{I256, U256};
14use thiserror::Error;
15use wp_evm_amm_math::AmmMathError;
16
17#[derive(Error, Debug)]
18pub enum QuoteError {
19 #[error("input token does not match pool")]
20 UnknownToken,
21 #[error("amm math error: {0}")]
22 Math(#[from] AmmMathError),
23 #[error("pool has insufficient liquidity to fulfil the requested swap")]
24 InsufficientLiquidity,
25 #[error("quote pipeline internal invariant violated: {0}")]
32 Internal(&'static str),
33}
34
35impl From<swap::SwapError> for QuoteError {
36 fn from(e: swap::SwapError) -> Self {
37 match e {
38 swap::SwapError::Math(m) => QuoteError::Math(m),
39 swap::SwapError::InsufficientLiquidity => QuoteError::InsufficientLiquidity,
40 swap::SwapError::ZeroAmount => {
46 QuoteError::Internal("swap returned ZeroAmount after quote-level guard")
47 }
48 swap::SwapError::InvalidPriceLimit => {
49 QuoteError::Internal("swap returned InvalidPriceLimit with quote-supplied sentinel")
50 }
51 swap::SwapError::Internal(msg) => QuoteError::Internal(msg),
52 }
53 }
54}
55
56pub fn exact_in(state: &PoolState, params: &ExactInParams) -> Result<Quote, QuoteError> {
57 exact_in_with_fee_fn(state, params, |s| s.fee)
58}
59
60pub fn exact_in_with_fee_fn<F>(
70 state: &PoolState,
71 params: &ExactInParams,
72 fee_fn: F,
73) -> Result<Quote, QuoteError>
74where
75 F: Fn(&PoolState) -> u32,
76{
77 let zero_for_one = params.token_in == state.token0;
78 if !zero_for_one && params.token_in != state.token1 {
79 return Err(QuoteError::UnknownToken);
80 }
81
82 if params.amount_in.is_zero() {
85 return Ok(Quote {
86 amount_in: U256::ZERO,
87 amount_out: U256::ZERO,
88 sqrt_price_x96_after: state.sqrt_price_x96,
89 price_impact_bps: 0,
90 });
91 }
92
93 let effective_fee = fee_fn(state);
94
95 let amount_specified = I256::try_from(params.amount_in)
96 .map_err(|_| QuoteError::Math(AmmMathError::MulDivOverflow))?;
97 let sqrt_price_limit_x96 = if zero_for_one {
98 swap::min_sqrt_ratio_plus_one()
99 } else {
100 swap::max_sqrt_ratio_minus_one()
101 };
102
103 let result =
104 swap::swap(state, zero_for_one, amount_specified, sqrt_price_limit_x96, effective_fee)?;
105
106 if result.amount_in != params.amount_in {
113 return Err(QuoteError::InsufficientLiquidity);
114 }
115
116 Ok(Quote {
117 amount_in: result.amount_in,
118 amount_out: result.amount_out,
119 sqrt_price_x96_after: result.sqrt_price_x96_after,
120 price_impact_bps: compute_price_impact_bps(
121 state.sqrt_price_x96,
122 result.sqrt_price_x96_after,
123 ),
124 })
125}
126
127pub fn exact_out(state: &PoolState, params: &ExactOutParams) -> Result<Quote, QuoteError> {
128 exact_out_with_fee_fn(state, params, |s| s.fee)
129}
130
131pub fn exact_out_with_fee_fn<F>(
140 state: &PoolState,
141 params: &ExactOutParams,
142 fee_fn: F,
143) -> Result<Quote, QuoteError>
144where
145 F: Fn(&PoolState) -> u32,
146{
147 let zero_for_one = params.token_in == state.token0;
148 if !zero_for_one && params.token_in != state.token1 {
149 return Err(QuoteError::UnknownToken);
150 }
151
152 if params.amount_out.is_zero() {
155 return Ok(Quote {
156 amount_in: U256::ZERO,
157 amount_out: U256::ZERO,
158 sqrt_price_x96_after: state.sqrt_price_x96,
159 price_impact_bps: 0,
160 });
161 }
162
163 let effective_fee = fee_fn(state);
164
165 let amount_out_i = I256::try_from(params.amount_out)
171 .map_err(|_| QuoteError::Math(AmmMathError::MulDivOverflow))?;
172 let amount_specified =
173 amount_out_i.checked_neg().ok_or(QuoteError::Math(AmmMathError::MulDivOverflow))?;
174
175 let sqrt_price_limit_x96 = if zero_for_one {
176 swap::min_sqrt_ratio_plus_one()
177 } else {
178 swap::max_sqrt_ratio_minus_one()
179 };
180
181 let result =
182 swap::swap(state, zero_for_one, amount_specified, sqrt_price_limit_x96, effective_fee)?;
183
184 if result.amount_out != params.amount_out {
190 return Err(QuoteError::InsufficientLiquidity);
191 }
192
193 Ok(Quote {
194 amount_in: result.amount_in,
195 amount_out: result.amount_out,
196 sqrt_price_x96_after: result.sqrt_price_x96_after,
197 price_impact_bps: compute_price_impact_bps(
198 state.sqrt_price_x96,
199 result.sqrt_price_x96_after,
200 ),
201 })
202}
203
204fn compute_price_impact_bps(before: U256, after: U256) -> u16 {
213 if before == U256::ZERO {
214 return 0;
215 }
216 let (num, denom) =
217 if after > before { (after - before, before) } else { (before - after, before) };
218 let bps = (num * U256::from(10_000u64)) / denom;
219 bps.saturating_to::<u16>()
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use crate::data::{ExactInParams, ExactOutParams, PoolState, TickInfo};
226 use alloy_primitives::{address, U256};
227
228 fn fixture_usdc_weth_03() -> PoolState {
254 let sqrt_price_x96: U256 =
257 U256::from_str_radix("3543191142285914205922034323214", 10).unwrap();
258 PoolState {
259 token0: address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), token1: address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), fee: 3000,
262 tick_spacing: 60,
263 sqrt_price_x96,
264 liquidity: 2_000_000_000_000_000_000_000u128, tick: 76012, ticks: vec![
267 TickInfo {
268 tick: 74940,
271 liquidity_net: 1_000_000_000_000_000_000_000i128,
272 liquidity_gross: 1_000_000_000_000_000_000_000u128,
273 },
274 TickInfo {
275 tick: 75960,
280 liquidity_net: 1_000_000_000_000_000_000_000i128,
281 liquidity_gross: 1_000_000_000_000_000_000_000u128,
282 },
283 TickInfo {
284 tick: 76020,
286 liquidity_net: -2_000_000_000_000_000_000_000i128,
287 liquidity_gross: 2_000_000_000_000_000_000_000u128,
288 },
289 ],
290 }
291 }
292
293 #[test]
294 fn exact_in_one_usdc_for_weth_within_tick() {
295 let s = fixture_usdc_weth_03();
296 let p = ExactInParams {
297 token_in: s.token0,
298 token_out: s.token1,
299 amount_in: U256::from(1_000_000u64), recipient: address!("0x0000000000000000000000000000000000000099"),
301 };
302 let q = exact_in(&s, &p).expect("quote ok");
303 assert!(q.amount_out > U256::ZERO);
304 assert!(q.amount_out < U256::from(1_000_000_000_000_000u64));
305 assert_eq!(q.amount_in, p.amount_in);
306 }
307
308 #[test]
309 fn exact_in_rejects_unknown_token() {
310 let s = fixture_usdc_weth_03();
311 let bogus_token = address!("0x000000000000000000000000000000000000dead");
312 let p = ExactInParams {
313 token_in: bogus_token,
314 token_out: s.token1,
315 amount_in: U256::from(1_000_000u64),
316 recipient: address!("0x0000000000000000000000000000000000000099"),
317 };
318 let err = exact_in(&s, &p).expect_err("should reject unknown token");
319 assert!(matches!(err, QuoteError::UnknownToken));
320 }
321
322 #[test]
323 fn exact_in_reverse_direction_weth_for_usdc() {
324 let s = fixture_usdc_weth_03();
325 let p = ExactInParams {
326 token_in: s.token1, token_out: s.token0, amount_in: U256::from(1_000_000_000_000_000u64), recipient: address!("0x0000000000000000000000000000000000000099"),
330 };
331 let q = exact_in(&s, &p).expect("reverse quote ok");
332 assert!(
338 q.amount_out > U256::from(480_000_000_000u64),
339 "amount_out too low: {}",
340 q.amount_out
341 );
342 assert!(
343 q.amount_out < U256::from(515_000_000_000u64),
344 "amount_out too high: {}",
345 q.amount_out
346 );
347 assert_eq!(q.amount_in, p.amount_in);
348 }
349
350 #[test]
351 fn exact_in_large_swap_crosses_tick() {
352 let s = fixture_usdc_weth_03();
353 let p = ExactInParams {
354 token_in: s.token0, token_out: s.token1, amount_in: U256::from(1_000_000_000_000_000_000u64), recipient: address!("0x0000000000000000000000000000000000000099"),
361 };
362 let q = exact_in(&s, &p).expect("large swap ok");
363 assert!(q.amount_out > U256::ZERO, "amount_out should be > 0");
364 let sqrt_at_75960 = U256::from_str_radix("3533845506420911390540068078527", 10).unwrap();
367 assert!(
368 q.sqrt_price_x96_after < sqrt_at_75960,
369 "expected sqrt_price to cross below tick 75960 (sqrt {}), got {}",
370 sqrt_at_75960,
371 q.sqrt_price_x96_after
372 );
373 assert_eq!(q.amount_in, p.amount_in);
374 }
375
376 #[test]
377 fn exact_out_round_trip_against_exact_in() {
378 let s = fixture_usdc_weth_03();
379 let p_in = ExactInParams {
380 token_in: s.token0,
381 token_out: s.token1,
382 amount_in: U256::from(1_000_000u64),
383 recipient: address!("0x0000000000000000000000000000000000000099"),
384 };
385 let q_in = exact_in(&s, &p_in).unwrap();
386
387 let p_out = ExactOutParams {
388 token_in: s.token0,
389 token_out: s.token1,
390 amount_out: q_in.amount_out,
391 recipient: p_in.recipient,
392 };
393 let q_out = exact_out(&s, &p_out).unwrap();
394
395 let diff = if q_out.amount_in > p_in.amount_in {
399 q_out.amount_in - p_in.amount_in
400 } else {
401 p_in.amount_in - q_out.amount_in
402 };
403 assert!(diff <= U256::from(1_000u64), "round-trip diff = {}", diff);
404 }
405
406 #[test]
407 fn exact_in_reports_price_impact() {
408 let s = fixture_usdc_weth_03();
409 let big = ExactInParams {
410 token_in: s.token0,
411 token_out: s.token1,
412 amount_in: U256::from(1_000_000_000_000_000_000u64), recipient: address!("0x0000000000000000000000000000000000000099"),
417 };
418 let q = exact_in(&s, &big).unwrap();
419 assert!(q.price_impact_bps > 0, "expected nonzero price impact, got 0");
420 }
421
422 #[test]
423 fn exact_in_small_swap_has_minimal_price_impact() {
424 let s = fixture_usdc_weth_03();
425 let p = ExactInParams {
426 token_in: s.token0,
427 token_out: s.token1,
428 amount_in: U256::from(1_000u64), recipient: address!("0x0000000000000000000000000000000000000099"),
430 };
431 let q = exact_in(&s, &p).unwrap();
432 assert!(q.price_impact_bps < 100, "expected tiny impact, got {}", q.price_impact_bps);
434 }
435
436 #[test]
437 fn exact_out_rejects_unknown_token() {
438 let s = fixture_usdc_weth_03();
439 let bogus = address!("0x000000000000000000000000000000000000dead");
440 let p = ExactOutParams {
441 token_in: bogus,
442 token_out: s.token1,
443 amount_out: U256::from(1_000_000_000_000_000u64),
444 recipient: address!("0x0000000000000000000000000000000000000099"),
445 };
446 let err = exact_out(&s, &p).expect_err("should reject unknown token");
447 assert!(matches!(err, QuoteError::UnknownToken));
448 }
449
450 #[test]
451 fn exact_in_with_fee_fn_uses_injected_fee() {
452 let s = fixture_usdc_weth_03();
453 let p = ExactInParams {
454 token_in: s.token0,
455 token_out: s.token1,
456 amount_in: U256::from(1_000u64),
457 recipient: address!("0x0000000000000000000000000000000000000099"),
458 };
459 let q_normal = exact_in(&s, &p).expect("normal quote ok");
461 let q_low_fee = exact_in_with_fee_fn(&s, &p, |_| 100).expect("low-fee quote ok");
466 assert!(
468 q_low_fee.amount_out > q_normal.amount_out,
469 "low-fee output should exceed normal output: {} vs {}",
470 q_low_fee.amount_out,
471 q_normal.amount_out
472 );
473 }
474
475 #[test]
478 fn exact_in_token_in_equals_token_out_returns_unknown_token() {
479 let s = fixture_usdc_weth_03();
495 let bogus = address!("0x000000000000000000000000000000000000dead");
496 let p = ExactInParams {
497 token_in: bogus,
498 token_out: bogus, amount_in: U256::from(1_000_000u64),
500 recipient: address!("0x0000000000000000000000000000000000000099"),
501 };
502 let err = exact_in(&s, &p).expect_err("should reject token not in pool");
503 assert!(matches!(err, QuoteError::UnknownToken));
504 }
505
506 #[test]
507 fn exact_out_token_in_equals_token_out_returns_unknown_token() {
508 let s = fixture_usdc_weth_03();
510 let bogus = address!("0x000000000000000000000000000000000000dead");
511 let p = ExactOutParams {
512 token_in: bogus,
513 token_out: bogus,
514 amount_out: U256::from(1_000_000u64),
515 recipient: address!("0x0000000000000000000000000000000000000099"),
516 };
517 let err = exact_out(&s, &p).expect_err("should reject token not in pool");
518 assert!(matches!(err, QuoteError::UnknownToken));
519 }
520
521 #[test]
522 fn exact_in_very_large_amount_returns_insufficient_liquidity() {
523 let s = fixture_usdc_weth_03();
529 let p = ExactInParams {
530 token_in: s.token0,
531 token_out: s.token1,
532 amount_in: U256::from_str_radix("10000000000000000000000000000000000000000", 10)
533 .unwrap(), recipient: address!("0x0000000000000000000000000000000000000099"),
535 };
536 let err = exact_in(&s, &p).expect_err("should error on oversized amount");
537 assert!(
538 matches!(err, QuoteError::InsufficientLiquidity),
539 "expected InsufficientLiquidity, got {:?}",
540 err
541 );
542 }
543
544 #[test]
545 fn exact_out_very_large_amount_returns_insufficient_liquidity() {
546 let s = fixture_usdc_weth_03();
553 let p = ExactOutParams {
554 token_in: s.token0,
555 token_out: s.token1,
556 amount_out: U256::from_str_radix("10000000000000000000000000000000000000000", 10)
557 .unwrap(), recipient: address!("0x0000000000000000000000000000000000000099"),
559 };
560 let err = exact_out(&s, &p).expect_err("should error on oversized output request");
561 assert!(
562 matches!(err, QuoteError::InsufficientLiquidity),
563 "expected InsufficientLiquidity, got {:?}",
564 err
565 );
566 }
567
568 #[test]
569 fn exact_out_zero_amount_returns_zero() {
570 let s = fixture_usdc_weth_03();
573 let p = ExactOutParams {
574 token_in: s.token0,
575 token_out: s.token1,
576 amount_out: U256::ZERO,
577 recipient: address!("0x0000000000000000000000000000000000000099"),
578 };
579 let q = exact_out(&s, &p).expect("zero-amount exact_out should not error");
580 assert_eq!(q.amount_in, U256::ZERO);
581 assert_eq!(q.amount_out, U256::ZERO);
582 assert_eq!(q.sqrt_price_x96_after, s.sqrt_price_x96);
583 assert_eq!(q.price_impact_bps, 0);
584 }
585
586 #[test]
587 fn exact_in_zero_amount_returns_zero_out() {
588 let s = fixture_usdc_weth_03();
591 let p = ExactInParams {
592 token_in: s.token0,
593 token_out: s.token1,
594 amount_in: U256::ZERO,
595 recipient: address!("0x0000000000000000000000000000000000000099"),
596 };
597 let q = exact_in(&s, &p).expect("zero-amount swap should not error");
599 assert_eq!(q.amount_out, U256::ZERO, "zero-in should produce zero-out");
600 assert_eq!(q.amount_in, U256::ZERO);
601 }
602
603 #[test]
604 fn exact_in_zero_amount_reverse_direction_returns_zero_out() {
605 let s = fixture_usdc_weth_03();
606 let p = ExactInParams {
607 token_in: s.token1,
608 token_out: s.token0,
609 amount_in: U256::ZERO,
610 recipient: address!("0x0000000000000000000000000000000000000099"),
611 };
612 let q = exact_in(&s, &p).expect("reverse-direction zero-input should not error");
613 assert_eq!(q.amount_in, U256::ZERO);
614 assert_eq!(q.amount_out, U256::ZERO);
615 assert_eq!(q.sqrt_price_x96_after, s.sqrt_price_x96);
616 }
617
618 #[test]
619 fn exact_out_zero_amount_reverse_direction_returns_zero() {
620 let s = fixture_usdc_weth_03();
621 let p = ExactOutParams {
622 token_in: s.token1,
623 token_out: s.token0,
624 amount_out: U256::ZERO,
625 recipient: address!("0x0000000000000000000000000000000000000099"),
626 };
627 let q = exact_out(&s, &p).expect("reverse-direction zero-output should not error");
628 assert_eq!(q.amount_in, U256::ZERO);
629 assert_eq!(q.amount_out, U256::ZERO);
630 assert_eq!(q.sqrt_price_x96_after, s.sqrt_price_x96);
631 }
632}