Skip to main content

riptide_amm_math/oracle/
mod.rs

1#![allow(dead_code)]
2use std::cmp::min;
3
4#[cfg(feature = "wasm")]
5use riptide_amm_macros::wasm_expose;
6
7use borsh::{BorshDeserialize, BorshSerialize};
8
9use super::{
10    error::{CoreError, ARITHMETIC_OVERFLOW},
11    quote::{Quote, QuoteType},
12    token::{a_to_b, b_to_a},
13};
14
15mod book;
16mod flat;
17mod spread;
18
19pub use book::BookSpacingType;
20
21use book::{book_liquidity, new_book_liquidity, BOOK_LIQUIDITY_LEVELS};
22use flat::flat_liquidity;
23use spread::spread_liquidity;
24
25// Represents the liquidity on one side of the order book (price and amount of out_token).
26// This is explicitly a fixed-size array to avoid heap allocation (which Vec does).
27// The `len` field is the actual number of initialized elements in the array.
28#[derive(Debug, Clone, Copy, Eq, PartialEq)]
29pub(crate) struct SingleSideLiquidity {
30    items: [(u128, u64); 32],
31    len: usize,
32}
33
34impl SingleSideLiquidity {
35    pub fn new() -> Self {
36        Self {
37            items: [(0, 0); 32],
38            len: 0,
39        }
40    }
41
42    pub fn push(&mut self, item: (u128, u64)) -> bool {
43        if self.len < 32 {
44            self.items[self.len] = item;
45            self.len += 1;
46            true
47        } else {
48            false
49        }
50    }
51
52    pub fn len(&self) -> usize {
53        self.len
54    }
55
56    pub fn as_slice(&self) -> &[(u128, u64)] {
57        &self.items[..self.len]
58    }
59
60    pub fn to_vec(&self) -> Vec<(u128, u64)> {
61        self.as_slice().to_vec()
62    }
63}
64
65#[cfg_attr(feature = "wasm", wasm_expose)]
66pub const PER_M_DENOMINATOR: i32 = 1_000_000;
67
68#[derive(Debug, Clone, Copy, Eq, PartialEq)]
69#[cfg_attr(true, derive(BorshDeserialize, BorshSerialize))]
70#[cfg_attr(feature = "wasm", wasm_expose)]
71pub enum OracleData {
72    Empty,
73    Flat {
74        price_q64_64: u128,
75    },
76    Spread {
77        price_q64_64: u128,
78        spread_a_to_b_per_m: i32,
79        spread_b_to_a_per_m: i32,
80    },
81    Book {
82        price_q64_64: u128,
83        // The spacing between each level of liquidity
84        spacing: BookSpacingType,
85        // Expressed as a fraction of the total reserves
86        bid_liquidity_per_m: [u32; BOOK_LIQUIDITY_LEVELS],
87        ask_liquidity_per_m: [u32; BOOK_LIQUIDITY_LEVELS],
88    },
89}
90
91impl OracleData {
92    pub(crate) fn swap(
93        &self,
94        amount: u64,
95        amount_is_token_a: bool,
96        amount_is_input: bool,
97        reserves_a: u64,
98        reserves_b: u64,
99    ) -> Result<Quote, CoreError> {
100        let quote_type = QuoteType::new(amount_is_token_a, amount_is_input);
101        let liquidity: SingleSideLiquidity = match self {
102            OracleData::Empty => SingleSideLiquidity::new(),
103            OracleData::Flat { price_q64_64 } => {
104                flat_liquidity(*price_q64_64, quote_type, reserves_a, reserves_b)?
105            }
106            OracleData::Spread {
107                price_q64_64,
108                spread_a_to_b_per_m,
109                spread_b_to_a_per_m,
110            } => spread_liquidity(
111                *price_q64_64,
112                *spread_a_to_b_per_m,
113                *spread_b_to_a_per_m,
114                quote_type,
115                reserves_a,
116                reserves_b,
117            )?,
118            OracleData::Book {
119                price_q64_64,
120                spacing,
121                bid_liquidity_per_m,
122                ask_liquidity_per_m,
123            } => book_liquidity(
124                quote_type,
125                *price_q64_64,
126                *spacing,
127                bid_liquidity_per_m,
128                ask_liquidity_per_m,
129                reserves_a,
130                reserves_b,
131            )?,
132        };
133
134        consume_liquidity(
135            amount,
136            amount_is_token_a,
137            amount_is_input,
138            liquidity.as_slice(),
139        )
140    }
141
142    pub(crate) fn new_oracle_data(
143        &self,
144        quote: &Quote,
145        reserves_a: u64,
146        reserves_b: u64,
147    ) -> Result<Self, CoreError> {
148        match self {
149            OracleData::Empty | OracleData::Flat { .. } | OracleData::Spread { .. } => Ok(*self),
150            OracleData::Book {
151                price_q64_64,
152                spacing,
153                bid_liquidity_per_m,
154                ask_liquidity_per_m,
155            } => new_book_liquidity(
156                quote,
157                *price_q64_64,
158                *spacing,
159                bid_liquidity_per_m,
160                ask_liquidity_per_m,
161                reserves_a,
162                reserves_b,
163            ),
164        }
165    }
166}
167
168pub(crate) fn consume_liquidity(
169    amount: u64,
170    amount_is_token_a: bool,
171    amount_is_input: bool,
172    liquidity: &[(u128, u64)],
173) -> Result<Quote, CoreError> {
174    let mut remaining_amount = amount;
175    let mut other_amount: u64 = 0;
176
177    // Start consuming the liquidity from the best price (first)
178    // Stop when either the liquidity runs out or the full amount is consumed
179    // `liquidity` represents the amount of output token that is available at the given price
180
181    for (price, liquidity) in liquidity {
182        if *price == 0 || *liquidity == 0 {
183            continue;
184        }
185
186        let (step_specified_amount, step_other_amount) = if amount_is_input {
187            // ExactIn
188            let max_amount = if amount_is_token_a {
189                b_to_a(*liquidity, (*price).into(), true)?
190            } else {
191                a_to_b(*liquidity, (*price).into(), true)?
192            };
193
194            let step_specified_amount = min(remaining_amount, max_amount);
195            let step_other_amount = if amount_is_token_a {
196                a_to_b(step_specified_amount, (*price).into(), false)?
197            } else {
198                b_to_a(step_specified_amount, (*price).into(), false)?
199            };
200
201            (step_specified_amount, min(step_other_amount, *liquidity))
202        } else {
203            // ExactOut
204
205            let step_specified_amount = min(remaining_amount, *liquidity);
206            let step_other_amount = if amount_is_token_a {
207                a_to_b(step_specified_amount, (*price).into(), true)?
208            } else {
209                b_to_a(step_specified_amount, (*price).into(), true)?
210            };
211
212            (step_specified_amount, step_other_amount)
213        };
214
215        remaining_amount = remaining_amount
216            .checked_sub(step_specified_amount)
217            .ok_or(ARITHMETIC_OVERFLOW)?;
218        other_amount = other_amount
219            .checked_add(step_other_amount)
220            .ok_or(ARITHMETIC_OVERFLOW)?;
221
222        if remaining_amount == 0 {
223            break;
224        }
225    }
226
227    let consumed_amount = amount - remaining_amount;
228
229    let quote_type = QuoteType::new(amount_is_token_a, amount_is_input);
230    let (amount_in, amount_out) = if amount_is_input {
231        (consumed_amount, other_amount)
232    } else {
233        (other_amount, consumed_amount)
234    };
235
236    Ok(Quote {
237        amount_in,
238        amount_out,
239        quote_type,
240    })
241}
242
243#[cfg(all(test, feature = "lib"))]
244mod tests {
245    use super::*;
246    use rstest::rstest;
247
248    #[rstest]
249    fn test_empty(
250        #[values(true, false)] amount_is_token_a: bool,
251        #[values(true, false)] amount_is_input: bool,
252    ) {
253        let quote = OracleData::Empty.swap(100, amount_is_token_a, amount_is_input, 1000, 1000);
254        let quote_type = QuoteType::new(amount_is_token_a, amount_is_input);
255        assert_eq!(
256            quote,
257            Ok(Quote {
258                amount_in: 0,
259                amount_out: 0,
260                quote_type,
261            })
262        );
263    }
264
265    #[rstest]
266    #[case(OracleData::Empty)]
267    #[case(OracleData::Flat { price_q64_64: 1 << 64 })]
268    #[case(OracleData::Spread { price_q64_64: 1 << 64, spread_a_to_b_per_m: 1000, spread_b_to_a_per_m: 1000 })]
269    fn test_new_liquidity_unchanged(#[case] oracle: OracleData) {
270        let quote = Quote {
271            amount_in: 100,
272            amount_out: 100,
273            quote_type: QuoteType::TokenAExactIn,
274        };
275        let new_oracle = OracleData::new_oracle_data(&oracle, &quote, 0, 0);
276        assert_eq!(new_oracle, Ok(oracle));
277    }
278
279    // Selling exact token_a. The lower the the price, the less token_b you get.
280    #[rstest]
281    #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::TokenAExactIn }))]
282    #[case(500, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::TokenAExactIn }))]
283    #[case(1100, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::TokenAExactIn }))]
284    #[case(2500, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::TokenAExactIn }))]
285    #[case(10000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::TokenAExactIn }))]
286    fn test_consume_liquidity_input_token_a(
287        #[case] amount: u64,
288        #[case] expected: Result<Quote, CoreError>,
289    ) {
290        let liquidity = vec![(1 << 64, 100), ((1 << 64) / 2, 500), ((1 << 64) / 4, 1000)];
291        let quote = consume_liquidity(amount, true, true, &liquidity);
292        assert_eq!(quote, expected);
293    }
294
295    // Selling exact token_b. The higher the the price, the less token_a you get.
296    #[rstest]
297    #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::TokenBExactIn }))]
298    #[case(500, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::TokenBExactIn }))]
299    #[case(1100, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::TokenBExactIn }))]
300    #[case(2500, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::TokenBExactIn }))]
301    #[case(10000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::TokenBExactIn }))]
302    fn test_consume_liquidity_input_token_b(
303        #[case] amount: u64,
304        #[case] expected: Result<Quote, CoreError>,
305    ) {
306        let liquidity = vec![(1 << 64, 100), (2 << 64, 500), (4 << 64, 1000)];
307        let quote = consume_liquidity(amount, false, true, &liquidity);
308        assert_eq!(quote, expected);
309    }
310
311    // Buying exact token_a. The higher the the price, the more token_b you pay.
312    #[rstest]
313    #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::TokenAExactOut }))]
314    #[case(300, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::TokenAExactOut }))]
315    #[case(600, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::TokenAExactOut }))]
316    #[case(950, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::TokenAExactOut }))]
317    #[case(2000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::TokenAExactOut }))]
318    fn test_consume_liquidity_output_token_a(
319        #[case] amount: u64,
320        #[case] expected: Result<Quote, CoreError>,
321    ) {
322        let liquidity = vec![(1 << 64, 100), (2 << 64, 500), (4 << 64, 1000)];
323        let quote = consume_liquidity(amount, true, false, &liquidity);
324        assert_eq!(quote, expected);
325    }
326
327    // Buying exact token_b. The lower the the price, the more token_a you pay.
328    #[rstest]
329    #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::TokenBExactOut }))]
330    #[case(300, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::TokenBExactOut }))]
331    #[case(600, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::TokenBExactOut }))]
332    #[case(950, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::TokenBExactOut }))]
333    #[case(2000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::TokenBExactOut }))]
334    fn test_consume_liquidity_output_token_b(
335        #[case] amount: u64,
336        #[case] expected: Result<Quote, CoreError>,
337    ) {
338        let liquidity = vec![(1 << 64, 100), ((1 << 64) / 2, 500), ((1 << 64) / 4, 1000)];
339        let quote = consume_liquidity(amount, false, false, &liquidity);
340        assert_eq!(quote, expected);
341    }
342
343    #[rstest]
344    #[case((1 << 64) / 8, true, true, Ok(Quote { amount_in: 100, amount_out: 12, quote_type: QuoteType::TokenAExactIn }))]
345    #[case((1 << 64) / 8, true, false, Ok(Quote { amount_in: 13, amount_out: 100, quote_type: QuoteType::TokenAExactOut }))]
346    #[case(8 << 64, false, true, Ok(Quote { amount_in: 100, amount_out: 12, quote_type: QuoteType::TokenBExactIn }))]
347    #[case(8 << 64, false, false, Ok(Quote { amount_in: 13, amount_out: 100, quote_type: QuoteType::TokenBExactOut }))]
348    fn test_consume_liquidity_rounding_direction(
349        #[case] price: u128,
350        #[case] amount_is_token_a: bool,
351        #[case] amount_is_input: bool,
352        #[case] expected: Result<Quote, CoreError>,
353    ) {
354        let liquidity = vec![(price, 1000)];
355        let result = consume_liquidity(100, amount_is_token_a, amount_is_input, &liquidity);
356        assert_eq!(result, expected);
357    }
358}