defituna_core/quote/
tuna_lp_position.rs

1#![allow(clippy::collapsible_else_if)]
2#![allow(clippy::too_many_arguments)]
3
4use crate::{calculate_tuna_protocol_fee, sqrt_price_x64_to_price_x64, COMPUTED_AMOUNT, HUNDRED_PERCENT, INVALID_ARGUMENTS};
5use fixed::types::U64F64;
6use fusionamm_core::{
7    position_ratio_x64, tick_index_to_sqrt_price, try_apply_swap_fee, try_get_amount_a_from_liquidity, try_get_amount_b_from_liquidity,
8    try_get_amount_delta_a, try_get_amount_delta_b, try_get_amounts_from_liquidity, try_get_liquidity_from_amount_a, try_get_liquidity_from_amount_b,
9    try_get_liquidity_from_amounts, CoreError, ARITHMETIC_OVERFLOW, Q64_RESOLUTION,
10};
11
12#[cfg(feature = "wasm")]
13use fusionamm_macros::wasm_expose;
14
15#[derive(Debug, Copy, Clone, PartialEq)]
16#[cfg_attr(feature = "wasm", wasm_expose)]
17pub struct LiquidationPrices {
18    pub lower: f64,
19    pub upper: f64,
20}
21
22///
23/// Calculates the liquidation prices inside the position's range.
24///
25/// t - liquidation threshold
26/// Dx - debt x
27/// Dy - debt y
28/// Lx - leftovers x
29/// Ly - leftovers y
30/// √P = x - current price
31/// √Pu = u - upper sqrt price
32/// √Pl = l - lower sqrt price
33/// L - liquidity
34///
35/// t = (Dx⋅P + Dy) / (Δx⋅P + Lx⋅P + Δy + Ly), where
36///   Δx = L⋅(1/x - 1/u)
37///   Δy = L⋅(x - l)
38///
39/// x1,2 = ...
40///
41/// # Parameters
42/// - `lower_sqrt_price`: The lower square root price boundary.
43/// - `upper_sqrt_price`: The upper square root price boundary.
44/// - `liquidity`: The liquidity provided by the user.
45/// - `leftovers_a`: The amount of leftovers A in the existing position.
46/// - `leftovers_b`: The amount of leftovers B in the existing position*
47/// - `debt_a`: The amount of tokens A borrowed.
48/// - `debt_b`: The amount of tokens B borrowed.
49/// - `liquidation_threshold`: The liquidation threshold of the liquidator.
50///
51/// # Returns
52/// - `LiquidationPrices`: An object containing lower/upper liquidation prices.
53fn compute_liquidation_prices_inside(
54    lower_sqrt_price: u128,
55    upper_sqrt_price: u128,
56    liquidity: u128,
57    leftovers_a: u64,
58    leftovers_b: u64,
59    debt_a: u64,
60    debt_b: u64,
61    liquidation_threshold: u32,
62) -> Result<LiquidationPrices, CoreError> {
63    let liquidation_threshold_f = liquidation_threshold as f64 / HUNDRED_PERCENT as f64;
64    let liquidity_f = liquidity as f64;
65    let lower_sqrt_price_f = lower_sqrt_price as f64 / Q64_RESOLUTION;
66    let upper_sqrt_price_f = upper_sqrt_price as f64 / Q64_RESOLUTION;
67
68    let a = debt_a as f64 + liquidation_threshold_f * (liquidity_f / upper_sqrt_price_f - leftovers_a as f64);
69    let b = -2.0 * liquidation_threshold_f * liquidity_f;
70    let c = debt_b as f64 + liquidation_threshold_f * (liquidity_f * lower_sqrt_price_f - leftovers_b as f64);
71    let d = b * b - 4.0 * a * c;
72
73    let mut lower_liquidation_sqrt_price = 0.0;
74    let mut upper_liquidation_sqrt_price = 0.0;
75
76    if d >= 0.0 {
77        lower_liquidation_sqrt_price = (-b - d.sqrt()) / (2.0 * a);
78        upper_liquidation_sqrt_price = (-b + d.sqrt()) / (2.0 * a);
79        if lower_liquidation_sqrt_price < 0.0 || lower_liquidation_sqrt_price < lower_sqrt_price_f {
80            lower_liquidation_sqrt_price = 0.0;
81        }
82        if upper_liquidation_sqrt_price < 0.0 || upper_liquidation_sqrt_price > upper_sqrt_price_f {
83            upper_liquidation_sqrt_price = 0.0;
84        }
85    }
86
87    Ok(LiquidationPrices {
88        lower: lower_liquidation_sqrt_price * lower_liquidation_sqrt_price,
89        upper: upper_liquidation_sqrt_price * upper_liquidation_sqrt_price,
90    })
91}
92
93/// Calculates a liquidation price for the outside range (above OR under).
94///
95/// liquidation_threshold = total_debt / (total_balance + leftovers)
96///
97/// t - liquidation threshold
98/// P - liquidation price
99/// Dx - debt x
100/// Dy - debt y
101/// Lx - leftovers x
102/// Ly - leftovers y
103/// X - amount of x tokens
104/// Y - amount of y tokens
105///
106/// Lower liquidation price:
107/// t = (Dx + Dy / P) / (X + Lx + Ly/P)  =>  P = (Dy - t⋅Ly) / (t⋅(X+Lx) - Dx)
108///
109/// Upper liquidation price:
110/// t = (Dx⋅P + Dy) / (Y + Lx⋅P + Ly)  =>  P = (t⋅(Y + Ly) - Dy) / (Dx - t⋅Lx)
111///
112/// # Parameters
113/// - `amount_a`: The amount of tokens A at the boundary price.
114/// - `amount_b`: The amount of tokens B at the boundary price.
115/// - `leftovers_a`: The amount of leftovers A.
116/// - `leftovers_b`: The amount of leftovers B.
117/// - `debt_a`: The amount of tokens A borrowed.
118/// - `debt_b`: The amount of tokens B borrowed.
119/// - `liquidation_threshold`: - The liquidation threshold of the liquidator.
120///
121/// # Returns
122/// The calculated liquidation price.
123fn calculate_liquidation_outside(
124    amount_a: u64,
125    amount_b: u64,
126    leftovers_a: u64,
127    leftovers_b: u64,
128    debt_a: u64,
129    debt_b: u64,
130    liquidation_threshold: u32,
131) -> Result<f64, CoreError> {
132    let liquidation_threshold_f = liquidation_threshold as f64 / HUNDRED_PERCENT as f64;
133
134    if amount_a == 0 && amount_b == 0 {
135        Ok(0.0)
136    } else if amount_a > 0 && amount_b == 0 {
137        // Lower liquidation price
138        let numerator = debt_b as f64 - liquidation_threshold_f * leftovers_b as f64;
139        let denominator = liquidation_threshold_f * (amount_a + leftovers_a) as f64 - debt_a as f64;
140        Ok(numerator / denominator)
141    } else if amount_a == 0 && amount_b > 0 {
142        // Upper liquidation price
143        let numerator = liquidation_threshold_f * (amount_b + leftovers_b) as f64 - debt_b as f64;
144        let denominator = debt_a as f64 - liquidation_threshold_f * leftovers_a as f64;
145        if denominator == 0.0 {
146            return Ok(0.0);
147        }
148        Ok(numerator / denominator)
149    } else {
150        Err(INVALID_ARGUMENTS)
151    }
152}
153
154/// Calculates the liquidation prices outside the position's range (above AND under).
155///
156/// # Parameters
157/// - `lower_sqrt_price`: The lower square root price boundary.
158/// - `upper_sqrt_price`: The upper square root price boundary.
159/// - `liquidity`: The liquidity provided by the user.
160/// - `leftovers_a`: The amount of leftovers A.
161/// - `leftovers_b`: The amount of leftovers B.
162/// - `debt_a`: The amount of tokens A borrowed.
163/// - `debt_b`: The amount of tokens B borrowed.
164/// - `liquidation_threshold`: The liquidation threshold of the liquidator.
165///
166/// # Returns
167/// - `LiquidationPrices`: An object containing lower/upper liquidation prices.
168fn compute_liquidation_prices_outside(
169    lower_sqrt_price: u128,
170    upper_sqrt_price: u128,
171    liquidity: u128,
172    leftovers_a: u64,
173    leftovers_b: u64,
174    debt_a: u64,
175    debt_b: u64,
176    liquidation_threshold: u32,
177) -> Result<LiquidationPrices, CoreError> {
178    let amount_a = try_get_amount_delta_a(lower_sqrt_price.into(), upper_sqrt_price.into(), liquidity.into(), false)?;
179    let amount_b = try_get_amount_delta_b(lower_sqrt_price.into(), upper_sqrt_price.into(), liquidity.into(), false)?;
180
181    let mut liquidation_price_for_a = calculate_liquidation_outside(amount_a, 0, leftovers_a, leftovers_b, debt_a, debt_b, liquidation_threshold)?;
182    let mut liquidation_price_for_b = calculate_liquidation_outside(0, amount_b, leftovers_a, leftovers_b, debt_a, debt_b, liquidation_threshold)?;
183
184    if liquidation_price_for_a < 0.0 || liquidation_price_for_a > (lower_sqrt_price as f64 / Q64_RESOLUTION).powf(2.0) {
185        liquidation_price_for_a = 0.0;
186    }
187    if liquidation_price_for_b < 0.0 || liquidation_price_for_b < (upper_sqrt_price as f64 / Q64_RESOLUTION).powf(2.0) {
188        liquidation_price_for_b = 0.0;
189    }
190
191    Ok(LiquidationPrices {
192        lower: liquidation_price_for_a,
193        upper: liquidation_price_for_b,
194    })
195}
196
197/// Computes the liquidation prices for an existing position.
198///
199/// # Parameters
200/// - `tick_lower_index`: The lower tick index of the position.
201/// - `tick_upper_index`: The upper tick index of the position.
202/// - `leftovers_a`: The amount of leftovers A in the position.
203/// - `leftovers_a`: The amount of leftovers B in the position.
204/// - `liquidity`: Liquidity of the position.
205/// - `debt_a`: The amount of tokens A borrowed.
206/// - `debt_b`: The amount of tokens B borrowed.
207/// - `liquidation_threshold`: The liquidation threshold of the market.
208///
209/// # Returns
210/// - `LiquidationPrices`: An object containing lower/upper liquidation prices.
211#[cfg_attr(feature = "wasm", wasm_expose)]
212pub fn get_lp_position_liquidation_prices(
213    tick_lower_index: i32,
214    tick_upper_index: i32,
215    liquidity: u128,
216    leftovers_a: u64,
217    leftovers_b: u64,
218    debt_a: u64,
219    debt_b: u64,
220    liquidation_threshold: u32,
221) -> Result<LiquidationPrices, CoreError> {
222    if tick_lower_index >= tick_upper_index {
223        return Err("Incorrect position tick index order: the lower tick must be less then the upper tick.");
224    }
225
226    if liquidation_threshold >= HUNDRED_PERCENT {
227        return Err("Incorrect liquidation_threshold value.");
228    }
229
230    let lower_sqrt_price = tick_index_to_sqrt_price(tick_lower_index);
231    let upper_sqrt_price = tick_index_to_sqrt_price(tick_upper_index);
232
233    let liquidation_price_inside = compute_liquidation_prices_inside(
234        lower_sqrt_price,
235        upper_sqrt_price,
236        liquidity,
237        leftovers_a,
238        leftovers_b,
239        debt_a,
240        debt_b,
241        liquidation_threshold,
242    )?;
243
244    let liquidation_price_outside = compute_liquidation_prices_outside(
245        lower_sqrt_price,
246        upper_sqrt_price,
247        liquidity,
248        leftovers_a,
249        leftovers_b,
250        debt_a,
251        debt_b,
252        liquidation_threshold,
253    )?;
254
255    let lower_liquidation_price = if liquidation_price_inside.lower > 0.0 {
256        liquidation_price_inside.lower
257    } else {
258        liquidation_price_outside.lower
259    };
260
261    let upper_liquidation_price = if liquidation_price_inside.upper > 0.0 {
262        liquidation_price_inside.upper
263    } else {
264        liquidation_price_outside.upper
265    };
266
267    Ok(LiquidationPrices {
268        lower: lower_liquidation_price,
269        upper: upper_liquidation_price,
270    })
271}
272
273#[derive(Debug, Copy, Clone, PartialEq)]
274#[cfg_attr(feature = "wasm", wasm_expose)]
275pub struct IncreaseLpPositionQuoteArgs {
276    /// Collateral in token A or COMPUTED_AMOUNT.
277    pub collateral_a: u64,
278    /// Collateral in token B or COMPUTED_AMOUNT.
279    pub collateral_b: u64,
280    /// Amount to borrow in token A. Must be set to COMPUTED_AMOUNT if collateral_a is COMPUTED_AMOUNT.
281    pub borrow_a: u64,
282    /// Amount to borrow in token B. Must be set to COMPUTED_AMOUNT if collateral_b is COMPUTED_AMOUNT.
283    pub borrow_b: u64,
284    /// Protocol fee rate from a market account represented as hundredths of a basis point (0.01% = 100).
285    pub protocol_fee_rate: u16,
286    /// Protocol fee rate from a market account represented as hundredths of a basis point (0.01% = 100).
287    pub protocol_fee_rate_on_collateral: u16,
288    /// The swap fee rate of a pool denominated in 1e6.
289    pub swap_fee_rate: u16,
290    /// Current sqrt price.
291    pub sqrt_price: u128,
292    /// Position lower tick index.
293    pub tick_lower_index: i32,
294    /// Position upper tick index.
295    pub tick_upper_index: i32,
296    /// Maximum slippage of the position total amount represented as hundredths of a basis point (0.01% = 100).
297    pub max_amount_slippage: u32,
298    /// The liquidation threshold of the market.
299    pub liquidation_threshold: u32,
300}
301
302#[derive(Debug, Copy, Clone, PartialEq)]
303#[cfg_attr(feature = "wasm", wasm_expose)]
304pub struct IncreaseLpPositionQuoteResult {
305    pub collateral_a: u64,
306    pub collateral_b: u64,
307    pub max_collateral_a: u64,
308    pub max_collateral_b: u64,
309    pub borrow_a: u64,
310    pub borrow_b: u64,
311    pub total_a: u64,
312    pub total_b: u64,
313    pub min_total_a: u64,
314    pub min_total_b: u64,
315    pub swap_input: u64,
316    pub swap_output: u64,
317    pub swap_a_to_b: bool,
318    pub protocol_fee_a: u64,
319    pub protocol_fee_b: u64,
320    pub liquidity: u128,
321    pub leverage: f64,
322    pub liquidation_lower_price: f64,
323    pub liquidation_upper_price: f64,
324}
325
326#[cfg_attr(feature = "wasm", wasm_expose)]
327pub fn get_increase_lp_position_quote(args: IncreaseLpPositionQuoteArgs) -> Result<IncreaseLpPositionQuoteResult, CoreError> {
328    let mut collateral_a = args.collateral_a;
329    let mut collateral_b = args.collateral_b;
330    let mut borrow_a = args.borrow_a;
331    let mut borrow_b = args.borrow_b;
332    let sqrt_price = args.sqrt_price;
333
334    if args.tick_lower_index >= args.tick_upper_index {
335        return Err("Incorrect position tick index order: the lower tick must be less than the upper tick.");
336    }
337
338    if args.max_amount_slippage > HUNDRED_PERCENT {
339        return Err("max_amount_slippage must be in range [0; HUNDRED_PERCENT]");
340    }
341
342    if collateral_a == COMPUTED_AMOUNT && collateral_b == COMPUTED_AMOUNT {
343        return Err("Both collateral amounts can't be set to COMPUTED_AMOUNT");
344    }
345
346    let max_amount_slippage = args.max_amount_slippage;
347
348    let mut max_collateral_a = collateral_a;
349    let mut max_collateral_b = collateral_b;
350
351    let lower_sqrt_price = tick_index_to_sqrt_price(args.tick_lower_index);
352    let upper_sqrt_price = tick_index_to_sqrt_price(args.tick_upper_index);
353
354    if collateral_a == COMPUTED_AMOUNT {
355        if sqrt_price <= lower_sqrt_price {
356            return Err("sqrtPrice must be greater than lower_sqrt_price if collateral A is computed.");
357        } else if sqrt_price < upper_sqrt_price {
358            let liquidity = try_get_liquidity_from_amount_b(collateral_b + borrow_b, lower_sqrt_price, sqrt_price)?;
359            let amount_a = try_get_amount_a_from_liquidity(liquidity, sqrt_price, upper_sqrt_price, false)?;
360            collateral_a = (amount_a * collateral_b) / (collateral_b + borrow_b);
361            borrow_a = amount_a - collateral_a;
362            max_collateral_a = collateral_a + ((collateral_a as u128 * max_amount_slippage as u128) / HUNDRED_PERCENT as u128) as u64;
363        } else {
364            collateral_a = 0;
365            max_collateral_a = 0;
366            borrow_a = 0;
367        }
368    } else if collateral_b == COMPUTED_AMOUNT {
369        if sqrt_price <= lower_sqrt_price {
370            collateral_b = 0;
371            max_collateral_b = 0;
372            borrow_b = 0;
373        } else if sqrt_price < upper_sqrt_price {
374            let liquidity = try_get_liquidity_from_amount_a(collateral_a + borrow_a, sqrt_price, upper_sqrt_price)?;
375            let amount_b = try_get_amount_b_from_liquidity(liquidity, lower_sqrt_price, sqrt_price, false)?;
376            collateral_b = (amount_b * collateral_a) / (collateral_a + borrow_a);
377            borrow_b = amount_b - collateral_b;
378            max_collateral_b = collateral_b + ((collateral_b as u128 * max_amount_slippage as u128) / HUNDRED_PERCENT as u128) as u64;
379        } else {
380            return Err("sqrtPrice must be less than upper_sqrt_price if collateral B is computed.");
381        }
382    }
383
384    let protocol_fee_a = calculate_tuna_protocol_fee(collateral_a, borrow_a, args.protocol_fee_rate_on_collateral, args.protocol_fee_rate);
385    let provided_a = collateral_a + borrow_a - protocol_fee_a;
386
387    let protocol_fee_b = calculate_tuna_protocol_fee(collateral_b, borrow_b, args.protocol_fee_rate_on_collateral, args.protocol_fee_rate);
388    let provided_b = collateral_b + borrow_b - protocol_fee_b;
389
390    let mut swap_input = 0;
391    let mut swap_output = 0;
392    let mut swap_a_to_b = false;
393    let mut total_a = provided_a;
394    let mut total_b = provided_b;
395
396    if args.collateral_a != COMPUTED_AMOUNT && args.collateral_b != COMPUTED_AMOUNT {
397        let position_ratio = position_ratio_x64(sqrt_price.into(), args.tick_lower_index, args.tick_upper_index);
398        let ratio_a = position_ratio.ratio_a as f64 / Q64_RESOLUTION;
399        let ratio_b = position_ratio.ratio_b as f64 / Q64_RESOLUTION;
400
401        let price = (sqrt_price as f64 / Q64_RESOLUTION).powf(2.0);
402
403        // Estimated total position size.
404        let mut total = (provided_a as f64 * price + provided_b as f64) as u64;
405        total_a = (total as f64 * ratio_a / price) as u64;
406        total_b = (total as f64 * ratio_b) as u64;
407
408        let mut fee_a = 0;
409        let mut fee_b = 0;
410
411        if total_a < provided_a {
412            swap_input = provided_a - total_a;
413            fee_a = swap_input - try_apply_swap_fee(swap_input, args.swap_fee_rate)?;
414            swap_output = ((swap_input - fee_a) as f64 * price) as u64;
415            swap_a_to_b = true;
416        } else if total_b < provided_b {
417            swap_input = provided_b - total_b;
418            fee_b = swap_input - try_apply_swap_fee(swap_input, args.swap_fee_rate)?;
419            swap_output = ((swap_input - fee_b) as f64 / price) as u64;
420            swap_a_to_b = false;
421        }
422
423        // Recompute totals with applied swap fee.
424        total = ((provided_a - fee_a) as f64 * price) as u64 + provided_b - fee_b;
425        total_a = ((total as f64 * ratio_a) / price) as u64;
426        total_b = (total as f64 * ratio_b) as u64;
427    }
428
429    let min_total_a = total_a - ((total_a as u128 * max_amount_slippage as u128) / HUNDRED_PERCENT as u128) as u64;
430    let min_total_b = total_b - ((total_b as u128 * max_amount_slippage as u128) / HUNDRED_PERCENT as u128) as u64;
431
432    let liquidity = try_get_liquidity_from_amounts(sqrt_price, lower_sqrt_price, upper_sqrt_price, total_a, total_b)?;
433    let liquidation_prices = get_lp_position_liquidation_prices(
434        args.tick_lower_index,
435        args.tick_upper_index,
436        liquidity,
437        0,
438        0,
439        borrow_a,
440        borrow_b,
441        args.liquidation_threshold,
442    )?;
443
444    let leverage = compute_leverage(total_a, total_b, borrow_a, borrow_b, sqrt_price)?;
445
446    Ok(IncreaseLpPositionQuoteResult {
447        collateral_a,
448        collateral_b,
449        max_collateral_a,
450        max_collateral_b,
451        borrow_a,
452        borrow_b,
453        total_a,
454        total_b,
455        min_total_a,
456        min_total_b,
457        swap_input,
458        swap_output,
459        swap_a_to_b,
460        protocol_fee_a,
461        protocol_fee_b,
462        liquidity,
463        leverage,
464        liquidation_lower_price: liquidation_prices.lower,
465        liquidation_upper_price: liquidation_prices.upper,
466    })
467}
468
469#[derive(Debug, Copy, Clone, PartialEq)]
470#[cfg_attr(feature = "wasm", wasm_expose)]
471pub struct RepayLpPositionDebtQuoteArgs {
472    /** The position liquidity */
473    pub liquidity: u128,
474    /** The current debt of a position in token A. */
475    pub debt_a: u64,
476    /** The current debt of a position in token B. */
477    pub debt_b: u64,
478    /** The leftovers of a position in token A. */
479    pub leftovers_a: u64,
480    /** The leftovers of a position in token B. */
481    pub leftovers_b: u64,
482    /** Position lower tick index. */
483    pub tick_lower_index: i32,
484    /** Position upper tick index. */
485    pub tick_upper_index: i32,
486    /** The amount of token A to repay. */
487    pub repay_a: u64,
488    /** The amount of token B to repay. */
489    pub repay_b: u64,
490    /** Current sqrt price. */
491    pub sqrt_price: u128,
492    /** The liquidation threshold of the market. */
493    pub liquidation_threshold: u32,
494}
495
496#[derive(Debug, Copy, Clone, PartialEq)]
497#[cfg_attr(feature = "wasm", wasm_expose)]
498pub struct RepayLpPositionDebtQuoteResult {
499    pub debt_a: u64,
500    pub debt_b: u64,
501    pub leverage: f64,
502    pub liquidation_lower_price: f64,
503    pub liquidation_upper_price: f64,
504}
505
506#[cfg_attr(feature = "wasm", wasm_expose)]
507pub fn get_repay_lp_position_debt_quote(args: RepayLpPositionDebtQuoteArgs) -> Result<RepayLpPositionDebtQuoteResult, CoreError> {
508    let mut debt_a = args.debt_a;
509    let mut debt_b = args.debt_b;
510    let repay_a = args.repay_a;
511    let repay_b = args.repay_b;
512
513    if args.liquidity == 0 {
514        return Err("Position liquidity can't be zero.");
515    }
516
517    if debt_a < repay_a {
518        return Err("Position debt A is less than the repaid amount.");
519    }
520
521    if debt_b < repay_b {
522        return Err("Position debt b is less than the repaid amount.");
523    }
524
525    debt_a -= repay_a;
526    debt_b -= repay_b;
527
528    let liquidation_prices = get_lp_position_liquidation_prices(
529        args.tick_lower_index,
530        args.tick_upper_index,
531        args.liquidity,
532        args.leftovers_a,
533        args.leftovers_b,
534        debt_a,
535        debt_b,
536        args.liquidation_threshold,
537    )?;
538
539    let total = try_get_amounts_from_liquidity(args.liquidity, args.sqrt_price, args.tick_lower_index, args.tick_upper_index, false)?;
540    let leverage = compute_leverage(total.a + args.leftovers_a, total.b + args.leftovers_b, debt_a, debt_b, args.sqrt_price)?;
541
542    Ok(RepayLpPositionDebtQuoteResult {
543        debt_a,
544        debt_b,
545        leverage,
546        liquidation_lower_price: liquidation_prices.lower,
547        liquidation_upper_price: liquidation_prices.upper,
548    })
549}
550
551#[cfg_attr(feature = "wasm", wasm_expose)]
552pub fn compute_leverage(total_a: u64, total_b: u64, debt_a: u64, debt_b: u64, sqrt_price: u128) -> Result<f64, CoreError> {
553    let price = sqrt_price_x64_to_price_x64(sqrt_price)?;
554
555    let total = U64F64::from(total_a)
556        .checked_mul(price)
557        .ok_or(ARITHMETIC_OVERFLOW)?
558        .to_num::<u64>()
559        .checked_add(total_b)
560        .ok_or(ARITHMETIC_OVERFLOW)?;
561
562    let debt = U64F64::from(debt_a)
563        .checked_mul(price)
564        .ok_or(ARITHMETIC_OVERFLOW)?
565        .to_num::<u64>()
566        .checked_add(debt_b)
567        .ok_or(ARITHMETIC_OVERFLOW)?;
568
569    // We assume that the leverage of an empty position is always 1.0x.
570    if total == 0 {
571        return Ok(1.0);
572    }
573
574    if debt >= total {
575        return Err("The debt is greater than the total size");
576    }
577
578    let leverage = total as f64 / (total - debt) as f64;
579    Ok(leverage)
580}
581
582#[cfg(all(test, not(feature = "wasm")))]
583mod tests {
584    use crate::{
585        get_increase_lp_position_quote, get_lp_position_liquidation_prices, get_repay_lp_position_debt_quote, IncreaseLpPositionQuoteArgs,
586        IncreaseLpPositionQuoteResult, LiquidationPrices, RepayLpPositionDebtQuoteArgs, COMPUTED_AMOUNT, HUNDRED_PERCENT,
587    };
588    use fusionamm_core::{price_to_sqrt_price, price_to_tick_index, tick_index_to_sqrt_price, try_get_liquidity_from_amount_b};
589    use once_cell::sync::Lazy;
590
591    pub static TICK_LOWER_INDEX: Lazy<i32> = Lazy::new(|| price_to_tick_index(180.736, 6, 6));
592    pub static TICK_UPPER_INDEX: Lazy<i32> = Lazy::new(|| price_to_tick_index(225.66, 6, 6));
593    pub static SQRT_PRICE: Lazy<u128> = Lazy::new(|| price_to_sqrt_price(213.41, 6, 6));
594    pub static LIQUIDITY: Lazy<u128> =
595        Lazy::new(|| try_get_liquidity_from_amount_b(10000_000_000, tick_index_to_sqrt_price(*TICK_LOWER_INDEX), *SQRT_PRICE).unwrap());
596
597    #[test]
598    fn test_liquidation_price_outside_range_lower() {
599        assert_eq!(
600            get_lp_position_liquidation_prices(
601                *TICK_LOWER_INDEX,
602                *TICK_UPPER_INDEX,
603                *LIQUIDITY,
604                0,                          // leftovers_a
605                0,                          // leftovers_b
606                0,                          // debt_a
607                9807_000_000,               // debt_b
608                HUNDRED_PERCENT * 83 / 100  // liquidation_threshold
609            ),
610            Ok(LiquidationPrices {
611                lower: 176.12815046153585,
612                upper: 0.0
613            })
614        );
615    }
616
617    #[test]
618    fn test_liquidation_price_outside_range_upper() {
619        assert_eq!(
620            get_lp_position_liquidation_prices(
621                *TICK_LOWER_INDEX,
622                *TICK_UPPER_INDEX,
623                *LIQUIDITY,
624                0,                          // leftovers_a
625                0,                          // leftovers_b
626                20_000_000,                 // debt_a
627                0,                          // debt_b
628                HUNDRED_PERCENT * 83 / 100  // liquidation_threshold
629            ),
630            Ok(LiquidationPrices {
631                lower: 0.0,
632                upper: 562.219410388
633            })
634        );
635    }
636
637    #[test]
638    fn test_liquidation_price_outside_range_lower_and_upper() {
639        assert_eq!(
640            get_lp_position_liquidation_prices(
641                *TICK_LOWER_INDEX,
642                *TICK_UPPER_INDEX,
643                *LIQUIDITY,
644                0,                          // leftovers_a
645                0,                          // leftovers_b
646                10_000_000,                 // debt_a
647                5000_000_000,               // debt_b
648                HUNDRED_PERCENT * 83 / 100  // liquidation_threshold
649            ),
650            Ok(LiquidationPrices {
651                lower: 109.45458168998225,
652                upper: 624.4388207760001
653            })
654        );
655    }
656
657    #[test]
658    fn test_liquidation_price_inside_range_lower() {
659        assert_eq!(
660            get_lp_position_liquidation_prices(
661                *TICK_LOWER_INDEX,
662                *TICK_UPPER_INDEX,
663                *LIQUIDITY,
664                0,                          // leftovers_a
665                0,                          // leftovers_b
666                0,                          // debt_a
667                11000_000_000,              // debt_b
668                HUNDRED_PERCENT * 83 / 100  // liquidation_threshold
669            ),
670            Ok(LiquidationPrices {
671                lower: 204.60489065334323,
672                upper: 0.0
673            })
674        );
675    }
676
677    #[test]
678    fn test_liquidation_price_inside_range_upper() {
679        assert_eq!(
680            get_lp_position_liquidation_prices(
681                *TICK_LOWER_INDEX,
682                *TICK_UPPER_INDEX,
683                *LIQUIDITY,
684                0,                          // leftovers_a
685                0,                          // leftovers_b
686                51_000_000,                 // debt_a
687                0,                          // debt_b
688                HUNDRED_PERCENT * 83 / 100  // liquidation_threshold
689            ),
690            Ok(LiquidationPrices {
691                lower: 0.0,
692                upper: 220.16318077637644
693            })
694        );
695    }
696
697    #[test]
698    fn test_liquidation_price_inside_range_lower_and_upper() {
699        assert_eq!(
700            get_lp_position_liquidation_prices(
701                *TICK_LOWER_INDEX,
702                *TICK_UPPER_INDEX,
703                *LIQUIDITY,
704                0,                          // leftovers_a
705                0,                          // leftovers_b
706                11_500_000,                 // debt_a
707                8700_000_000,               // debt_b
708                HUNDRED_PERCENT * 83 / 100  // liquidation_threshold
709            ),
710            Ok(LiquidationPrices {
711                lower: 210.75514596082337,
712                upper: 219.48595430071575
713            })
714        );
715    }
716
717    #[test]
718    fn test_lp_increase_quote_collateral_a_and_b_provided() {
719        assert_eq!(
720            get_increase_lp_position_quote(IncreaseLpPositionQuoteArgs {
721                collateral_a: 1000000,
722                collateral_b: 1000000,
723                borrow_a: 2000000,
724                borrow_b: 2000000,
725                tick_lower_index: price_to_tick_index(1.0, 1, 1),
726                sqrt_price: price_to_sqrt_price(2.0, 1, 1),
727                tick_upper_index: price_to_tick_index(4.0, 1, 1),
728                protocol_fee_rate: (HUNDRED_PERCENT / 100) as u16,
729                protocol_fee_rate_on_collateral: (HUNDRED_PERCENT / 100) as u16,
730                swap_fee_rate: 10000, // 1%
731                max_amount_slippage: HUNDRED_PERCENT / 10,
732                liquidation_threshold: HUNDRED_PERCENT * 83 / 100,
733            }),
734            Ok(IncreaseLpPositionQuoteResult {
735                collateral_a: 1000000,
736                collateral_b: 1000000,
737                max_collateral_a: 1000000,
738                max_collateral_b: 1000000,
739                borrow_a: 2000000,
740                borrow_b: 2000000,
741                total_a: 2223701,
742                total_b: 4447744,
743                min_total_a: 2001331,
744                min_total_b: 4002970,
745                swap_input: 742586,
746                swap_output: 1470320,
747                swap_a_to_b: true,
748                protocol_fee_a: 30000,
749                protocol_fee_b: 30000,
750                liquidity: 10737803,
751                leverage: 3.0724343435529677,
752                liquidation_lower_price: 0.8143170288470588,
753                liquidation_upper_price: 3.4020457909456225,
754            })
755        );
756    }
757
758    #[test]
759    fn test_lp_increase_quote_collateral_a_provided() {
760        assert_eq!(
761            get_increase_lp_position_quote(IncreaseLpPositionQuoteArgs {
762                collateral_a: 10000000,
763                collateral_b: 0,
764                borrow_a: 0,
765                borrow_b: 0,
766                tick_lower_index: price_to_tick_index(0.25, 6, 9),
767                sqrt_price: price_to_sqrt_price(0.5, 6, 9),
768                tick_upper_index: price_to_tick_index(1.0, 6, 9),
769                protocol_fee_rate: (HUNDRED_PERCENT / 100) as u16,
770                protocol_fee_rate_on_collateral: (HUNDRED_PERCENT / 100) as u16,
771                swap_fee_rate: 10000, // 1%
772                max_amount_slippage: HUNDRED_PERCENT / 10,
773                liquidation_threshold: HUNDRED_PERCENT * 83 / 100,
774            }),
775            Ok(IncreaseLpPositionQuoteResult {
776                collateral_a: 10000000,
777                collateral_b: 0,
778                max_collateral_a: 10000000,
779                max_collateral_b: 0,
780                borrow_a: 0,
781                borrow_b: 0,
782                total_a: 4925137,
783                total_b: 2462680451,
784                min_total_a: 4432624,
785                min_total_b: 2216412406,
786                swap_input: 4950113,
787                swap_output: 2450305500,
788                swap_a_to_b: true,
789                protocol_fee_a: 100000,
790                protocol_fee_b: 0,
791                liquidity: 376005629,
792                leverage: 1.0,
793                liquidation_lower_price: 0.0,
794                liquidation_upper_price: 0.0,
795            })
796        );
797    }
798
799    #[test]
800    fn test_lp_increase_quote_collateral_a_provided_b_computed() {
801        assert_eq!(
802            get_increase_lp_position_quote(IncreaseLpPositionQuoteArgs {
803                collateral_a: 10000000,
804                collateral_b: COMPUTED_AMOUNT,
805                borrow_a: 2000000,
806                borrow_b: COMPUTED_AMOUNT,
807                tick_lower_index: price_to_tick_index(1.0, 1, 1),
808                sqrt_price: price_to_sqrt_price(3.0, 1, 1),
809                tick_upper_index: price_to_tick_index(4.0, 1, 1),
810                protocol_fee_rate: 0,
811                protocol_fee_rate_on_collateral: 0,
812                swap_fee_rate: 0,
813                max_amount_slippage: HUNDRED_PERCENT / 10,
814                liquidation_threshold: HUNDRED_PERCENT * 83 / 100,
815            }),
816            Ok(IncreaseLpPositionQuoteResult {
817                collateral_a: 10000000,
818                collateral_b: 94660495,
819                max_collateral_a: 10000000,
820                max_collateral_b: 104126544,
821                borrow_a: 2000000,
822                borrow_b: 18932100,
823                total_a: 12000000,
824                total_b: 113592595,
825                min_total_a: 10800000,
826                min_total_b: 102233336,
827                swap_input: 0,
828                swap_output: 0,
829                swap_a_to_b: false,
830                protocol_fee_a: 0,
831                protocol_fee_b: 0,
832                liquidity: 155170370,
833                leverage: 1.2,
834                liquidation_lower_price: 0.30342990360419136,
835                liquidation_upper_price: 54.925553349999994
836            })
837        );
838    }
839
840    #[test]
841    fn test_lp_increase_quote_collateral_a_computed_b_provided() {
842        assert_eq!(
843            get_increase_lp_position_quote(IncreaseLpPositionQuoteArgs {
844                collateral_a: COMPUTED_AMOUNT,
845                collateral_b: 1000000,
846                borrow_a: COMPUTED_AMOUNT,
847                borrow_b: 2000000,
848                tick_lower_index: price_to_tick_index(1.0, 1, 1),
849                sqrt_price: price_to_sqrt_price(3.0, 1, 1),
850                tick_upper_index: price_to_tick_index(4.0, 1, 1),
851                protocol_fee_rate: 0,
852                protocol_fee_rate_on_collateral: 0,
853                swap_fee_rate: 0,
854                max_amount_slippage: HUNDRED_PERCENT / 10,
855                liquidation_threshold: HUNDRED_PERCENT * 83 / 100,
856            }),
857            Ok(IncreaseLpPositionQuoteResult {
858                collateral_a: 105640,
859                collateral_b: 1000000,
860                max_collateral_a: 116204,
861                max_collateral_b: 1000000,
862                borrow_a: 211282,
863                borrow_b: 2000000,
864                total_a: 316922,
865                total_b: 3000000,
866                min_total_a: 285230,
867                min_total_b: 2700000,
868                swap_input: 0,
869                swap_output: 0,
870                swap_a_to_b: false,
871                protocol_fee_a: 0,
872                protocol_fee_b: 0,
873                liquidity: 4098075,
874                leverage: 3.000003796737843,
875                liquidation_lower_price: 1.4306915613018771,
876                liquidation_upper_price: 6.63182675287057
877            })
878        );
879    }
880
881    #[test]
882    fn test_lp_increase_quote_one_sided_collateral_a_provided_b_computed() {
883        assert_eq!(
884            get_increase_lp_position_quote(IncreaseLpPositionQuoteArgs {
885                collateral_a: 10000000,
886                collateral_b: COMPUTED_AMOUNT,
887                borrow_a: 2000000,
888                borrow_b: COMPUTED_AMOUNT,
889                tick_lower_index: price_to_tick_index(1.0, 1, 1),
890                sqrt_price: price_to_sqrt_price(0.5, 1, 1),
891                tick_upper_index: price_to_tick_index(4.0, 1, 1),
892                protocol_fee_rate: 0,
893                protocol_fee_rate_on_collateral: 0,
894                swap_fee_rate: 0,
895                max_amount_slippage: HUNDRED_PERCENT / 10,
896                liquidation_threshold: HUNDRED_PERCENT * 83 / 100,
897            }),
898            Ok(IncreaseLpPositionQuoteResult {
899                collateral_a: 10000000,
900                collateral_b: 0,
901                max_collateral_a: 10000000,
902                max_collateral_b: 0,
903                borrow_a: 2000000,
904                borrow_b: 0,
905                total_a: 12000000,
906                total_b: 0,
907                min_total_a: 10800000,
908                min_total_b: 0,
909                swap_input: 0,
910                swap_output: 0,
911                swap_a_to_b: false,
912                protocol_fee_a: 0,
913                protocol_fee_b: 0,
914                liquidity: 24000764,
915                leverage: 1.2,
916                liquidation_lower_price: 0.0,
917                liquidation_upper_price: 9.959682525
918            })
919        );
920    }
921
922    #[test]
923    fn test_lp_increase_quote_one_sided_collateral_a_computed_b_provided() {
924        assert_eq!(
925            get_increase_lp_position_quote(IncreaseLpPositionQuoteArgs {
926                collateral_a: COMPUTED_AMOUNT,
927                collateral_b: 1000000,
928                borrow_a: COMPUTED_AMOUNT,
929                borrow_b: 2000000,
930                tick_lower_index: price_to_tick_index(1.0, 1, 1),
931                sqrt_price: price_to_sqrt_price(5.0, 1, 1),
932                tick_upper_index: price_to_tick_index(4.0, 1, 1),
933                protocol_fee_rate: 0,
934                protocol_fee_rate_on_collateral: 0,
935                swap_fee_rate: 0,
936                max_amount_slippage: HUNDRED_PERCENT / 10,
937                liquidation_threshold: HUNDRED_PERCENT * 83 / 100,
938            }),
939            Ok(IncreaseLpPositionQuoteResult {
940                collateral_a: 0,
941                collateral_b: 1000000,
942                max_collateral_a: 0,
943                max_collateral_b: 1000000,
944                borrow_a: 0,
945                borrow_b: 2000000,
946                total_a: 0,
947                total_b: 3000000,
948                min_total_a: 0,
949                min_total_b: 2700000,
950                swap_input: 0,
951                swap_output: 0,
952                swap_a_to_b: false,
953                protocol_fee_a: 0,
954                protocol_fee_b: 0,
955                liquidity: 3000191,
956                leverage: 3.0,
957                liquidation_lower_price: 1.8840617719481452,
958                liquidation_upper_price: 0.0
959            })
960        );
961    }
962
963    #[test]
964    fn test_lp_increase_quote_verify_liquidation_prices() {
965        let quote = get_increase_lp_position_quote(IncreaseLpPositionQuoteArgs {
966            collateral_a: 0,
967            collateral_b: 1000_000_000,
968            borrow_a: 3_000_000,
969            borrow_b: 100_000_000,
970            tick_lower_index: price_to_tick_index(180.736, 6, 6),
971            sqrt_price: price_to_sqrt_price(213.41, 6, 6),
972            tick_upper_index: price_to_tick_index(225.66, 6, 6),
973            protocol_fee_rate: 500,
974            protocol_fee_rate_on_collateral: 500,
975            swap_fee_rate: 40,
976            max_amount_slippage: HUNDRED_PERCENT / 10,
977            liquidation_threshold: HUNDRED_PERCENT * 83 / 100,
978        })
979        .unwrap();
980
981        assert_eq!(quote.liquidation_lower_price, 23.805241869982023);
982        assert_eq!(quote.liquidation_upper_price, 451.38033819333333);
983    }
984
985    #[test]
986    fn test_repay_debt_quote() {
987        let quote = get_repay_lp_position_debt_quote(RepayLpPositionDebtQuoteArgs {
988            repay_a: 1_000_000,
989            repay_b: 30_000_000,
990            liquidity: 1109671058,
991            debt_a: 3_000_000,
992            debt_b: 100_000_000,
993            leftovers_a: 2,
994            leftovers_b: 15,
995            tick_lower_index: price_to_tick_index(180.736, 6, 6),
996            sqrt_price: price_to_sqrt_price(213.41, 6, 6),
997            tick_upper_index: price_to_tick_index(225.66, 6, 6),
998            liquidation_threshold: HUNDRED_PERCENT * 83 / 100,
999        })
1000        .unwrap();
1001
1002        assert_eq!(quote.debt_a, 2_000_000);
1003        assert_eq!(quote.debt_b, 70_000_000);
1004        assert_eq!(quote.liquidation_lower_price, 13.459576327110664);
1005        assert_eq!(quote.liquidation_upper_price, 692.0710879340029);
1006    }
1007}