Skip to main content

fusionamm_core/quote/
limit_order.rs

1//
2// Copyright (c) Cryptic Dot
3//
4// Licensed under FusionAMM SDK Source-Available License v1.0
5// See the LICENSE file in the project root for license information.
6//
7
8use crate::math::get_limit_order_output_amount;
9use crate::{
10    tick_index_to_sqrt_price, try_apply_transfer_fee, try_mul_div, try_reverse_apply_swap_fee, CoreError, FusionPoolFacade, LimitOrderDecreaseQuote,
11    LimitOrderFacade, TickFacade, TransferFee, AMOUNT_EXCEEDS_LIMIT_ORDER_INPUT_AMOUNT, AMOUNT_EXCEEDS_MAX_U64, FEE_RATE_MUL_VALUE,
12    LIMIT_ORDER_AND_POOL_ARE_OUT_OF_SYNC, PROTOCOL_FEE_RATE_MUL_VALUE,
13};
14
15#[cfg(feature = "wasm")]
16use fusionamm_macros::wasm_expose;
17
18/// Computes the limit order output amount by input amount.
19/// ### Parameters
20/// - `amount_in` - The input token amount of a limit order.
21/// - `a_to_b_order` - The limit order direction.
22/// - `tick_index` - The tick index of an order.
23/// - `fusion_pool` - The fusion_pool state.
24#[cfg_attr(feature = "wasm", wasm_expose)]
25pub fn limit_order_quote_by_input_token(
26    amount_in: u64,
27    a_to_b_order: bool,
28    tick_index: i32,
29    fusion_pool: FusionPoolFacade,
30) -> Result<u64, CoreError> {
31    let sqrt_price: u128 = tick_index_to_sqrt_price(tick_index).into();
32    let mut amount_out = get_limit_order_output_amount(amount_in, a_to_b_order, sqrt_price, false)?;
33    amount_out += limit_order_reward_by_output_token(amount_out, fusion_pool.fee_rate, fusion_pool.protocol_fee_rate)?;
34    Ok(amount_out)
35}
36
37/// Computes the limit order input amount by output amount.
38/// ### Parameters
39/// - `amount_out` - The output token amount of a limit order.
40/// - `a_to_b_order` - The limit order direction.
41/// - `tick_index` - The tick index of an order.
42/// - `fusion_pool` - The fusion_pool state.
43#[cfg_attr(feature = "wasm", wasm_expose)]
44pub fn limit_order_quote_by_output_token(
45    amount_out: u64,
46    a_to_b_order: bool,
47    tick_index: i32,
48    fusion_pool: FusionPoolFacade,
49) -> Result<u64, CoreError> {
50    let sqrt_price: u128 = tick_index_to_sqrt_price(tick_index).into();
51
52    let f = fusion_pool.fee_rate as f64 / FEE_RATE_MUL_VALUE as f64;
53    let p = fusion_pool.protocol_fee_rate as f64 / PROTOCOL_FEE_RATE_MUL_VALUE as f64;
54
55    // Output amount without reward = O
56    // Limit order reward = R = swap_fee⋅(1-p) = O⋅f/(1-f)⋅(1-p)
57    // Output amount with fees = O' = O + R = O ⋅ (1 + f/(1-f)⋅(1-p))
58    let denominator = 1.0 + (f / (1.0 - f) * (1.0 - p));
59    let amount_out_with_fees = amount_out as f64 / denominator;
60
61    if amount_out_with_fees < 0.0 || amount_out_with_fees > u64::MAX as f64 {
62        return Err(AMOUNT_EXCEEDS_MAX_U64);
63    }
64
65    let amount_in = get_limit_order_output_amount(amount_out_with_fees as u64, !a_to_b_order, sqrt_price, true)?;
66
67    Ok(amount_in)
68}
69
70/// Computes the limit order reward by input amount.
71/// ### Parameters
72/// - `amount_out` - The output token amount of a limit order (swap input).
73/// - `a_to_b_order` - The limit order direction.
74/// - `tick_index` - The tick index of an order.
75/// - `fusion_pool` - The fusion_pool state.
76#[cfg_attr(feature = "wasm", wasm_expose)]
77pub fn limit_order_reward_by_output_token(amount_out: u64, fee_rate: u16, protocol_fee_rate: u16) -> Result<u64, CoreError> {
78    // Reward = swap_fee⋅(1 - protocol_fee_rate)
79    let swap_fee = try_reverse_apply_swap_fee(amount_out.into(), fee_rate)? - amount_out;
80    // Deduct the protocol fee from the total swap fee.
81    let reward = swap_fee - try_mul_div(swap_fee, protocol_fee_rate as u128, PROTOCOL_FEE_RATE_MUL_VALUE as u128, false)?;
82    Ok(reward)
83}
84
85#[cfg_attr(feature = "wasm", wasm_expose)]
86pub fn decrease_limit_order_quote(
87    fusion_pool: FusionPoolFacade,
88    limit_order: LimitOrderFacade,
89    tick: TickFacade,
90    amount: u64,
91    transfer_fee_a: Option<TransferFee>,
92    transfer_fee_b: Option<TransferFee>,
93) -> Result<LimitOrderDecreaseQuote, CoreError> {
94    if amount > limit_order.amount {
95        return Err(AMOUNT_EXCEEDS_LIMIT_ORDER_INPUT_AMOUNT);
96    }
97
98    // Not filled
99    let (amount_in, amount_out) = if limit_order.age == tick.age {
100        (amount, 0)
101    }
102    // Partially filled
103    else if limit_order.age + 1 == tick.age {
104        if tick.part_filled_orders_input == 0 {
105            return Err(LIMIT_ORDER_AND_POOL_ARE_OUT_OF_SYNC);
106        }
107        let sqrt_price: u128 = tick_index_to_sqrt_price(limit_order.tick_index).into();
108        let remaining_input = try_mul_div(amount, tick.part_filled_orders_remaining_input as u128, tick.part_filled_orders_input as u128, false)?;
109        let amount_out = get_limit_order_output_amount(amount - remaining_input, limit_order.a_to_b, sqrt_price, false)?;
110        (remaining_input, amount_out)
111    }
112    // Fulfilled
113    else if limit_order.age + 2 <= tick.age {
114        let sqrt_price: u128 = tick_index_to_sqrt_price(limit_order.tick_index).into();
115        let amount_out = get_limit_order_output_amount(amount, limit_order.a_to_b, sqrt_price, false)?;
116        (0, amount_out)
117    } else {
118        return Err(LIMIT_ORDER_AND_POOL_ARE_OUT_OF_SYNC);
119    };
120
121    let mut amount_out_a;
122    let mut amount_out_b;
123    let mut reward_a = 0;
124    let mut reward_b = 0;
125
126    if limit_order.a_to_b {
127        let filled_amount = amount - amount_in;
128        // Fees and rewards are paid in the output token B of a limit order. The reward amount is based on the portion of the order that is filled.
129        if filled_amount > 0 {
130            if fusion_pool.orders_filled_amount_a == 0 {
131                return Err(LIMIT_ORDER_AND_POOL_ARE_OUT_OF_SYNC);
132            }
133            reward_b = try_mul_div(fusion_pool.olp_fee_owed_b, filled_amount as u128, fusion_pool.orders_filled_amount_a as u128, false)?;
134        }
135        // How much of tokens A and B transfer to the owner.
136        amount_out_a = amount_in;
137        amount_out_b = amount_out + reward_b;
138    } else {
139        let filled_amount = amount - amount_in;
140        // Fees and rewards are paid in the output token A of a limit order. The reward amount is based on the portion of the order that is filled.
141        if filled_amount > 0 {
142            if fusion_pool.orders_filled_amount_b == 0 {
143                return Err(LIMIT_ORDER_AND_POOL_ARE_OUT_OF_SYNC);
144            }
145            reward_a = try_mul_div(fusion_pool.olp_fee_owed_a, filled_amount as u128, fusion_pool.orders_filled_amount_b as u128, false)?;
146        }
147        // How much of tokens A and B transfer to the owner.
148        amount_out_a = amount_out + reward_a;
149        amount_out_b = amount_in;
150    }
151
152    amount_out_a = try_apply_transfer_fee(amount_out_a, transfer_fee_a.unwrap_or_default())?;
153    amount_out_b = try_apply_transfer_fee(amount_out_b, transfer_fee_b.unwrap_or_default())?;
154
155    Ok(LimitOrderDecreaseQuote {
156        amount_out_a,
157        amount_out_b,
158        reward_a,
159        reward_b,
160    })
161}
162
163#[cfg(all(test, not(feature = "wasm")))]
164mod tests {
165    use crate::{
166        decrease_limit_order_quote, limit_order_quote_by_input_token, limit_order_quote_by_output_token, price_to_tick_index,
167        sqrt_price_to_tick_index, FusionPoolFacade, LimitOrderFacade, TickFacade,
168    };
169    const TEN_PCT: u16 = 1000;
170    const ONE_PCT_FEE_RATE: u16 = 10000;
171
172    fn test_fusion_pool(sqrt_price: u128, fee_rate: u16, protocol_fee_rate: u16) -> FusionPoolFacade {
173        let tick_current_index = sqrt_price_to_tick_index(sqrt_price);
174        FusionPoolFacade {
175            tick_current_index,
176            fee_rate,
177            protocol_fee_rate,
178            sqrt_price,
179            tick_spacing: 2,
180            ..FusionPoolFacade::default()
181        }
182    }
183
184    #[test]
185    // Equal to a similar test in limit_order_manager::calculate_modify_limit_order_unit_tests of the FusionAMM program.
186    fn partially_decrease_not_filled_a_to_b_order() {
187        let quote = decrease_limit_order_quote(
188            FusionPoolFacade {
189                ..FusionPoolFacade::default()
190            },
191            LimitOrderFacade {
192                tick_index: 128,
193                amount: 50_000,
194                a_to_b: true,
195                age: 5,
196            },
197            TickFacade {
198                age: 5,
199                open_orders_input: 100_000,
200                part_filled_orders_input: 0,
201                part_filled_orders_remaining_input: 0,
202                fulfilled_a_to_b_orders_input: 0,
203                fulfilled_b_to_a_orders_input: 0,
204                ..TickFacade::default()
205            },
206            25_000,
207            None,
208            None,
209        )
210        .unwrap();
211
212        assert_eq!(quote.amount_out_a, 25_000);
213        assert_eq!(quote.amount_out_b, 0);
214    }
215
216    #[test]
217    // Equal to a similar test in limit_order_manager::calculate_modify_limit_order_unit_tests of the FusionAMM program.
218    fn partially_decrease_semi_filled_a_to_b_order() {
219        let quote = decrease_limit_order_quote(
220            FusionPoolFacade {
221                protocol_fee_rate: TEN_PCT,
222                orders_filled_amount_a: 80_000,
223                olp_fee_owed_b: 500,
224                ..FusionPoolFacade::default()
225            },
226            LimitOrderFacade {
227                tick_index: 128,
228                amount: 50_000,
229                a_to_b: true,
230                age: 5,
231            },
232            TickFacade {
233                age: 6,
234                open_orders_input: 0,
235                part_filled_orders_input: 200_000,
236                part_filled_orders_remaining_input: 120_000,
237                fulfilled_a_to_b_orders_input: 0,
238                fulfilled_b_to_a_orders_input: 0,
239                ..TickFacade::default()
240            },
241            25_000,
242            None,
243            None,
244        )
245        .unwrap();
246
247        assert_eq!(quote.amount_out_a, 15000);
248        assert_eq!(quote.amount_out_b, 10190);
249        assert_eq!(quote.reward_a, 0);
250        assert_eq!(quote.reward_b, 62);
251    }
252
253    #[test]
254    // Equal to a similar test in limit_order_manager::calculate_modify_limit_order_unit_tests of the FusionAMM program.
255    fn partially_decrease_semi_filled_b_to_a_order() {
256        let quote = decrease_limit_order_quote(
257            FusionPoolFacade {
258                protocol_fee_rate: TEN_PCT,
259                orders_filled_amount_b: 80_000,
260                olp_fee_owed_a: 500,
261                ..FusionPoolFacade::default()
262            },
263            LimitOrderFacade {
264                tick_index: 128,
265                amount: 50_000,
266                a_to_b: false,
267                age: 5,
268            },
269            TickFacade {
270                age: 6,
271                open_orders_input: 0,
272                part_filled_orders_input: 200_000,
273                part_filled_orders_remaining_input: 120_000,
274                fulfilled_a_to_b_orders_input: 0,
275                fulfilled_b_to_a_orders_input: 0,
276                ..TickFacade::default()
277            },
278            25_000,
279            None,
280            None,
281        )
282        .unwrap();
283
284        assert_eq!(quote.amount_out_a, 9934);
285        assert_eq!(quote.amount_out_b, 15000);
286        assert_eq!(quote.reward_a, 62);
287        assert_eq!(quote.reward_b, 0);
288    }
289
290    #[test]
291    // Equal to a similar test in limit_order_manager::calculate_modify_limit_order_unit_tests of the FusionAMM program.
292    fn partially_decrease_fulfilled_a_to_b() {
293        let quote = decrease_limit_order_quote(
294            FusionPoolFacade {
295                protocol_fee_rate: TEN_PCT,
296                orders_filled_amount_a: 100_000,
297                olp_fee_owed_b: 500,
298                ..FusionPoolFacade::default()
299            },
300            LimitOrderFacade {
301                tick_index: 128,
302                amount: 100_000,
303                a_to_b: true,
304                age: 5,
305            },
306            TickFacade {
307                age: 7,
308                open_orders_input: 0,
309                part_filled_orders_input: 0,
310                part_filled_orders_remaining_input: 0,
311                fulfilled_a_to_b_orders_input: 100_000,
312                fulfilled_b_to_a_orders_input: 80_000,
313                ..TickFacade::default()
314            },
315            10_000,
316            None,
317            None,
318        )
319        .unwrap();
320
321        assert_eq!(quote.amount_out_a, 0);
322        assert_eq!(quote.amount_out_b, 10178);
323        assert_eq!(quote.reward_a, 0);
324        assert_eq!(quote.reward_b, 50);
325    }
326
327    #[test]
328    // Equal to a similar test in limit_order_manager::calculate_modify_limit_order_unit_tests of the FusionAMM program.
329    fn partially_decrease_fulfilled_b_to_a() {
330        let quote = decrease_limit_order_quote(
331            FusionPoolFacade {
332                protocol_fee_rate: TEN_PCT,
333                orders_filled_amount_b: 80_000,
334                olp_fee_owed_a: 500,
335                ..FusionPoolFacade::default()
336            },
337            LimitOrderFacade {
338                tick_index: 128,
339                amount: 100_000,
340                a_to_b: false,
341                age: 5,
342            },
343            TickFacade {
344                age: 7,
345                open_orders_input: 0,
346                part_filled_orders_input: 0,
347                part_filled_orders_remaining_input: 0,
348                fulfilled_a_to_b_orders_input: 100_000,
349                fulfilled_b_to_a_orders_input: 80_000,
350                ..TickFacade::default()
351            },
352            10_000,
353            None,
354            None,
355        )
356        .unwrap();
357
358        assert_eq!(quote.amount_out_a, 9934);
359        assert_eq!(quote.amount_out_b, 0);
360        assert_eq!(quote.reward_a, 62);
361        assert_eq!(quote.reward_b, 0);
362    }
363
364    #[test]
365    fn test_limit_order_quote_by_input_token() {
366        // zero swap fee
367        assert_eq!(
368            limit_order_quote_by_input_token(10_000, true, price_to_tick_index(2.0, 1, 1), test_fusion_pool(1 << 64, 0, 0)).unwrap(),
369            19998
370        );
371
372        // 1% swap fee
373        assert_eq!(
374            limit_order_quote_by_input_token(10_000, true, price_to_tick_index(2.0, 1, 1), test_fusion_pool(1 << 64, ONE_PCT_FEE_RATE, 0)).unwrap(),
375            20200
376        );
377
378        // 1% swap fee, protocol_fee = 10%
379        assert_eq!(
380            limit_order_quote_by_input_token(10_000, true, price_to_tick_index(2.0, 1, 1), test_fusion_pool(1 << 64, ONE_PCT_FEE_RATE, TEN_PCT))
381                .unwrap(),
382            20180
383        );
384    }
385
386    #[test]
387    fn test_limit_order_quote_by_output_token() {
388        // zero swap fee
389        assert_eq!(
390            limit_order_quote_by_output_token(19998, true, price_to_tick_index(2.0, 1, 1), test_fusion_pool(1 << 64, 0, TEN_PCT)).unwrap(),
391            10_000
392        );
393
394        // 1% swap fee
395        assert_eq!(
396            limit_order_quote_by_output_token(20200, true, price_to_tick_index(2.0, 1, 1), test_fusion_pool(1 << 64, ONE_PCT_FEE_RATE, 0)).unwrap(),
397            10_000
398        );
399
400        // 1% swap fee, protocol_fee = 10%
401        assert_eq!(
402            limit_order_quote_by_output_token(20180, true, price_to_tick_index(2.0, 1, 1), test_fusion_pool(1 << 64, ONE_PCT_FEE_RATE, TEN_PCT))
403                .unwrap(),
404            10_000
405        );
406    }
407}