nautilus_model/defi/pool_analysis/
swap_math.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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
36/// Computes the result of swapping some amount in, or amount out, given the parameters of the swap.
37///
38/// # Arguments
39///
40/// - `sqrt_ratio_current_x96` - The current sqrt price of the pool
41/// - `sqrt_ratio_target_x96` - The price that cannot be exceeded, from which the direction of the swap is inferred
42/// - `liquidity` - The usable liquidity
43/// - `amount_remaining` - How much input or output amount is remaining to be swapped in/out
44/// - `fee_pips` - The fee taken from the input amount, expressed in hundredths of a bip
45///
46/// # Errors
47///
48/// This function returns an error if:
49/// - Fee adjustment calculations overflow or encounter division by zero.
50/// - Amount or price delta calculations overflow the supported numeric range.
51pub 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    // Represent a direction of the swap, should we move price down (swap token0 for token1)
62    // or price up (swap token1 for token0)
63    let zero_for_one = sqrt_ratio_current_x96 >= sqrt_ratio_target_x96;
64
65    // true = exact input swap (know input amount, calculate output)
66    // false = exact output swap (know desired output, calculate required input)
67    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        // Calculate how much input is needed to reach target, considering fees
75        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        // Calculate how much output can be obtained to reach target
106        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    // get the input/output amounts
137    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    // cap the output amount to not exceed the remaining output amount
172    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        // we didn't reach the target, so take the remainder of the maximum input as fee
178        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////////////////////////////////////////////////////////////////////////////////
192// Tests
193////////////////////////////////////////////////////////////////////////////////
194
195#[cfg(test)]
196mod tests {
197    // Most of the tests are from https://github.com/Uniswap/v3-core/blob/main/test/SwapMath.spec.ts
198    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        // entire amount is not used
234        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, // zero_for_one = false
241        );
242
243        // price is capped at price target
244        assert_eq!(result.sqrt_ratio_next_x96, price_target);
245        // price is less than price after whole input amount
246        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        // entire amount is used
280        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, // zero_for_one = false
287        );
288
289        // price does not reach price target
290        assert!(result.sqrt_ratio_next_x96 < price_target);
291        // price is equal to price after whole input amount
292        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        // Negative amount for exact output
307        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, // zero_for_one = false
327        );
328
329        // price does not reach price target
330        assert!(result.sqrt_ratio_next_x96 < price_target);
331        // price is equal to price after whole output amount
332        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)); // would be 2 if not capped
349        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        // sqrtPTarget = sqrtP * 11 / 10
376        let sqrt_p_target = U160::from(U256::from(sqrt_p) * U256::from(11) / U256::from(10));
377        let liquidity = 1024;
378        // virtual reserves of one are only 4
379        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        // sqrtPTarget = sqrtP * 9 / 10
396        let sqrt_p_target = U160::from(U256::from(sqrt_p) * U256::from(9) / U256::from(10));
397        let liquidity = 1024;
398        // virtual reserves of zero are only 262144
399        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}