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
18use book::{book_liquidity, BookSpacingType};
19use flat::flat_liquidity;
20use spread::spread_liquidity;
21
22#[derive(Debug, Clone, Copy, Eq, PartialEq)]
26pub(crate) struct SingleSideLiquidity {
27 items: [(u128, u64); 32],
28 len: usize,
29}
30
31#[allow(dead_code)]
32impl SingleSideLiquidity {
33 pub fn new() -> Self {
34 Self {
35 items: [(0, 0); 32],
36 len: 0,
37 }
38 }
39
40 pub fn push(&mut self, item: (u128, u64)) -> bool {
41 if self.len < 32 {
42 self.items[self.len] = item;
43 self.len += 1;
44 true
45 } else {
46 false
47 }
48 }
49
50 pub fn len(&self) -> usize {
51 self.len
52 }
53
54 pub fn as_slice(&self) -> &[(u128, u64)] {
55 &self.items[..self.len]
56 }
57
58 pub fn to_vec(&self) -> Vec<(u128, u64)> {
59 self.as_slice().to_vec()
60 }
61}
62
63#[cfg_attr(feature = "wasm", wasm_expose)]
64pub const PER_1M_DENOMINATOR: i32 = 1_000_000;
65
66#[derive(Debug, Clone, Copy, Eq, PartialEq)]
67#[cfg_attr(true, derive(BorshDeserialize, BorshSerialize))]
68#[cfg_attr(feature = "wasm", wasm_expose)]
69pub enum OracleData {
70 Empty,
71 Flat {
72 price_q64_64: u128,
73 },
74 Spread {
75 price_q64_64: u128,
76 spread_a_to_b_per_1m: i32,
77 spread_b_to_a_per_1m: i32,
78 },
79 Book {
80 price_q64_64: u128,
81 spacing: BookSpacingType,
83 bid_liquidity_per_1m: [u32; 32],
85 ask_liquidity_per_1m: [u32; 32],
86 },
87}
88
89impl OracleData {
90 pub(crate) fn swap(
91 &self,
92 amount: u64,
93 amount_is_token_a: bool,
94 amount_is_input: bool,
95 reserves_a: u64,
96 reserves_b: u64,
97 ) -> Result<Quote, CoreError> {
98 let liquidity: SingleSideLiquidity = match self {
99 OracleData::Empty => SingleSideLiquidity::new(),
100 OracleData::Flat { price_q64_64 } => flat_liquidity(
101 *price_q64_64,
102 amount_is_token_a,
103 amount_is_input,
104 reserves_a,
105 reserves_b,
106 )?,
107 OracleData::Spread {
108 price_q64_64,
109 spread_a_to_b_per_1m,
110 spread_b_to_a_per_1m,
111 } => spread_liquidity(
112 *price_q64_64,
113 *spread_a_to_b_per_1m,
114 *spread_b_to_a_per_1m,
115 amount_is_token_a,
116 amount_is_input,
117 reserves_a,
118 reserves_b,
119 )?,
120 OracleData::Book {
121 price_q64_64,
122 spacing,
123 bid_liquidity_per_1m,
124 ask_liquidity_per_1m,
125 } => book_liquidity(
126 amount_is_token_a,
127 amount_is_input,
128 *price_q64_64,
129 *spacing,
130 bid_liquidity_per_1m,
131 ask_liquidity_per_1m,
132 reserves_a,
133 reserves_b,
134 )?,
135 };
136
137 consume_liquidity(
138 amount,
139 amount_is_token_a,
140 amount_is_input,
141 &liquidity.as_slice(),
142 )
143 }
144}
145
146pub(crate) fn consume_liquidity(
147 amount: u64,
148 amount_is_token_a: bool,
149 amount_is_input: bool,
150 liquidity: &[(u128, u64)],
151) -> Result<Quote, CoreError> {
152 let mut remaining_amount = amount;
153 let mut other_amount: u64 = 0;
154
155 for (price, liquidity) in liquidity {
160 let (step_specified_amount, step_other_amount) = if amount_is_input {
161 let max_amount = if amount_is_token_a {
163 b_to_a(*liquidity, (*price).into(), true)?
164 } else {
165 a_to_b(*liquidity, (*price).into(), true)?
166 };
167
168 let step_specified_amount = min(remaining_amount, max_amount);
169 let step_other_amount = if amount_is_token_a {
170 a_to_b(step_specified_amount, (*price).into(), false)?
171 } else {
172 b_to_a(step_specified_amount, (*price).into(), false)?
173 };
174
175 (step_specified_amount, min(step_other_amount, *liquidity))
176 } else {
177 let step_specified_amount = min(remaining_amount, *liquidity);
180 let step_other_amount = if amount_is_token_a {
181 a_to_b(step_specified_amount, (*price).into(), true)?
182 } else {
183 b_to_a(step_specified_amount, (*price).into(), true)?
184 };
185
186 (step_specified_amount, step_other_amount)
187 };
188
189 remaining_amount = remaining_amount
190 .checked_sub(step_specified_amount)
191 .ok_or(ARITHMETIC_OVERFLOW)?;
192 other_amount = other_amount
193 .checked_add(step_other_amount)
194 .ok_or(ARITHMETIC_OVERFLOW)?;
195
196 if remaining_amount == 0 {
197 break;
198 }
199 }
200
201 let consumed_amount = amount - remaining_amount;
202
203 if amount_is_input {
204 Ok(Quote {
205 amount_in: consumed_amount,
206 amount_out: other_amount,
207 quote_type: QuoteType::ExactIn,
208 })
209 } else {
210 Ok(Quote {
211 amount_in: other_amount,
212 amount_out: consumed_amount,
213 quote_type: QuoteType::ExactOut,
214 })
215 }
216}
217
218#[cfg(all(test, feature = "lib"))]
219mod tests {
220 use super::*;
221 use rstest::rstest;
222
223 #[rstest]
224 fn test_empty(
225 #[values(true, false)] amount_is_token_a: bool,
226 #[values(true, false)] amount_is_input: bool,
227 ) {
228 let quote = OracleData::Empty.swap(100, amount_is_token_a, amount_is_input, 1000, 1000);
229 let quote_type = if amount_is_input {
230 QuoteType::ExactIn
231 } else {
232 QuoteType::ExactOut
233 };
234 assert_eq!(
235 quote,
236 Ok(Quote {
237 amount_in: 0,
238 amount_out: 0,
239 quote_type,
240 })
241 );
242 }
243
244 #[rstest]
246 #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::ExactIn }))]
247 #[case(500, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::ExactIn }))]
248 #[case(1100, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::ExactIn }))]
249 #[case(2500, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::ExactIn }))]
250 #[case(10000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::ExactIn }))]
251 fn test_consume_liquidity_input_token_a(
252 #[case] amount: u64,
253 #[case] expected: Result<Quote, CoreError>,
254 ) {
255 let liquidity = vec![(1 << 64, 100), ((1 << 64) / 2, 500), ((1 << 64) / 4, 1000)];
256 let quote = consume_liquidity(amount, true, true, &liquidity);
257 assert_eq!(quote, expected);
258 }
259
260 #[rstest]
262 #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::ExactIn }))]
263 #[case(500, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::ExactIn }))]
264 #[case(1100, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::ExactIn }))]
265 #[case(2500, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::ExactIn }))]
266 #[case(10000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::ExactIn }))]
267 fn test_consume_liquidity_input_token_b(
268 #[case] amount: u64,
269 #[case] expected: Result<Quote, CoreError>,
270 ) {
271 let liquidity = vec![(1 << 64, 100), (2 << 64, 500), (4 << 64, 1000)];
272 let quote = consume_liquidity(amount, false, true, &liquidity);
273 assert_eq!(quote, expected);
274 }
275
276 #[rstest]
278 #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::ExactOut }))]
279 #[case(300, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::ExactOut }))]
280 #[case(600, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::ExactOut }))]
281 #[case(950, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::ExactOut }))]
282 #[case(2000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::ExactOut }))]
283 fn test_consume_liquidity_output_token_a(
284 #[case] amount: u64,
285 #[case] expected: Result<Quote, CoreError>,
286 ) {
287 let liquidity = vec![(1 << 64, 100), (2 << 64, 500), (4 << 64, 1000)];
288 let quote = consume_liquidity(amount, true, false, &liquidity);
289 assert_eq!(quote, expected);
290 }
291
292 #[rstest]
294 #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::ExactOut }))]
295 #[case(300, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::ExactOut }))]
296 #[case(600, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::ExactOut }))]
297 #[case(950, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::ExactOut }))]
298 #[case(2000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::ExactOut }))]
299 fn test_consume_liquidity_output_token_b(
300 #[case] amount: u64,
301 #[case] expected: Result<Quote, CoreError>,
302 ) {
303 let liquidity = vec![(1 << 64, 100), ((1 << 64) / 2, 500), ((1 << 64) / 4, 1000)];
304 let quote = consume_liquidity(amount, false, false, &liquidity);
305 assert_eq!(quote, expected);
306 }
307
308 #[rstest]
309 #[case((1 << 64) / 8, true, true, Ok(Quote { amount_in: 100, amount_out: 12, quote_type: QuoteType::ExactIn }))]
310 #[case((1 << 64) / 8, true, false, Ok(Quote { amount_in: 13, amount_out: 100, quote_type: QuoteType::ExactOut }))]
311 #[case(8 << 64, false, true, Ok(Quote { amount_in: 100, amount_out: 12, quote_type: QuoteType::ExactIn }))]
312 #[case(8 << 64, false, false, Ok(Quote { amount_in: 13, amount_out: 100, quote_type: QuoteType::ExactOut }))]
313 fn test_consume_liquidity_rounding_direction(
314 #[case] price: u128,
315 #[case] amount_is_token_a: bool,
316 #[case] amount_is_input: bool,
317 #[case] expected: Result<Quote, CoreError>,
318 ) {
319 let liquidity = vec![(price, 1000)];
320 let result = consume_liquidity(100, amount_is_token_a, amount_is_input, &liquidity);
321 assert_eq!(result, expected);
322 }
323}