nautilus_model/defi/pool_analysis/
swap_math.rs1use alloy_primitives::{I256, U160, U256};
17
18use crate::defi::tick_map::{
19 full_math::FullMath,
20 sqrt_price_math::{
21 get_amount0_delta, get_amount1_delta, get_next_sqrt_price_from_input,
22 get_next_sqrt_price_from_output,
23 },
24};
25
26#[derive(Debug, Clone)]
27pub struct SwapStepResult {
28 pub sqrt_ratio_next_x96: U160,
29 pub amount_in: U256,
30 pub amount_out: U256,
31 pub fee_amount: U256,
32}
33
34pub const MAX_FEE: U256 = U256::from_limbs([1000000, 0, 0, 0]);
35
36pub fn compute_swap_step(
52 sqrt_ratio_current_x96: U160,
53 sqrt_ratio_target_x96: U160,
54 liquidity: u128,
55 amount_remaining: I256,
56 fee_pips: u32,
57) -> anyhow::Result<SwapStepResult> {
58 let fee_pips = U256::from(fee_pips);
59 let fee_complement = MAX_FEE - fee_pips;
60
61 let zero_for_one = sqrt_ratio_current_x96 >= sqrt_ratio_target_x96;
64
65 let exact_in = amount_remaining.is_positive() || amount_remaining.is_zero();
68
69 let sqrt_ratio_next_x96: U160;
70 let mut amount_in: U256 = U256::ZERO;
71 let mut amount_out: U256 = U256::ZERO;
72
73 if exact_in {
74 let amount_remaining_less_fee =
76 FullMath::mul_div(amount_remaining.into_raw(), fee_complement, MAX_FEE)?;
77
78 amount_in = if zero_for_one {
79 get_amount0_delta(
80 sqrt_ratio_target_x96,
81 sqrt_ratio_current_x96,
82 liquidity,
83 true,
84 )
85 } else {
86 get_amount1_delta(
87 sqrt_ratio_current_x96,
88 sqrt_ratio_target_x96,
89 liquidity,
90 true,
91 )
92 };
93
94 if amount_remaining_less_fee >= amount_in {
95 sqrt_ratio_next_x96 = sqrt_ratio_target_x96;
96 } else {
97 sqrt_ratio_next_x96 = get_next_sqrt_price_from_input(
98 sqrt_ratio_current_x96,
99 liquidity,
100 amount_remaining_less_fee,
101 zero_for_one,
102 );
103 }
104 } else {
105 amount_out = if zero_for_one {
107 get_amount1_delta(
108 sqrt_ratio_target_x96,
109 sqrt_ratio_current_x96,
110 liquidity,
111 false,
112 )
113 } else {
114 get_amount0_delta(
115 sqrt_ratio_current_x96,
116 sqrt_ratio_target_x96,
117 liquidity,
118 false,
119 )
120 };
121
122 if U256::from(amount_remaining.unsigned_abs()) >= amount_out {
123 sqrt_ratio_next_x96 = sqrt_ratio_target_x96;
124 } else {
125 sqrt_ratio_next_x96 = get_next_sqrt_price_from_output(
126 sqrt_ratio_current_x96,
127 liquidity,
128 U256::from(amount_remaining.unsigned_abs()),
129 zero_for_one,
130 );
131 }
132 }
133
134 let max = sqrt_ratio_target_x96 == sqrt_ratio_next_x96;
135
136 if zero_for_one {
138 amount_in = if max && exact_in {
139 amount_in
140 } else {
141 get_amount0_delta(sqrt_ratio_next_x96, sqrt_ratio_current_x96, liquidity, true)
142 };
143 amount_out = if max && !exact_in {
144 amount_out
145 } else {
146 get_amount1_delta(
147 sqrt_ratio_next_x96,
148 sqrt_ratio_current_x96,
149 liquidity,
150 false,
151 )
152 };
153 } else {
154 amount_in = if max && exact_in {
155 amount_in
156 } else {
157 get_amount1_delta(sqrt_ratio_current_x96, sqrt_ratio_next_x96, liquidity, true)
158 };
159 amount_out = if max && !exact_in {
160 amount_out
161 } else {
162 get_amount0_delta(
163 sqrt_ratio_current_x96,
164 sqrt_ratio_next_x96,
165 liquidity,
166 false,
167 )
168 };
169 }
170
171 if !exact_in && amount_out > U256::from(amount_remaining.unsigned_abs()) {
173 amount_out = U256::from(amount_remaining.unsigned_abs());
174 }
175
176 let fee_amount: U256 = if exact_in && sqrt_ratio_next_x96 != sqrt_ratio_target_x96 {
177 U256::from(amount_remaining.unsigned_abs()) - amount_in
179 } else {
180 FullMath::mul_div_rounding_up(amount_in, fee_pips, fee_complement)?
181 };
182
183 Ok(SwapStepResult {
184 sqrt_ratio_next_x96,
185 amount_in,
186 amount_out,
187 fee_amount,
188 })
189}
190
191#[cfg(test)]
196mod tests {
197 use std::str::FromStr;
199
200 use rstest::rstest;
201
202 use super::*;
203 use crate::defi::tick_map::sqrt_price_math::{encode_sqrt_ratio_x96, expand_to_18_decimals};
204
205 #[rstest]
206 fn test_exact_amount_in_that_gets_capped_at_price_target_in_one_for_zero() {
207 let price = encode_sqrt_ratio_x96(1, 1);
208 let price_target = encode_sqrt_ratio_x96(101, 100);
209 let liquidity = expand_to_18_decimals(2);
210 let amount = expand_to_18_decimals(1);
211 let fee = 600;
212
213 let result = compute_swap_step(
214 price,
215 price_target,
216 liquidity,
217 I256::from_str(&amount.to_string()).unwrap(),
218 fee,
219 )
220 .unwrap();
221
222 assert_eq!(
223 result.amount_in,
224 U256::from_str("9975124224178055").unwrap()
225 );
226 assert_eq!(
227 result.amount_out,
228 U256::from_str("9925619580021728").unwrap()
229 );
230 assert_eq!(result.fee_amount, U256::from_str("5988667735148").unwrap());
231 assert_eq!(result.sqrt_ratio_next_x96, price_target);
232
233 assert!(result.amount_in + result.fee_amount < U256::from(amount));
235
236 let price_after_whole_input_amount = get_next_sqrt_price_from_input(
237 price,
238 liquidity,
239 U256::from(amount),
240 false, );
242
243 assert_eq!(result.sqrt_ratio_next_x96, price_target);
245 assert!(result.sqrt_ratio_next_x96 < price_after_whole_input_amount);
247 }
248
249 #[rstest]
250 fn test_exact_amount_in_that_is_fully_spent_in_one_for_zero() {
251 let price = encode_sqrt_ratio_x96(1, 1);
252 let price_target = encode_sqrt_ratio_x96(1000, 100);
253 let liquidity = expand_to_18_decimals(2);
254 let amount = expand_to_18_decimals(1);
255 let fee = 600;
256
257 let result = compute_swap_step(
258 price,
259 price_target,
260 liquidity,
261 I256::from_str(&amount.to_string()).unwrap(),
262 fee,
263 )
264 .unwrap();
265
266 assert_eq!(
267 result.amount_in,
268 U256::from_str("999400000000000000").unwrap()
269 );
270 assert_eq!(
271 result.fee_amount,
272 U256::from_str("600000000000000").unwrap()
273 );
274 assert_eq!(
275 result.amount_out,
276 U256::from_str("666399946655997866").unwrap()
277 );
278
279 assert_eq!(result.amount_in + result.fee_amount, U256::from(amount));
281
282 let price_after_whole_input_amount_less_fee = get_next_sqrt_price_from_input(
283 price,
284 liquidity,
285 U256::from(amount) - result.fee_amount,
286 false, );
288
289 assert!(result.sqrt_ratio_next_x96 < price_target);
291 assert_eq!(
293 result.sqrt_ratio_next_x96,
294 price_after_whole_input_amount_less_fee
295 );
296 }
297
298 #[rstest]
299 fn test_exact_amount_out_that_is_fully_received_in_one_for_zero() {
300 let price = encode_sqrt_ratio_x96(1, 1);
301 let price_target = encode_sqrt_ratio_x96(10000, 100);
302 let liquidity = expand_to_18_decimals(2);
303 let amount = expand_to_18_decimals(1);
304 let fee = 600;
305
306 let amount_negative = -I256::from_str(&amount.to_string()).unwrap();
308
309 let result =
310 compute_swap_step(price, price_target, liquidity, amount_negative, fee).unwrap();
311
312 assert_eq!(
313 result.amount_in,
314 U256::from_str("2000000000000000000").unwrap()
315 );
316 assert_eq!(
317 result.fee_amount,
318 U256::from_str("1200720432259356").unwrap()
319 );
320 assert_eq!(result.amount_out, U256::from(amount));
321
322 let price_after_whole_output_amount = get_next_sqrt_price_from_output(
323 price,
324 liquidity,
325 U256::from(amount),
326 false, );
328
329 assert!(result.sqrt_ratio_next_x96 < price_target);
331 assert_eq!(result.sqrt_ratio_next_x96, price_after_whole_output_amount);
333 }
334
335 #[rstest]
336 fn test_amount_out_is_capped_at_the_desired_amount_out() {
337 let result = compute_swap_step(
338 U160::from_str("417332158212080721273783715441582").unwrap(),
339 U160::from_str("1452870262520218020823638996").unwrap(),
340 159344665391607089467575320103,
341 I256::from_str("-1").unwrap(),
342 1,
343 )
344 .unwrap();
345
346 assert_eq!(result.amount_in, U256::from(1));
347 assert_eq!(result.fee_amount, U256::from(1));
348 assert_eq!(result.amount_out, U256::from(1)); assert_eq!(
350 result.sqrt_ratio_next_x96,
351 U160::from_str("417332158212080721273783715441581").unwrap()
352 );
353 }
354
355 #[rstest]
356 fn test_entire_input_amount_taken_as_fee() {
357 let result = compute_swap_step(
358 U160::from_str("2413").unwrap(),
359 U160::from_str("79887613182836312").unwrap(),
360 1985041575832132834610021537970,
361 I256::from_str("10").unwrap(),
362 1872,
363 )
364 .unwrap();
365
366 assert_eq!(result.amount_in, U256::ZERO);
367 assert_eq!(result.fee_amount, U256::from(10));
368 assert_eq!(result.amount_out, U256::ZERO);
369 assert_eq!(result.sqrt_ratio_next_x96, U160::from_str("2413").unwrap());
370 }
371
372 #[rstest]
373 fn test_handles_intermediate_insufficient_liquidity_in_zero_for_one_exact_output_case() {
374 let sqrt_p = U160::from_str("20282409603651670423947251286016").unwrap();
375 let sqrt_p_target = U160::from(U256::from(sqrt_p) * U256::from(11) / U256::from(10));
377 let liquidity = 1024;
378 let amount_remaining = I256::from_str("-4").unwrap();
380 let fee_pips = 3000;
381
382 let result =
383 compute_swap_step(sqrt_p, sqrt_p_target, liquidity, amount_remaining, fee_pips)
384 .unwrap();
385
386 assert_eq!(result.amount_out, U256::ZERO);
387 assert_eq!(result.sqrt_ratio_next_x96, sqrt_p_target);
388 assert_eq!(result.amount_in, U256::from(26215));
389 assert_eq!(result.fee_amount, U256::from(79));
390 }
391
392 #[rstest]
393 fn test_handles_intermediate_insufficient_liquidity_in_one_for_zero_exact_output_case() {
394 let sqrt_p = U160::from_str("20282409603651670423947251286016").unwrap();
395 let sqrt_p_target = U160::from(U256::from(sqrt_p) * U256::from(9) / U256::from(10));
397 let liquidity = 1024;
398 let amount_remaining = I256::from_str("-263000").unwrap();
400 let fee_pips = 3000;
401
402 let result =
403 compute_swap_step(sqrt_p, sqrt_p_target, liquidity, amount_remaining, fee_pips)
404 .unwrap();
405
406 assert_eq!(result.amount_out, U256::from(26214));
407 assert_eq!(result.sqrt_ratio_next_x96, sqrt_p_target);
408 assert_eq!(result.amount_in, U256::from(1));
409 assert_eq!(result.fee_amount, U256::from(1));
410 }
411}