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