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) -> Result<SingleSideLiquidity, CoreError> {
204 let liquidity = build_base_liquidity(&payload.data, quote_type, reserves_a, reserves_b)?;
205
206 let price = match &payload.data {
207 OracleData::Empty | OracleData::AutomatedMarketMaker { .. } => return Ok(liquidity),
208 OracleData::FlatPrice { price_q64_64 }
209 | OracleData::SimpleSpread { price_q64_64, .. }
210 | OracleData::OrderBook { price_q64_64, .. } => *price_q64_64,
211 };
212
213 let deviation = deviation_per_m(U128::from(price), reserves_a, reserves_b)?;
214 let skew_per_m = payload.skew.compute_skew_per_m(deviation, quote_type)?;
215 apply_skew_to_liquidity(liquidity, skew_per_m, quote_type)
216}
217
218pub(crate) fn build_price(
219 liquidity: &SingleSideLiquidity,
220 oracle: &OracleData,
221 quote_type: QuoteType,
222 reserves_a: u64,
223 reserves_b: u64,
224) -> Result<Price, CoreError> {
225 let best_price = liquidity
226 .as_slice()
227 .iter()
228 .find(|(_, liquidity)| *liquidity > 0)
229 .map(|(price, _)| *price)
230 .unwrap_or(0);
231
232 let oracle_price = match oracle {
233 OracleData::Empty => 0,
234 OracleData::FlatPrice { price_q64_64 } => *price_q64_64,
235 OracleData::SimpleSpread { price_q64_64, .. } => *price_q64_64,
236 OracleData::OrderBook { price_q64_64, .. } => *price_q64_64,
237 OracleData::AutomatedMarketMaker { liquidity_type, .. } => {
238 amm_price(*liquidity_type, reserves_a, reserves_b)?
239 }
240 };
241
242 let diff = if quote_type.a_to_b() {
243 I256::from(oracle_price)
244 .checked_sub(I256::from(best_price))
245 .ok_or(ARITHMETIC_OVERFLOW)?
246 } else {
247 I256::from(best_price)
248 .checked_sub(I256::from(oracle_price))
249 .ok_or(ARITHMETIC_OVERFLOW)?
250 };
251
252 let spread = if best_price > 0 {
253 diff.checked_mul(I256::from(PER_M_DENOMINATOR))
254 .ok_or(ARITHMETIC_OVERFLOW)?
255 .checked_div(I256::from(oracle_price))
256 .ok_or(ARITHMETIC_OVERFLOW)?
257 .try_into()
258 .map_err(|_| AMOUNT_EXCEEDS_MAX_U32)?
259 } else {
260 0
261 };
262
263 Ok(Price {
264 oracle_price_q64_64: oracle_price,
265 best_price_q64_64: best_price,
266 spread_per_m: spread,
267 })
268}
269
270pub(crate) fn consume_liquidity(
271 amount: u64,
272 quote_type: QuoteType,
273 liquidity: &SingleSideLiquidity,
274) -> Result<Quote, CoreError> {
275 let mut remaining_amount = amount;
276 let mut other_amount: u64 = 0;
277
278 for (price, liquidity) in liquidity.as_slice() {
283 if *price == 0 || *liquidity == 0 {
284 continue;
285 }
286
287 let (step_specified_amount, step_other_amount) = if quote_type.exact_in() {
288 let max_amount = if quote_type.input_is_token_a() {
290 b_to_a(*liquidity, U128::from(*price), true)?
291 } else {
292 a_to_b(*liquidity, U128::from(*price), true)?
293 };
294
295 let step_specified_amount = min(remaining_amount, max_amount);
296 let step_other_amount = if quote_type.input_is_token_a() {
297 a_to_b(step_specified_amount, U128::from(*price), false)?
298 } else {
299 b_to_a(step_specified_amount, U128::from(*price), false)?
300 };
301
302 (step_specified_amount, min(step_other_amount, *liquidity))
303 } else {
304 let step_specified_amount = min(remaining_amount, *liquidity);
307 let step_other_amount = if quote_type.output_is_token_a() {
308 a_to_b(step_specified_amount, U128::from(*price), true)?
309 } else {
310 b_to_a(step_specified_amount, U128::from(*price), true)?
311 };
312
313 (step_specified_amount, step_other_amount)
314 };
315
316 remaining_amount = remaining_amount
317 .checked_sub(step_specified_amount)
318 .ok_or(ARITHMETIC_OVERFLOW)?;
319 other_amount = other_amount
320 .checked_add(step_other_amount)
321 .ok_or(ARITHMETIC_OVERFLOW)?;
322
323 if remaining_amount == 0 {
324 break;
325 }
326 }
327
328 let consumed_amount = amount - remaining_amount;
329
330 let (amount_in, amount_out) = if quote_type.exact_in() {
331 (consumed_amount, other_amount)
332 } else {
333 (other_amount, consumed_amount)
334 };
335
336 Ok(Quote {
337 amount_in,
338 amount_out,
339 quote_type,
340 })
341}
342
343pub(crate) fn new_oracle_data(
344 oracle: &OracleData,
345 quote: &Quote,
346 reserves_a: u64,
347 reserves_b: u64,
348) -> Result<OracleData, CoreError> {
349 match oracle {
350 OracleData::Empty
351 | OracleData::FlatPrice { .. }
352 | OracleData::SimpleSpread { .. }
353 | OracleData::AutomatedMarketMaker { .. } => Ok(*oracle),
354 OracleData::OrderBook {
355 price_q64_64,
356 spacing,
357 bid_liquidity_per_m,
358 ask_liquidity_per_m,
359 } => new_book_liquidity(
360 quote,
361 *price_q64_64,
362 *spacing,
363 bid_liquidity_per_m,
364 ask_liquidity_per_m,
365 reserves_a,
366 reserves_b,
367 ),
368 }
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374 use rstest::rstest;
375
376 #[rstest]
377 fn test_empty(
378 #[values(true, false)] amount_is_token_a: bool,
379 #[values(true, false)] amount_is_input: bool,
380 ) {
381 let quote_type = QuoteType::new(amount_is_token_a, amount_is_input);
382 let payload = OraclePayload {
383 data: OracleData::Empty,
384 skew: SkewMode::None,
385 };
386 let liquidity = build_liquidity(&payload, quote_type, 1000, 1000).unwrap();
387 let quote = consume_liquidity(100, quote_type, &liquidity).unwrap();
388 assert_eq!(
389 quote,
390 Quote {
391 amount_in: 0,
392 amount_out: 0,
393 quote_type,
394 }
395 );
396 }
397
398 #[rstest]
399 #[case(OracleData::FlatPrice { price_q64_64: 100 }, QuoteType::TokenAExactIn, 100, 100, 0)]
400 #[case(OracleData::FlatPrice { price_q64_64: 100 }, QuoteType::TokenBExactIn, 100, 100, 0)]
401 #[case(OracleData::SimpleSpread { price_q64_64: 100, bid_spread_per_m: 10000, ask_spread_per_m: 20000 }, QuoteType::TokenAExactIn, 100, 99, 10000)]
402 #[case(OracleData::SimpleSpread { price_q64_64: 100, bid_spread_per_m: 10000, ask_spread_per_m: 20000 }, QuoteType::TokenBExactIn, 100, 102, 20000)]
403 #[case(OracleData::SimpleSpread { price_q64_64: 100, bid_spread_per_m: -10000, ask_spread_per_m: -20000 }, QuoteType::TokenAExactIn, 100, 101, -10000)]
404 #[case(OracleData::SimpleSpread { price_q64_64: 100, bid_spread_per_m: -10000, ask_spread_per_m: -20000 }, QuoteType::TokenBExactIn, 100, 98, -20000)]
405 fn test_build_price(
406 #[case] oracle: OracleData,
407 #[case] quote_type: QuoteType,
408 #[case] expected_oracle_price: u128,
409 #[case] expected_best_price: u128,
410 #[case] expected_spread_per_m: i32,
411 ) {
412 let reserves_a = 1_000_000;
413 let reserves_b = 1_000_000;
414
415 let payload = OraclePayload {
416 data: oracle,
417 skew: SkewMode::None,
418 };
419 let liquidity = build_liquidity(&payload, quote_type, reserves_a, reserves_b).unwrap();
420
421 let price = build_price(&liquidity, &oracle, quote_type, reserves_a, reserves_b).unwrap();
422
423 let expected = Price {
424 oracle_price_q64_64: expected_oracle_price,
425 best_price_q64_64: expected_best_price,
426 spread_per_m: expected_spread_per_m,
427 };
428
429 assert_eq!(price, expected);
430 }
431
432 #[rstest]
433 #[case(vec![500_000, 500_000, 0], 100, 100, 0)]
434 #[case(vec![0, 500_000, 500_000], 100, 99, 10000)]
435 #[case(vec![0, 0, 1_000_000], 100, 98, 20000)]
436 #[case(vec![0, 0, 0], 100, 0, 0)]
437 fn test_build_price_book(
438 #[case] liquidity: Vec<u32>,
439 #[case] expected_oracle_price: u128,
440 #[case] expected_best_price: u128,
441 #[case] expected_spread_per_m: i32,
442 ) {
443 let mut liquidity_per_m = [0u32; 32];
444 liquidity_per_m[..liquidity.len()].copy_from_slice(&liquidity);
445 let reserves_a = 1_000_000;
446 let reserves_b = 1_000_000;
447 let oracle = OracleData::OrderBook {
448 price_q64_64: 100,
449 spacing: BookSpacingType::Linear(10000),
450 bid_liquidity_per_m: liquidity_per_m,
451 ask_liquidity_per_m: [0; 32],
452 };
453
454 let payload = OraclePayload {
455 data: oracle,
456 skew: SkewMode::None,
457 };
458 let liquidity =
459 build_liquidity(&payload, QuoteType::TokenAExactIn, reserves_a, reserves_b).unwrap();
460
461 let price = build_price(
462 &liquidity,
463 &oracle,
464 QuoteType::TokenAExactIn,
465 reserves_a,
466 reserves_b,
467 )
468 .unwrap();
469
470 let expected = Price {
471 oracle_price_q64_64: expected_oracle_price,
472 best_price_q64_64: expected_best_price,
473 spread_per_m: expected_spread_per_m,
474 };
475
476 assert_eq!(price, expected);
477 }
478
479 #[rstest]
480 #[case(OracleData::Empty)]
481 #[case(OracleData::FlatPrice { price_q64_64: 1 << 64 })]
482 #[case(OracleData::SimpleSpread { price_q64_64: 1 << 64, bid_spread_per_m: 1000, ask_spread_per_m: 1000 })]
483 #[case(OracleData::AutomatedMarketMaker { liquidity_type: LiquidityType::ConstantProduct, bid_spread_per_m: 1000, ask_spread_per_m: 1000 })]
484 fn test_new_liquidity_unchanged(#[case] oracle: OracleData) {
485 let quote = Quote {
486 amount_in: 100,
487 amount_out: 100,
488 quote_type: QuoteType::TokenAExactIn,
489 };
490 let new_oracle = new_oracle_data(&oracle, "e, 0, 0).unwrap();
491 assert_eq!(new_oracle, oracle);
492 }
493
494 #[rstest]
496 #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::TokenAExactIn }))]
497 #[case(500, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::TokenAExactIn }))]
498 #[case(1100, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::TokenAExactIn }))]
499 #[case(2500, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::TokenAExactIn }))]
500 #[case(10000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::TokenAExactIn }))]
501 fn test_consume_liquidity_input_token_a(
502 #[case] amount: u64,
503 #[case] expected: Result<Quote, CoreError>,
504 ) {
505 let liquidity = SingleSideLiquidity::from_slice(&[
506 (1 << 64, 100),
507 ((1 << 64) / 2, 500),
508 ((1 << 64) / 4, 1000),
509 ]);
510 let quote = consume_liquidity(amount, QuoteType::TokenAExactIn, &liquidity);
511 assert_eq!(quote, expected);
512 }
513
514 #[rstest]
516 #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::TokenBExactIn }))]
517 #[case(500, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::TokenBExactIn }))]
518 #[case(1100, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::TokenBExactIn }))]
519 #[case(2500, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::TokenBExactIn }))]
520 #[case(10000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::TokenBExactIn }))]
521 fn test_consume_liquidity_input_token_b(
522 #[case] amount: u64,
523 #[case] expected: Result<Quote, CoreError>,
524 ) {
525 let liquidity =
526 SingleSideLiquidity::from_slice(&[(1 << 64, 100), (2 << 64, 500), (4 << 64, 1000)]);
527 let quote = consume_liquidity(amount, QuoteType::TokenBExactIn, &liquidity);
528 assert_eq!(quote, expected);
529 }
530
531 #[rstest]
533 #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::TokenAExactOut }))]
534 #[case(300, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::TokenAExactOut }))]
535 #[case(600, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::TokenAExactOut }))]
536 #[case(950, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::TokenAExactOut }))]
537 #[case(2000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::TokenAExactOut }))]
538 fn test_consume_liquidity_output_token_a(
539 #[case] amount: u64,
540 #[case] expected: Result<Quote, CoreError>,
541 ) {
542 let liquidity =
543 SingleSideLiquidity::from_slice(&[(1 << 64, 100), (2 << 64, 500), (4 << 64, 1000)]);
544 let quote = consume_liquidity(amount, QuoteType::TokenAExactOut, &liquidity);
545 assert_eq!(quote, expected);
546 }
547
548 #[rstest]
550 #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::TokenBExactOut }))]
551 #[case(300, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::TokenBExactOut }))]
552 #[case(600, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::TokenBExactOut }))]
553 #[case(950, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::TokenBExactOut }))]
554 #[case(2000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::TokenBExactOut }))]
555 fn test_consume_liquidity_output_token_b(
556 #[case] amount: u64,
557 #[case] expected: Result<Quote, CoreError>,
558 ) {
559 let liquidity = SingleSideLiquidity::from_slice(&[
560 (1 << 64, 100),
561 ((1 << 64) / 2, 500),
562 ((1 << 64) / 4, 1000),
563 ]);
564 let quote = consume_liquidity(amount, QuoteType::TokenBExactOut, &liquidity);
565 assert_eq!(quote, expected);
566 }
567
568 #[rstest]
569 #[case((1 << 64) / 8, true, true, Ok(Quote { amount_in: 100, amount_out: 12, quote_type: QuoteType::TokenAExactIn }))]
570 #[case((1 << 64) / 8, true, false, Ok(Quote { amount_in: 13, amount_out: 100, quote_type: QuoteType::TokenAExactOut }))]
571 #[case(8 << 64, false, true, Ok(Quote { amount_in: 100, amount_out: 12, quote_type: QuoteType::TokenBExactIn }))]
572 #[case(8 << 64, false, false, Ok(Quote { amount_in: 13, amount_out: 100, quote_type: QuoteType::TokenBExactOut }))]
573 fn test_consume_liquidity_rounding_direction(
574 #[case] price: u128,
575 #[case] amount_is_token_a: bool,
576 #[case] amount_is_input: bool,
577 #[case] expected: Result<Quote, CoreError>,
578 ) {
579 let liquidity = SingleSideLiquidity::from_slice(&[(price, 1000)]);
580 let quote_type = QuoteType::new(amount_is_token_a, amount_is_input);
581 let result = consume_liquidity(100, quote_type, &liquidity);
582 assert_eq!(result, expected);
583 }
584
585 const PRICE_ONE: u128 = 1 << 64;
586
587 #[rstest]
590 #[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(
601 #[case] oracle: OracleData,
602 #[case] reserves_a: u64,
603 #[case] reserves_b: u64,
604 #[case] skew_mode: SkewMode,
605 #[case] price_lower: bool,
606 ) {
607 let payload_skew = OraclePayload {
608 data: oracle,
609 skew: skew_mode,
610 };
611 let payload_none = OraclePayload {
612 data: oracle,
613 skew: SkewMode::None,
614 };
615 let result_skew = build_liquidity(
616 &payload_skew,
617 QuoteType::TokenAExactIn,
618 reserves_a,
619 reserves_b,
620 )
621 .unwrap();
622 let result_none = build_liquidity(
623 &payload_none,
624 QuoteType::TokenAExactIn,
625 reserves_a,
626 reserves_b,
627 )
628 .unwrap();
629 let (skew_price, _) = result_skew.as_slice()[0];
630 let (none_price, _) = result_none.as_slice()[0];
631 assert_eq!(skew_price < none_price, price_lower);
632 }
633
634 #[test]
635 fn test_build_liquidity_skew_empty() {
636 let payload = OraclePayload {
637 data: OracleData::Empty,
638 skew: SkewMode::Polynomial {
639 exponent: SkewExponent::Linear,
640 positive_bid_per_m: 500_000,
641 negative_bid_per_m: 500_000,
642 positive_ask_per_m: 500_000,
643 negative_ask_per_m: 500_000,
644 },
645 };
646 let result = build_liquidity(&payload, QuoteType::TokenAExactIn, 750, 250).unwrap();
647 assert_eq!(result.len(), 0);
648 }
649}