use ethnum::U256;
#[cfg(feature = "wasm")]
use fusionamm_macros::wasm_expose;
use crate::{
try_apply_transfer_fee, CollectFeesQuote, CoreError, FusionPoolFacade, PositionFacade, TickFacade, TransferFee, AMOUNT_EXCEEDS_MAX_U64,
ARITHMETIC_OVERFLOW,
};
#[allow(clippy::too_many_arguments)]
#[cfg_attr(feature = "wasm", wasm_expose)]
pub fn collect_fees_quote(
fusion_pool: FusionPoolFacade,
position: PositionFacade,
tick_lower: TickFacade,
tick_upper: TickFacade,
transfer_fee_a: Option<TransferFee>,
transfer_fee_b: Option<TransferFee>,
) -> Result<CollectFeesQuote, CoreError> {
let mut fee_growth_below_a: u128 = tick_lower.fee_growth_outside_a;
let mut fee_growth_above_a: u128 = tick_upper.fee_growth_outside_a;
let mut fee_growth_below_b: u128 = tick_lower.fee_growth_outside_b;
let mut fee_growth_above_b: u128 = tick_upper.fee_growth_outside_b;
if fusion_pool.tick_current_index < position.tick_lower_index {
fee_growth_below_a = fusion_pool.fee_growth_global_a.wrapping_sub(fee_growth_below_a);
fee_growth_below_b = fusion_pool.fee_growth_global_b.wrapping_sub(fee_growth_below_b);
}
if fusion_pool.tick_current_index >= position.tick_upper_index {
fee_growth_above_a = fusion_pool.fee_growth_global_a.wrapping_sub(fee_growth_above_a);
fee_growth_above_b = fusion_pool.fee_growth_global_b.wrapping_sub(fee_growth_above_b);
}
let fee_growth_inside_a = fusion_pool
.fee_growth_global_a
.wrapping_sub(fee_growth_below_a)
.wrapping_sub(fee_growth_above_a);
let fee_growth_inside_b = fusion_pool
.fee_growth_global_b
.wrapping_sub(fee_growth_below_b)
.wrapping_sub(fee_growth_above_b);
let fee_growth_delta_a = fee_growth_inside_a.wrapping_sub(position.fee_growth_checkpoint_a);
let fee_growth_delta_b = fee_growth_inside_b.wrapping_sub(position.fee_growth_checkpoint_b);
let fee_owed_delta_a: U256 = <U256>::from(fee_growth_delta_a)
.checked_mul(position.liquidity.into())
.ok_or(ARITHMETIC_OVERFLOW)?
>> 64;
let fee_owed_delta_b: U256 = <U256>::from(fee_growth_delta_b)
.checked_mul(position.liquidity.into())
.ok_or(ARITHMETIC_OVERFLOW)?
>> 64;
let fee_owed_delta_a: u64 = fee_owed_delta_a.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)?;
let fee_owed_delta_b: u64 = fee_owed_delta_b.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)?;
let withdrawable_fee_a = position.fee_owed_a + fee_owed_delta_a;
let withdrawable_fee_b = position.fee_owed_b + fee_owed_delta_b;
let fee_owed_a = try_apply_transfer_fee(withdrawable_fee_a, transfer_fee_a.unwrap_or_default())?;
let fee_owed_b = try_apply_transfer_fee(withdrawable_fee_b, transfer_fee_b.unwrap_or_default())?;
Ok(CollectFeesQuote { fee_owed_a, fee_owed_b })
}
#[cfg(all(test, not(feature = "wasm")))]
mod tests {
use super::*;
fn test_fusion_pool(tick_index: i32) -> FusionPoolFacade {
FusionPoolFacade {
tick_current_index: tick_index,
fee_growth_global_a: 800,
fee_growth_global_b: 1000,
..FusionPoolFacade::default()
}
}
fn test_position() -> PositionFacade {
PositionFacade {
liquidity: 10000000000000000000,
tick_lower_index: 5,
tick_upper_index: 10,
fee_growth_checkpoint_a: 0,
fee_owed_a: 400,
fee_growth_checkpoint_b: 0,
fee_owed_b: 600,
..PositionFacade::default()
}
}
fn test_tick() -> TickFacade {
TickFacade {
fee_growth_outside_a: 50,
fee_growth_outside_b: 20,
..TickFacade::default()
}
}
#[test]
fn test_collect_out_of_range_lower() {
let result = collect_fees_quote(test_fusion_pool(0), test_position(), test_tick(), test_tick(), None, None).unwrap();
assert_eq!(result.fee_owed_a, 400);
assert_eq!(result.fee_owed_b, 600);
}
#[test]
fn test_in_range() {
let result = collect_fees_quote(test_fusion_pool(7), test_position(), test_tick(), test_tick(), None, None).unwrap();
assert_eq!(result.fee_owed_a, 779);
assert_eq!(result.fee_owed_b, 1120);
}
#[test]
fn test_collect_out_of_range_upper() {
let result = collect_fees_quote(test_fusion_pool(15), test_position(), test_tick(), test_tick(), None, None).unwrap();
assert_eq!(result.fee_owed_a, 400);
assert_eq!(result.fee_owed_b, 600);
}
#[test]
fn test_collect_on_range_lower() {
let result = collect_fees_quote(test_fusion_pool(5), test_position(), test_tick(), test_tick(), None, None).unwrap();
assert_eq!(result.fee_owed_a, 779);
assert_eq!(result.fee_owed_b, 1120);
}
#[test]
fn test_collect_on_upper() {
let result = collect_fees_quote(test_fusion_pool(10), test_position(), test_tick(), test_tick(), None, None).unwrap();
assert_eq!(result.fee_owed_a, 400);
assert_eq!(result.fee_owed_b, 600);
}
#[test]
fn test_collect_transfer_fee() {
let result = collect_fees_quote(
test_fusion_pool(7),
test_position(),
test_tick(),
test_tick(),
Some(TransferFee::new(2000)),
Some(TransferFee::new(5000)),
)
.unwrap();
assert_eq!(result.fee_owed_a, 623);
assert_eq!(result.fee_owed_b, 560);
}
#[test]
fn test_cyclic_growth_checkpoint() {
let position = PositionFacade {
liquidity: 91354442895,
tick_lower_index: 15168,
tick_upper_index: 19648,
fee_growth_checkpoint_a: 340282366920938463463368367551765494643,
fee_growth_checkpoint_b: 340282366920938463463235752370561182038,
..PositionFacade::default()
};
let fusion_pool = FusionPoolFacade {
tick_current_index: 18158,
fee_growth_global_a: 388775621815491196,
fee_growth_global_b: 2114651338550574490,
..FusionPoolFacade::default()
};
let tick_lower = TickFacade {
fee_growth_outside_a: 334295763697402279,
fee_growth_outside_b: 1816428862338027402,
..TickFacade::default()
};
let tick_upper = TickFacade {
fee_growth_outside_a: 48907059211668900,
fee_growth_outside_b: 369439434559592375,
..TickFacade::default()
};
let result = collect_fees_quote(fusion_pool, position, tick_lower, tick_upper, None, None).unwrap();
assert_eq!(result.fee_owed_a, 58500334);
assert_eq!(result.fee_owed_b, 334966494);
}
}