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},
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, compute_deviation_per_m};
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_M_DENOMINATOR: i32 = 1_000_000;
90
91#[derive(Debug, Clone, Copy, Eq, PartialEq)]
92#[cfg_attr(true, derive(BorshDeserialize, BorshSerialize))]
93#[cfg_attr(feature = "wasm", wasm_expose)]
94pub enum OracleData {
95 Empty,
96 FlatPrice {
97 price_q64_64: u128,
98 },
99 SimpleSpread {
100 price_q64_64: u128,
101 bid_spread_per_m: i32,
102 ask_spread_per_m: i32,
103 },
104 OrderBook {
105 price_q64_64: u128,
106 spacing: BookSpacingType,
108 bid_liquidity_per_m: [u32; BOOK_LIQUIDITY_LEVELS],
110 ask_liquidity_per_m: [u32; BOOK_LIQUIDITY_LEVELS],
111 },
112 AutomatedMarketMaker {
113 liquidity_type: LiquidityType,
114 bid_spread_per_m: i32,
115 ask_spread_per_m: i32,
116 },
117}
118
119#[derive(Debug, Clone, Copy, Eq, PartialEq)]
120pub struct OraclePayload {
121 pub data: OracleData,
122 pub skew: SkewMode,
123}
124
125impl BorshDeserialize for OraclePayload {
126 fn deserialize_reader<R: borsh::io::Read>(reader: &mut R) -> borsh::io::Result<Self> {
127 const _: () = assert!(ORACLE_DATA_LEN + SKEW_LEN <= ORACLE_PAYLOAD_LEN);
128
129 let mut buf = [0u8; ORACLE_PAYLOAD_LEN];
130 reader.read_exact(&mut buf)?;
131 let mut data_slice = &buf[..ORACLE_DATA_LEN];
132 let data = OracleData::deserialize(&mut data_slice)?;
133 let mut skew_slice = &buf[SKEW_OFFSET..SKEW_OFFSET + SKEW_LEN];
134 let skew = SkewMode::deserialize(&mut skew_slice)?;
135 Ok(Self { data, skew })
136 }
137}
138
139#[inline(never)]
140fn build_base_liquidity(
141 data: &OracleData,
142 quote_type: QuoteType,
143 reserves_a: u64,
144 reserves_b: u64,
145) -> Result<SingleSideLiquidity, CoreError> {
146 match data {
147 OracleData::Empty => Ok(SingleSideLiquidity::new()),
148 OracleData::FlatPrice { price_q64_64 } => {
149 flat_liquidity(*price_q64_64, quote_type, reserves_a, reserves_b)
150 }
151 OracleData::SimpleSpread {
152 price_q64_64,
153 bid_spread_per_m,
154 ask_spread_per_m,
155 } => spread_liquidity(
156 *price_q64_64,
157 *bid_spread_per_m,
158 *ask_spread_per_m,
159 quote_type,
160 reserves_a,
161 reserves_b,
162 ),
163 OracleData::OrderBook {
164 price_q64_64,
165 spacing,
166 bid_liquidity_per_m,
167 ask_liquidity_per_m,
168 } => book_liquidity(
169 quote_type,
170 *price_q64_64,
171 *spacing,
172 bid_liquidity_per_m,
173 ask_liquidity_per_m,
174 reserves_a,
175 reserves_b,
176 ),
177 OracleData::AutomatedMarketMaker {
178 liquidity_type,
179 bid_spread_per_m,
180 ask_spread_per_m,
181 } => amm_liquidity(
182 *liquidity_type,
183 *bid_spread_per_m,
184 *ask_spread_per_m,
185 quote_type,
186 reserves_a,
187 reserves_b,
188 ),
189 }
190}
191
192pub(crate) fn build_liquidity(
193 payload: &OraclePayload,
194 quote_type: QuoteType,
195 reserves_a: u64,
196 reserves_b: u64,
197) -> Result<SingleSideLiquidity, CoreError> {
198 let liquidity = build_base_liquidity(&payload.data, quote_type, reserves_a, reserves_b)?;
199
200 let price = match &payload.data {
201 OracleData::Empty | OracleData::AutomatedMarketMaker { .. } => return Ok(liquidity),
202 OracleData::FlatPrice { price_q64_64 }
203 | OracleData::SimpleSpread { price_q64_64, .. }
204 | OracleData::OrderBook { price_q64_64, .. } => *price_q64_64,
205 };
206
207 let deviation = compute_deviation_per_m(price, reserves_a, reserves_b)?;
208 let skew_per_m = payload.skew.compute_skew_per_m(deviation, quote_type)?;
209 apply_skew_to_liquidity(liquidity, skew_per_m, quote_type)
210}
211
212pub(crate) fn build_price(
213 liquidity: &SingleSideLiquidity,
214 oracle: &OracleData,
215 quote_type: QuoteType,
216 reserves_a: u64,
217 reserves_b: u64,
218) -> Result<Price, CoreError> {
219 let best_price = liquidity
220 .as_slice()
221 .iter()
222 .find(|(_, liquidity)| *liquidity > 0)
223 .map(|(price, _)| *price)
224 .unwrap_or(0);
225
226 let oracle_price = match oracle {
227 OracleData::Empty => 0,
228 OracleData::FlatPrice { price_q64_64 } => *price_q64_64,
229 OracleData::SimpleSpread { price_q64_64, .. } => *price_q64_64,
230 OracleData::OrderBook { price_q64_64, .. } => *price_q64_64,
231 OracleData::AutomatedMarketMaker { liquidity_type, .. } => {
232 amm_price(*liquidity_type, reserves_a, reserves_b)?
233 }
234 };
235
236 let diff = if quote_type.a_to_b() {
237 I256::from(oracle_price)
238 .checked_sub(I256::from(best_price))
239 .ok_or(ARITHMETIC_OVERFLOW)?
240 } else {
241 I256::from(best_price)
242 .checked_sub(I256::from(oracle_price))
243 .ok_or(ARITHMETIC_OVERFLOW)?
244 };
245
246 let spread = if best_price > 0 {
247 diff.checked_mul(I256::from(PER_M_DENOMINATOR))
248 .ok_or(ARITHMETIC_OVERFLOW)?
249 .checked_div(I256::from(oracle_price))
250 .ok_or(ARITHMETIC_OVERFLOW)?
251 .try_into()
252 .map_err(|_| AMOUNT_EXCEEDS_MAX_U32)?
253 } else {
254 0
255 };
256
257 Ok(Price {
258 oracle_price_q64_64: oracle_price,
259 best_price_q64_64: best_price,
260 spread_per_m: spread,
261 })
262}
263
264pub(crate) fn consume_liquidity(
265 amount: u64,
266 quote_type: QuoteType,
267 liquidity: &SingleSideLiquidity,
268) -> Result<Quote, CoreError> {
269 let mut remaining_amount = amount;
270 let mut other_amount: u64 = 0;
271
272 for (price, liquidity) in liquidity.as_slice() {
277 if *price == 0 || *liquidity == 0 {
278 continue;
279 }
280
281 let (step_specified_amount, step_other_amount) = if quote_type.exact_in() {
282 let max_amount = if quote_type.input_is_token_a() {
284 b_to_a(*liquidity, U128::from(*price), true)?
285 } else {
286 a_to_b(*liquidity, U128::from(*price), true)?
287 };
288
289 let step_specified_amount = min(remaining_amount, max_amount);
290 let step_other_amount = if quote_type.input_is_token_a() {
291 a_to_b(step_specified_amount, U128::from(*price), false)?
292 } else {
293 b_to_a(step_specified_amount, U128::from(*price), false)?
294 };
295
296 (step_specified_amount, min(step_other_amount, *liquidity))
297 } else {
298 let step_specified_amount = min(remaining_amount, *liquidity);
301 let step_other_amount = if quote_type.output_is_token_a() {
302 a_to_b(step_specified_amount, U128::from(*price), true)?
303 } else {
304 b_to_a(step_specified_amount, U128::from(*price), true)?
305 };
306
307 (step_specified_amount, step_other_amount)
308 };
309
310 remaining_amount = remaining_amount
311 .checked_sub(step_specified_amount)
312 .ok_or(ARITHMETIC_OVERFLOW)?;
313 other_amount = other_amount
314 .checked_add(step_other_amount)
315 .ok_or(ARITHMETIC_OVERFLOW)?;
316
317 if remaining_amount == 0 {
318 break;
319 }
320 }
321
322 let consumed_amount = amount - remaining_amount;
323
324 let (amount_in, amount_out) = if quote_type.exact_in() {
325 (consumed_amount, other_amount)
326 } else {
327 (other_amount, consumed_amount)
328 };
329
330 Ok(Quote {
331 amount_in,
332 amount_out,
333 quote_type,
334 })
335}
336
337pub(crate) fn new_oracle_data(
338 oracle: &OracleData,
339 quote: &Quote,
340 reserves_a: u64,
341 reserves_b: u64,
342) -> Result<OracleData, CoreError> {
343 match oracle {
344 OracleData::Empty
345 | OracleData::FlatPrice { .. }
346 | OracleData::SimpleSpread { .. }
347 | OracleData::AutomatedMarketMaker { .. } => Ok(*oracle),
348 OracleData::OrderBook {
349 price_q64_64,
350 spacing,
351 bid_liquidity_per_m,
352 ask_liquidity_per_m,
353 } => new_book_liquidity(
354 quote,
355 *price_q64_64,
356 *spacing,
357 bid_liquidity_per_m,
358 ask_liquidity_per_m,
359 reserves_a,
360 reserves_b,
361 ),
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368 use rstest::rstest;
369
370 #[rstest]
371 fn test_empty(
372 #[values(true, false)] amount_is_token_a: bool,
373 #[values(true, false)] amount_is_input: bool,
374 ) {
375 let quote_type = QuoteType::new(amount_is_token_a, amount_is_input);
376 let payload = OraclePayload {
377 data: OracleData::Empty,
378 skew: SkewMode::None,
379 };
380 let liquidity = build_liquidity(&payload, quote_type, 1000, 1000).unwrap();
381 let quote = consume_liquidity(100, quote_type, &liquidity).unwrap();
382 assert_eq!(
383 quote,
384 Quote {
385 amount_in: 0,
386 amount_out: 0,
387 quote_type,
388 }
389 );
390 }
391
392 #[rstest]
393 #[case(OracleData::FlatPrice { price_q64_64: 100 }, QuoteType::TokenAExactIn, 100, 100, 0)]
394 #[case(OracleData::FlatPrice { price_q64_64: 100 }, QuoteType::TokenBExactIn, 100, 100, 0)]
395 #[case(OracleData::SimpleSpread { price_q64_64: 100, bid_spread_per_m: 10000, ask_spread_per_m: 20000 }, QuoteType::TokenAExactIn, 100, 99, 10000)]
396 #[case(OracleData::SimpleSpread { price_q64_64: 100, bid_spread_per_m: 10000, ask_spread_per_m: 20000 }, QuoteType::TokenBExactIn, 100, 102, 20000)]
397 #[case(OracleData::SimpleSpread { price_q64_64: 100, bid_spread_per_m: -10000, ask_spread_per_m: -20000 }, QuoteType::TokenAExactIn, 100, 101, -10000)]
398 #[case(OracleData::SimpleSpread { price_q64_64: 100, bid_spread_per_m: -10000, ask_spread_per_m: -20000 }, QuoteType::TokenBExactIn, 100, 98, -20000)]
399 fn test_build_price(
400 #[case] oracle: OracleData,
401 #[case] quote_type: QuoteType,
402 #[case] expected_oracle_price: u128,
403 #[case] expected_best_price: u128,
404 #[case] expected_spread_per_m: i32,
405 ) {
406 let reserves_a = 1_000_000;
407 let reserves_b = 1_000_000;
408
409 let payload = OraclePayload {
410 data: oracle,
411 skew: SkewMode::None,
412 };
413 let liquidity = build_liquidity(&payload, quote_type, reserves_a, reserves_b).unwrap();
414
415 let price = build_price(&liquidity, &oracle, quote_type, reserves_a, reserves_b).unwrap();
416
417 let expected = Price {
418 oracle_price_q64_64: expected_oracle_price,
419 best_price_q64_64: expected_best_price,
420 spread_per_m: expected_spread_per_m,
421 };
422
423 assert_eq!(price, expected);
424 }
425
426 #[rstest]
427 #[case(vec![500_000, 500_000, 0], 100, 100, 0)]
428 #[case(vec![0, 500_000, 500_000], 100, 99, 10000)]
429 #[case(vec![0, 0, 1_000_000], 100, 98, 20000)]
430 #[case(vec![0, 0, 0], 100, 0, 0)]
431 fn test_build_price_book(
432 #[case] liquidity: Vec<u32>,
433 #[case] expected_oracle_price: u128,
434 #[case] expected_best_price: u128,
435 #[case] expected_spread_per_m: i32,
436 ) {
437 let mut liquidity_per_m = [0u32; 32];
438 liquidity_per_m[..liquidity.len()].copy_from_slice(&liquidity);
439 let reserves_a = 1_000_000;
440 let reserves_b = 1_000_000;
441 let oracle = OracleData::OrderBook {
442 price_q64_64: 100,
443 spacing: BookSpacingType::Linear(10000),
444 bid_liquidity_per_m: liquidity_per_m,
445 ask_liquidity_per_m: [0; 32],
446 };
447
448 let payload = OraclePayload {
449 data: oracle,
450 skew: SkewMode::None,
451 };
452 let liquidity =
453 build_liquidity(&payload, QuoteType::TokenAExactIn, reserves_a, reserves_b).unwrap();
454
455 let price = build_price(
456 &liquidity,
457 &oracle,
458 QuoteType::TokenAExactIn,
459 reserves_a,
460 reserves_b,
461 )
462 .unwrap();
463
464 let expected = Price {
465 oracle_price_q64_64: expected_oracle_price,
466 best_price_q64_64: expected_best_price,
467 spread_per_m: expected_spread_per_m,
468 };
469
470 assert_eq!(price, expected);
471 }
472
473 #[rstest]
474 #[case(OracleData::Empty)]
475 #[case(OracleData::FlatPrice { price_q64_64: 1 << 64 })]
476 #[case(OracleData::SimpleSpread { price_q64_64: 1 << 64, bid_spread_per_m: 1000, ask_spread_per_m: 1000 })]
477 #[case(OracleData::AutomatedMarketMaker { liquidity_type: LiquidityType::ConstantProduct, bid_spread_per_m: 1000, ask_spread_per_m: 1000 })]
478 fn test_new_liquidity_unchanged(#[case] oracle: OracleData) {
479 let quote = Quote {
480 amount_in: 100,
481 amount_out: 100,
482 quote_type: QuoteType::TokenAExactIn,
483 };
484 let new_oracle = new_oracle_data(&oracle, "e, 0, 0).unwrap();
485 assert_eq!(new_oracle, oracle);
486 }
487
488 #[rstest]
490 #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::TokenAExactIn }))]
491 #[case(500, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::TokenAExactIn }))]
492 #[case(1100, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::TokenAExactIn }))]
493 #[case(2500, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::TokenAExactIn }))]
494 #[case(10000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::TokenAExactIn }))]
495 fn test_consume_liquidity_input_token_a(
496 #[case] amount: u64,
497 #[case] expected: Result<Quote, CoreError>,
498 ) {
499 let liquidity = SingleSideLiquidity::from_slice(&[
500 (1 << 64, 100),
501 ((1 << 64) / 2, 500),
502 ((1 << 64) / 4, 1000),
503 ]);
504 let quote = consume_liquidity(amount, QuoteType::TokenAExactIn, &liquidity);
505 assert_eq!(quote, expected);
506 }
507
508 #[rstest]
510 #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::TokenBExactIn }))]
511 #[case(500, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::TokenBExactIn }))]
512 #[case(1100, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::TokenBExactIn }))]
513 #[case(2500, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::TokenBExactIn }))]
514 #[case(10000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::TokenBExactIn }))]
515 fn test_consume_liquidity_input_token_b(
516 #[case] amount: u64,
517 #[case] expected: Result<Quote, CoreError>,
518 ) {
519 let liquidity =
520 SingleSideLiquidity::from_slice(&[(1 << 64, 100), (2 << 64, 500), (4 << 64, 1000)]);
521 let quote = consume_liquidity(amount, QuoteType::TokenBExactIn, &liquidity);
522 assert_eq!(quote, expected);
523 }
524
525 #[rstest]
527 #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::TokenAExactOut }))]
528 #[case(300, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::TokenAExactOut }))]
529 #[case(600, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::TokenAExactOut }))]
530 #[case(950, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::TokenAExactOut }))]
531 #[case(2000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::TokenAExactOut }))]
532 fn test_consume_liquidity_output_token_a(
533 #[case] amount: u64,
534 #[case] expected: Result<Quote, CoreError>,
535 ) {
536 let liquidity =
537 SingleSideLiquidity::from_slice(&[(1 << 64, 100), (2 << 64, 500), (4 << 64, 1000)]);
538 let quote = consume_liquidity(amount, QuoteType::TokenAExactOut, &liquidity);
539 assert_eq!(quote, expected);
540 }
541
542 #[rstest]
544 #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::TokenBExactOut }))]
545 #[case(300, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::TokenBExactOut }))]
546 #[case(600, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::TokenBExactOut }))]
547 #[case(950, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::TokenBExactOut }))]
548 #[case(2000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::TokenBExactOut }))]
549 fn test_consume_liquidity_output_token_b(
550 #[case] amount: u64,
551 #[case] expected: Result<Quote, CoreError>,
552 ) {
553 let liquidity = SingleSideLiquidity::from_slice(&[
554 (1 << 64, 100),
555 ((1 << 64) / 2, 500),
556 ((1 << 64) / 4, 1000),
557 ]);
558 let quote = consume_liquidity(amount, QuoteType::TokenBExactOut, &liquidity);
559 assert_eq!(quote, expected);
560 }
561
562 #[rstest]
563 #[case((1 << 64) / 8, true, true, Ok(Quote { amount_in: 100, amount_out: 12, quote_type: QuoteType::TokenAExactIn }))]
564 #[case((1 << 64) / 8, true, false, Ok(Quote { amount_in: 13, amount_out: 100, quote_type: QuoteType::TokenAExactOut }))]
565 #[case(8 << 64, false, true, Ok(Quote { amount_in: 100, amount_out: 12, quote_type: QuoteType::TokenBExactIn }))]
566 #[case(8 << 64, false, false, Ok(Quote { amount_in: 13, amount_out: 100, quote_type: QuoteType::TokenBExactOut }))]
567 fn test_consume_liquidity_rounding_direction(
568 #[case] price: u128,
569 #[case] amount_is_token_a: bool,
570 #[case] amount_is_input: bool,
571 #[case] expected: Result<Quote, CoreError>,
572 ) {
573 let liquidity = SingleSideLiquidity::from_slice(&[(price, 1000)]);
574 let quote_type = QuoteType::new(amount_is_token_a, amount_is_input);
575 let result = consume_liquidity(100, quote_type, &liquidity);
576 assert_eq!(result, expected);
577 }
578
579 const PRICE_ONE: u128 = 1 << 64;
580
581 #[rstest]
584 #[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(
595 #[case] oracle: OracleData,
596 #[case] reserves_a: u64,
597 #[case] reserves_b: u64,
598 #[case] skew_mode: SkewMode,
599 #[case] price_lower: bool,
600 ) {
601 let payload_skew = OraclePayload {
602 data: oracle,
603 skew: skew_mode,
604 };
605 let payload_none = OraclePayload {
606 data: oracle,
607 skew: SkewMode::None,
608 };
609 let result_skew = build_liquidity(
610 &payload_skew,
611 QuoteType::TokenAExactIn,
612 reserves_a,
613 reserves_b,
614 )
615 .unwrap();
616 let result_none = build_liquidity(
617 &payload_none,
618 QuoteType::TokenAExactIn,
619 reserves_a,
620 reserves_b,
621 )
622 .unwrap();
623 let (skew_price, _) = result_skew.as_slice()[0];
624 let (none_price, _) = result_none.as_slice()[0];
625 assert_eq!(skew_price < none_price, price_lower);
626 }
627
628 #[test]
629 fn test_build_liquidity_skew_empty() {
630 let payload = OraclePayload {
631 data: OracleData::Empty,
632 skew: SkewMode::Polynomial {
633 exponent: SkewExponent::Linear,
634 positive_bid_per_m: 500_000,
635 negative_bid_per_m: 500_000,
636 positive_ask_per_m: 500_000,
637 negative_ask_per_m: 500_000,
638 },
639 };
640 let result = build_liquidity(&payload, QuoteType::TokenAExactIn, 750, 250).unwrap();
641 assert_eq!(result.len(), 0);
642 }
643}