Skip to main content

wp_solana_amm_math/
liquidity_math.rs

1use ethnum::U256;
2
3use crate::{tick_math::tick_to_sqrt_price_x64, AmmMathError};
4
5/// Computes the amount of token 0 for a given liquidity and price range.
6///
7/// `amount0 = liquidity * (sqrt_price_b - sqrt_price_a) * 2^64 / (sqrt_price_a
8/// * sqrt_price_b)`
9///
10/// Expects `sqrt_price_a <= sqrt_price_b` (internally sorted).
11/// Uses U256 for the entire computation to avoid intermediate overflow.
12pub 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    // amount0 = liquidity * diff * 2^64 / (lower * upper)
34    // Compute entirely in U256 to avoid intermediate u128 overflow.
35    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
50/// Computes the amount of token 1 for a given liquidity and price range.
51///
52/// `amount1 = liquidity * (sqrt_price_b - sqrt_price_a) / 2^64`
53///
54/// The division by 2^64 is because the prices are in Q64.64 format.
55/// Uses U256 for the entire computation to avoid intermediate overflow.
56pub 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    // amount1 = liquidity * diff / 2^64
74    // Compute entirely in U256 to avoid intermediate overflow.
75    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
90/// Compute the token amounts held by a position given its liquidity, the
91/// current pool sqrt_price, and the position's tick boundaries.
92///
93/// This is the inverse of `liquidity_for_amounts` and matches the
94/// behaviour of `orca_whirlpools_core::try_get_token_estimates_from_liquidity`.
95///
96/// The concentrated-liquidity formulas are:
97///
98/// * If current price is **below** the range (`sqrt_price <= sqrt_lower`): all
99///   value is in token A.
100/// * If current price is **above** the range (`sqrt_price >= sqrt_upper`): all
101///   value is in token B.
102/// * If current price is **inside** the range: both tokens are present.
103///
104/// `round_up` controls rounding direction (use `false` for estimates,
105/// `true` for minimum deposit calculations).
106pub 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
125/// Same as [`token_amounts_from_liquidity`] but accepts pre-computed
126/// sqrt prices instead of tick indices.
127pub 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        // Current price is below range: all token A
143        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        // Current price is above range: all token B
148        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        // Current price is inside range: both tokens
153        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// ---------------------------------------------------------------------------
161// Transfer fee type
162// ---------------------------------------------------------------------------
163
164/// Token-2022 transfer fee parameters.
165///
166/// This is a shared definition replacing `orca_whirlpools_core::TransferFee`
167/// so that downstream crates no longer need a direct dependency on that crate.
168#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
169pub struct TransferFee {
170    /// Fee rate in basis points (e.g. 100 = 1%).
171    pub fee_bps: u16,
172    /// Maximum fee in token lamports (caps the calculated fee).
173    pub max_fee: u64,
174}
175
176impl TransferFee {
177    /// Create a `TransferFee` with the given rate and no max-fee cap.
178    pub fn new(fee_bps: u16) -> Self {
179        Self { fee_bps, max_fee: u64::MAX }
180    }
181
182    /// Create a `TransferFee` with the given rate and max-fee cap.
183    pub fn new_with_max(fee_bps: u16, max_fee: u64) -> Self {
184        Self { fee_bps, max_fee }
185    }
186}
187
188// ---------------------------------------------------------------------------
189// Tick range utilities
190// ---------------------------------------------------------------------------
191
192/// Ordered tick range (lower <= upper).
193#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
194pub struct TickRange {
195    /// Lower tick index of the range.
196    pub tick_lower_index: i32,
197    /// Upper tick index of the range.
198    pub tick_upper_index: i32,
199}
200
201/// Order two tick indexes so that lower <= upper.
202pub 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
210// ---------------------------------------------------------------------------
211// Quote types
212// ---------------------------------------------------------------------------
213
214/// Quote error type (mirrors `orca_whirlpools_core::CoreError`).
215pub type QuoteError = &'static str;
216
217/// An arithmetic operation overflowed or underflowed.
218pub const ARITHMETIC_OVERFLOW: QuoteError = "Arithmetic over- or underflow";
219/// The computed amount exceeds `u64::MAX`.
220pub const AMOUNT_EXCEEDS_MAX_U64: QuoteError = "Amount exceeds max u64";
221/// The transfer fee parameters are invalid.
222pub const INVALID_TRANSFER_FEE: QuoteError = "Invalid transfer fee";
223/// The slippage tolerance is out of the valid range.
224pub const INVALID_SLIPPAGE_TOLERANCE: QuoteError = "Invalid slippage tolerance";
225
226const BPS_DENOMINATOR: u16 = 10000;
227
228/// Quote for an increase-liquidity operation.
229#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
230pub struct IncreaseLiquidityQuote {
231    /// Net liquidity to add.
232    pub liquidity_delta: u128,
233    /// Estimated token A amount required.
234    pub token_est_a: u64,
235    /// Estimated token B amount required.
236    pub token_est_b: u64,
237    /// Maximum token A amount (after slippage).
238    pub token_max_a: u64,
239    /// Maximum token B amount (after slippage).
240    pub token_max_b: u64,
241}
242
243/// Quote for a decrease-liquidity operation.
244#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
245pub struct DecreaseLiquidityQuote {
246    /// Net liquidity to remove.
247    pub liquidity_delta: u128,
248    /// Estimated token A amount returned.
249    pub token_est_a: u64,
250    /// Estimated token B amount returned.
251    pub token_est_b: u64,
252    /// Minimum token A amount (after slippage).
253    pub token_min_a: u64,
254    /// Minimum token B amount (after slippage).
255    pub token_min_b: u64,
256}
257
258// ---------------------------------------------------------------------------
259// Position status
260// ---------------------------------------------------------------------------
261
262#[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
290// ---------------------------------------------------------------------------
291// Transfer-fee helpers
292// ---------------------------------------------------------------------------
293
294fn 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
334// ---------------------------------------------------------------------------
335// Slippage helpers
336// ---------------------------------------------------------------------------
337
338fn 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
370// ---------------------------------------------------------------------------
371// Token amount from liquidity (for quotes)
372// ---------------------------------------------------------------------------
373
374fn 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
453// ---------------------------------------------------------------------------
454// Token estimates from liquidity
455// ---------------------------------------------------------------------------
456
457fn 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
513// ---------------------------------------------------------------------------
514// Public quote functions
515// ---------------------------------------------------------------------------
516
517/// Calculate the quote for increasing liquidity given a raw liquidity delta.
518pub 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
563/// Calculate the quote for increasing liquidity given a token A amount.
564pub 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
608/// Calculate the quote for increasing liquidity given a token B amount.
609pub 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
653/// Calculate the quote for decreasing liquidity.
654pub 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
695/// Calculate the quote for decreasing liquidity given a token A amount.
696pub 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
741/// Calculate the quote for decreasing liquidity given a token B amount.
742pub 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        // Two prices close to 1.0 in Q64.64
796        let price_a = Q64; // 1.0
797        let price_b = Q64 * 2; // 2.0
798        let liquidity = 1_000_000u128;
799
800        let amount = get_amount_0_delta(price_a, price_b, liquidity, false).unwrap();
801        // amount0 = 1_000_000 * (2-1) / (1*2) = 500_000
802        assert_eq!(amount, 500_000);
803    }
804
805    #[test]
806    fn test_amount_1_delta_basic() {
807        let price_a = Q64; // 1.0
808        let price_b = Q64 * 2; // 2.0
809        let liquidity = 1_000_000u128;
810
811        let amount = get_amount_1_delta(price_a, price_b, liquidity, false).unwrap();
812        // amount1 = 1_000_000 * (2*Q64 - Q64) / Q64 = 1_000_000 * 1 = 1_000_000
813        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        // ceil >= floor always
837        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        // liquidity * diff > u128::MAX, but the final result fits in u64.
884        // liquidity = 2^64, sqrt_price_a = 2^64, sqrt_price_b = 2^65
885        // amount0 = 2^64 * (2^65 - 2^64) * 2^64 / (2^64 * 2^65)
886        //         = 2^64 * 2^64 * 2^64 / (2^64 * 2^65)
887        //         = 2^192 / 2^129
888        //         = 2^63
889        let liquidity = 1u128 << 64;
890        let sqrt_price_a = Q64; // 2^64
891        let sqrt_price_b = Q64 * 2; // 2^65
892
893        let amount = get_amount_0_delta(sqrt_price_a, sqrt_price_b, liquidity, false).unwrap();
894        assert_eq!(amount, 1u64 << 63);
895
896        // Round-up should also work
897        let amount_up = get_amount_0_delta(sqrt_price_a, sqrt_price_b, liquidity, true).unwrap();
898        assert_eq!(amount_up, 1u64 << 63); // exact division, no rounding needed
899    }
900
901    #[test]
902    fn test_large_liquidity_amount_1_no_false_overflow() {
903        // liquidity = 2^64, diff = 2^64
904        // amount1 = 2^64 * 2^64 / 2^64 = 2^64 => overflows u64
905        // But liquidity = 2^64, diff = 2^63 => amount1 = 2^63 => fits
906        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    // ---------------------------------------------------------------
915    // Tests for token_amounts_from_liquidity
916    // ---------------------------------------------------------------
917
918    #[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        // Current price at tick 0, range [-100, 100]
928        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        // Both tokens should be non-zero when price is in range
932        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        // Current price at tick -200, range [-100, 100]
939        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        // All in token A when price is below range
943        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        // Current price at tick 200, range [-100, 100]
950        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        // All in token B when price is above range
954        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        // Price exactly at lower tick => all token A
973        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        // Price exactly at upper tick => all token B
983        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    // ---- Deterministic RNG fuzz tests ----
1008
1009    #[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            // Use moderate liquidity to avoid overflow on result
1019            let liquidity: u128 = rng.random_range(1..=1_000_000_000_000u128);
1020            let round_up = rng.random_range(0u8..=1) == 1;
1021            // Should not panic — may return Ok or Err(Overflow)
1022            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            // Should not panic
1095            let _ = token_amounts_from_liquidity(
1096                liquidity, sqrt_price, tick_lower, tick_upper, round_up,
1097            );
1098        }
1099    }
1100}