1use super::SingleSideLiquidity;
2
3use super::error::{CoreError, ARITHMETIC_OVERFLOW, INVALID_ORACLE_DATA};
4
5use super::guards::{check_guards, check_oracle_validity, GuardParams};
6
7use super::oracle::{build_liquidity, build_price, consume_liquidity, OraclePayload};
8
9use borsh::BorshDeserialize;
10
11use riptide_amm_macros::alias;
12
13#[cfg(feature = "wasm")]
14use riptide_amm_macros::wasm_expose;
15
16#[derive(Debug, Clone, Copy, Eq, PartialEq)]
17#[cfg_attr(feature = "wasm", wasm_expose)]
18pub enum QuoteType {
19 TokenAExactIn,
20 TokenAExactOut,
21 TokenBExactIn,
22 TokenBExactOut,
23}
24
25impl QuoteType {
26 pub(crate) fn new(amount_is_token_a: bool, amount_is_input: bool) -> Self {
27 match (amount_is_token_a, amount_is_input) {
28 (true, true) => QuoteType::TokenAExactIn,
29 (true, false) => QuoteType::TokenAExactOut,
30 (false, true) => QuoteType::TokenBExactIn,
31 (false, false) => QuoteType::TokenBExactOut,
32 }
33 }
34
35 pub fn exact_in(&self) -> bool {
36 matches!(self, QuoteType::TokenAExactIn | QuoteType::TokenBExactIn)
37 }
38
39 pub fn exact_out(&self) -> bool {
40 matches!(self, QuoteType::TokenAExactOut | QuoteType::TokenBExactOut)
41 }
42
43 #[alias(output_is_token_b, a_to_b)]
44 pub fn input_is_token_a(&self) -> bool {
45 matches!(self, QuoteType::TokenAExactIn | QuoteType::TokenBExactOut)
46 }
47
48 #[alias(output_is_token_a, b_to_a)]
49 pub fn input_is_token_b(&self) -> bool {
50 matches!(self, QuoteType::TokenBExactIn | QuoteType::TokenAExactOut)
51 }
52}
53
54#[derive(Debug, Clone, Copy, Eq, PartialEq)]
55#[cfg_attr(feature = "wasm", wasm_expose)]
56pub struct Quote {
57 pub amount_in: u64,
58 pub amount_out: u64,
59 pub quote_type: QuoteType,
60}
61
62impl Quote {
63 pub fn apply_to_reserves(
64 &self,
65 reserves_a: u64,
66 reserves_b: u64,
67 ) -> Result<(u64, u64), CoreError> {
68 if self.quote_type.input_is_token_a() {
69 let post_a = reserves_a
70 .checked_add(self.amount_in)
71 .ok_or(ARITHMETIC_OVERFLOW)?;
72 let post_b = reserves_b
73 .checked_sub(self.amount_out)
74 .ok_or(ARITHMETIC_OVERFLOW)?;
75 Ok((post_a, post_b))
76 } else {
77 let post_a = reserves_a
78 .checked_sub(self.amount_out)
79 .ok_or(ARITHMETIC_OVERFLOW)?;
80 let post_b = reserves_b
81 .checked_add(self.amount_in)
82 .ok_or(ARITHMETIC_OVERFLOW)?;
83 Ok((post_a, post_b))
84 }
85 }
86}
87
88#[derive(Default, Debug, Clone, Eq, PartialEq)]
89#[cfg_attr(feature = "wasm", wasm_expose)]
90pub struct Prices {
91 pub oracle_price_q64_64: u128,
92 pub best_bid_price_q64_64: u128,
93 pub best_ask_price_q64_64: u128,
94 pub ask_spread_per_m: i32,
95 pub bid_spread_per_m: i32,
96}
97
98#[derive(Default, Debug, Clone, Eq, PartialEq)]
99#[cfg_attr(feature = "wasm", wasm_expose)]
100pub struct Price {
101 pub oracle_price_q64_64: u128,
102 pub best_price_q64_64: u128,
103 pub spread_per_m: i32,
104}
105
106#[derive(Debug, Clone, Eq, PartialEq)]
107#[cfg_attr(feature = "wasm", wasm_expose)]
108pub struct GuardedQuote {
109 pub quote: Quote,
110 pub post_reserves_a: u64,
111 pub post_reserves_b: u64,
112 pub price: Price,
113}
114
115pub type QuoteError = &'static str;
116
117#[derive(Debug)]
118struct InnerQuoteResult {
119 quote: Quote,
120 payload: OraclePayload,
121 liquidity: SingleSideLiquidity,
122}
123
124fn quote(
125 amount: u64,
126 amount_is_token_a: bool,
127 amount_is_input: bool,
128 oracle_data: &[u8],
129 reserves_a: u64,
130 reserves_b: u64,
131 skew_cliff_min_per_m: i32,
132 skew_cliff_max_per_m: i32,
133) -> Result<InnerQuoteResult, CoreError> {
134 let mut oracle_data = oracle_data;
135 let payload = OraclePayload::deserialize(&mut oracle_data).map_err(|_| INVALID_ORACLE_DATA)?;
136
137 let quote_type = QuoteType::new(amount_is_token_a, amount_is_input);
138
139 let liquidity = build_liquidity(
140 &payload,
141 quote_type,
142 reserves_a,
143 reserves_b,
144 skew_cliff_min_per_m,
145 skew_cliff_max_per_m,
146 )?;
147
148 let quote = consume_liquidity(amount, quote_type, &liquidity)?;
149
150 Ok(InnerQuoteResult {
151 quote,
152 payload,
153 liquidity,
154 })
155}
156
157#[cfg_attr(feature = "wasm", wasm_expose)]
158pub fn quote_exact_in(
159 amount: u64,
160 amount_is_token_a: bool,
161 oracle_data: &[u8],
162 reserves_a: u64,
163 reserves_b: u64,
164 skew_cliff_min_per_m: i32,
165 skew_cliff_max_per_m: i32,
166) -> Result<Quote, CoreError> {
167 quote(
168 amount,
169 amount_is_token_a,
170 true,
171 oracle_data,
172 reserves_a,
173 reserves_b,
174 skew_cliff_min_per_m,
175 skew_cliff_max_per_m,
176 )
177 .map(|r| r.quote)
178}
179
180#[cfg_attr(feature = "wasm", wasm_expose)]
181pub fn quote_exact_out(
182 amount: u64,
183 amount_is_token_a: bool,
184 oracle_data: &[u8],
185 reserves_a: u64,
186 reserves_b: u64,
187 skew_cliff_min_per_m: i32,
188 skew_cliff_max_per_m: i32,
189) -> Result<Quote, CoreError> {
190 quote(
191 amount,
192 amount_is_token_a,
193 false,
194 oracle_data,
195 reserves_a,
196 reserves_b,
197 skew_cliff_min_per_m,
198 skew_cliff_max_per_m,
199 )
200 .map(|r| r.quote)
201}
202
203fn quote_with_guards(
204 amount: u64,
205 amount_is_token_a: bool,
206 amount_is_input: bool,
207 oracle_data: &[u8],
208 reserves_a: u64,
209 reserves_b: u64,
210 skew_cliff_min_per_m: i32,
211 skew_cliff_max_per_m: i32,
212 current_slot: u64,
213 params: &GuardParams,
214) -> Result<GuardedQuote, QuoteError> {
215 check_oracle_validity(current_slot, params.valid_until)?;
216
217 let inner = quote(
218 amount,
219 amount_is_token_a,
220 amount_is_input,
221 oracle_data,
222 reserves_a,
223 reserves_b,
224 skew_cliff_min_per_m,
225 skew_cliff_max_per_m,
226 )?;
227
228 let quote_type = QuoteType::new(amount_is_token_a, amount_is_input);
229
230 let price = build_price(
231 &inner.liquidity,
232 &inner.payload.data,
233 quote_type,
234 reserves_a,
235 reserves_b,
236 )?;
237
238 let (post_reserves_a, post_reserves_b) =
239 inner.quote.apply_to_reserves(reserves_a, reserves_b)?;
240
241 check_guards(post_reserves_a, post_reserves_b, &price, params)?;
242
243 Ok(GuardedQuote {
244 quote: inner.quote,
245 post_reserves_a,
246 post_reserves_b,
247 price,
248 })
249}
250
251pub fn quote_exact_in_with_guards(
252 amount: u64,
253 amount_is_token_a: bool,
254 oracle_data: &[u8],
255 reserves_a: u64,
256 reserves_b: u64,
257 skew_cliff_min_per_m: i32,
258 skew_cliff_max_per_m: i32,
259 current_slot: u64,
260 params: &GuardParams,
261) -> Result<GuardedQuote, QuoteError> {
262 quote_with_guards(
263 amount,
264 amount_is_token_a,
265 true,
266 oracle_data,
267 reserves_a,
268 reserves_b,
269 skew_cliff_min_per_m,
270 skew_cliff_max_per_m,
271 current_slot,
272 params,
273 )
274}
275
276pub fn quote_exact_out_with_guards(
277 amount: u64,
278 amount_is_token_a: bool,
279 oracle_data: &[u8],
280 reserves_a: u64,
281 reserves_b: u64,
282 skew_cliff_min_per_m: i32,
283 skew_cliff_max_per_m: i32,
284 current_slot: u64,
285 params: &GuardParams,
286) -> Result<GuardedQuote, QuoteError> {
287 quote_with_guards(
288 amount,
289 amount_is_token_a,
290 false,
291 oracle_data,
292 reserves_a,
293 reserves_b,
294 skew_cliff_min_per_m,
295 skew_cliff_max_per_m,
296 current_slot,
297 params,
298 )
299}
300
301#[cfg_attr(feature = "wasm", wasm_expose)]
302pub fn bid_price(
303 oracle_data: &[u8],
304 liquidity: SingleSideLiquidity,
305 reserves_a: u64,
306 reserves_b: u64,
307) -> Result<Price, CoreError> {
308 let mut oracle_data = oracle_data;
309 let payload = OraclePayload::deserialize(&mut oracle_data).map_err(|_| INVALID_ORACLE_DATA)?;
310 build_price(
311 &liquidity,
312 &payload.data,
313 QuoteType::TokenAExactIn,
314 reserves_a,
315 reserves_b,
316 )
317}
318
319#[cfg_attr(feature = "wasm", wasm_expose)]
320pub fn ask_price(
321 oracle_data: &[u8],
322 liquidity: SingleSideLiquidity,
323 reserves_a: u64,
324 reserves_b: u64,
325) -> Result<Price, CoreError> {
326 let mut oracle_data = oracle_data;
327 let payload = OraclePayload::deserialize(&mut oracle_data).map_err(|_| INVALID_ORACLE_DATA)?;
328 build_price(
329 &liquidity,
330 &payload.data,
331 QuoteType::TokenBExactIn,
332 reserves_a,
333 reserves_b,
334 )
335}
336
337#[cfg_attr(feature = "wasm", wasm_expose)]
338pub fn bid_liquidity(
339 oracle_data: &[u8],
340 reserves_a: u64,
341 reserves_b: u64,
342 skew_cliff_min_per_m: i32,
343 skew_cliff_max_per_m: i32,
344) -> Result<SingleSideLiquidity, CoreError> {
345 let mut oracle_data = oracle_data;
346 let payload = OraclePayload::deserialize(&mut oracle_data).map_err(|_| INVALID_ORACLE_DATA)?;
347 build_liquidity(
348 &payload,
349 QuoteType::TokenAExactIn,
350 reserves_a,
351 reserves_b,
352 skew_cliff_min_per_m,
353 skew_cliff_max_per_m,
354 )
355}
356
357#[cfg_attr(feature = "wasm", wasm_expose)]
358pub fn ask_liquidity(
359 oracle_data: &[u8],
360 reserves_a: u64,
361 reserves_b: u64,
362 skew_cliff_min_per_m: i32,
363 skew_cliff_max_per_m: i32,
364) -> Result<SingleSideLiquidity, CoreError> {
365 let mut oracle_data = oracle_data;
366 let payload = OraclePayload::deserialize(&mut oracle_data).map_err(|_| INVALID_ORACLE_DATA)?;
367 build_liquidity(
368 &payload,
369 QuoteType::TokenBExactIn,
370 reserves_a,
371 reserves_b,
372 skew_cliff_min_per_m,
373 skew_cliff_max_per_m,
374 )
375}
376
377#[cfg(test)]
378mod tests {
379 use super::{
380 super::guards::{
381 INVENTORY_IMBALANCE, ORACLE_EXPIRED, ORACLE_PRICE_BELOW_MIN, SPREAD_BELOW_MIN,
382 },
383 *,
384 };
385 use borsh::BorshSerialize;
386 use rstest::rstest;
387
388 use super::super::oracle::{
389 OracleData, SkewMode, ORACLE_DATA_LEN, ORACLE_PAYLOAD_LEN, SKEW_OFFSET,
390 };
391
392 fn flat_oracle_data(price_q64_64: u128) -> [u8; ORACLE_PAYLOAD_LEN] {
393 let data = OracleData::FlatPrice { price_q64_64 };
394 let skew = SkewMode::None;
395
396 let mut buf = [0u8; ORACLE_PAYLOAD_LEN];
397
398 let mut data_slice = &mut buf[..ORACLE_DATA_LEN];
399 data.serialize(&mut data_slice).unwrap();
400
401 let mut skew_slice = &mut buf[SKEW_OFFSET..];
402 skew.serialize(&mut skew_slice).unwrap();
403
404 buf
405 }
406
407 fn pass_through_params() -> GuardParams {
408 GuardParams {
409 max_inventory_imbalance_per_m: i32::MAX,
410 max_a_inventory_per_m: 0,
411 max_b_inventory_per_m: 0,
412 min_spread_per_m: i32::MIN,
413 min_oracle_price: 0,
414 max_oracle_price: u128::MAX,
415 valid_until: u64::MAX,
416 }
417 }
418
419 #[rstest]
420 #[case::a_to_b_ok(100, 80, QuoteType::TokenAExactIn, 1000, 1000, Ok((1100, 920)))]
421 #[case::b_to_a_ok(80, 100, QuoteType::TokenBExactIn, 1000, 1000, Ok((900, 1080)))]
422 #[case::a_to_b_exact_out(100, 80, QuoteType::TokenBExactOut, 1000, 1000, Ok((1100, 920)))]
423 #[case::b_to_a_exact_out(80, 100, QuoteType::TokenAExactOut, 1000, 1000, Ok((900, 1080)))]
424 #[case::output_underflow_a(
425 100,
426 2000,
427 QuoteType::TokenAExactIn,
428 1000,
429 1000,
430 Err(ARITHMETIC_OVERFLOW)
431 )]
432 #[case::output_underflow_b(
433 100,
434 2000,
435 QuoteType::TokenBExactIn,
436 1000,
437 1000,
438 Err(ARITHMETIC_OVERFLOW)
439 )]
440 #[case::input_overflow_a(
441 1,
442 0,
443 QuoteType::TokenAExactIn,
444 u64::MAX,
445 1000,
446 Err(ARITHMETIC_OVERFLOW)
447 )]
448 #[case::input_overflow_b(
449 1,
450 0,
451 QuoteType::TokenBExactIn,
452 1000,
453 u64::MAX,
454 Err(ARITHMETIC_OVERFLOW)
455 )]
456 fn test_apply_to_reserves(
457 #[case] amount_in: u64,
458 #[case] amount_out: u64,
459 #[case] quote_type: QuoteType,
460 #[case] reserves_a: u64,
461 #[case] reserves_b: u64,
462 #[case] expected: Result<(u64, u64), CoreError>,
463 ) {
464 let quote = Quote {
465 amount_in,
466 amount_out,
467 quote_type,
468 };
469
470 let result = quote.apply_to_reserves(reserves_a, reserves_b);
471
472 assert_eq!(result, expected);
473 }
474
475 #[test]
476 fn test_guards_happy_path() {
477 let oracle = flat_oracle_data(1 << 64);
478 let params = pass_through_params();
479
480 let result = quote_exact_in_with_guards(100, true, &oracle, 1000, 1000, 0, 0, 0, ¶ms);
481
482 let guarded = result.unwrap();
483 assert_eq!(guarded.quote.amount_in, 100);
484 assert_eq!(guarded.quote.amount_out, 100);
485 assert_eq!(guarded.post_reserves_a, 1100);
486 assert_eq!(guarded.post_reserves_b, 900);
487 }
488
489 #[test]
490 fn test_guards_oracle_expired() {
491 let oracle = flat_oracle_data(1 << 64);
492 let params = GuardParams {
493 valid_until: 5,
494 ..pass_through_params()
495 };
496
497 let result = quote_exact_in_with_guards(100, true, &oracle, 1000, 1000, 0, 0, 10, ¶ms);
498
499 assert_eq!(result, Err(ORACLE_EXPIRED));
500 }
501
502 #[test]
503 fn test_guards_inventory_guard_fail() {
504 let oracle = flat_oracle_data(1 << 64);
505 let params = GuardParams {
506 max_inventory_imbalance_per_m: 10_000,
507 ..pass_through_params()
508 };
509
510 let result = quote_exact_in_with_guards(100, true, &oracle, 1500, 500, 0, 0, 0, ¶ms);
511
512 assert_eq!(result, Err(INVENTORY_IMBALANCE));
513 }
514
515 #[test]
516 fn test_guards_spread_guard_fail() {
517 let oracle = flat_oracle_data(1 << 64);
518 let params = GuardParams {
519 min_spread_per_m: 100,
520 ..pass_through_params()
521 };
522
523 let result = quote_exact_in_with_guards(100, true, &oracle, 1000, 1000, 0, 0, 0, ¶ms);
524
525 assert_eq!(result, Err(SPREAD_BELOW_MIN));
526 }
527
528 #[test]
529 fn test_guards_price_below_min() {
530 let price = 1u128 << 64;
531 let oracle = flat_oracle_data(price);
532 let params = GuardParams {
533 min_oracle_price: price + 1,
534 ..pass_through_params()
535 };
536
537 let result = quote_exact_in_with_guards(100, true, &oracle, 1000, 1000, 0, 0, 0, ¶ms);
538
539 assert_eq!(result, Err(ORACLE_PRICE_BELOW_MIN));
540 }
541
542 #[test]
543 fn test_guards_invalid_oracle() {
544 let params = pass_through_params();
545
546 let result = quote_exact_in_with_guards(100, true, &[0u8; 4], 1000, 1000, 0, 0, 0, ¶ms);
547
548 assert_eq!(result, Err(INVALID_ORACLE_DATA));
549 }
550}