Skip to main content

riptide_amm_math/oracle/
mod.rs

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// Represents the liquidity on one side of the order book (price and amount of out_token).
42// This is explicitly a fixed-size array to avoid heap allocation (which Vec does).
43// The `len` field is the actual number of initialized elements in the array.
44#[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        // The spacing between each level of liquidity
113        spacing: BookSpacingType,
114        // Expressed as a fraction of the total reserves
115        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    // Start consuming the liquidity from the best price (first)
286    // Stop when either the liquidity runs out or the full amount is consumed
287    // `liquidity` represents the amount of output token that is available at the given price
288
289    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            // ExactIn
296            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            // ExactOut
312
313            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, &quote, 0, 0).unwrap();
535        assert_eq!(new_oracle, oracle);
536    }
537
538    // Selling exact token_a. The lower the the price, the less token_b you get.
539    #[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    // Selling exact token_b. The higher the the price, the less token_a you get.
559    #[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    // Buying exact token_a. The higher the the price, the more token_b you pay.
576    #[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    // Buying exact token_b. The lower the the price, the more token_a you pay.
593    #[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    // build_liquidity applies skew to SingleSideLiquidity prices
632    // adjusted_price = price * (PER_M - skew) / PER_M
633    #[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)] // balanced, no skew
635    #[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)] // balanced, skew=0
636    #[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)] // excess A, price down
637    #[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)] // excess B, price up
638    #[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)] // low intensity, still lowers
639    #[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)] // max intensity, stronger shift
640    #[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)] // quadratic skew
641    #[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)] // FlatPrice also skewed
642    #[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)] // asymmetric, excess A
643    #[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)] // asymmetric, excess B
644    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}