1#![allow(dead_code)]
2use std::cmp::min;
3
4#[cfg(feature = "wasm")]
5use riptide_amm_macros::wasm_expose;
6
7use borsh::{BorshDeserialize, BorshSerialize};
8
9use super::{
10 error::{CoreError, ARITHMETIC_OVERFLOW},
11 quote::{Quote, QuoteType},
12 token::{a_to_b, b_to_a},
13};
14
15mod book;
16mod flat;
17mod spread;
18
19pub use book::BookSpacingType;
20
21use book::{book_liquidity, new_book_liquidity, BOOK_LIQUIDITY_LEVELS};
22use flat::flat_liquidity;
23use spread::spread_liquidity;
24
25#[derive(Debug, Clone, Copy, Eq, PartialEq)]
29pub(crate) struct SingleSideLiquidity {
30 items: [(u128, u64); 32],
31 len: usize,
32}
33
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 spacing: BookSpacingType,
85 bid_liquidity_per_m: [u32; BOOK_LIQUIDITY_LEVELS],
87 ask_liquidity_per_m: [u32; BOOK_LIQUIDITY_LEVELS],
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 quote_type = QuoteType::new(amount_is_token_a, amount_is_input);
101 let liquidity: SingleSideLiquidity = match self {
102 OracleData::Empty => SingleSideLiquidity::new(),
103 OracleData::Flat { price_q64_64 } => {
104 flat_liquidity(*price_q64_64, quote_type, reserves_a, reserves_b)?
105 }
106 OracleData::Spread {
107 price_q64_64,
108 spread_a_to_b_per_m,
109 spread_b_to_a_per_m,
110 } => spread_liquidity(
111 *price_q64_64,
112 *spread_a_to_b_per_m,
113 *spread_b_to_a_per_m,
114 quote_type,
115 reserves_a,
116 reserves_b,
117 )?,
118 OracleData::Book {
119 price_q64_64,
120 spacing,
121 bid_liquidity_per_m,
122 ask_liquidity_per_m,
123 } => book_liquidity(
124 quote_type,
125 *price_q64_64,
126 *spacing,
127 bid_liquidity_per_m,
128 ask_liquidity_per_m,
129 reserves_a,
130 reserves_b,
131 )?,
132 };
133
134 consume_liquidity(
135 amount,
136 amount_is_token_a,
137 amount_is_input,
138 liquidity.as_slice(),
139 )
140 }
141
142 pub(crate) fn new_oracle_data(
143 &self,
144 quote: &Quote,
145 reserves_a: u64,
146 reserves_b: u64,
147 ) -> Result<Self, CoreError> {
148 match self {
149 OracleData::Empty | OracleData::Flat { .. } | OracleData::Spread { .. } => Ok(*self),
150 OracleData::Book {
151 price_q64_64,
152 spacing,
153 bid_liquidity_per_m,
154 ask_liquidity_per_m,
155 } => new_book_liquidity(
156 quote,
157 *price_q64_64,
158 *spacing,
159 bid_liquidity_per_m,
160 ask_liquidity_per_m,
161 reserves_a,
162 reserves_b,
163 ),
164 }
165 }
166}
167
168pub(crate) fn consume_liquidity(
169 amount: u64,
170 amount_is_token_a: bool,
171 amount_is_input: bool,
172 liquidity: &[(u128, u64)],
173) -> Result<Quote, CoreError> {
174 let mut remaining_amount = amount;
175 let mut other_amount: u64 = 0;
176
177 for (price, liquidity) in liquidity {
182 if *price == 0 || *liquidity == 0 {
183 continue;
184 }
185
186 let (step_specified_amount, step_other_amount) = if amount_is_input {
187 let max_amount = if amount_is_token_a {
189 b_to_a(*liquidity, (*price).into(), true)?
190 } else {
191 a_to_b(*liquidity, (*price).into(), true)?
192 };
193
194 let step_specified_amount = min(remaining_amount, max_amount);
195 let step_other_amount = if amount_is_token_a {
196 a_to_b(step_specified_amount, (*price).into(), false)?
197 } else {
198 b_to_a(step_specified_amount, (*price).into(), false)?
199 };
200
201 (step_specified_amount, min(step_other_amount, *liquidity))
202 } else {
203 let step_specified_amount = min(remaining_amount, *liquidity);
206 let step_other_amount = if amount_is_token_a {
207 a_to_b(step_specified_amount, (*price).into(), true)?
208 } else {
209 b_to_a(step_specified_amount, (*price).into(), true)?
210 };
211
212 (step_specified_amount, step_other_amount)
213 };
214
215 remaining_amount = remaining_amount
216 .checked_sub(step_specified_amount)
217 .ok_or(ARITHMETIC_OVERFLOW)?;
218 other_amount = other_amount
219 .checked_add(step_other_amount)
220 .ok_or(ARITHMETIC_OVERFLOW)?;
221
222 if remaining_amount == 0 {
223 break;
224 }
225 }
226
227 let consumed_amount = amount - remaining_amount;
228
229 let quote_type = QuoteType::new(amount_is_token_a, amount_is_input);
230 let (amount_in, amount_out) = if amount_is_input {
231 (consumed_amount, other_amount)
232 } else {
233 (other_amount, consumed_amount)
234 };
235
236 Ok(Quote {
237 amount_in,
238 amount_out,
239 quote_type,
240 })
241}
242
243#[cfg(all(test, feature = "lib"))]
244mod tests {
245 use super::*;
246 use rstest::rstest;
247
248 #[rstest]
249 fn test_empty(
250 #[values(true, false)] amount_is_token_a: bool,
251 #[values(true, false)] amount_is_input: bool,
252 ) {
253 let quote = OracleData::Empty.swap(100, amount_is_token_a, amount_is_input, 1000, 1000);
254 let quote_type = QuoteType::new(amount_is_token_a, amount_is_input);
255 assert_eq!(
256 quote,
257 Ok(Quote {
258 amount_in: 0,
259 amount_out: 0,
260 quote_type,
261 })
262 );
263 }
264
265 #[rstest]
266 #[case(OracleData::Empty)]
267 #[case(OracleData::Flat { price_q64_64: 1 << 64 })]
268 #[case(OracleData::Spread { price_q64_64: 1 << 64, spread_a_to_b_per_m: 1000, spread_b_to_a_per_m: 1000 })]
269 fn test_new_liquidity_unchanged(#[case] oracle: OracleData) {
270 let quote = Quote {
271 amount_in: 100,
272 amount_out: 100,
273 quote_type: QuoteType::TokenAExactIn,
274 };
275 let new_oracle = OracleData::new_oracle_data(&oracle, "e, 0, 0);
276 assert_eq!(new_oracle, Ok(oracle));
277 }
278
279 #[rstest]
281 #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::TokenAExactIn }))]
282 #[case(500, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::TokenAExactIn }))]
283 #[case(1100, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::TokenAExactIn }))]
284 #[case(2500, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::TokenAExactIn }))]
285 #[case(10000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::TokenAExactIn }))]
286 fn test_consume_liquidity_input_token_a(
287 #[case] amount: u64,
288 #[case] expected: Result<Quote, CoreError>,
289 ) {
290 let liquidity = vec![(1 << 64, 100), ((1 << 64) / 2, 500), ((1 << 64) / 4, 1000)];
291 let quote = consume_liquidity(amount, true, true, &liquidity);
292 assert_eq!(quote, expected);
293 }
294
295 #[rstest]
297 #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::TokenBExactIn }))]
298 #[case(500, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::TokenBExactIn }))]
299 #[case(1100, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::TokenBExactIn }))]
300 #[case(2500, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::TokenBExactIn }))]
301 #[case(10000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::TokenBExactIn }))]
302 fn test_consume_liquidity_input_token_b(
303 #[case] amount: u64,
304 #[case] expected: Result<Quote, CoreError>,
305 ) {
306 let liquidity = vec![(1 << 64, 100), (2 << 64, 500), (4 << 64, 1000)];
307 let quote = consume_liquidity(amount, false, true, &liquidity);
308 assert_eq!(quote, expected);
309 }
310
311 #[rstest]
313 #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::TokenAExactOut }))]
314 #[case(300, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::TokenAExactOut }))]
315 #[case(600, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::TokenAExactOut }))]
316 #[case(950, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::TokenAExactOut }))]
317 #[case(2000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::TokenAExactOut }))]
318 fn test_consume_liquidity_output_token_a(
319 #[case] amount: u64,
320 #[case] expected: Result<Quote, CoreError>,
321 ) {
322 let liquidity = vec![(1 << 64, 100), (2 << 64, 500), (4 << 64, 1000)];
323 let quote = consume_liquidity(amount, true, false, &liquidity);
324 assert_eq!(quote, expected);
325 }
326
327 #[rstest]
329 #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::TokenBExactOut }))]
330 #[case(300, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::TokenBExactOut }))]
331 #[case(600, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::TokenBExactOut }))]
332 #[case(950, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::TokenBExactOut }))]
333 #[case(2000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::TokenBExactOut }))]
334 fn test_consume_liquidity_output_token_b(
335 #[case] amount: u64,
336 #[case] expected: Result<Quote, CoreError>,
337 ) {
338 let liquidity = vec![(1 << 64, 100), ((1 << 64) / 2, 500), ((1 << 64) / 4, 1000)];
339 let quote = consume_liquidity(amount, false, false, &liquidity);
340 assert_eq!(quote, expected);
341 }
342
343 #[rstest]
344 #[case((1 << 64) / 8, true, true, Ok(Quote { amount_in: 100, amount_out: 12, quote_type: QuoteType::TokenAExactIn }))]
345 #[case((1 << 64) / 8, true, false, Ok(Quote { amount_in: 13, amount_out: 100, quote_type: QuoteType::TokenAExactOut }))]
346 #[case(8 << 64, false, true, Ok(Quote { amount_in: 100, amount_out: 12, quote_type: QuoteType::TokenBExactIn }))]
347 #[case(8 << 64, false, false, Ok(Quote { amount_in: 13, amount_out: 100, quote_type: QuoteType::TokenBExactOut }))]
348 fn test_consume_liquidity_rounding_direction(
349 #[case] price: u128,
350 #[case] amount_is_token_a: bool,
351 #[case] amount_is_input: bool,
352 #[case] expected: Result<Quote, CoreError>,
353 ) {
354 let liquidity = vec![(price, 1000)];
355 let result = consume_liquidity(100, amount_is_token_a, amount_is_input, &liquidity);
356 assert_eq!(result, expected);
357 }
358}