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