use crate::math::get_limit_order_output_amount;
use crate::{
tick_index_to_sqrt_price, try_apply_swap_fee, try_mul_div, try_reverse_apply_swap_fee, CoreError, FusionPoolFacade, LimitOrderDecreaseQuote,
LimitOrderFacade, TickFacade, AMOUNT_EXCEEDS_LIMIT_ORDER_INPUT_AMOUNT, AMOUNT_EXCEEDS_MAX_U64, FEE_RATE_DENOMINATOR,
LIMIT_ORDER_AND_POOL_ARE_OUT_OF_SYNC, MAX_CLP_TO_OLP_REWARD_RATIO, PROTOCOL_FEE_RATE_MUL_VALUE,
};
#[cfg(feature = "wasm")]
use fusionamm_macros::wasm_expose;
#[cfg_attr(feature = "wasm", wasm_expose)]
pub fn limit_order_quote_by_input_token(
amount_in: u64,
a_to_b_order: bool,
tick_index: i32,
fusion_pool: FusionPoolFacade,
) -> Result<u64, CoreError> {
let sqrt_price: u128 = tick_index_to_sqrt_price(tick_index).into();
let mut amount_out = get_limit_order_output_amount(amount_in, a_to_b_order, sqrt_price, false)?;
let swap_fee = try_reverse_apply_swap_fee(amount_out.into(), fusion_pool.fee_rate)? - amount_out;
let mut olp_reward = swap_fee - try_mul_div(swap_fee, fusion_pool.clp_to_olp_reward_ratio as u128, MAX_CLP_TO_OLP_REWARD_RATIO as u128, false)?;
olp_reward -= try_mul_div(olp_reward, fusion_pool.protocol_fee_rate as u128, PROTOCOL_FEE_RATE_MUL_VALUE, false)?;
amount_out = try_apply_swap_fee(amount_out.into(), fusion_pool.order_protocol_fee_rate)?;
amount_out += olp_reward;
Ok(amount_out)
}
#[cfg_attr(feature = "wasm", wasm_expose)]
pub fn limit_order_quote_by_output_token(
amount_out: u64,
a_to_b_order: bool,
tick_index: i32,
fusion_pool: FusionPoolFacade,
) -> Result<u64, CoreError> {
let sqrt_price: u128 = tick_index_to_sqrt_price(tick_index).into();
let f = fusion_pool.fee_rate as f64 / FEE_RATE_DENOMINATOR as f64;
let po = fusion_pool.order_protocol_fee_rate as f64 / FEE_RATE_DENOMINATOR as f64;
let p = fusion_pool.protocol_fee_rate as f64 / PROTOCOL_FEE_RATE_MUL_VALUE as f64;
let r = fusion_pool.clp_to_olp_reward_ratio as f64 / MAX_CLP_TO_OLP_REWARD_RATIO as f64;
let denominator = 1.0 - po + (f * (1.0 - r) * (1.0 - p) / (1.0 - f));
let amount_out_with_fees = amount_out as f64 / denominator;
if amount_out_with_fees < 0.0 || amount_out_with_fees > u64::MAX as f64 {
return Err(AMOUNT_EXCEEDS_MAX_U64);
}
let amount_in = get_limit_order_output_amount(amount_out_with_fees as u64, !a_to_b_order, sqrt_price, true)?;
Ok(amount_in)
}
#[cfg_attr(feature = "wasm", wasm_expose)]
pub fn decrease_limit_order_quote(
fusion_pool: FusionPoolFacade,
limit_order: LimitOrderFacade,
tick: TickFacade,
amount: u64,
) -> Result<LimitOrderDecreaseQuote, CoreError> {
if amount > limit_order.amount {
return Err(AMOUNT_EXCEEDS_LIMIT_ORDER_INPUT_AMOUNT);
}
let protocol_fee_rate = fusion_pool.order_protocol_fee_rate;
let (amount_in, mut amount_out) = if limit_order.age == tick.age {
(amount, 0)
}
else if limit_order.age + 1 == tick.age {
if tick.part_filled_orders_input == 0 {
return Err(LIMIT_ORDER_AND_POOL_ARE_OUT_OF_SYNC);
}
let sqrt_price: u128 = tick_index_to_sqrt_price(limit_order.tick_index).into();
let remaining_input = try_mul_div(amount, tick.part_filled_orders_remaining_input as u128, tick.part_filled_orders_input as u128, false)?;
let amount_out = get_limit_order_output_amount(amount - remaining_input, limit_order.a_to_b, sqrt_price, false)?;
(remaining_input, amount_out)
}
else if limit_order.age + 2 <= tick.age {
let sqrt_price: u128 = tick_index_to_sqrt_price(limit_order.tick_index).into();
let amount_out = get_limit_order_output_amount(amount, limit_order.a_to_b, sqrt_price, false)?;
(0, amount_out)
} else {
return Err(LIMIT_ORDER_AND_POOL_ARE_OUT_OF_SYNC);
};
let amount_out_with_fee = amount_out;
amount_out = try_apply_swap_fee(amount_out, protocol_fee_rate)?;
let protocol_fee = amount_out_with_fee - amount_out;
let amount_out_a;
let amount_out_b;
let mut fee_a = 0;
let mut fee_b = 0;
let mut reward_a = 0;
let mut reward_b = 0;
if limit_order.a_to_b {
let filled_amount = amount - amount_in;
fee_b = protocol_fee;
if filled_amount > 0 {
if fusion_pool.orders_filled_amount_a == 0 {
return Err(LIMIT_ORDER_AND_POOL_ARE_OUT_OF_SYNC);
}
reward_b = try_mul_div(fusion_pool.olp_fee_owed_b, filled_amount as u128, fusion_pool.orders_filled_amount_a as u128, false)?;
}
amount_out_a = amount_in;
amount_out_b = amount_out + reward_b;
} else {
let filled_amount = amount - amount_in;
fee_a = protocol_fee;
if filled_amount > 0 {
if fusion_pool.orders_filled_amount_b == 0 {
return Err(LIMIT_ORDER_AND_POOL_ARE_OUT_OF_SYNC);
}
reward_a = try_mul_div(fusion_pool.olp_fee_owed_a, filled_amount as u128, fusion_pool.orders_filled_amount_b as u128, false)?;
}
amount_out_a = amount_out + reward_a;
amount_out_b = amount_in;
}
Ok(LimitOrderDecreaseQuote {
amount_out_a,
amount_out_b,
fee_a,
fee_b,
reward_a,
reward_b,
})
}
#[cfg(all(test, not(feature = "wasm")))]
mod tests {
use crate::{
decrease_limit_order_quote, limit_order_quote_by_input_token, limit_order_quote_by_output_token, price_to_tick_index,
sqrt_price_to_tick_index, FusionPoolFacade, LimitOrderFacade, TickFacade,
};
const ONE_PCT: u16 = 10000;
fn test_fusion_pool(sqrt_price: u128, fee_rate: u16, order_protocol_fee_rate: u16, protocol_fee_rate: u16) -> FusionPoolFacade {
let tick_current_index = sqrt_price_to_tick_index(sqrt_price);
FusionPoolFacade {
tick_current_index,
fee_rate,
order_protocol_fee_rate,
protocol_fee_rate,
sqrt_price,
tick_spacing: 2,
..FusionPoolFacade::default()
}
}
#[test]
fn partially_decrease_not_filled_a_to_b_order() {
let quote = decrease_limit_order_quote(
FusionPoolFacade {
order_protocol_fee_rate: ONE_PCT,
..FusionPoolFacade::default()
},
LimitOrderFacade {
tick_index: 128,
amount: 50_000,
a_to_b: true,
age: 5,
},
TickFacade {
age: 5,
open_orders_input: 100_000,
part_filled_orders_input: 0,
part_filled_orders_remaining_input: 0,
fulfilled_a_to_b_orders_input: 0,
fulfilled_b_to_a_orders_input: 0,
..TickFacade::default()
},
25_000,
)
.unwrap();
assert_eq!(quote.amount_out_a, 25_000);
assert_eq!(quote.amount_out_b, 0);
assert_eq!(quote.fee_a, 0);
assert_eq!(quote.fee_b, 0);
}
#[test]
fn partially_decrease_semi_filled_a_to_b_order() {
let quote = decrease_limit_order_quote(
FusionPoolFacade {
order_protocol_fee_rate: ONE_PCT,
orders_filled_amount_a: 80_000,
olp_fee_owed_b: 500,
..FusionPoolFacade::default()
},
LimitOrderFacade {
tick_index: 128,
amount: 50_000,
a_to_b: true,
age: 5,
},
TickFacade {
age: 6,
open_orders_input: 0,
part_filled_orders_input: 200_000,
part_filled_orders_remaining_input: 120_000,
fulfilled_a_to_b_orders_input: 0,
fulfilled_b_to_a_orders_input: 0,
..TickFacade::default()
},
25_000,
)
.unwrap();
assert_eq!(quote.amount_out_a, 15000);
assert_eq!(quote.amount_out_b, 10088);
assert_eq!(quote.fee_a, 0);
assert_eq!(quote.fee_b, 102);
assert_eq!(quote.reward_a, 0);
assert_eq!(quote.reward_b, 62);
}
#[test]
fn partially_decrease_semi_filled_b_to_a_order() {
let quote = decrease_limit_order_quote(
FusionPoolFacade {
order_protocol_fee_rate: ONE_PCT,
orders_filled_amount_b: 80_000,
olp_fee_owed_a: 500,
..FusionPoolFacade::default()
},
LimitOrderFacade {
tick_index: 128,
amount: 50_000,
a_to_b: false,
age: 5,
},
TickFacade {
age: 6,
open_orders_input: 0,
part_filled_orders_input: 200_000,
part_filled_orders_remaining_input: 120_000,
fulfilled_a_to_b_orders_input: 0,
fulfilled_b_to_a_orders_input: 0,
..TickFacade::default()
},
25_000,
)
.unwrap();
assert_eq!(quote.amount_out_a, 9835);
assert_eq!(quote.amount_out_b, 15000);
assert_eq!(quote.fee_a, 99);
assert_eq!(quote.fee_b, 0);
assert_eq!(quote.reward_a, 62);
assert_eq!(quote.reward_b, 0);
}
#[test]
fn partially_decrease_fulfilled_a_to_b() {
let quote = decrease_limit_order_quote(
FusionPoolFacade {
order_protocol_fee_rate: ONE_PCT,
orders_filled_amount_a: 100_000,
olp_fee_owed_b: 500,
..FusionPoolFacade::default()
},
LimitOrderFacade {
tick_index: 128,
amount: 100_000,
a_to_b: true,
age: 5,
},
TickFacade {
age: 7,
open_orders_input: 0,
part_filled_orders_input: 0,
part_filled_orders_remaining_input: 0,
fulfilled_a_to_b_orders_input: 100_000,
fulfilled_b_to_a_orders_input: 80_000,
..TickFacade::default()
},
10_000,
)
.unwrap();
assert_eq!(quote.amount_out_a, 0);
assert_eq!(quote.amount_out_b, 10076);
assert_eq!(quote.fee_a, 0);
assert_eq!(quote.fee_b, 102);
assert_eq!(quote.reward_a, 0);
assert_eq!(quote.reward_b, 50);
}
#[test]
fn partially_decrease_fulfilled_b_to_a() {
let quote = decrease_limit_order_quote(
FusionPoolFacade {
order_protocol_fee_rate: ONE_PCT,
orders_filled_amount_b: 80_000,
olp_fee_owed_a: 500,
..FusionPoolFacade::default()
},
LimitOrderFacade {
tick_index: 128,
amount: 100_000,
a_to_b: false,
age: 5,
},
TickFacade {
age: 7,
open_orders_input: 0,
part_filled_orders_input: 0,
part_filled_orders_remaining_input: 0,
fulfilled_a_to_b_orders_input: 100_000,
fulfilled_b_to_a_orders_input: 80_000,
..TickFacade::default()
},
10_000,
)
.unwrap();
assert_eq!(quote.amount_out_a, 9835);
assert_eq!(quote.amount_out_b, 0);
assert_eq!(quote.fee_a, 99);
assert_eq!(quote.fee_b, 0);
assert_eq!(quote.reward_a, 62);
assert_eq!(quote.reward_b, 0);
}
#[test]
fn test_limit_order_quote_by_input_token() {
assert_eq!(
limit_order_quote_by_input_token(10_000, true, price_to_tick_index(2.0, 1, 1), test_fusion_pool(1 << 64, 0, 0, 1000)).unwrap(),
19998
);
assert_eq!(
limit_order_quote_by_input_token(10_000, true, price_to_tick_index(2.0, 1, 1), test_fusion_pool(1 << 64, 0, ONE_PCT, 1000)).unwrap(),
19798
);
assert_eq!(
limit_order_quote_by_input_token(10_000, true, price_to_tick_index(2.0, 1, 1), test_fusion_pool(1 << 64, ONE_PCT, 0, 0)).unwrap(),
20200
);
assert_eq!(
limit_order_quote_by_input_token(10_000, true, price_to_tick_index(2.0, 1, 1), test_fusion_pool(1 << 64, ONE_PCT, 0, 1000)).unwrap(),
20180
);
}
#[test]
fn test_limit_order_quote_by_output_token() {
assert_eq!(
limit_order_quote_by_output_token(19998, true, price_to_tick_index(2.0, 1, 1), test_fusion_pool(1 << 64, 0, 0, 1000)).unwrap(),
10_000
);
assert_eq!(
limit_order_quote_by_output_token(19798, true, price_to_tick_index(2.0, 1, 1), test_fusion_pool(1 << 64, 0, ONE_PCT, 1000)).unwrap(),
10_000
);
assert_eq!(
limit_order_quote_by_output_token(20200, true, price_to_tick_index(2.0, 1, 1), test_fusion_pool(1 << 64, ONE_PCT, 0, 0)).unwrap(),
10_000
);
assert_eq!(
limit_order_quote_by_output_token(20180, true, price_to_tick_index(2.0, 1, 1), test_fusion_pool(1 << 64, ONE_PCT, 0, 1000)).unwrap(),
10_000
);
}
}