Skip to main content

riptide_amm/math/oracle/
mod.rs

1use std::cmp::min;
2
3#[cfg(feature = "wasm")]
4use riptide_amm_macros_lib::wasm_expose;
5
6use borsh::{BorshDeserialize, BorshSerialize};
7
8use super::{
9    error::{CoreError, ARITHMETIC_OVERFLOW},
10    quote::{Quote, QuoteType},
11    token::{a_to_b, b_to_a},
12};
13
14mod book;
15mod flat;
16mod spread;
17
18pub use book::BookSpacingType;
19
20use book::book_liquidity;
21use flat::flat_liquidity;
22use spread::spread_liquidity;
23
24// Represents the liquidity on one side of the order book (price and amount of out_token).
25// This is explicitly a fixed-size array to avoid heap allocation (which Vec does).
26// The `len` field is the actual number of initialized elements in the array.
27#[derive(Debug, Clone, Copy, Eq, PartialEq)]
28pub(crate) struct SingleSideLiquidity {
29    items: [(u128, u64); 32],
30    len: usize,
31}
32
33#[allow(dead_code)]
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; 32],
87        ask_liquidity_per_m: [u32; 32],
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 liquidity: SingleSideLiquidity = match self {
101            OracleData::Empty => SingleSideLiquidity::new(),
102            OracleData::Flat { price_q64_64 } => flat_liquidity(
103                *price_q64_64,
104                amount_is_token_a,
105                amount_is_input,
106                reserves_a,
107                reserves_b,
108            )?,
109            OracleData::Spread {
110                price_q64_64,
111                spread_a_to_b_per_m,
112                spread_b_to_a_per_m,
113            } => spread_liquidity(
114                *price_q64_64,
115                *spread_a_to_b_per_m,
116                *spread_b_to_a_per_m,
117                amount_is_token_a,
118                amount_is_input,
119                reserves_a,
120                reserves_b,
121            )?,
122            OracleData::Book {
123                price_q64_64,
124                spacing,
125                bid_liquidity_per_m,
126                ask_liquidity_per_m,
127            } => book_liquidity(
128                amount_is_token_a,
129                amount_is_input,
130                *price_q64_64,
131                *spacing,
132                bid_liquidity_per_m,
133                ask_liquidity_per_m,
134                reserves_a,
135                reserves_b,
136            )?,
137        };
138
139        consume_liquidity(
140            amount,
141            amount_is_token_a,
142            amount_is_input,
143            liquidity.as_slice(),
144        )
145    }
146}
147
148pub(crate) fn consume_liquidity(
149    amount: u64,
150    amount_is_token_a: bool,
151    amount_is_input: bool,
152    liquidity: &[(u128, u64)],
153) -> Result<Quote, CoreError> {
154    let mut remaining_amount = amount;
155    let mut other_amount: u64 = 0;
156
157    // Start consuming the liquidity from the best price (first)
158    // Stop when either the liquidity runs out or the full amount is consumed
159    // `liquidity` represents the amount of output token that is available at the given price
160
161    for (price, liquidity) in liquidity {
162        if *price == 0 || *liquidity == 0 {
163            continue;
164        }
165
166        let (step_specified_amount, step_other_amount) = if amount_is_input {
167            // ExactIn
168            let max_amount = if amount_is_token_a {
169                b_to_a(*liquidity, (*price).into(), true)?
170            } else {
171                a_to_b(*liquidity, (*price).into(), true)?
172            };
173
174            let step_specified_amount = min(remaining_amount, max_amount);
175            let step_other_amount = if amount_is_token_a {
176                a_to_b(step_specified_amount, (*price).into(), false)?
177            } else {
178                b_to_a(step_specified_amount, (*price).into(), false)?
179            };
180
181            (step_specified_amount, min(step_other_amount, *liquidity))
182        } else {
183            // ExactOut
184
185            let step_specified_amount = min(remaining_amount, *liquidity);
186            let step_other_amount = if amount_is_token_a {
187                a_to_b(step_specified_amount, (*price).into(), true)?
188            } else {
189                b_to_a(step_specified_amount, (*price).into(), true)?
190            };
191
192            (step_specified_amount, step_other_amount)
193        };
194
195        remaining_amount = remaining_amount
196            .checked_sub(step_specified_amount)
197            .ok_or(ARITHMETIC_OVERFLOW)?;
198        other_amount = other_amount
199            .checked_add(step_other_amount)
200            .ok_or(ARITHMETIC_OVERFLOW)?;
201
202        if remaining_amount == 0 {
203            break;
204        }
205    }
206
207    let consumed_amount = amount - remaining_amount;
208
209    if amount_is_input {
210        Ok(Quote {
211            amount_in: consumed_amount,
212            amount_out: other_amount,
213            quote_type: QuoteType::ExactIn,
214        })
215    } else {
216        Ok(Quote {
217            amount_in: other_amount,
218            amount_out: consumed_amount,
219            quote_type: QuoteType::ExactOut,
220        })
221    }
222}
223
224#[cfg(all(test, feature = "lib"))]
225mod tests {
226    use super::*;
227    use rstest::rstest;
228
229    #[rstest]
230    fn test_empty(
231        #[values(true, false)] amount_is_token_a: bool,
232        #[values(true, false)] amount_is_input: bool,
233    ) {
234        let quote = OracleData::Empty.swap(100, amount_is_token_a, amount_is_input, 1000, 1000);
235        let quote_type = if amount_is_input {
236            QuoteType::ExactIn
237        } else {
238            QuoteType::ExactOut
239        };
240        assert_eq!(
241            quote,
242            Ok(Quote {
243                amount_in: 0,
244                amount_out: 0,
245                quote_type,
246            })
247        );
248    }
249
250    // Selling exact token_a. The lower the the price, the less token_b you get.
251    #[rstest]
252    #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::ExactIn }))]
253    #[case(500, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::ExactIn }))]
254    #[case(1100, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::ExactIn }))]
255    #[case(2500, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::ExactIn }))]
256    #[case(10000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::ExactIn }))]
257    fn test_consume_liquidity_input_token_a(
258        #[case] amount: u64,
259        #[case] expected: Result<Quote, CoreError>,
260    ) {
261        let liquidity = vec![(1 << 64, 100), ((1 << 64) / 2, 500), ((1 << 64) / 4, 1000)];
262        let quote = consume_liquidity(amount, true, true, &liquidity);
263        assert_eq!(quote, expected);
264    }
265
266    // Selling exact token_b. The higher the the price, the less token_a you get.
267    #[rstest]
268    #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::ExactIn }))]
269    #[case(500, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::ExactIn }))]
270    #[case(1100, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::ExactIn }))]
271    #[case(2500, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::ExactIn }))]
272    #[case(10000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::ExactIn }))]
273    fn test_consume_liquidity_input_token_b(
274        #[case] amount: u64,
275        #[case] expected: Result<Quote, CoreError>,
276    ) {
277        let liquidity = vec![(1 << 64, 100), (2 << 64, 500), (4 << 64, 1000)];
278        let quote = consume_liquidity(amount, false, true, &liquidity);
279        assert_eq!(quote, expected);
280    }
281
282    // Buying exact token_a. The higher the the price, the more token_b you pay.
283    #[rstest]
284    #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::ExactOut }))]
285    #[case(300, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::ExactOut }))]
286    #[case(600, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::ExactOut }))]
287    #[case(950, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::ExactOut }))]
288    #[case(2000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::ExactOut }))]
289    fn test_consume_liquidity_output_token_a(
290        #[case] amount: u64,
291        #[case] expected: Result<Quote, CoreError>,
292    ) {
293        let liquidity = vec![(1 << 64, 100), (2 << 64, 500), (4 << 64, 1000)];
294        let quote = consume_liquidity(amount, true, false, &liquidity);
295        assert_eq!(quote, expected);
296    }
297
298    // Buying exact token_b. The lower the the price, the more token_a you pay.
299    #[rstest]
300    #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::ExactOut }))]
301    #[case(300, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::ExactOut }))]
302    #[case(600, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::ExactOut }))]
303    #[case(950, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::ExactOut }))]
304    #[case(2000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::ExactOut }))]
305    fn test_consume_liquidity_output_token_b(
306        #[case] amount: u64,
307        #[case] expected: Result<Quote, CoreError>,
308    ) {
309        let liquidity = vec![(1 << 64, 100), ((1 << 64) / 2, 500), ((1 << 64) / 4, 1000)];
310        let quote = consume_liquidity(amount, false, false, &liquidity);
311        assert_eq!(quote, expected);
312    }
313
314    #[rstest]
315    #[case((1 << 64) / 8, true, true, Ok(Quote { amount_in: 100, amount_out: 12, quote_type: QuoteType::ExactIn }))]
316    #[case((1 << 64) / 8, true, false, Ok(Quote { amount_in: 13, amount_out: 100, quote_type: QuoteType::ExactOut }))]
317    #[case(8 << 64, false, true, Ok(Quote { amount_in: 100, amount_out: 12, quote_type: QuoteType::ExactIn }))]
318    #[case(8 << 64, false, false, Ok(Quote { amount_in: 13, amount_out: 100, quote_type: QuoteType::ExactOut }))]
319    fn test_consume_liquidity_rounding_direction(
320        #[case] price: u128,
321        #[case] amount_is_token_a: bool,
322        #[case] amount_is_input: bool,
323        #[case] expected: Result<Quote, CoreError>,
324    ) {
325        let liquidity = vec![(price, 1000)];
326        let result = consume_liquidity(100, amount_is_token_a, amount_is_input, &liquidity);
327        assert_eq!(result, expected);
328    }
329}