orca_whirlpools_core/quote/
fees.rs1use 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#[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}