defituna_core/quote/
tuna_lp_position.rs

1#![allow(clippy::collapsible_else_if)]
2#![allow(clippy::too_many_arguments)]
3
4use crate::{CoreError, HUNDRED_PERCENT, INVALID_ARGUMENTS};
5use fusionamm_core::{try_get_amount_delta_a, try_get_amount_delta_b, Q64_RESOLUTION, U128};
6
7#[cfg(feature = "wasm")]
8use fusionamm_macros::wasm_expose;
9
10#[derive(Debug, Copy, Clone, PartialEq)]
11#[cfg_attr(feature = "wasm", wasm_expose)]
12pub struct LiquidationPrices {
13    pub lower: f64,
14    pub upper: f64,
15}
16
17///
18/// Calculates the liquidation prices inside the position's range.
19///
20/// t - liquidation threshold
21/// Dx - debt x
22/// Dy - debt y
23/// Lx - leftovers x
24/// Ly - leftovers y
25/// √P = x - current price
26/// √Pu = u - upper sqrt price
27/// √Pl = l - lower sqrt price
28/// L - liquidity
29///
30/// t = (Dx⋅P + Dy) / (Δx⋅P + Lx⋅P + Δy + Ly), where
31///   Δx = L⋅(1/x - 1/u)
32///   Δy = L⋅(x - l)
33///
34/// x1,2 = ...
35///
36/// # Parameters
37/// - `lower_sqrt_price`: The lower square root price boundary.
38/// - `upper_sqrt_price`: The upper square root price boundary.
39/// - `liquidity`: The liquidity provided by the user.
40/// - `leftovers_a`: The amount of leftovers A in the existing position.
41/// - `leftovers_b`: The amount of leftovers B in the existing position*
42/// - `debt_a`: The amount of tokens A borrowed.
43/// - `debt_b`: The amount of tokens B borrowed.
44/// - `liquidation_threshold`: The liquidation threshold of the liquidator.
45///
46/// # Returns
47/// - `LiquidationPrices`: An object containing lower/upper liquidation prices.
48fn compute_liquidation_prices_inside(
49    lower_sqrt_price: u128,
50    upper_sqrt_price: u128,
51    liquidity: u128,
52    leftovers_a: u64,
53    leftovers_b: u64,
54    debt_a: u64,
55    debt_b: u64,
56    liquidation_threshold: u32,
57) -> Result<LiquidationPrices, CoreError> {
58    let liquidation_threshold_f = liquidation_threshold as f64 / HUNDRED_PERCENT as f64;
59    let liquidity_f = liquidity as f64;
60    let lower_sqrt_price_f = lower_sqrt_price as f64 / Q64_RESOLUTION;
61    let upper_sqrt_price_f = upper_sqrt_price as f64 / Q64_RESOLUTION;
62
63    let a = debt_a as f64 + liquidation_threshold_f * (liquidity_f / upper_sqrt_price_f - leftovers_a as f64);
64    let b = -2.0 * liquidation_threshold_f * liquidity_f;
65    let c = debt_b as f64 + liquidation_threshold_f * (liquidity_f * lower_sqrt_price_f - leftovers_b as f64);
66    let d = b * b - 4.0 * a * c;
67
68    let mut lower_liquidation_sqrt_price = 0.0;
69    let mut upper_liquidation_sqrt_price = 0.0;
70
71    if d >= 0.0 {
72        lower_liquidation_sqrt_price = (-b - d.sqrt()) / (2.0 * a);
73        upper_liquidation_sqrt_price = (-b + d.sqrt()) / (2.0 * a);
74        if lower_liquidation_sqrt_price < 0.0 || lower_liquidation_sqrt_price < lower_sqrt_price_f {
75            lower_liquidation_sqrt_price = 0.0;
76        }
77        if upper_liquidation_sqrt_price < 0.0 || upper_liquidation_sqrt_price > upper_sqrt_price_f {
78            upper_liquidation_sqrt_price = 0.0;
79        }
80    }
81
82    Ok(LiquidationPrices {
83        lower: lower_liquidation_sqrt_price * lower_liquidation_sqrt_price,
84        upper: upper_liquidation_sqrt_price * upper_liquidation_sqrt_price,
85    })
86}
87
88/// Calculates a liquidation price for the outside range (above OR under).
89///
90/// liquidation_threshold = total_debt / (total_balance + leftovers)
91///
92/// t - liquidation threshold
93/// P - liquidation price
94/// Dx - debt x
95/// Dy - debt y
96/// Lx - leftovers x
97/// Ly - leftovers y
98/// X - amount of x tokens
99/// Y - amount of y tokens
100///
101/// Lower liquidation price:
102/// t = (Dx + Dy / P) / (X + Lx + Ly/P)  =>  P = (Dy - t⋅Ly) / (t⋅(X+Lx) - Dx)
103///
104/// Upper liquidation price:
105/// t = (Dx⋅P + Dy) / (Y + Lx⋅P + Ly)  =>  P = (t⋅(Y + Ly) - Dy) / (Dx - t⋅Lx)
106///
107/// # Parameters
108/// - `amount_a`: The amount of tokens A at the boundary price.
109/// - `amount_b`: The amount of tokens B at the boundary price.
110/// - `leftovers_a`: The amount of leftovers A.
111/// - `leftovers_b`: The amount of leftovers B.
112/// - `debt_a`: The amount of tokens A borrowed.
113/// - `debt_b`: The amount of tokens B borrowed.
114/// - `liquidation_threshold`: - The liquidation threshold of the liquidator.
115///
116/// # Returns
117/// The calculated liquidation price.
118fn calculate_liquidation_outside(
119    amount_a: u64,
120    amount_b: u64,
121    leftovers_a: u64,
122    leftovers_b: u64,
123    debt_a: u64,
124    debt_b: u64,
125    liquidation_threshold: u32,
126) -> Result<f64, CoreError> {
127    let liquidation_threshold_f = liquidation_threshold as f64 / HUNDRED_PERCENT as f64;
128
129    if amount_a == 0 && amount_b == 0 {
130        Ok(0.0)
131    } else if amount_a > 0 && amount_b == 0 {
132        // Lower liquidation price
133        let numerator = debt_b as f64 - liquidation_threshold_f * leftovers_b as f64;
134        let denominator = liquidation_threshold_f * (amount_a + leftovers_a) as f64 - debt_a as f64;
135        Ok(numerator / denominator)
136    } else if amount_a == 0 && amount_b > 0 {
137        // Upper liquidation price
138        let numerator = liquidation_threshold_f * (amount_b + leftovers_b) as f64 - debt_b as f64;
139        let denominator = debt_a as f64 - liquidation_threshold_f * leftovers_a as f64;
140        if denominator == 0.0 {
141            return Ok(0.0);
142        }
143        Ok(numerator / denominator)
144    } else {
145        Err(INVALID_ARGUMENTS)
146    }
147}
148
149/// Calculates the liquidation prices outside the position's range (above AND under).
150///
151/// # Parameters
152/// - `lower_sqrt_price`: The lower square root price boundary.
153/// - `upper_sqrt_price`: The upper square root price boundary.
154/// - `liquidity`: The liquidity provided by the user.
155/// - `leftovers_a`: The amount of leftovers A.
156/// - `leftovers_b`: The amount of leftovers B.///
157/// - `debt_a`: The amount of tokens A borrowed.
158/// - `debt_b`: The amount of tokens B borrowed.
159/// - `liquidation_threshold`: The liquidation threshold of the liquidator.
160///
161/// # Returns
162/// - `LiquidationPrices`: An object containing lower/upper liquidation prices.
163fn compute_liquidation_prices_outside(
164    lower_sqrt_price: u128,
165    upper_sqrt_price: u128,
166    liquidity: u128,
167    leftovers_a: u64,
168    leftovers_b: u64,
169    debt_a: u64,
170    debt_b: u64,
171    liquidation_threshold: u32,
172) -> Result<LiquidationPrices, CoreError> {
173    let amount_a = try_get_amount_delta_a(lower_sqrt_price.into(), upper_sqrt_price.into(), liquidity.into(), false)?;
174    let amount_b = try_get_amount_delta_b(lower_sqrt_price.into(), upper_sqrt_price.into(), liquidity.into(), false)?;
175
176    let mut liquidation_price_for_a = calculate_liquidation_outside(amount_a, 0, leftovers_a, leftovers_b, debt_a, debt_b, liquidation_threshold)?;
177    let mut liquidation_price_for_b = calculate_liquidation_outside(0, amount_b, leftovers_a, leftovers_b, debt_a, debt_b, liquidation_threshold)?;
178
179    if liquidation_price_for_a < 0.0 || liquidation_price_for_a > (lower_sqrt_price as f64 / Q64_RESOLUTION).powf(2.0) {
180        liquidation_price_for_a = 0.0;
181    }
182    if liquidation_price_for_b < 0.0 || liquidation_price_for_b < (upper_sqrt_price as f64 / Q64_RESOLUTION).powf(2.0) {
183        liquidation_price_for_b = 0.0;
184    }
185
186    Ok(LiquidationPrices {
187        lower: liquidation_price_for_a,
188        upper: liquidation_price_for_b,
189    })
190}
191
192/// Computes the liquidation prices for an existing position.
193///
194/// # Parameters
195/// - `lower_sqrt_price`: The lower sqrt price of the position.
196/// - `upper_sqrt_price`: The upper sqrt price of the position.
197/// - `leftovers_a`: The amount of leftovers A in the existing position.
198/// - `leftovers_a`: The amount of leftovers B in the existing position.
199/// - `liquidity`: Liquidity of the existing position.
200/// - `debt_a`: The amount of tokens A borrowed.
201/// - `debt_b`: The amount of tokens B borrowed.
202/// - `liquidation_threshold`: The liquidation threshold of the market.
203///
204/// # Returns
205/// - `LiquidationPrices`: An object containing lower/upper liquidation prices.
206#[cfg_attr(feature = "wasm", wasm_expose)]
207pub fn get_lp_position_liquidation_prices(
208    lower_sqrt_price: U128,
209    upper_sqrt_price: U128,
210    liquidity: U128,
211    leftovers_a: u64,
212    leftovers_b: u64,
213    debt_a: u64,
214    debt_b: u64,
215    liquidation_threshold: u32,
216) -> Result<LiquidationPrices, CoreError> {
217    let lower_sqrt_price: u128 = lower_sqrt_price.into();
218    let upper_sqrt_price: u128 = upper_sqrt_price.into();
219    let liquidity: u128 = liquidity.into();
220
221    let liquidation_price_inside = compute_liquidation_prices_inside(
222        lower_sqrt_price,
223        upper_sqrt_price,
224        liquidity,
225        leftovers_a,
226        leftovers_b,
227        debt_a,
228        debt_b,
229        liquidation_threshold,
230    )?;
231
232    let liquidation_price_outside = compute_liquidation_prices_outside(
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 lower_liquidation_price = if liquidation_price_inside.lower > 0.0 {
244        liquidation_price_inside.lower
245    } else {
246        liquidation_price_outside.lower
247    };
248
249    let upper_liquidation_price = if liquidation_price_inside.upper > 0.0 {
250        liquidation_price_inside.upper
251    } else {
252        liquidation_price_outside.upper
253    };
254
255    Ok(LiquidationPrices {
256        lower: lower_liquidation_price,
257        upper: upper_liquidation_price,
258    })
259}
260
261#[cfg(all(test, not(feature = "wasm")))]
262mod tests {
263    use crate::{get_lp_position_liquidation_prices, LiquidationPrices, HUNDRED_PERCENT};
264    use fusionamm_core::{price_to_sqrt_price, try_get_liquidity_from_b};
265    use once_cell::sync::Lazy;
266
267    pub static LOWER_SQRT_PRICE: Lazy<u128> = Lazy::new(|| price_to_sqrt_price(180.736, 6, 6));
268    pub static SQRT_PRICE: Lazy<u128> = Lazy::new(|| price_to_sqrt_price(213.41, 6, 6));
269    pub static UPPER_SQRT_PRICE: Lazy<u128> = Lazy::new(|| price_to_sqrt_price(225.66, 6, 6));
270    pub static LIQUIDITY: Lazy<u128> = Lazy::new(|| try_get_liquidity_from_b(10000_000_000, *LOWER_SQRT_PRICE, *SQRT_PRICE).unwrap());
271
272    #[test]
273    fn test_liquidation_price_outside_range_lower() {
274        assert_eq!(
275            get_lp_position_liquidation_prices(
276                *LOWER_SQRT_PRICE,
277                *UPPER_SQRT_PRICE,
278                *LIQUIDITY,
279                0,                          // leftovers_a
280                0,                          // leftovers_b
281                0,                          // debt_a
282                9807_000_000,               // debt_b
283                HUNDRED_PERCENT * 83 / 100  // liquidation_threshold
284            ),
285            Ok(LiquidationPrices {
286                lower: 176.1105408775386,
287                upper: 0.0
288            })
289        );
290    }
291
292    #[test]
293    fn test_liquidation_price_outside_range_upper() {
294        assert_eq!(
295            get_lp_position_liquidation_prices(
296                *LOWER_SQRT_PRICE,
297                *UPPER_SQRT_PRICE,
298                *LIQUIDITY,
299                0,                          // leftovers_a
300                0,                          // leftovers_b
301                20_000_000,                 // debt_a
302                0,                          // debt_b
303                HUNDRED_PERCENT * 83 / 100  // liquidation_threshold
304            ),
305            Ok(LiquidationPrices {
306                lower: 0.0,
307                upper: 562.303070321
308            })
309        );
310    }
311
312    #[test]
313    fn test_liquidation_price_outside_range_lower_and_upper() {
314        assert_eq!(
315            get_lp_position_liquidation_prices(
316                *LOWER_SQRT_PRICE,
317                *UPPER_SQRT_PRICE,
318                *LIQUIDITY,
319                0,                          // leftovers_a
320                0,                          // leftovers_b
321                10_000_000,                 // debt_a
322                5000_000_000,               // debt_b
323                HUNDRED_PERCENT * 83 / 100  // liquidation_threshold
324            ),
325            Ok(LiquidationPrices {
326                lower: 109.44124291015223,
327                upper: 624.606140642
328            })
329        );
330    }
331
332    #[test]
333    fn test_liquidation_price_inside_range_lower() {
334        assert_eq!(
335            get_lp_position_liquidation_prices(
336                *LOWER_SQRT_PRICE,
337                *UPPER_SQRT_PRICE,
338                *LIQUIDITY,
339                0,                          // leftovers_a
340                0,                          // leftovers_b
341                0,                          // debt_a
342                11000_000_000,              // debt_b
343                HUNDRED_PERCENT * 83 / 100  // liquidation_threshold
344            ),
345            Ok(LiquidationPrices {
346                lower: 204.54056605629555,
347                upper: 0.0
348            })
349        );
350    }
351
352    #[test]
353    fn test_liquidation_price_inside_range_upper() {
354        assert_eq!(
355            get_lp_position_liquidation_prices(
356                *LOWER_SQRT_PRICE,
357                *UPPER_SQRT_PRICE,
358                *LIQUIDITY,
359                0,                          // leftovers_a
360                0,                          // leftovers_b
361                51_000_000,                 // debt_a
362                0,                          // debt_b
363                HUNDRED_PERCENT * 83 / 100  // liquidation_threshold
364            ),
365            Ok(LiquidationPrices {
366                lower: 0.0,
367                upper: 220.2000409301412
368            })
369        );
370    }
371
372    #[test]
373    fn test_liquidation_price_inside_range_lower_and_upper() {
374        assert_eq!(
375            get_lp_position_liquidation_prices(
376                *LOWER_SQRT_PRICE,
377                *UPPER_SQRT_PRICE,
378                *LIQUIDITY,
379                0,                          // leftovers_a
380                0,                          // leftovers_b
381                11_500_000,                 // debt_a
382                8700_000_000,               // debt_b
383                HUNDRED_PERCENT * 83 / 100  // liquidation_threshold
384            ),
385            Ok(LiquidationPrices {
386                lower: 210.44477952403906,
387                upper: 219.81463329620027
388            })
389        );
390    }
391}