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