1#![allow(dead_code)]
2use core::cmp::min;
3
4use ethnum::I256;
5
6#[cfg(feature = "wasm")]
7use riptide_amm_macros::wasm_expose;
8
9use borsh::{BorshDeserialize, BorshSerialize};
10
11use super::{
12 error::{CoreError, AMOUNT_EXCEEDS_MAX_U32, ARITHMETIC_OVERFLOW},
13 quote::{Price, Quote, QuoteType},
14 token::{a_to_b, b_to_a, deviation_per_m},
15 U128,
16};
17
18mod amm;
19mod book;
20mod flat;
21mod skew;
22mod spread;
23
24pub use amm::LiquidityType;
25pub use book::BookSpacingType;
26pub use skew::{SkewExponent, SkewMode};
27
28use amm::{amm_liquidity, amm_price};
29use book::{book_liquidity, new_book_liquidity, BOOK_LIQUIDITY_LEVELS};
30use flat::flat_liquidity;
31use skew::apply_skew_to_liquidity;
32use spread::spread_liquidity;
33
34pub(crate) const LIQUIDITY_LEVELS: usize = 32;
35
36pub const ORACLE_DATA_LEN: usize = 276;
37pub const SKEW_LEN: usize = 32;
38pub const ORACLE_PAYLOAD_LEN: usize = 512;
39pub const SKEW_OFFSET: usize = ORACLE_PAYLOAD_LEN - SKEW_LEN;
40
41#[allow(clippy::len_without_is_empty)]
45#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)]
46#[cfg_attr(feature = "wasm", wasm_expose)]
47pub struct SingleSideLiquidity {
48 items: [(u128, u64); LIQUIDITY_LEVELS],
49 len: usize,
50}
51
52impl SingleSideLiquidity {
53 pub fn new() -> Self {
54 Self {
55 items: [(0, 0); LIQUIDITY_LEVELS],
56 len: 0,
57 }
58 }
59
60 pub fn from_slice(slice: &[(u128, u64)]) -> Self {
61 let mut items = [(0, 0); LIQUIDITY_LEVELS];
62 items[..slice.len()].copy_from_slice(slice);
63 Self {
64 items,
65 len: slice.len(),
66 }
67 }
68
69 pub fn push(&mut self, item: (u128, u64)) -> bool {
70 if self.len < 32 {
71 self.items[self.len] = item;
72 self.len += 1;
73 true
74 } else {
75 false
76 }
77 }
78
79 pub fn len(&self) -> usize {
80 self.len
81 }
82
83 pub fn as_slice(&self) -> &[(u128, u64)] {
84 &self.items[..self.len]
85 }
86}
87
88#[cfg_attr(feature = "wasm", wasm_expose)]
89pub const PER_CENT_DENOMINATOR: i8 = 100;
90
91#[cfg_attr(feature = "wasm", wasm_expose)]
92pub const BPS_DENOMINATOR: i16 = 10000;
93
94#[cfg_attr(feature = "wasm", wasm_expose)]
95pub const PER_M_DENOMINATOR: i32 = 1_000_000;
96
97#[derive(Debug, Clone, Copy, Eq, PartialEq)]
98#[cfg_attr(true, derive(BorshDeserialize, BorshSerialize))]
99#[cfg_attr(feature = "wasm", wasm_expose)]
100pub enum OracleData {
101 Empty,
102 FlatPrice {
103 price_q64_64: u128,
104 },
105 SimpleSpread {
106 price_q64_64: u128,
107 bid_spread_per_m: i32,
108 ask_spread_per_m: i32,
109 },
110 OrderBook {
111 price_q64_64: u128,
112 spacing: BookSpacingType,
114 bid_liquidity_per_m: [u32; BOOK_LIQUIDITY_LEVELS],
116 ask_liquidity_per_m: [u32; BOOK_LIQUIDITY_LEVELS],
117 },
118 AutomatedMarketMaker {
119 liquidity_type: LiquidityType,
120 bid_spread_per_m: i32,
121 ask_spread_per_m: i32,
122 },
123}
124
125#[derive(Debug, Clone, Copy, Eq, PartialEq)]
126pub struct OraclePayload {
127 pub data: OracleData,
128 pub skew: SkewMode,
129}
130
131impl BorshDeserialize for OraclePayload {
132 fn deserialize_reader<R: borsh::io::Read>(reader: &mut R) -> borsh::io::Result<Self> {
133 const _: () = assert!(ORACLE_DATA_LEN + SKEW_LEN <= ORACLE_PAYLOAD_LEN);
134
135 let mut buf = [0u8; ORACLE_PAYLOAD_LEN];
136 reader.read_exact(&mut buf)?;
137 let mut data_slice = &buf[..ORACLE_DATA_LEN];
138 let data = OracleData::deserialize(&mut data_slice)?;
139 let mut skew_slice = &buf[SKEW_OFFSET..SKEW_OFFSET + SKEW_LEN];
140 let skew = SkewMode::deserialize(&mut skew_slice)?;
141 Ok(Self { data, skew })
142 }
143}
144
145#[inline(never)]
146fn build_base_liquidity(
147 data: &OracleData,
148 quote_type: QuoteType,
149 reserves_a: u64,
150 reserves_b: u64,
151) -> Result<SingleSideLiquidity, CoreError> {
152 match data {
153 OracleData::Empty => Ok(SingleSideLiquidity::new()),
154 OracleData::FlatPrice { price_q64_64 } => {
155 flat_liquidity(*price_q64_64, quote_type, reserves_a, reserves_b)
156 }
157 OracleData::SimpleSpread {
158 price_q64_64,
159 bid_spread_per_m,
160 ask_spread_per_m,
161 } => spread_liquidity(
162 *price_q64_64,
163 *bid_spread_per_m,
164 *ask_spread_per_m,
165 quote_type,
166 reserves_a,
167 reserves_b,
168 ),
169 OracleData::OrderBook {
170 price_q64_64,
171 spacing,
172 bid_liquidity_per_m,
173 ask_liquidity_per_m,
174 } => book_liquidity(
175 quote_type,
176 *price_q64_64,
177 *spacing,
178 bid_liquidity_per_m,
179 ask_liquidity_per_m,
180 reserves_a,
181 reserves_b,
182 ),
183 OracleData::AutomatedMarketMaker {
184 liquidity_type,
185 bid_spread_per_m,
186 ask_spread_per_m,
187 } => amm_liquidity(
188 *liquidity_type,
189 *bid_spread_per_m,
190 *ask_spread_per_m,
191 quote_type,
192 reserves_a,
193 reserves_b,
194 ),
195 }
196}
197
198pub(crate) fn build_liquidity(
199 payload: &OraclePayload,
200 quote_type: QuoteType,
201 reserves_a: u64,
202 reserves_b: u64,
203 skew_cliff_min_per_m: i32,
204 skew_cliff_max_per_m: i32,
205) -> Result<SingleSideLiquidity, CoreError> {
206 let liquidity = build_base_liquidity(&payload.data, quote_type, reserves_a, reserves_b)?;
207
208 let price = match &payload.data {
209 OracleData::Empty | OracleData::AutomatedMarketMaker { .. } => return Ok(liquidity),
210 OracleData::FlatPrice { price_q64_64 }
211 | OracleData::SimpleSpread { price_q64_64, .. }
212 | OracleData::OrderBook { price_q64_64, .. } => *price_q64_64,
213 };
214
215 let deviation = deviation_per_m(U128::from(price), reserves_a, reserves_b)?;
216 let skew_per_m = payload.skew.compute_skew_per_m(
217 deviation,
218 quote_type,
219 skew_cliff_min_per_m,
220 skew_cliff_max_per_m,
221 )?;
222 apply_skew_to_liquidity(liquidity, skew_per_m, quote_type)
223}
224
225pub(crate) fn build_price(
226 liquidity: &SingleSideLiquidity,
227 oracle: &OracleData,
228 quote_type: QuoteType,
229 reserves_a: u64,
230 reserves_b: u64,
231) -> Result<Price, CoreError> {
232 let best_price = liquidity
233 .as_slice()
234 .iter()
235 .find(|(_, liquidity)| *liquidity > 0)
236 .map(|(price, _)| *price)
237 .unwrap_or(0);
238
239 let oracle_price = match oracle {
240 OracleData::Empty => 0,
241 OracleData::FlatPrice { price_q64_64 } => *price_q64_64,
242 OracleData::SimpleSpread { price_q64_64, .. } => *price_q64_64,
243 OracleData::OrderBook { price_q64_64, .. } => *price_q64_64,
244 OracleData::AutomatedMarketMaker { liquidity_type, .. } => {
245 amm_price(*liquidity_type, reserves_a, reserves_b)?
246 }
247 };
248
249 let diff = if quote_type.a_to_b() {
250 I256::from(oracle_price)
251 .checked_sub(I256::from(best_price))
252 .ok_or(ARITHMETIC_OVERFLOW)?
253 } else {
254 I256::from(best_price)
255 .checked_sub(I256::from(oracle_price))
256 .ok_or(ARITHMETIC_OVERFLOW)?
257 };
258
259 let spread = if best_price > 0 {
260 diff.checked_mul(I256::from(PER_M_DENOMINATOR))
261 .ok_or(ARITHMETIC_OVERFLOW)?
262 .checked_div(I256::from(oracle_price))
263 .ok_or(ARITHMETIC_OVERFLOW)?
264 .try_into()
265 .map_err(|_| AMOUNT_EXCEEDS_MAX_U32)?
266 } else {
267 0
268 };
269
270 Ok(Price {
271 oracle_price_q64_64: oracle_price,
272 best_price_q64_64: best_price,
273 spread_per_m: spread,
274 })
275}
276
277pub(crate) fn consume_liquidity(
278 amount: u64,
279 quote_type: QuoteType,
280 liquidity: &SingleSideLiquidity,
281) -> Result<Quote, CoreError> {
282 let mut remaining_amount = amount;
283 let mut other_amount: u64 = 0;
284
285 for (price, liquidity) in liquidity.as_slice() {
290 if *price == 0 || *liquidity == 0 {
291 continue;
292 }
293
294 let (step_specified_amount, step_other_amount) = if quote_type.exact_in() {
295 let max_amount = if quote_type.input_is_token_a() {
297 b_to_a(*liquidity, U128::from(*price), true)?
298 } else {
299 a_to_b(*liquidity, U128::from(*price), true)?
300 };
301
302 let step_specified_amount = min(remaining_amount, max_amount);
303 let step_other_amount = if quote_type.input_is_token_a() {
304 a_to_b(step_specified_amount, U128::from(*price), false)?
305 } else {
306 b_to_a(step_specified_amount, U128::from(*price), false)?
307 };
308
309 (step_specified_amount, min(step_other_amount, *liquidity))
310 } else {
311 let step_specified_amount = min(remaining_amount, *liquidity);
314 let step_other_amount = if quote_type.output_is_token_a() {
315 a_to_b(step_specified_amount, U128::from(*price), true)?
316 } else {
317 b_to_a(step_specified_amount, U128::from(*price), true)?
318 };
319
320 (step_specified_amount, step_other_amount)
321 };
322
323 remaining_amount = remaining_amount
324 .checked_sub(step_specified_amount)
325 .ok_or(ARITHMETIC_OVERFLOW)?;
326 other_amount = other_amount
327 .checked_add(step_other_amount)
328 .ok_or(ARITHMETIC_OVERFLOW)?;
329
330 if remaining_amount == 0 {
331 break;
332 }
333 }
334
335 let consumed_amount = amount - remaining_amount;
336
337 let (amount_in, amount_out) = if quote_type.exact_in() {
338 (consumed_amount, other_amount)
339 } else {
340 (other_amount, consumed_amount)
341 };
342
343 Ok(Quote {
344 amount_in,
345 amount_out,
346 quote_type,
347 })
348}
349
350pub(crate) fn new_oracle_data(
351 oracle: &OracleData,
352 quote: &Quote,
353 reserves_a: u64,
354 reserves_b: u64,
355) -> Result<OracleData, CoreError> {
356 match oracle {
357 OracleData::Empty
358 | OracleData::FlatPrice { .. }
359 | OracleData::SimpleSpread { .. }
360 | OracleData::AutomatedMarketMaker { .. } => Ok(*oracle),
361 OracleData::OrderBook {
362 price_q64_64,
363 spacing,
364 bid_liquidity_per_m,
365 ask_liquidity_per_m,
366 } => new_book_liquidity(
367 quote,
368 *price_q64_64,
369 *spacing,
370 bid_liquidity_per_m,
371 ask_liquidity_per_m,
372 reserves_a,
373 reserves_b,
374 ),
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::{super::error::INVALID_ORACLE_DATA, *};
381 use rstest::rstest;
382
383 #[rstest]
384 fn test_empty(
385 #[values(true, false)] amount_is_token_a: bool,
386 #[values(true, false)] amount_is_input: bool,
387 ) {
388 let quote_type = QuoteType::new(amount_is_token_a, amount_is_input);
389 let payload = OraclePayload {
390 data: OracleData::Empty,
391 skew: SkewMode::None,
392 };
393 let liquidity = build_liquidity(&payload, quote_type, 1000, 1000, 0, 0).unwrap();
394 let quote = consume_liquidity(100, quote_type, &liquidity).unwrap();
395 assert_eq!(
396 quote,
397 Quote {
398 amount_in: 0,
399 amount_out: 0,
400 quote_type,
401 }
402 );
403 }
404
405 #[rstest]
406 #[case(OracleData::FlatPrice { price_q64_64: 100 }, QuoteType::TokenAExactIn, 100, 100, 0)]
407 #[case(OracleData::FlatPrice { price_q64_64: 100 }, QuoteType::TokenBExactIn, 100, 100, 0)]
408 #[case(OracleData::SimpleSpread { price_q64_64: 100, bid_spread_per_m: 10000, ask_spread_per_m: 20000 }, QuoteType::TokenAExactIn, 100, 99, 10000)]
409 #[case(OracleData::SimpleSpread { price_q64_64: 100, bid_spread_per_m: 10000, ask_spread_per_m: 20000 }, QuoteType::TokenBExactIn, 100, 102, 20000)]
410 #[case(OracleData::SimpleSpread { price_q64_64: 100, bid_spread_per_m: -10000, ask_spread_per_m: -20000 }, QuoteType::TokenAExactIn, 100, 101, -10000)]
411 #[case(OracleData::SimpleSpread { price_q64_64: 100, bid_spread_per_m: -10000, ask_spread_per_m: -20000 }, QuoteType::TokenBExactIn, 100, 98, -20000)]
412 fn test_build_price(
413 #[case] oracle: OracleData,
414 #[case] quote_type: QuoteType,
415 #[case] expected_oracle_price: u128,
416 #[case] expected_best_price: u128,
417 #[case] expected_spread_per_m: i32,
418 ) {
419 let reserves_a = 1_000_000;
420 let reserves_b = 1_000_000;
421
422 let payload = OraclePayload {
423 data: oracle,
424 skew: SkewMode::None,
425 };
426 let liquidity =
427 build_liquidity(&payload, quote_type, reserves_a, reserves_b, 0, 0).unwrap();
428
429 let price = build_price(&liquidity, &oracle, quote_type, reserves_a, reserves_b).unwrap();
430
431 let expected = Price {
432 oracle_price_q64_64: expected_oracle_price,
433 best_price_q64_64: expected_best_price,
434 spread_per_m: expected_spread_per_m,
435 };
436
437 assert_eq!(price, expected);
438 }
439
440 #[rstest]
441 #[case(vec![500_000, 500_000, 0], 100, 100, 0)]
442 #[case(vec![0, 500_000, 500_000], 100, 99, 10000)]
443 #[case(vec![0, 0, 1_000_000], 100, 98, 20000)]
444 fn test_build_price_book(
445 #[case] liquidity: Vec<u32>,
446 #[case] expected_oracle_price: u128,
447 #[case] expected_best_price: u128,
448 #[case] expected_spread_per_m: i32,
449 ) {
450 let mut liquidity_per_m = [0u32; 32];
451 liquidity_per_m[..liquidity.len()].copy_from_slice(&liquidity);
452 let reserves_a = 1_000_000;
453 let reserves_b = 1_000_000;
454 let oracle = OracleData::OrderBook {
455 price_q64_64: 100,
456 spacing: BookSpacingType::Linear(10000),
457 bid_liquidity_per_m: liquidity_per_m,
458 ask_liquidity_per_m: [0; 32],
459 };
460
461 let payload = OraclePayload {
462 data: oracle,
463 skew: SkewMode::None,
464 };
465 let liquidity = build_liquidity(
466 &payload,
467 QuoteType::TokenAExactIn,
468 reserves_a,
469 reserves_b,
470 0,
471 0,
472 )
473 .unwrap();
474
475 let price = build_price(
476 &liquidity,
477 &oracle,
478 QuoteType::TokenAExactIn,
479 reserves_a,
480 reserves_b,
481 )
482 .unwrap();
483
484 let expected = Price {
485 oracle_price_q64_64: expected_oracle_price,
486 best_price_q64_64: expected_best_price,
487 spread_per_m: expected_spread_per_m,
488 };
489
490 assert_eq!(price, expected);
491 }
492
493 #[rstest]
494 #[case(vec![0; 32])]
495 #[case({ let mut v = vec![0u32; 32]; v[0] = 999_999; v })]
496 #[case({ let mut v = vec![0u32; 32]; v[0] = 1_000_001; v })]
497 #[case({ let mut v = vec![0u32; 32]; v[0] = 500_000; v[1] = 499_999; v })]
498 fn test_build_liquidity_book_rejects_mismatched_sum(#[case] liquidity: Vec<u32>) {
499 let mut bid_liquidity_per_m = [0u32; 32];
500 bid_liquidity_per_m.copy_from_slice(&liquidity);
501 let oracle = OracleData::OrderBook {
502 price_q64_64: 100,
503 spacing: BookSpacingType::Linear(10000),
504 bid_liquidity_per_m,
505 ask_liquidity_per_m: bid_liquidity_per_m,
506 };
507 let payload = OraclePayload {
508 data: oracle,
509 skew: SkewMode::None,
510 };
511
512 let result = build_liquidity(
513 &payload,
514 QuoteType::TokenAExactIn,
515 1_000_000,
516 1_000_000,
517 0,
518 0,
519 );
520 assert_eq!(result.unwrap_err(), INVALID_ORACLE_DATA);
521 }
522
523 #[rstest]
524 #[case(OracleData::Empty)]
525 #[case(OracleData::FlatPrice { price_q64_64: 1 << 64 })]
526 #[case(OracleData::SimpleSpread { price_q64_64: 1 << 64, bid_spread_per_m: 1000, ask_spread_per_m: 1000 })]
527 #[case(OracleData::AutomatedMarketMaker { liquidity_type: LiquidityType::ConstantProduct, bid_spread_per_m: 1000, ask_spread_per_m: 1000 })]
528 fn test_new_liquidity_unchanged(#[case] oracle: OracleData) {
529 let quote = Quote {
530 amount_in: 100,
531 amount_out: 100,
532 quote_type: QuoteType::TokenAExactIn,
533 };
534 let new_oracle = new_oracle_data(&oracle, "e, 0, 0).unwrap();
535 assert_eq!(new_oracle, oracle);
536 }
537
538 #[rstest]
540 #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::TokenAExactIn }))]
541 #[case(500, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::TokenAExactIn }))]
542 #[case(1100, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::TokenAExactIn }))]
543 #[case(2500, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::TokenAExactIn }))]
544 #[case(10000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::TokenAExactIn }))]
545 fn test_consume_liquidity_input_token_a(
546 #[case] amount: u64,
547 #[case] expected: Result<Quote, CoreError>,
548 ) {
549 let liquidity = SingleSideLiquidity::from_slice(&[
550 (1 << 64, 100),
551 ((1 << 64) / 2, 500),
552 ((1 << 64) / 4, 1000),
553 ]);
554 let quote = consume_liquidity(amount, QuoteType::TokenAExactIn, &liquidity);
555 assert_eq!(quote, expected);
556 }
557
558 #[rstest]
560 #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::TokenBExactIn }))]
561 #[case(500, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::TokenBExactIn }))]
562 #[case(1100, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::TokenBExactIn }))]
563 #[case(2500, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::TokenBExactIn }))]
564 #[case(10000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::TokenBExactIn }))]
565 fn test_consume_liquidity_input_token_b(
566 #[case] amount: u64,
567 #[case] expected: Result<Quote, CoreError>,
568 ) {
569 let liquidity =
570 SingleSideLiquidity::from_slice(&[(1 << 64, 100), (2 << 64, 500), (4 << 64, 1000)]);
571 let quote = consume_liquidity(amount, QuoteType::TokenBExactIn, &liquidity);
572 assert_eq!(quote, expected);
573 }
574
575 #[rstest]
577 #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::TokenAExactOut }))]
578 #[case(300, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::TokenAExactOut }))]
579 #[case(600, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::TokenAExactOut }))]
580 #[case(950, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::TokenAExactOut }))]
581 #[case(2000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::TokenAExactOut }))]
582 fn test_consume_liquidity_output_token_a(
583 #[case] amount: u64,
584 #[case] expected: Result<Quote, CoreError>,
585 ) {
586 let liquidity =
587 SingleSideLiquidity::from_slice(&[(1 << 64, 100), (2 << 64, 500), (4 << 64, 1000)]);
588 let quote = consume_liquidity(amount, QuoteType::TokenAExactOut, &liquidity);
589 assert_eq!(quote, expected);
590 }
591
592 #[rstest]
594 #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::TokenBExactOut }))]
595 #[case(300, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::TokenBExactOut }))]
596 #[case(600, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::TokenBExactOut }))]
597 #[case(950, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::TokenBExactOut }))]
598 #[case(2000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::TokenBExactOut }))]
599 fn test_consume_liquidity_output_token_b(
600 #[case] amount: u64,
601 #[case] expected: Result<Quote, CoreError>,
602 ) {
603 let liquidity = SingleSideLiquidity::from_slice(&[
604 (1 << 64, 100),
605 ((1 << 64) / 2, 500),
606 ((1 << 64) / 4, 1000),
607 ]);
608 let quote = consume_liquidity(amount, QuoteType::TokenBExactOut, &liquidity);
609 assert_eq!(quote, expected);
610 }
611
612 #[rstest]
613 #[case((1 << 64) / 8, true, true, Ok(Quote { amount_in: 100, amount_out: 12, quote_type: QuoteType::TokenAExactIn }))]
614 #[case((1 << 64) / 8, true, false, Ok(Quote { amount_in: 13, amount_out: 100, quote_type: QuoteType::TokenAExactOut }))]
615 #[case(8 << 64, false, true, Ok(Quote { amount_in: 100, amount_out: 12, quote_type: QuoteType::TokenBExactIn }))]
616 #[case(8 << 64, false, false, Ok(Quote { amount_in: 13, amount_out: 100, quote_type: QuoteType::TokenBExactOut }))]
617 fn test_consume_liquidity_rounding_direction(
618 #[case] price: u128,
619 #[case] amount_is_token_a: bool,
620 #[case] amount_is_input: bool,
621 #[case] expected: Result<Quote, CoreError>,
622 ) {
623 let liquidity = SingleSideLiquidity::from_slice(&[(price, 1000)]);
624 let quote_type = QuoteType::new(amount_is_token_a, amount_is_input);
625 let result = consume_liquidity(100, quote_type, &liquidity);
626 assert_eq!(result, expected);
627 }
628
629 const PRICE_ONE: u128 = 1 << 64;
630
631 #[rstest]
634 #[case(OracleData::SimpleSpread { price_q64_64: PRICE_ONE, bid_spread_per_m: 10_000, ask_spread_per_m: 10_000 }, 500, 500, SkewMode::None, false)] #[case(OracleData::SimpleSpread { price_q64_64: PRICE_ONE, bid_spread_per_m: 10_000, ask_spread_per_m: 10_000 }, 500, 500, SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 500_000, negative_bid_per_m: 500_000, positive_ask_per_m: 500_000, negative_ask_per_m: 500_000 }, false)] #[case(OracleData::SimpleSpread { price_q64_64: PRICE_ONE, bid_spread_per_m: 10_000, ask_spread_per_m: 10_000 }, 750, 250, SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 500_000, negative_bid_per_m: 500_000, positive_ask_per_m: 500_000, negative_ask_per_m: 500_000 }, true)] #[case(OracleData::SimpleSpread { price_q64_64: PRICE_ONE, bid_spread_per_m: 10_000, ask_spread_per_m: 10_000 }, 250, 750, SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 500_000, negative_bid_per_m: 500_000, positive_ask_per_m: 500_000, negative_ask_per_m: 500_000 }, false)] #[case(OracleData::SimpleSpread { price_q64_64: PRICE_ONE, bid_spread_per_m: 10_000, ask_spread_per_m: 10_000 }, 750, 250, SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 100_000, negative_bid_per_m: 100_000, positive_ask_per_m: 100_000, negative_ask_per_m: 100_000 }, true)] #[case(OracleData::SimpleSpread { price_q64_64: PRICE_ONE, bid_spread_per_m: 10_000, ask_spread_per_m: 10_000 }, 750, 250, SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 1_000_000, negative_bid_per_m: 1_000_000, positive_ask_per_m: 1_000_000, negative_ask_per_m: 1_000_000 }, true)] #[case(OracleData::SimpleSpread { price_q64_64: PRICE_ONE, bid_spread_per_m: 10_000, ask_spread_per_m: 10_000 }, 750, 250, SkewMode::Polynomial { exponent: SkewExponent::Quadratic, positive_bid_per_m: 500_000, negative_bid_per_m: 500_000, positive_ask_per_m: 500_000, negative_ask_per_m: 500_000 }, true)] #[case(OracleData::FlatPrice { price_q64_64: PRICE_ONE }, 750, 250, SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 500_000, negative_bid_per_m: 500_000, positive_ask_per_m: 500_000, negative_ask_per_m: 500_000 }, true)] #[case(OracleData::SimpleSpread { price_q64_64: PRICE_ONE, bid_spread_per_m: 10_000, ask_spread_per_m: 10_000 }, 750, 250, SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 100_000, negative_bid_per_m: 200_000, positive_ask_per_m: 300_000, negative_ask_per_m: 400_000 }, true)] #[case(OracleData::SimpleSpread { price_q64_64: PRICE_ONE, bid_spread_per_m: 10_000, ask_spread_per_m: 10_000 }, 250, 750, SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 100_000, negative_bid_per_m: 200_000, positive_ask_per_m: 300_000, negative_ask_per_m: 400_000 }, false)] fn test_build_liquidity_skew(
645 #[case] oracle: OracleData,
646 #[case] reserves_a: u64,
647 #[case] reserves_b: u64,
648 #[case] skew_mode: SkewMode,
649 #[case] price_lower: bool,
650 ) {
651 let payload_skew = OraclePayload {
652 data: oracle,
653 skew: skew_mode,
654 };
655 let payload_none = OraclePayload {
656 data: oracle,
657 skew: SkewMode::None,
658 };
659 let result_skew = build_liquidity(
660 &payload_skew,
661 QuoteType::TokenAExactIn,
662 reserves_a,
663 reserves_b,
664 0,
665 0,
666 )
667 .unwrap();
668 let result_none = build_liquidity(
669 &payload_none,
670 QuoteType::TokenAExactIn,
671 reserves_a,
672 reserves_b,
673 0,
674 0,
675 )
676 .unwrap();
677 let (skew_price, _) = result_skew.as_slice()[0];
678 let (none_price, _) = result_none.as_slice()[0];
679 assert_eq!(skew_price < none_price, price_lower);
680 }
681
682 #[test]
683 fn test_build_liquidity_skew_empty() {
684 let payload = OraclePayload {
685 data: OracleData::Empty,
686 skew: SkewMode::Polynomial {
687 exponent: SkewExponent::Linear,
688 positive_bid_per_m: 500_000,
689 negative_bid_per_m: 500_000,
690 positive_ask_per_m: 500_000,
691 negative_ask_per_m: 500_000,
692 },
693 };
694 let result = build_liquidity(&payload, QuoteType::TokenAExactIn, 750, 250, 0, 0).unwrap();
695 assert_eq!(result.len(), 0);
696 }
697}