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        ask_liquidity
214            .iter_mut()
215            .find(|x| **x > 0)
216            .ok_or(INVALID_ORACLE_DATA)?
217    } else {
218        bid_liquidity
219            .iter_mut()
220            .find(|x| **x > 0)
221            .ok_or(INVALID_ORACLE_DATA)?
222    };
223    *best_other_level = best_other_level
224        .checked_add(quote.amount_in as u128)
225        .ok_or(ARITHMETIC_OVERFLOW)?;
226
227    // Since the we don't know the input slice size, we explicitly allocate BOOK_LIQUIDITY_LEVELS
228    // for each side
229    let mut new_bid_liquidity_per_m = [0u32; BOOK_LIQUIDITY_LEVELS];
230    let mut new_ask_liquidity_per_m = [0u32; BOOK_LIQUIDITY_LEVELS];
231
232    // Calculate the new per_m liquidity for each side
233    for i in 0..BOOK_LIQUIDITY_LEVELS {
234        if new_reserves_b > 0 {
235            new_bid_liquidity_per_m[i] = bid_liquidity[i]
236                .checked_mul(PER_M_DENOMINATOR as u128)
237                .ok_or(ARITHMETIC_OVERFLOW)?
238                .checked_div(new_reserves_b as u128)
239                .ok_or(ARITHMETIC_OVERFLOW)?
240                .try_into()
241                .map_err(|_| AMOUNT_EXCEEDS_MAX_U32)?;
242        }
243        if new_reserves_a > 0 {
244            new_ask_liquidity_per_m[i] = ask_liquidity[i]
245                .checked_mul(PER_M_DENOMINATOR as u128)
246                .ok_or(ARITHMETIC_OVERFLOW)?
247                .checked_div(new_reserves_a as u128)
248                .ok_or(ARITHMETIC_OVERFLOW)?
249                .try_into()
250                .map_err(|_| AMOUNT_EXCEEDS_MAX_U32)?;
251        }
252    }
253
254    // Normalize the liquidity to equal PER_M_DENOMINATOR by adding
255    // the missing liquidity to the last price level
256    let total_bid_per_m = new_bid_liquidity_per_m.iter().sum::<u32>();
257    let total_ask_per_m = new_ask_liquidity_per_m.iter().sum::<u32>();
258
259    let missing_bid_per_m = PER_M_DENOMINATOR as u32 - total_bid_per_m;
260    let missing_ask_per_m = PER_M_DENOMINATOR as u32 - total_ask_per_m;
261
262    let worst_bid_index = new_bid_liquidity_per_m
263        .iter()
264        .enumerate()
265        .rev()
266        .find(|(_, x)| **x > 0)
267        .map(|(i, _)| i)
268        .unwrap_or(BOOK_LIQUIDITY_LEVELS - 1);
269    let worst_ask_index = new_ask_liquidity_per_m
270        .iter()
271        .enumerate()
272        .rev()
273        .find(|(_, x)| **x > 0)
274        .map(|(i, _)| i)
275        .unwrap_or(BOOK_LIQUIDITY_LEVELS - 1);
276
277    new_bid_liquidity_per_m[worst_bid_index] = new_bid_liquidity_per_m[worst_bid_index]
278        .checked_add(missing_bid_per_m)
279        .ok_or(ARITHMETIC_OVERFLOW)?;
280    new_ask_liquidity_per_m[worst_ask_index] = new_ask_liquidity_per_m[worst_ask_index]
281        .checked_add(missing_ask_per_m)
282        .ok_or(ARITHMETIC_OVERFLOW)?;
283
284    Ok(OracleData::Book {
285        price_q64_64: price,
286        spacing: spacing,
287        bid_liquidity_per_m: new_bid_liquidity_per_m,
288        ask_liquidity_per_m: new_ask_liquidity_per_m,
289    })
290}
291
292#[cfg(all(test, feature = "lib"))]
293mod tests {
294    use super::*;
295    use rstest::rstest;
296
297    #[rstest]
298    #[case(11111, true, Ok(vec![1000, 989, 978, 967, 956]))]
299    #[case(11111, false, Ok(vec![1000, 1012, 1023, 1034, 1045]))]
300    #[case(22222, true, Ok(vec![1000, 978, 956, 934, 912]))]
301    #[case(22222, false, Ok(vec![1000, 1023, 1045, 1067, 1089]))]
302    #[case(0, true, Ok(vec![1000, 1000, 1000, 1000, 1000]))]
303    #[case(0, false, Ok(vec![1000, 1000, 1000, 1000, 1000]))]
304    fn test_linear_spacing(
305        #[case] spacing_per_m: u16,
306        #[case] a_to_b: bool,
307        #[case] expected: Result<Vec<u128>, CoreError>,
308    ) {
309        let spacing = BookSpacingType::Linear(spacing_per_m);
310        let prices = (0..5)
311            .map(|i| spacing.price(1000, i, a_to_b))
312            .collect::<Result<Vec<u128>, CoreError>>();
313        assert_eq!(prices, expected);
314    }
315
316    #[rstest]
317    #[case(10000, true, Ok(vec![1000, 989, 980, 969, 960]))]
318    #[case(10000, false, Ok(vec![1000, 1009, 1020, 1029, 1040]))]
319    #[case(20000, true, Ok(vec![1000, 979, 960, 940, 922]))]
320    #[case(20000, false, Ok(vec![1000, 1019, 1040, 1060, 1082]))]
321    #[case(0, true, Ok(vec![1000, 1000, 1000, 1000, 1000]))]
322    #[case(0, false, Ok(vec![1000, 1000, 1000, 1000, 1000]))]
323    fn test_exponential_spacing(
324        #[case] spacing_per_m: u16,
325        #[case] a_to_b: bool,
326        #[case] expected: Result<Vec<u128>, CoreError>,
327    ) {
328        let spacing = BookSpacingType::Exponential(spacing_per_m);
329        let prices = (0..5)
330            .map(|i| spacing.price(1000, i, a_to_b))
331            .collect::<Result<Vec<u128>, CoreError>>();
332        assert_eq!(prices, expected);
333    }
334
335    #[rstest]
336    #[case(QuoteType::TokenAExactIn, Ok(vec![400, 1000, 600]))]
337    #[case(QuoteType::TokenBExactIn, Ok(vec![300, 500, 200]))]
338    #[case(QuoteType::TokenAExactOut, Ok(vec![300, 500, 200]))]
339    #[case(QuoteType::TokenBExactOut, Ok(vec![400, 1000, 600]))]
340    fn test_book_liquidity(
341        #[case] quote_type: QuoteType,
342        #[case] expected: Result<Vec<u64>, CoreError>,
343    ) {
344        let liquidity = book_liquidity(
345            quote_type,
346            1000,
347            BookSpacingType::Linear(0),
348            &[200_000, 0, 500_000, 300_000],
349            &[300_000, 0, 500_000, 200_000],
350            1000,
351            2000,
352        )
353        .map(|liquidity| liquidity.to_vec().iter().map(|x| x.1).collect());
354        assert_eq!(liquidity, expected);
355    }
356
357    #[rstest]
358    #[case(QuoteType::TokenAExactIn, 100, 50, vec![0, 157894, 0, 526315, 315791], vec![0, 363636, 0, 454545, 181819])]
359    #[case(QuoteType::TokenAExactIn, 50, 100, vec![0, 111111, 0, 555555, 333334], vec![0, 333333, 0, 476190, 190477])]
360    #[case(QuoteType::TokenBExactIn, 100, 50, vec![0, 272727, 0, 454545, 272728], vec![0, 263157, 0, 526315, 210528])]
361    #[case(QuoteType::TokenBExactIn, 50, 100, vec![0, 238095, 0, 476190, 285715], vec![0, 222222, 0, 555555, 222223])]
362    #[case(QuoteType::TokenAExactIn, 500, 400, vec![0, 0, 0, 500000, 500000], vec![0, 533333, 0, 333333, 133334])]
363    #[case(QuoteType::TokenAExactIn, 400, 500, vec![0, 0, 0, 400000, 600000], vec![0, 500000, 0, 357142, 142858])]
364    #[case(QuoteType::TokenBExactIn, 500, 400, vec![0, 466666, 0, 333333, 200001], vec![0, 0, 0, 666666, 333334])]
365    #[case(QuoteType::TokenBExactIn, 400, 500, vec![0, 428571, 0, 357142, 214287], vec![0, 0, 0, 600000, 400000])]
366    #[case(QuoteType::TokenAExactIn, 0, 0, vec![0, 200_000, 0, 500_000, 300_000], vec![0, 300_000, 0, 500_000, 200_000])]
367    #[case(QuoteType::TokenBExactIn, 0, 0, vec![0, 200_000, 0, 500_000, 300_000], vec![0, 300_000, 0, 500_000, 200_000])]
368    fn test_new_book_liquidity(
369        #[case] quote_type: QuoteType,
370        #[case] amount_in: u64,
371        #[case] amount_out: u64,
372        #[case] expected_bid_liquidity_per_m: Vec<u32>,
373        #[case] expected_ask_liquidity_per_m: Vec<u32>,
374    ) {
375        let price = 1 << 64;
376        let spacing = BookSpacingType::Linear(0);
377        let liquidity = new_book_liquidity(
378            &Quote {
379                amount_in,
380                amount_out,
381                quote_type,
382            },
383            price,
384            spacing,
385            &[0, 200_000, 0, 500_000, 300_000],
386            &[0, 300_000, 0, 500_000, 200_000],
387            1000,
388            1000,
389        )
390        .unwrap();
391
392        let mut bid_liquidity_per_m = [0u32; 32];
393        let mut ask_liquidity_per_m = [0u32; 32];
394
395        bid_liquidity_per_m[..expected_bid_liquidity_per_m.len()]
396            .copy_from_slice(&expected_bid_liquidity_per_m);
397        ask_liquidity_per_m[..expected_ask_liquidity_per_m.len()]
398            .copy_from_slice(&expected_ask_liquidity_per_m);
399
400        let expected = OracleData::Book {
401            price_q64_64: 1 << 64,
402            spacing: BookSpacingType::Linear(0),
403            bid_liquidity_per_m,
404            ask_liquidity_per_m,
405        };
406        assert_eq!(liquidity, expected);
407    }
408}