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