Skip to main content

riptide_amm_math/oracle/
book.rs

1use super::super::{
2    error::{
3        CoreError, AMOUNT_EXCEEDS_MAX_U128, AMOUNT_EXCEEDS_MAX_U32, AMOUNT_EXCEEDS_MAX_U64,
4        ARITHMETIC_OVERFLOW, INVALID_ORACLE_DATA,
5    },
6    oracle::{OracleData, PER_M_DENOMINATOR},
7    quote::{Quote, QuoteType},
8};
9
10use borsh::{BorshDeserialize, BorshSerialize};
11
12use ethnum::U256;
13#[cfg(feature = "wasm")]
14use riptide_amm_macros::wasm_expose;
15
16use super::SingleSideLiquidity;
17
18pub(crate) const BOOK_LIQUIDITY_LEVELS: usize = 32;
19
20#[derive(Debug, Clone, Copy, Eq, PartialEq)]
21#[cfg_attr(true, derive(BorshDeserialize, BorshSerialize))]
22#[cfg_attr(feature = "wasm", wasm_expose)]
23pub enum BookSpacingType {
24    // Price = price_q64_64 ± (X * i * price_q64_64)
25    Linear(u16),
26    // Price = price_q64_64 * (1 ± X)^i
27    Exponential(u16),
28}
29
30impl BookSpacingType {
31    pub fn price(&self, price_q64_64: u128, i: usize, a_to_b: bool) -> Result<u128, CoreError> {
32        match *self {
33            BookSpacingType::Linear(spacing_per_m) => {
34                let product = price_q64_64
35                    .checked_mul(spacing_per_m as u128)
36                    .ok_or(ARITHMETIC_OVERFLOW)?
37                    .checked_mul(i as u128)
38                    .ok_or(ARITHMETIC_OVERFLOW)?;
39                let quotient = product
40                    .checked_div(PER_M_DENOMINATOR as u128)
41                    .ok_or(ARITHMETIC_OVERFLOW)?;
42                let remainder = product
43                    .checked_rem(PER_M_DENOMINATOR as u128)
44                    .ok_or(ARITHMETIC_OVERFLOW)?;
45                let spread = if !a_to_b && remainder > 0 {
46                    quotient + 1
47                } else {
48                    quotient
49                };
50                if a_to_b {
51                    Ok(price_q64_64 - spread)
52                } else {
53                    Ok(price_q64_64 + spread)
54                }
55            }
56            BookSpacingType::Exponential(spacing_per_m) => {
57                let factor = if a_to_b {
58                    PER_M_DENOMINATOR - spacing_per_m as i32
59                } else {
60                    PER_M_DENOMINATOR + spacing_per_m as i32
61                };
62                let mut base = u128::from(factor as u32)
63                    .checked_shl(64)
64                    .ok_or(ARITHMETIC_OVERFLOW)?
65                    .checked_div(PER_M_DENOMINATOR as u128)
66                    .ok_or(ARITHMETIC_OVERFLOW)?;
67
68                let mut exp = i as u32;
69                let mut result: u128 = price_q64_64;
70
71                while exp > 0 {
72                    if exp & 1 == 1 {
73                        result = U256::from(result)
74                            .checked_mul(U256::from(base))
75                            .ok_or(ARITHMETIC_OVERFLOW)?
76                            .checked_shr(64)
77                            .ok_or(ARITHMETIC_OVERFLOW)?
78                            .try_into()
79                            .map_err(|_| AMOUNT_EXCEEDS_MAX_U128)?;
80                    }
81
82                    base = U256::from(base)
83                        .checked_mul(U256::from(base))
84                        .ok_or(ARITHMETIC_OVERFLOW)?
85                        .checked_shr(64)
86                        .ok_or(ARITHMETIC_OVERFLOW)?
87                        .try_into()
88                        .map_err(|_| AMOUNT_EXCEEDS_MAX_U128)?;
89                    exp >>= 1;
90                }
91
92                Ok(result)
93            }
94        }
95    }
96}
97
98pub(crate) fn book_liquidity(
99    quote_type: QuoteType,
100    price: u128,
101    spacing: BookSpacingType,
102    bid_liquidity_per_m: &[u32],
103    ask_liquidity_per_m: &[u32],
104    reserves_a: u64,
105    reserves_b: u64,
106) -> Result<SingleSideLiquidity, CoreError> {
107    let liquidity_per_m = if quote_type.a_to_b() {
108        bid_liquidity_per_m
109    } else {
110        ask_liquidity_per_m
111    };
112
113    let total_liquidity = if quote_type.a_to_b() {
114        reserves_b
115    } else {
116        reserves_a
117    };
118
119    if liquidity_per_m.iter().sum::<u32>() > PER_M_DENOMINATOR as u32 {
120        return Err(INVALID_ORACLE_DATA);
121    }
122
123    let mut book = SingleSideLiquidity::new();
124
125    for (i, liquidity_share) in liquidity_per_m.iter().enumerate() {
126        if *liquidity_share == 0 {
127            continue;
128        }
129
130        let price = spacing.price(price, i, quote_type.a_to_b())?;
131
132        let liquidity: u64 = u128::from(total_liquidity)
133            .checked_mul(*liquidity_share as u128)
134            .ok_or(ARITHMETIC_OVERFLOW)?
135            .checked_div(PER_M_DENOMINATOR as u128)
136            .ok_or(ARITHMETIC_OVERFLOW)?
137            .try_into()
138            .map_err(|_| AMOUNT_EXCEEDS_MAX_U64)?;
139
140        book.push((price, liquidity));
141    }
142
143    Ok(book)
144}
145
146pub(crate) fn new_book_liquidity(
147    quote: &Quote,
148    price: u128,
149    spacing: BookSpacingType,
150    bid_liquidity_per_m: &[u32],
151    ask_liquidity_per_m: &[u32],
152    reserves_a: u64,
153    reserves_b: u64,
154) -> Result<OracleData, CoreError> {
155    let mut bid_liquidity = [0u128; BOOK_LIQUIDITY_LEVELS];
156    let mut ask_liquidity = [0u128; BOOK_LIQUIDITY_LEVELS];
157
158    for (i, liquidity_per_m) in bid_liquidity_per_m.iter().enumerate() {
159        bid_liquidity[i] = u128::from(*liquidity_per_m)
160            .checked_mul(reserves_b as u128)
161            .ok_or(ARITHMETIC_OVERFLOW)?
162            .checked_div(PER_M_DENOMINATOR as u128)
163            .ok_or(ARITHMETIC_OVERFLOW)?;
164    }
165
166    for (i, liquidity_per_m) in ask_liquidity_per_m.iter().enumerate() {
167        ask_liquidity[i] = u128::from(*liquidity_per_m)
168            .checked_mul(reserves_a as u128)
169            .ok_or(ARITHMETIC_OVERFLOW)?
170            .checked_div(PER_M_DENOMINATOR as u128)
171            .ok_or(ARITHMETIC_OVERFLOW)?;
172    }
173
174    let new_reserves_a = if quote.quote_type.output_is_token_a() {
175        reserves_a
176            .checked_sub(quote.amount_out)
177            .ok_or(ARITHMETIC_OVERFLOW)?
178    } else {
179        reserves_a
180            .checked_add(quote.amount_in)
181            .ok_or(ARITHMETIC_OVERFLOW)?
182    };
183
184    let new_reserves_b = if quote.quote_type.output_is_token_b() {
185        reserves_b
186            .checked_sub(quote.amount_out)
187            .ok_or(ARITHMETIC_OVERFLOW)?
188    } else {
189        reserves_b
190            .checked_add(quote.amount_in)
191            .ok_or(ARITHMETIC_OVERFLOW)?
192    };
193
194    let mut remaining_consumed = quote.amount_out as u128;
195
196    // Remove the liquidity from one side, starting from the best price
197    for i in 0..BOOK_LIQUIDITY_LEVELS {
198        let current_level = if quote.quote_type.a_to_b() {
199            &mut bid_liquidity[i]
200        } else {
201            &mut ask_liquidity[i]
202        };
203        let current_liquidity = *current_level;
204        *current_level = current_liquidity.saturating_sub(remaining_consumed);
205        remaining_consumed = remaining_consumed.saturating_sub(current_liquidity);
206        if remaining_consumed == 0 {
207            break;
208        }
209    }
210
211    // Add liquidity back to the other side at the best price
212    let best_other_level = if quote.quote_type.a_to_b() {
213        let best_index = ask_liquidity
214            .iter_mut()
215            .position(|x| *x > 0)
216            .unwrap_or(BOOK_LIQUIDITY_LEVELS - 1);
217        &mut ask_liquidity[best_index]
218    } else {
219        let best_index = bid_liquidity
220            .iter_mut()
221            .position(|x| *x > 0)
222            .unwrap_or(BOOK_LIQUIDITY_LEVELS - 1);
223        &mut bid_liquidity[best_index]
224    };
225    *best_other_level = best_other_level
226        .checked_add(quote.amount_in as u128)
227        .ok_or(ARITHMETIC_OVERFLOW)?;
228
229    // Since the we don't know the input slice size, we explicitly allocate BOOK_LIQUIDITY_LEVELS
230    // for each side
231    let mut new_bid_liquidity_per_m = [0u32; BOOK_LIQUIDITY_LEVELS];
232    let mut new_ask_liquidity_per_m = [0u32; BOOK_LIQUIDITY_LEVELS];
233
234    // Calculate the new per_m liquidity for each side
235    for i in 0..BOOK_LIQUIDITY_LEVELS {
236        if new_reserves_b > 0 {
237            new_bid_liquidity_per_m[i] = bid_liquidity[i]
238                .checked_mul(PER_M_DENOMINATOR as u128)
239                .ok_or(ARITHMETIC_OVERFLOW)?
240                .checked_div(new_reserves_b as u128)
241                .ok_or(ARITHMETIC_OVERFLOW)?
242                .try_into()
243                .map_err(|_| AMOUNT_EXCEEDS_MAX_U32)?;
244        }
245        if new_reserves_a > 0 {
246            new_ask_liquidity_per_m[i] = ask_liquidity[i]
247                .checked_mul(PER_M_DENOMINATOR as u128)
248                .ok_or(ARITHMETIC_OVERFLOW)?
249                .checked_div(new_reserves_a as u128)
250                .ok_or(ARITHMETIC_OVERFLOW)?
251                .try_into()
252                .map_err(|_| AMOUNT_EXCEEDS_MAX_U32)?;
253        }
254    }
255
256    // Normalize the liquidity to equal PER_M_DENOMINATOR by adding
257    // the missing liquidity to the last price level
258    let total_bid_per_m = new_bid_liquidity_per_m.iter().sum::<u32>();
259    let total_ask_per_m = new_ask_liquidity_per_m.iter().sum::<u32>();
260
261    let missing_bid_per_m = PER_M_DENOMINATOR as u32 - total_bid_per_m;
262    let missing_ask_per_m = PER_M_DENOMINATOR as u32 - total_ask_per_m;
263
264    let worst_bid_index = new_bid_liquidity_per_m
265        .iter()
266        .enumerate()
267        .rev()
268        .find(|(_, x)| **x > 0)
269        .map(|(i, _)| i)
270        .unwrap_or(BOOK_LIQUIDITY_LEVELS - 1);
271    let worst_ask_index = new_ask_liquidity_per_m
272        .iter()
273        .enumerate()
274        .rev()
275        .find(|(_, x)| **x > 0)
276        .map(|(i, _)| i)
277        .unwrap_or(BOOK_LIQUIDITY_LEVELS - 1);
278
279    new_bid_liquidity_per_m[worst_bid_index] = new_bid_liquidity_per_m[worst_bid_index]
280        .checked_add(missing_bid_per_m)
281        .ok_or(ARITHMETIC_OVERFLOW)?;
282    new_ask_liquidity_per_m[worst_ask_index] = new_ask_liquidity_per_m[worst_ask_index]
283        .checked_add(missing_ask_per_m)
284        .ok_or(ARITHMETIC_OVERFLOW)?;
285
286    Ok(OracleData::OrderBook {
287        price_q64_64: price,
288        spacing,
289        bid_liquidity_per_m: new_bid_liquidity_per_m,
290        ask_liquidity_per_m: new_ask_liquidity_per_m,
291    })
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use rstest::rstest;
298
299    #[rstest]
300    #[case(11111, true, Ok(vec![1000, 989, 978, 967, 956]))]
301    #[case(11111, false, Ok(vec![1000, 1012, 1023, 1034, 1045]))]
302    #[case(22222, true, Ok(vec![1000, 978, 956, 934, 912]))]
303    #[case(22222, false, Ok(vec![1000, 1023, 1045, 1067, 1089]))]
304    #[case(0, true, Ok(vec![1000, 1000, 1000, 1000, 1000]))]
305    #[case(0, false, Ok(vec![1000, 1000, 1000, 1000, 1000]))]
306    fn test_linear_spacing(
307        #[case] spacing_per_m: u16,
308        #[case] a_to_b: bool,
309        #[case] expected: Result<Vec<u128>, CoreError>,
310    ) {
311        let spacing = BookSpacingType::Linear(spacing_per_m);
312        let prices = (0..5)
313            .map(|i| spacing.price(1000, i, a_to_b))
314            .collect::<Result<Vec<u128>, CoreError>>();
315        assert_eq!(prices, expected);
316    }
317
318    #[rstest]
319    #[case(10000, true, Ok(vec![1000, 989, 980, 969, 960]))]
320    #[case(10000, false, Ok(vec![1000, 1009, 1020, 1029, 1040]))]
321    #[case(20000, true, Ok(vec![1000, 979, 960, 940, 922]))]
322    #[case(20000, false, Ok(vec![1000, 1019, 1040, 1060, 1082]))]
323    #[case(0, true, Ok(vec![1000, 1000, 1000, 1000, 1000]))]
324    #[case(0, false, Ok(vec![1000, 1000, 1000, 1000, 1000]))]
325    fn test_exponential_spacing(
326        #[case] spacing_per_m: u16,
327        #[case] a_to_b: bool,
328        #[case] expected: Result<Vec<u128>, CoreError>,
329    ) {
330        let spacing = BookSpacingType::Exponential(spacing_per_m);
331        let prices = (0..5)
332            .map(|i| spacing.price(1000, i, a_to_b))
333            .collect::<Result<Vec<u128>, CoreError>>();
334        assert_eq!(prices, expected);
335    }
336
337    #[rstest]
338    #[case(QuoteType::TokenAExactIn, Ok(vec![400, 1000, 600]))]
339    #[case(QuoteType::TokenBExactIn, Ok(vec![300, 500, 200]))]
340    #[case(QuoteType::TokenAExactOut, Ok(vec![300, 500, 200]))]
341    #[case(QuoteType::TokenBExactOut, Ok(vec![400, 1000, 600]))]
342    fn test_book_liquidity(
343        #[case] quote_type: QuoteType,
344        #[case] expected: Result<Vec<u64>, CoreError>,
345    ) {
346        let liquidity = book_liquidity(
347            quote_type,
348            1000,
349            BookSpacingType::Linear(0),
350            &[200_000, 0, 500_000, 300_000],
351            &[300_000, 0, 500_000, 200_000],
352            1000,
353            2000,
354        )
355        .map(|liquidity| liquidity.as_slice().iter().map(|x| x.1).collect());
356        assert_eq!(liquidity, expected);
357    }
358
359    #[rstest]
360    #[case(QuoteType::TokenAExactIn, 100, 50, vec![0, 157894, 0, 526315, 315791], vec![0, 363636, 0, 454545, 181819])]
361    #[case(QuoteType::TokenAExactIn, 50, 100, vec![0, 111111, 0, 555555, 333334], vec![0, 333333, 0, 476190, 190477])]
362    #[case(QuoteType::TokenBExactIn, 100, 50, vec![0, 272727, 0, 454545, 272728], vec![0, 263157, 0, 526315, 210528])]
363    #[case(QuoteType::TokenBExactIn, 50, 100, vec![0, 238095, 0, 476190, 285715], vec![0, 222222, 0, 555555, 222223])]
364    #[case(QuoteType::TokenAExactIn, 500, 400, vec![0, 0, 0, 500000, 500000], vec![0, 533333, 0, 333333, 133334])]
365    #[case(QuoteType::TokenAExactIn, 400, 500, vec![0, 0, 0, 400000, 600000], vec![0, 500000, 0, 357142, 142858])]
366    #[case(QuoteType::TokenBExactIn, 500, 400, vec![0, 466666, 0, 333333, 200001], vec![0, 0, 0, 666666, 333334])]
367    #[case(QuoteType::TokenBExactIn, 400, 500, vec![0, 428571, 0, 357142, 214287], vec![0, 0, 0, 600000, 400000])]
368    #[case(QuoteType::TokenAExactIn, 0, 0, vec![0, 200_000, 0, 500_000, 300_000], vec![0, 300_000, 0, 500_000, 200_000])]
369    #[case(QuoteType::TokenBExactIn, 0, 0, vec![0, 200_000, 0, 500_000, 300_000], vec![0, 300_000, 0, 500_000, 200_000])]
370    fn test_new_book_liquidity(
371        #[case] quote_type: QuoteType,
372        #[case] amount_in: u64,
373        #[case] amount_out: u64,
374        #[case] expected_bid_liquidity_per_m: Vec<u32>,
375        #[case] expected_ask_liquidity_per_m: Vec<u32>,
376    ) {
377        let price = 1 << 64;
378        let spacing = BookSpacingType::Linear(0);
379        let liquidity = new_book_liquidity(
380            &Quote {
381                amount_in,
382                amount_out,
383                quote_type,
384            },
385            price,
386            spacing,
387            &[0, 200_000, 0, 500_000, 300_000],
388            &[0, 300_000, 0, 500_000, 200_000],
389            1000,
390            1000,
391        )
392        .unwrap();
393
394        let mut bid_liquidity_per_m = [0u32; 32];
395        let mut ask_liquidity_per_m = [0u32; 32];
396
397        bid_liquidity_per_m[..expected_bid_liquidity_per_m.len()]
398            .copy_from_slice(&expected_bid_liquidity_per_m);
399        ask_liquidity_per_m[..expected_ask_liquidity_per_m.len()]
400            .copy_from_slice(&expected_ask_liquidity_per_m);
401
402        let expected = OracleData::OrderBook {
403            price_q64_64: 1 << 64,
404            spacing: BookSpacingType::Linear(0),
405            bid_liquidity_per_m,
406            ask_liquidity_per_m,
407        };
408        assert_eq!(liquidity, expected);
409    }
410
411    #[rstest]
412    #[case(100, 50, vec![0, 157894, 0, 526315, 315791])]
413    #[case(50, 100, vec![0, 111111, 0, 555555, 333334])]
414    fn test_new_book_liquidity_no_a_liquidity(
415        #[case] amount_in: u64,
416        #[case] amount_out: u64,
417        #[case] expected_bid_liquidity_per_m: Vec<u32>,
418    ) {
419        let price = 1 << 64;
420        let spacing = BookSpacingType::Linear(0);
421        let liquidity = new_book_liquidity(
422            &Quote {
423                amount_in,
424                amount_out,
425                quote_type: QuoteType::TokenAExactIn,
426            },
427            price,
428            spacing,
429            &[0, 200_000, 0, 500_000, 300_000],
430            &[0, 0, 0, 0, 0],
431            0,
432            1000,
433        )
434        .unwrap();
435
436        let mut bid_liquidity_per_m = [0u32; 32];
437        let mut ask_liquidity_per_m = [0u32; 32];
438
439        bid_liquidity_per_m[..expected_bid_liquidity_per_m.len()]
440            .copy_from_slice(&expected_bid_liquidity_per_m);
441        ask_liquidity_per_m[BOOK_LIQUIDITY_LEVELS - 1] = PER_M_DENOMINATOR as u32;
442
443        let expected = OracleData::OrderBook {
444            price_q64_64: 1 << 64,
445            spacing: BookSpacingType::Linear(0),
446            bid_liquidity_per_m,
447            ask_liquidity_per_m,
448        };
449        assert_eq!(liquidity, expected);
450    }
451
452    #[rstest]
453    #[case(100, 50, vec![0, 263157, 0, 526315, 210528])]
454    #[case(50, 100, vec![0, 222222, 0, 555555, 222223])]
455    fn test_new_book_liquidity_no_b_liquidity(
456        #[case] amount_in: u64,
457        #[case] amount_out: u64,
458        #[case] expected_ask_liquidity_per_m: Vec<u32>,
459    ) {
460        let price = 1 << 64;
461        let spacing = BookSpacingType::Linear(0);
462        let liquidity = new_book_liquidity(
463            &Quote {
464                amount_in,
465                amount_out,
466                quote_type: QuoteType::TokenBExactIn,
467            },
468            price,
469            spacing,
470            &[0, 0, 0, 0, 0],
471            &[0, 300_000, 0, 500_000, 200_000],
472            1000,
473            0,
474        )
475        .unwrap();
476
477        let mut bid_liquidity_per_m = [0u32; 32];
478        let mut ask_liquidity_per_m = [0u32; 32];
479
480        bid_liquidity_per_m[BOOK_LIQUIDITY_LEVELS - 1] = PER_M_DENOMINATOR as u32;
481        ask_liquidity_per_m[..expected_ask_liquidity_per_m.len()]
482            .copy_from_slice(&expected_ask_liquidity_per_m);
483
484        let expected = OracleData::OrderBook {
485            price_q64_64: 1 << 64,
486            spacing: BookSpacingType::Linear(0),
487            bid_liquidity_per_m,
488            ask_liquidity_per_m,
489        };
490        assert_eq!(liquidity, expected);
491    }
492}