1use ethnum::U256;
2
3use crate::{tick_math::tick_to_sqrt_price_x64, AmmMathError};
4
5pub fn get_amount_0_delta(
13 sqrt_price_a: u128,
14 sqrt_price_b: u128,
15 liquidity: u128,
16 round_up: bool,
17) -> Result<u64, AmmMathError> {
18 if sqrt_price_a == 0 || sqrt_price_b == 0 {
19 return Err(AmmMathError::DivisionByZero);
20 }
21
22 let (lower, upper) = if sqrt_price_a <= sqrt_price_b {
23 (sqrt_price_a, sqrt_price_b)
24 } else {
25 (sqrt_price_b, sqrt_price_a)
26 };
27
28 let diff = upper - lower;
29 if diff == 0 || liquidity == 0 {
30 return Ok(0);
31 }
32
33 let numerator = U256::from(liquidity) * U256::from(diff) * U256::from(1u128 << 64);
36 let denominator = U256::from(lower) * U256::from(upper);
37
38 let result = if round_up {
39 (numerator + denominator - U256::from(1u128)) / denominator
40 } else {
41 numerator / denominator
42 };
43
44 if result > U256::from(u64::MAX) {
45 return Err(AmmMathError::Overflow);
46 }
47 Ok(result.as_u128() as u64)
48}
49
50pub fn get_amount_1_delta(
57 sqrt_price_a: u128,
58 sqrt_price_b: u128,
59 liquidity: u128,
60 round_up: bool,
61) -> Result<u64, AmmMathError> {
62 let (lower, upper) = if sqrt_price_a <= sqrt_price_b {
63 (sqrt_price_a, sqrt_price_b)
64 } else {
65 (sqrt_price_b, sqrt_price_a)
66 };
67
68 let diff = upper - lower;
69 if diff == 0 || liquidity == 0 {
70 return Ok(0);
71 }
72
73 let numerator = U256::from(liquidity) * U256::from(diff);
76 let denominator = U256::from(1u128 << 64);
77
78 let result = if round_up {
79 (numerator + denominator - U256::from(1u128)) / denominator
80 } else {
81 numerator / denominator
82 };
83
84 if result > U256::from(u64::MAX) {
85 return Err(AmmMathError::Overflow);
86 }
87 Ok(result.as_u128() as u64)
88}
89
90pub fn token_amounts_from_liquidity(
107 liquidity: u128,
108 sqrt_price_x64: u128,
109 tick_lower: i32,
110 tick_upper: i32,
111 round_up: bool,
112) -> Result<(u64, u64), AmmMathError> {
113 let sqrt_price_lower_x64 = tick_to_sqrt_price_x64(tick_lower)?;
114 let sqrt_price_upper_x64 = tick_to_sqrt_price_x64(tick_upper)?;
115
116 token_amounts_from_liquidity_with_sqrt_prices(
117 liquidity,
118 sqrt_price_x64,
119 sqrt_price_lower_x64,
120 sqrt_price_upper_x64,
121 round_up,
122 )
123}
124
125pub fn token_amounts_from_liquidity_with_sqrt_prices(
128 liquidity: u128,
129 sqrt_price_x64: u128,
130 sqrt_price_lower_x64: u128,
131 sqrt_price_upper_x64: u128,
132 round_up: bool,
133) -> Result<(u64, u64), AmmMathError> {
134 if liquidity == 0 {
135 return Ok((0, 0));
136 }
137
138 let amount_a;
139 let amount_b;
140
141 if sqrt_price_x64 <= sqrt_price_lower_x64 {
142 amount_a =
144 get_amount_0_delta(sqrt_price_lower_x64, sqrt_price_upper_x64, liquidity, round_up)?;
145 amount_b = 0;
146 } else if sqrt_price_x64 >= sqrt_price_upper_x64 {
147 amount_a = 0;
149 amount_b =
150 get_amount_1_delta(sqrt_price_lower_x64, sqrt_price_upper_x64, liquidity, round_up)?;
151 } else {
152 amount_a = get_amount_0_delta(sqrt_price_x64, sqrt_price_upper_x64, liquidity, round_up)?;
154 amount_b = get_amount_1_delta(sqrt_price_lower_x64, sqrt_price_x64, liquidity, round_up)?;
155 }
156
157 Ok((amount_a, amount_b))
158}
159
160#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
169pub struct TransferFee {
170 pub fee_bps: u16,
172 pub max_fee: u64,
174}
175
176impl TransferFee {
177 pub fn new(fee_bps: u16) -> Self {
179 Self { fee_bps, max_fee: u64::MAX }
180 }
181
182 pub fn new_with_max(fee_bps: u16, max_fee: u64) -> Self {
184 Self { fee_bps, max_fee }
185 }
186}
187
188#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
194pub struct TickRange {
195 pub tick_lower_index: i32,
197 pub tick_upper_index: i32,
199}
200
201pub fn order_tick_indexes(tick_index_1: i32, tick_index_2: i32) -> TickRange {
203 if tick_index_1 < tick_index_2 {
204 TickRange { tick_lower_index: tick_index_1, tick_upper_index: tick_index_2 }
205 } else {
206 TickRange { tick_lower_index: tick_index_2, tick_upper_index: tick_index_1 }
207 }
208}
209
210pub type QuoteError = &'static str;
216
217pub const ARITHMETIC_OVERFLOW: QuoteError = "Arithmetic over- or underflow";
219pub const AMOUNT_EXCEEDS_MAX_U64: QuoteError = "Amount exceeds max u64";
221pub const INVALID_TRANSFER_FEE: QuoteError = "Invalid transfer fee";
223pub const INVALID_SLIPPAGE_TOLERANCE: QuoteError = "Invalid slippage tolerance";
225
226const BPS_DENOMINATOR: u16 = 10000;
227
228#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
230pub struct IncreaseLiquidityQuote {
231 pub liquidity_delta: u128,
233 pub token_est_a: u64,
235 pub token_est_b: u64,
237 pub token_max_a: u64,
239 pub token_max_b: u64,
241}
242
243#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
245pub struct DecreaseLiquidityQuote {
246 pub liquidity_delta: u128,
248 pub token_est_a: u64,
250 pub token_est_b: u64,
252 pub token_min_a: u64,
254 pub token_min_b: u64,
256}
257
258#[derive(Copy, Clone, Debug, PartialEq, Eq)]
263enum PositionStatus {
264 PriceInRange,
265 PriceBelowRange,
266 PriceAboveRange,
267 Invalid,
268}
269
270fn position_status(
271 current_sqrt_price: u128,
272 tick_index_1: i32,
273 tick_index_2: i32,
274) -> PositionStatus {
275 let tick_range = order_tick_indexes(tick_index_1, tick_index_2);
276 let sqrt_price_lower = tick_to_sqrt_price_x64(tick_range.tick_lower_index).unwrap_or(0);
277 let sqrt_price_upper = tick_to_sqrt_price_x64(tick_range.tick_upper_index).unwrap_or(u128::MAX);
278
279 if tick_index_1 == tick_index_2 {
280 PositionStatus::Invalid
281 } else if current_sqrt_price <= sqrt_price_lower {
282 PositionStatus::PriceBelowRange
283 } else if current_sqrt_price >= sqrt_price_upper {
284 PositionStatus::PriceAboveRange
285 } else {
286 PositionStatus::PriceInRange
287 }
288}
289
290fn try_apply_transfer_fee(amount: u64, tf: TransferFee) -> Result<u64, QuoteError> {
295 if tf.fee_bps > BPS_DENOMINATOR {
296 return Err(INVALID_TRANSFER_FEE);
297 }
298 if tf.fee_bps == 0 || amount == 0 {
299 return Ok(amount);
300 }
301 let numerator = (amount as u128).checked_mul(tf.fee_bps as u128).ok_or(ARITHMETIC_OVERFLOW)?;
302 let raw_fee: u64 = numerator
303 .div_ceil(BPS_DENOMINATOR as u128)
304 .try_into()
305 .map_err(|_| AMOUNT_EXCEEDS_MAX_U64)?;
306 let fee = raw_fee.min(tf.max_fee);
307 Ok(amount - fee)
308}
309
310fn try_reverse_apply_transfer_fee(amount: u64, tf: TransferFee) -> Result<u64, QuoteError> {
311 if tf.fee_bps > BPS_DENOMINATOR {
312 Err(INVALID_TRANSFER_FEE)
313 } else if tf.fee_bps == 0 {
314 Ok(amount)
315 } else if amount == 0 {
316 Ok(0)
317 } else if tf.fee_bps == BPS_DENOMINATOR {
318 amount.checked_add(tf.max_fee).ok_or(AMOUNT_EXCEEDS_MAX_U64)
319 } else {
320 let numerator =
321 (amount as u128).checked_mul(BPS_DENOMINATOR as u128).ok_or(ARITHMETIC_OVERFLOW)?;
322 let denominator = (BPS_DENOMINATOR as u128) - (tf.fee_bps as u128);
323 let raw_pre_fee_amount = numerator.div_ceil(denominator);
324 let fee_amount =
325 raw_pre_fee_amount.checked_sub(amount as u128).ok_or(AMOUNT_EXCEEDS_MAX_U64)?;
326 if fee_amount >= tf.max_fee as u128 {
327 amount.checked_add(tf.max_fee).ok_or(AMOUNT_EXCEEDS_MAX_U64)
328 } else {
329 raw_pre_fee_amount.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)
330 }
331 }
332}
333
334fn try_mul_div(
339 amount: u64,
340 product: u128,
341 denominator: u128,
342 round_up: bool,
343) -> Result<u64, QuoteError> {
344 if amount == 0 || product == 0 {
345 return Ok(0);
346 }
347 let numerator = (amount as u128).checked_mul(product).ok_or(ARITHMETIC_OVERFLOW)?;
348 let quotient = numerator / denominator;
349 let remainder = numerator % denominator;
350 let result = if round_up && remainder != 0 { quotient + 1 } else { quotient };
351 result.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)
352}
353
354fn try_get_max_amount_with_slippage(amount: u64, slippage_bps: u16) -> Result<u64, QuoteError> {
355 if slippage_bps > BPS_DENOMINATOR {
356 return Err(INVALID_SLIPPAGE_TOLERANCE);
357 }
358 let product = (BPS_DENOMINATOR as u128) + (slippage_bps as u128);
359 try_mul_div(amount, product, BPS_DENOMINATOR as u128, true)
360}
361
362fn try_get_min_amount_with_slippage(amount: u64, slippage_bps: u16) -> Result<u64, QuoteError> {
363 if slippage_bps > BPS_DENOMINATOR {
364 return Err(INVALID_SLIPPAGE_TOLERANCE);
365 }
366 let product = (BPS_DENOMINATOR as u128) - (slippage_bps as u128);
367 try_mul_div(amount, product, BPS_DENOMINATOR as u128, false)
368}
369
370fn try_get_token_a_from_liquidity(
375 liquidity_delta: u128,
376 sqrt_price_lower: u128,
377 sqrt_price_upper: u128,
378 round_up: bool,
379) -> Result<u64, QuoteError> {
380 let sqrt_price_diff =
381 sqrt_price_upper.checked_sub(sqrt_price_lower).ok_or(ARITHMETIC_OVERFLOW)?;
382 if sqrt_price_lower == 0 || sqrt_price_upper == 0 {
383 return Err(ARITHMETIC_OVERFLOW);
384 }
385 let numerator: U256 = U256::from(liquidity_delta)
386 .checked_mul(sqrt_price_diff.into())
387 .ok_or(ARITHMETIC_OVERFLOW)?
388 .checked_shl(64)
389 .ok_or(ARITHMETIC_OVERFLOW)?;
390 let denominator = U256::from(sqrt_price_upper)
391 .checked_mul(U256::from(sqrt_price_lower))
392 .ok_or(ARITHMETIC_OVERFLOW)?;
393 if denominator == U256::ZERO {
394 return Err(ARITHMETIC_OVERFLOW);
395 }
396 let quotient = numerator / denominator;
397 let remainder = numerator % denominator;
398 let result = if round_up && remainder != U256::ZERO { quotient + 1 } else { quotient };
399 result.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)
400}
401
402fn try_get_token_b_from_liquidity(
403 liquidity_delta: u128,
404 sqrt_price_lower: u128,
405 sqrt_price_upper: u128,
406 round_up: bool,
407) -> Result<u64, QuoteError> {
408 let sqrt_price_diff =
409 sqrt_price_upper.checked_sub(sqrt_price_lower).ok_or(ARITHMETIC_OVERFLOW)?;
410 let product: U256 = U256::from(liquidity_delta)
411 .checked_mul(sqrt_price_diff.into())
412 .ok_or(ARITHMETIC_OVERFLOW)?;
413 let quotient: U256 = product >> 64;
414 let should_round = round_up && product & U256::from(u64::MAX) > U256::ZERO;
415 let result = if should_round { quotient + 1 } else { quotient };
416 result.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)
417}
418
419fn try_get_liquidity_from_a(
420 token_delta_a: u64,
421 sqrt_price_lower: u128,
422 sqrt_price_upper: u128,
423) -> Result<u128, QuoteError> {
424 let sqrt_price_diff =
425 sqrt_price_upper.checked_sub(sqrt_price_lower).ok_or(ARITHMETIC_OVERFLOW)?;
426 if sqrt_price_diff == 0 {
427 return Err(ARITHMETIC_OVERFLOW);
428 }
429 let mul: U256 = U256::from(token_delta_a)
430 .checked_mul(sqrt_price_lower.into())
431 .ok_or(ARITHMETIC_OVERFLOW)?
432 .checked_mul(sqrt_price_upper.into())
433 .ok_or(ARITHMETIC_OVERFLOW)?;
434 let result: U256 = (mul / U256::from(sqrt_price_diff)) >> 64;
435 result.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)
436}
437
438fn try_get_liquidity_from_b(
439 token_delta_b: u64,
440 sqrt_price_lower: u128,
441 sqrt_price_upper: u128,
442) -> Result<u128, QuoteError> {
443 let numerator: U256 = U256::from(token_delta_b).checked_shl(64).ok_or(ARITHMETIC_OVERFLOW)?;
444 let sqrt_price_diff =
445 sqrt_price_upper.checked_sub(sqrt_price_lower).ok_or(ARITHMETIC_OVERFLOW)?;
446 if sqrt_price_diff == 0 {
447 return Err(ARITHMETIC_OVERFLOW);
448 }
449 let result = numerator / U256::from(sqrt_price_diff);
450 result.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)
451}
452
453fn try_get_token_estimates_from_liquidity(
458 liquidity_delta: u128,
459 current_sqrt_price: u128,
460 tick_lower_index: i32,
461 tick_upper_index: i32,
462 round_up: bool,
463) -> Result<(u64, u64), QuoteError> {
464 if liquidity_delta == 0 {
465 return Ok((0, 0));
466 }
467
468 let sqrt_price_lower =
469 tick_to_sqrt_price_x64(tick_lower_index).map_err(|_| ARITHMETIC_OVERFLOW)?;
470 let sqrt_price_upper =
471 tick_to_sqrt_price_x64(tick_upper_index).map_err(|_| ARITHMETIC_OVERFLOW)?;
472
473 let status = position_status(current_sqrt_price, tick_lower_index, tick_upper_index);
474
475 match status {
476 PositionStatus::PriceBelowRange => {
477 let token_a = try_get_token_a_from_liquidity(
478 liquidity_delta,
479 sqrt_price_lower,
480 sqrt_price_upper,
481 round_up,
482 )?;
483 Ok((token_a, 0))
484 }
485 PositionStatus::PriceInRange => {
486 let token_a = try_get_token_a_from_liquidity(
487 liquidity_delta,
488 current_sqrt_price,
489 sqrt_price_upper,
490 round_up,
491 )?;
492 let token_b = try_get_token_b_from_liquidity(
493 liquidity_delta,
494 sqrt_price_lower,
495 current_sqrt_price,
496 round_up,
497 )?;
498 Ok((token_a, token_b))
499 }
500 PositionStatus::PriceAboveRange => {
501 let token_b = try_get_token_b_from_liquidity(
502 liquidity_delta,
503 sqrt_price_lower,
504 sqrt_price_upper,
505 round_up,
506 )?;
507 Ok((0, token_b))
508 }
509 PositionStatus::Invalid => Ok((0, 0)),
510 }
511}
512
513pub fn increase_liquidity_quote(
519 liquidity_delta: u128,
520 slippage_tolerance_bps: u16,
521 current_sqrt_price: u128,
522 tick_index_1: i32,
523 tick_index_2: i32,
524 transfer_fee_a: Option<TransferFee>,
525 transfer_fee_b: Option<TransferFee>,
526) -> Result<IncreaseLiquidityQuote, QuoteError> {
527 if liquidity_delta == 0 {
528 return Ok(IncreaseLiquidityQuote::default());
529 }
530
531 let tick_range = order_tick_indexes(tick_index_1, tick_index_2);
532
533 let (token_est_before_fees_a, token_est_before_fees_b) =
534 try_get_token_estimates_from_liquidity(
535 liquidity_delta,
536 current_sqrt_price,
537 tick_range.tick_lower_index,
538 tick_range.tick_upper_index,
539 true,
540 )?;
541
542 let token_est_a = try_reverse_apply_transfer_fee(
543 token_est_before_fees_a,
544 transfer_fee_a.unwrap_or_default(),
545 )?;
546 let token_est_b = try_reverse_apply_transfer_fee(
547 token_est_before_fees_b,
548 transfer_fee_b.unwrap_or_default(),
549 )?;
550
551 let token_max_a = try_get_max_amount_with_slippage(token_est_a, slippage_tolerance_bps)?;
552 let token_max_b = try_get_max_amount_with_slippage(token_est_b, slippage_tolerance_bps)?;
553
554 Ok(IncreaseLiquidityQuote {
555 liquidity_delta,
556 token_est_a,
557 token_est_b,
558 token_max_a,
559 token_max_b,
560 })
561}
562
563pub fn increase_liquidity_quote_a(
565 token_amount_a: u64,
566 slippage_tolerance_bps: u16,
567 current_sqrt_price: u128,
568 tick_index_1: i32,
569 tick_index_2: i32,
570 transfer_fee_a: Option<TransferFee>,
571 transfer_fee_b: Option<TransferFee>,
572) -> Result<IncreaseLiquidityQuote, QuoteError> {
573 let tick_range = order_tick_indexes(tick_index_1, tick_index_2);
574 let token_delta_a = try_apply_transfer_fee(token_amount_a, transfer_fee_a.unwrap_or_default())?;
575
576 if token_delta_a == 0 {
577 return Ok(IncreaseLiquidityQuote::default());
578 }
579
580 let sqrt_price_lower =
581 tick_to_sqrt_price_x64(tick_range.tick_lower_index).map_err(|_| ARITHMETIC_OVERFLOW)?;
582 let sqrt_price_upper =
583 tick_to_sqrt_price_x64(tick_range.tick_upper_index).map_err(|_| ARITHMETIC_OVERFLOW)?;
584
585 let status = position_status(current_sqrt_price, tick_index_1, tick_index_2);
586
587 let liquidity: u128 = match status {
588 PositionStatus::PriceBelowRange => {
589 try_get_liquidity_from_a(token_delta_a, sqrt_price_lower, sqrt_price_upper)?
590 }
591 PositionStatus::Invalid | PositionStatus::PriceAboveRange => 0,
592 PositionStatus::PriceInRange => {
593 try_get_liquidity_from_a(token_delta_a, current_sqrt_price, sqrt_price_upper)?
594 }
595 };
596
597 increase_liquidity_quote(
598 liquidity,
599 slippage_tolerance_bps,
600 current_sqrt_price,
601 tick_index_1,
602 tick_index_2,
603 transfer_fee_a,
604 transfer_fee_b,
605 )
606}
607
608pub fn increase_liquidity_quote_b(
610 token_amount_b: u64,
611 slippage_tolerance_bps: u16,
612 current_sqrt_price: u128,
613 tick_index_1: i32,
614 tick_index_2: i32,
615 transfer_fee_a: Option<TransferFee>,
616 transfer_fee_b: Option<TransferFee>,
617) -> Result<IncreaseLiquidityQuote, QuoteError> {
618 let tick_range = order_tick_indexes(tick_index_1, tick_index_2);
619 let token_delta_b = try_apply_transfer_fee(token_amount_b, transfer_fee_b.unwrap_or_default())?;
620
621 if token_delta_b == 0 {
622 return Ok(IncreaseLiquidityQuote::default());
623 }
624
625 let sqrt_price_lower =
626 tick_to_sqrt_price_x64(tick_range.tick_lower_index).map_err(|_| ARITHMETIC_OVERFLOW)?;
627 let sqrt_price_upper =
628 tick_to_sqrt_price_x64(tick_range.tick_upper_index).map_err(|_| ARITHMETIC_OVERFLOW)?;
629
630 let status = position_status(current_sqrt_price, tick_index_1, tick_index_2);
631
632 let liquidity: u128 = match status {
633 PositionStatus::Invalid | PositionStatus::PriceBelowRange => 0,
634 PositionStatus::PriceAboveRange => {
635 try_get_liquidity_from_b(token_delta_b, sqrt_price_lower, sqrt_price_upper)?
636 }
637 PositionStatus::PriceInRange => {
638 try_get_liquidity_from_b(token_delta_b, sqrt_price_lower, current_sqrt_price)?
639 }
640 };
641
642 increase_liquidity_quote(
643 liquidity,
644 slippage_tolerance_bps,
645 current_sqrt_price,
646 tick_index_1,
647 tick_index_2,
648 transfer_fee_a,
649 transfer_fee_b,
650 )
651}
652
653pub fn decrease_liquidity_quote(
655 liquidity_delta: u128,
656 slippage_tolerance_bps: u16,
657 current_sqrt_price: u128,
658 tick_index_1: i32,
659 tick_index_2: i32,
660 transfer_fee_a: Option<TransferFee>,
661 transfer_fee_b: Option<TransferFee>,
662) -> Result<DecreaseLiquidityQuote, QuoteError> {
663 if liquidity_delta == 0 {
664 return Ok(DecreaseLiquidityQuote::default());
665 }
666
667 let tick_range = order_tick_indexes(tick_index_1, tick_index_2);
668
669 let (token_est_before_fees_a, token_est_before_fees_b) =
670 try_get_token_estimates_from_liquidity(
671 liquidity_delta,
672 current_sqrt_price,
673 tick_range.tick_lower_index,
674 tick_range.tick_upper_index,
675 false,
676 )?;
677
678 let token_est_a =
679 try_apply_transfer_fee(token_est_before_fees_a, transfer_fee_a.unwrap_or_default())?;
680 let token_est_b =
681 try_apply_transfer_fee(token_est_before_fees_b, transfer_fee_b.unwrap_or_default())?;
682
683 let token_min_a = try_get_min_amount_with_slippage(token_est_a, slippage_tolerance_bps)?;
684 let token_min_b = try_get_min_amount_with_slippage(token_est_b, slippage_tolerance_bps)?;
685
686 Ok(DecreaseLiquidityQuote {
687 liquidity_delta,
688 token_est_a,
689 token_est_b,
690 token_min_a,
691 token_min_b,
692 })
693}
694
695pub fn decrease_liquidity_quote_a(
697 token_amount_a: u64,
698 slippage_tolerance_bps: u16,
699 current_sqrt_price: u128,
700 tick_index_1: i32,
701 tick_index_2: i32,
702 transfer_fee_a: Option<TransferFee>,
703 transfer_fee_b: Option<TransferFee>,
704) -> Result<DecreaseLiquidityQuote, QuoteError> {
705 let tick_range = order_tick_indexes(tick_index_1, tick_index_2);
706 let token_delta_a =
707 try_reverse_apply_transfer_fee(token_amount_a, transfer_fee_a.unwrap_or_default())?;
708
709 if token_delta_a == 0 {
710 return Ok(DecreaseLiquidityQuote::default());
711 }
712
713 let sqrt_price_lower =
714 tick_to_sqrt_price_x64(tick_range.tick_lower_index).map_err(|_| ARITHMETIC_OVERFLOW)?;
715 let sqrt_price_upper =
716 tick_to_sqrt_price_x64(tick_range.tick_upper_index).map_err(|_| ARITHMETIC_OVERFLOW)?;
717
718 let status = position_status(current_sqrt_price, tick_index_1, tick_index_2);
719
720 let liquidity: u128 = match status {
721 PositionStatus::PriceBelowRange => {
722 try_get_liquidity_from_a(token_delta_a, sqrt_price_lower, sqrt_price_upper)?
723 }
724 PositionStatus::Invalid | PositionStatus::PriceAboveRange => 0,
725 PositionStatus::PriceInRange => {
726 try_get_liquidity_from_a(token_delta_a, current_sqrt_price, sqrt_price_upper)?
727 }
728 };
729
730 decrease_liquidity_quote(
731 liquidity,
732 slippage_tolerance_bps,
733 current_sqrt_price,
734 tick_index_1,
735 tick_index_2,
736 transfer_fee_a,
737 transfer_fee_b,
738 )
739}
740
741pub fn decrease_liquidity_quote_b(
743 token_amount_b: u64,
744 slippage_tolerance_bps: u16,
745 current_sqrt_price: u128,
746 tick_index_1: i32,
747 tick_index_2: i32,
748 transfer_fee_a: Option<TransferFee>,
749 transfer_fee_b: Option<TransferFee>,
750) -> Result<DecreaseLiquidityQuote, QuoteError> {
751 let tick_range = order_tick_indexes(tick_index_1, tick_index_2);
752 let token_delta_b =
753 try_reverse_apply_transfer_fee(token_amount_b, transfer_fee_b.unwrap_or_default())?;
754
755 if token_delta_b == 0 {
756 return Ok(DecreaseLiquidityQuote::default());
757 }
758
759 let sqrt_price_lower =
760 tick_to_sqrt_price_x64(tick_range.tick_lower_index).map_err(|_| ARITHMETIC_OVERFLOW)?;
761 let sqrt_price_upper =
762 tick_to_sqrt_price_x64(tick_range.tick_upper_index).map_err(|_| ARITHMETIC_OVERFLOW)?;
763
764 let status = position_status(current_sqrt_price, tick_index_1, tick_index_2);
765
766 let liquidity: u128 = match status {
767 PositionStatus::Invalid | PositionStatus::PriceBelowRange => 0,
768 PositionStatus::PriceAboveRange => {
769 try_get_liquidity_from_b(token_delta_b, sqrt_price_lower, sqrt_price_upper)?
770 }
771 PositionStatus::PriceInRange => {
772 try_get_liquidity_from_b(token_delta_b, sqrt_price_lower, current_sqrt_price)?
773 }
774 };
775
776 decrease_liquidity_quote(
777 liquidity,
778 slippage_tolerance_bps,
779 current_sqrt_price,
780 tick_index_1,
781 tick_index_2,
782 transfer_fee_a,
783 transfer_fee_b,
784 )
785}
786
787#[cfg(test)]
788mod tests {
789 use super::*;
790
791 const Q64: u128 = 1u128 << 64;
792
793 #[test]
794 fn test_amount_0_delta_basic() {
795 let price_a = Q64; let price_b = Q64 * 2; let liquidity = 1_000_000u128;
799
800 let amount = get_amount_0_delta(price_a, price_b, liquidity, false).unwrap();
801 assert_eq!(amount, 500_000);
803 }
804
805 #[test]
806 fn test_amount_1_delta_basic() {
807 let price_a = Q64; let price_b = Q64 * 2; let liquidity = 1_000_000u128;
810
811 let amount = get_amount_1_delta(price_a, price_b, liquidity, false).unwrap();
812 assert_eq!(amount, 1_000_000);
814 }
815
816 #[test]
817 fn test_zero_liquidity() {
818 assert_eq!(get_amount_0_delta(Q64, Q64 * 2, 0, false).unwrap(), 0);
819 assert_eq!(get_amount_1_delta(Q64, Q64 * 2, 0, false).unwrap(), 0);
820 }
821
822 #[test]
823 fn test_same_price() {
824 assert_eq!(get_amount_0_delta(Q64, Q64, 1_000_000, false).unwrap(), 0);
825 assert_eq!(get_amount_1_delta(Q64, Q64, 1_000_000, false).unwrap(), 0);
826 }
827
828 #[test]
829 fn test_rounding() {
830 let price_a = Q64;
831 let price_b = Q64 * 3;
832 let liquidity = 100u128;
833
834 let floor = get_amount_0_delta(price_a, price_b, liquidity, false).unwrap();
835 let ceil = get_amount_0_delta(price_a, price_b, liquidity, true).unwrap();
836 assert!(ceil >= floor);
838
839 let floor1 = get_amount_1_delta(price_a, price_b, liquidity, false).unwrap();
840 let ceil1 = get_amount_1_delta(price_a, price_b, liquidity, true).unwrap();
841 assert!(ceil1 >= floor1);
842 }
843
844 #[test]
845 fn test_price_order_invariant() {
846 let price_a = Q64;
847 let price_b = Q64 * 2;
848 let liquidity = 1_000_000u128;
849
850 let a0 = get_amount_0_delta(price_a, price_b, liquidity, false).unwrap();
851 let a0_rev = get_amount_0_delta(price_b, price_a, liquidity, false).unwrap();
852 assert_eq!(a0, a0_rev);
853 }
854
855 #[test]
856 fn test_zero_sqrt_price_errors() {
857 assert!(get_amount_0_delta(0, Q64, 1000, false).is_err());
858 assert!(get_amount_0_delta(Q64, 0, 1000, false).is_err());
859 }
860
861 #[test]
862 fn amount_delta_known_values() {
863 use crate::tick_math::tick_to_sqrt_price_x64;
864 let sqrt_a = tick_to_sqrt_price_x64(-100).unwrap();
865 let sqrt_b = tick_to_sqrt_price_x64(100).unwrap();
866 let liquidity = 1_000_000_000u128;
867 let amount0 = get_amount_0_delta(sqrt_a, sqrt_b, liquidity, false).unwrap();
868 let amount1 = get_amount_1_delta(sqrt_a, sqrt_b, liquidity, false).unwrap();
869 assert!(amount0 > 0 && amount1 > 0);
870 }
871
872 #[test]
873 fn zero_liquidity_returns_zero() {
874 use crate::tick_math::tick_to_sqrt_price_x64;
875 let sqrt_a = tick_to_sqrt_price_x64(0).unwrap();
876 let sqrt_b = tick_to_sqrt_price_x64(100).unwrap();
877 assert_eq!(get_amount_0_delta(sqrt_a, sqrt_b, 0, false).unwrap(), 0);
878 assert_eq!(get_amount_1_delta(sqrt_a, sqrt_b, 0, false).unwrap(), 0);
879 }
880
881 #[test]
882 fn test_large_liquidity_no_false_overflow() {
883 let liquidity = 1u128 << 64;
890 let sqrt_price_a = Q64; let sqrt_price_b = Q64 * 2; let amount = get_amount_0_delta(sqrt_price_a, sqrt_price_b, liquidity, false).unwrap();
894 assert_eq!(amount, 1u64 << 63);
895
896 let amount_up = get_amount_0_delta(sqrt_price_a, sqrt_price_b, liquidity, true).unwrap();
898 assert_eq!(amount_up, 1u64 << 63); }
900
901 #[test]
902 fn test_large_liquidity_amount_1_no_false_overflow() {
903 let liquidity = 1u128 << 64;
907 let sqrt_price_a = Q64;
908 let sqrt_price_b = Q64 + (1u128 << 63);
909
910 let amount = get_amount_1_delta(sqrt_price_a, sqrt_price_b, liquidity, false).unwrap();
911 assert_eq!(amount, 1u64 << 63);
912 }
913
914 #[test]
919 fn test_token_amounts_zero_liquidity() {
920 let (a, b) = token_amounts_from_liquidity(0, Q64, -100, 100, false).unwrap();
921 assert_eq!(a, 0);
922 assert_eq!(b, 0);
923 }
924
925 #[test]
926 fn test_token_amounts_price_in_range() {
927 let sqrt_price = tick_to_sqrt_price_x64(0).unwrap();
929 let liquidity = 1_000_000_000u128;
930 let (a, b) = token_amounts_from_liquidity(liquidity, sqrt_price, -100, 100, false).unwrap();
931 assert!(a > 0, "amount_a should be > 0, got {}", a);
933 assert!(b > 0, "amount_b should be > 0, got {}", b);
934 }
935
936 #[test]
937 fn test_token_amounts_price_below_range() {
938 let sqrt_price = tick_to_sqrt_price_x64(-200).unwrap();
940 let liquidity = 1_000_000_000u128;
941 let (a, b) = token_amounts_from_liquidity(liquidity, sqrt_price, -100, 100, false).unwrap();
942 assert!(a > 0, "amount_a should be > 0, got {}", a);
944 assert_eq!(b, 0, "amount_b should be 0 when price below range");
945 }
946
947 #[test]
948 fn test_token_amounts_price_above_range() {
949 let sqrt_price = tick_to_sqrt_price_x64(200).unwrap();
951 let liquidity = 1_000_000_000u128;
952 let (a, b) = token_amounts_from_liquidity(liquidity, sqrt_price, -100, 100, false).unwrap();
953 assert_eq!(a, 0, "amount_a should be 0 when price above range");
955 assert!(b > 0, "amount_b should be > 0, got {}", b);
956 }
957
958 #[test]
959 fn test_token_amounts_rounding() {
960 let sqrt_price = tick_to_sqrt_price_x64(0).unwrap();
961 let liquidity = 100u128;
962 let (a_floor, b_floor) =
963 token_amounts_from_liquidity(liquidity, sqrt_price, -100, 100, false).unwrap();
964 let (a_ceil, b_ceil) =
965 token_amounts_from_liquidity(liquidity, sqrt_price, -100, 100, true).unwrap();
966 assert!(a_ceil >= a_floor);
967 assert!(b_ceil >= b_floor);
968 }
969
970 #[test]
971 fn test_token_amounts_price_at_lower_boundary() {
972 let sqrt_price = tick_to_sqrt_price_x64(-100).unwrap();
974 let liquidity = 1_000_000_000u128;
975 let (a, b) = token_amounts_from_liquidity(liquidity, sqrt_price, -100, 100, false).unwrap();
976 assert!(a > 0);
977 assert_eq!(b, 0);
978 }
979
980 #[test]
981 fn test_token_amounts_price_at_upper_boundary() {
982 let sqrt_price = tick_to_sqrt_price_x64(100).unwrap();
984 let liquidity = 1_000_000_000u128;
985 let (a, b) = token_amounts_from_liquidity(liquidity, sqrt_price, -100, 100, false).unwrap();
986 assert_eq!(a, 0);
987 assert!(b > 0);
988 }
989
990 #[test]
991 fn test_token_amounts_with_sqrt_prices_variant() {
992 let sqrt_price = tick_to_sqrt_price_x64(0).unwrap();
993 let sqrt_lower = tick_to_sqrt_price_x64(-100).unwrap();
994 let sqrt_upper = tick_to_sqrt_price_x64(100).unwrap();
995 let liquidity = 1_000_000_000u128;
996
997 let (a1, b1) =
998 token_amounts_from_liquidity(liquidity, sqrt_price, -100, 100, false).unwrap();
999 let (a2, b2) = token_amounts_from_liquidity_with_sqrt_prices(
1000 liquidity, sqrt_price, sqrt_lower, sqrt_upper, false,
1001 )
1002 .unwrap();
1003 assert_eq!(a1, a2);
1004 assert_eq!(b1, b2);
1005 }
1006
1007 #[test]
1010 fn fuzz_amount_0_delta_no_panic() {
1011 use rand::Rng;
1012
1013 use crate::tick_math::{MAX_SQRT_PRICE, MIN_SQRT_PRICE};
1014 let mut rng = rand::rng();
1015 for _ in 0..1000 {
1016 let sqrt_price_a: u128 = rng.random_range(MIN_SQRT_PRICE..=MAX_SQRT_PRICE);
1017 let sqrt_price_b: u128 = rng.random_range(MIN_SQRT_PRICE..=MAX_SQRT_PRICE);
1018 let liquidity: u128 = rng.random_range(1..=1_000_000_000_000u128);
1020 let round_up = rng.random_range(0u8..=1) == 1;
1021 let _ = get_amount_0_delta(sqrt_price_a, sqrt_price_b, liquidity, round_up);
1023 }
1024 }
1025
1026 #[test]
1027 fn fuzz_amount_1_delta_no_panic() {
1028 use rand::Rng;
1029
1030 use crate::tick_math::{MAX_SQRT_PRICE, MIN_SQRT_PRICE};
1031 let mut rng = rand::rng();
1032 for _ in 0..1000 {
1033 let sqrt_price_a: u128 = rng.random_range(MIN_SQRT_PRICE..=MAX_SQRT_PRICE);
1034 let sqrt_price_b: u128 = rng.random_range(MIN_SQRT_PRICE..=MAX_SQRT_PRICE);
1035 let liquidity: u128 = rng.random_range(1..=1_000_000_000_000u128);
1036 let round_up = rng.random_range(0u8..=1) == 1;
1037 let _ = get_amount_1_delta(sqrt_price_a, sqrt_price_b, liquidity, round_up);
1038 }
1039 }
1040
1041 #[test]
1042 fn fuzz_amount_delta_rounding_direction() {
1043 use rand::Rng;
1044
1045 use crate::tick_math::{MAX_SQRT_PRICE, MIN_SQRT_PRICE};
1046 let mut rng = rand::rng();
1047 for _ in 0..1000 {
1048 let sqrt_price_a: u128 = rng.random_range(MIN_SQRT_PRICE..=MAX_SQRT_PRICE);
1049 let sqrt_price_b: u128 = rng.random_range(MIN_SQRT_PRICE..=MAX_SQRT_PRICE);
1050 let liquidity: u128 = rng.random_range(1..=1_000_000_000u128);
1051
1052 if let (Ok(down_0), Ok(up_0)) = (
1053 get_amount_0_delta(sqrt_price_a, sqrt_price_b, liquidity, false),
1054 get_amount_0_delta(sqrt_price_a, sqrt_price_b, liquidity, true),
1055 ) {
1056 assert!(
1057 up_0 >= down_0,
1058 "round_up < round_down for amount_0: a={sqrt_price_a}, b={sqrt_price_b}, \
1059 liq={liquidity}, down={down_0}, up={up_0}"
1060 );
1061 }
1062
1063 if let (Ok(down_1), Ok(up_1)) = (
1064 get_amount_1_delta(sqrt_price_a, sqrt_price_b, liquidity, false),
1065 get_amount_1_delta(sqrt_price_a, sqrt_price_b, liquidity, true),
1066 ) {
1067 assert!(
1068 up_1 >= down_1,
1069 "round_up < round_down for amount_1: a={sqrt_price_a}, b={sqrt_price_b}, \
1070 liq={liquidity}, down={down_1}, up={up_1}"
1071 );
1072 }
1073 }
1074 }
1075
1076 #[test]
1077 fn fuzz_token_amounts_from_liquidity_no_panic() {
1078 use rand::Rng;
1079
1080 use crate::tick_math::{MAX_TICK, MIN_TICK};
1081 let mut rng = rand::rng();
1082 for _ in 0..1000 {
1083 let tick_a: i32 = rng.random_range(MIN_TICK..=MAX_TICK);
1084 let tick_b: i32 = rng.random_range(MIN_TICK..=MAX_TICK);
1085 let (tick_lower, tick_upper) =
1086 if tick_a <= tick_b { (tick_a, tick_b) } else { (tick_b, tick_a) };
1087 if tick_lower == tick_upper {
1088 continue;
1089 }
1090 let current_tick: i32 = rng.random_range(MIN_TICK..=MAX_TICK);
1091 let sqrt_price = tick_to_sqrt_price_x64(current_tick).unwrap();
1092 let liquidity: u128 = rng.random_range(0..=1_000_000_000u128);
1093 let round_up = rng.random_range(0u8..=1) == 1;
1094 let _ = token_amounts_from_liquidity(
1096 liquidity, sqrt_price, tick_lower, tick_upper, round_up,
1097 );
1098 }
1099 }
1100}