Skip to main content

orca_whirlpools_core/quote/
fees.rs

1use ethnum::U256;
2
3#[cfg(feature = "wasm")]
4use orca_whirlpools_macros::wasm_expose;
5
6use crate::{
7    try_apply_transfer_fee, CollectFeesQuote, CoreError, PositionFacade, TickFacade, TransferFee,
8    WhirlpoolFacade, AMOUNT_EXCEEDS_MAX_U64, ARITHMETIC_OVERFLOW,
9};
10
11/// Calculate fees owed for a position
12///
13/// # Paramters
14/// - `whirlpool`: The whirlpool state
15/// - `position`: The position state
16/// - `tick_lower`: The lower tick state
17/// - `tick_upper`: The upper tick state
18/// - `transfer_fee_a`: The transfer fee for token A
19/// - `transfer_fee_b`: The transfer fee for token B
20///
21/// # Returns
22/// - `CollectFeesQuote`: The fees owed for token A and token B
23#[allow(clippy::too_many_arguments)]
24#[cfg_attr(feature = "wasm", wasm_expose)]
25pub fn collect_fees_quote(
26    whirlpool: WhirlpoolFacade,
27    position: PositionFacade,
28    tick_lower: TickFacade,
29    tick_upper: TickFacade,
30    transfer_fee_a: Option<TransferFee>,
31    transfer_fee_b: Option<TransferFee>,
32) -> Result<CollectFeesQuote, CoreError> {
33    let mut fee_growth_below_a: u128 = tick_lower.fee_growth_outside_a;
34    let mut fee_growth_above_a: u128 = tick_upper.fee_growth_outside_a;
35    let mut fee_growth_below_b: u128 = tick_lower.fee_growth_outside_b;
36    let mut fee_growth_above_b: u128 = tick_upper.fee_growth_outside_b;
37
38    if whirlpool.tick_current_index < position.tick_lower_index {
39        fee_growth_below_a = whirlpool
40            .fee_growth_global_a
41            .wrapping_sub(fee_growth_below_a);
42        fee_growth_below_b = whirlpool
43            .fee_growth_global_b
44            .wrapping_sub(fee_growth_below_b);
45    }
46
47    if whirlpool.tick_current_index >= position.tick_upper_index {
48        fee_growth_above_a = whirlpool
49            .fee_growth_global_a
50            .wrapping_sub(fee_growth_above_a);
51        fee_growth_above_b = whirlpool
52            .fee_growth_global_b
53            .wrapping_sub(fee_growth_above_b);
54    }
55
56    let fee_growth_inside_a = whirlpool
57        .fee_growth_global_a
58        .wrapping_sub(fee_growth_below_a)
59        .wrapping_sub(fee_growth_above_a);
60
61    let fee_growth_inside_b = whirlpool
62        .fee_growth_global_b
63        .wrapping_sub(fee_growth_below_b)
64        .wrapping_sub(fee_growth_above_b);
65
66    let fee_growth_delta_a = fee_growth_inside_a.wrapping_sub(position.fee_growth_checkpoint_a);
67
68    let fee_growth_delta_b = fee_growth_inside_b.wrapping_sub(position.fee_growth_checkpoint_b);
69
70    let fee_owed_delta_a: U256 = <U256>::from(fee_growth_delta_a)
71        .checked_mul(position.liquidity.into())
72        .ok_or(ARITHMETIC_OVERFLOW)?
73        >> 64;
74
75    let fee_owed_delta_b: U256 = <U256>::from(fee_growth_delta_b)
76        .checked_mul(position.liquidity.into())
77        .ok_or(ARITHMETIC_OVERFLOW)?
78        >> 64;
79
80    let fee_owed_delta_a: u64 = fee_owed_delta_a
81        .try_into()
82        .map_err(|_| AMOUNT_EXCEEDS_MAX_U64)?;
83    let fee_owed_delta_b: u64 = fee_owed_delta_b
84        .try_into()
85        .map_err(|_| AMOUNT_EXCEEDS_MAX_U64)?;
86
87    let withdrawable_fee_a = position.fee_owed_a + fee_owed_delta_a;
88    let withdrawable_fee_b = position.fee_owed_b + fee_owed_delta_b;
89
90    let fee_owed_a =
91        try_apply_transfer_fee(withdrawable_fee_a, transfer_fee_a.unwrap_or_default())?;
92    let fee_owed_b =
93        try_apply_transfer_fee(withdrawable_fee_b, transfer_fee_b.unwrap_or_default())?;
94
95    Ok(CollectFeesQuote {
96        fee_owed_a,
97        fee_owed_b,
98    })
99}
100
101#[cfg(all(test, not(feature = "wasm")))]
102mod tests {
103    use super::*;
104
105    fn test_whirlpool(tick_index: i32) -> WhirlpoolFacade {
106        WhirlpoolFacade {
107            tick_current_index: tick_index,
108            fee_growth_global_a: 800,
109            fee_growth_global_b: 1000,
110            ..WhirlpoolFacade::default()
111        }
112    }
113
114    fn test_position() -> PositionFacade {
115        PositionFacade {
116            liquidity: 10000000000000000000,
117            tick_lower_index: 5,
118            tick_upper_index: 10,
119            fee_growth_checkpoint_a: 0,
120            fee_owed_a: 400,
121            fee_growth_checkpoint_b: 0,
122            fee_owed_b: 600,
123            ..PositionFacade::default()
124        }
125    }
126
127    fn test_tick() -> TickFacade {
128        TickFacade {
129            fee_growth_outside_a: 50,
130            fee_growth_outside_b: 20,
131            ..TickFacade::default()
132        }
133    }
134
135    #[test]
136    fn test_collect_out_of_range_lower() {
137        let result = collect_fees_quote(
138            test_whirlpool(0),
139            test_position(),
140            test_tick(),
141            test_tick(),
142            None,
143            None,
144        )
145        .unwrap();
146        assert_eq!(result.fee_owed_a, 400);
147        assert_eq!(result.fee_owed_b, 600);
148    }
149
150    #[test]
151    fn test_in_range() {
152        let result = collect_fees_quote(
153            test_whirlpool(7),
154            test_position(),
155            test_tick(),
156            test_tick(),
157            None,
158            None,
159        )
160        .unwrap();
161        assert_eq!(result.fee_owed_a, 779);
162        assert_eq!(result.fee_owed_b, 1120);
163    }
164
165    #[test]
166    fn test_collect_out_of_range_upper() {
167        let result = collect_fees_quote(
168            test_whirlpool(15),
169            test_position(),
170            test_tick(),
171            test_tick(),
172            None,
173            None,
174        )
175        .unwrap();
176        assert_eq!(result.fee_owed_a, 400);
177        assert_eq!(result.fee_owed_b, 600);
178    }
179
180    #[test]
181    fn test_collect_on_range_lower() {
182        let result = collect_fees_quote(
183            test_whirlpool(5),
184            test_position(),
185            test_tick(),
186            test_tick(),
187            None,
188            None,
189        )
190        .unwrap();
191        assert_eq!(result.fee_owed_a, 779);
192        assert_eq!(result.fee_owed_b, 1120);
193    }
194
195    #[test]
196    fn test_collect_on_upper() {
197        let result = collect_fees_quote(
198            test_whirlpool(10),
199            test_position(),
200            test_tick(),
201            test_tick(),
202            None,
203            None,
204        )
205        .unwrap();
206        assert_eq!(result.fee_owed_a, 400);
207        assert_eq!(result.fee_owed_b, 600);
208    }
209
210    #[test]
211    fn test_collect_transfer_fee() {
212        let result = collect_fees_quote(
213            test_whirlpool(7),
214            test_position(),
215            test_tick(),
216            test_tick(),
217            Some(TransferFee::new(2000)),
218            Some(TransferFee::new(5000)),
219        )
220        .unwrap();
221        assert_eq!(result.fee_owed_a, 623);
222        assert_eq!(result.fee_owed_b, 560);
223    }
224
225    #[test]
226    fn test_cyclic_growth_checkpoint() {
227        let position = PositionFacade {
228            liquidity: 91354442895,
229            tick_lower_index: 15168,
230            tick_upper_index: 19648,
231            fee_growth_checkpoint_a: 340282366920938463463368367551765494643,
232            fee_growth_checkpoint_b: 340282366920938463463235752370561182038,
233            ..PositionFacade::default()
234        };
235
236        let whirlpool = WhirlpoolFacade {
237            tick_current_index: 18158,
238            fee_growth_global_a: 388775621815491196,
239            fee_growth_global_b: 2114651338550574490,
240            ..WhirlpoolFacade::default()
241        };
242
243        let tick_lower = TickFacade {
244            fee_growth_outside_a: 334295763697402279,
245            fee_growth_outside_b: 1816428862338027402,
246            ..TickFacade::default()
247        };
248
249        let tick_upper = TickFacade {
250            fee_growth_outside_a: 48907059211668900,
251            fee_growth_outside_b: 369439434559592375,
252            ..TickFacade::default()
253        };
254
255        let result =
256            collect_fees_quote(whirlpool, position, tick_lower, tick_upper, None, None).unwrap();
257        assert_eq!(result.fee_owed_a, 58500334);
258        assert_eq!(result.fee_owed_b, 334966494);
259    }
260}