Skip to main content

evm_dex_pool/v3/
swap_math.rs

1use alloy::primitives::{aliases::U24, Uint, I256, U160, U256};
2use anyhow::{anyhow, Result};
3
4use crate::v3::{
5    get_amount_0_delta, get_amount_1_delta, get_next_sqrt_price_from_input,
6    get_next_sqrt_price_from_output, mul_div, mul_div_rounding_up, MIN_TICK_I32,
7};
8
9use super::{
10    add_delta, get_sqrt_ratio_at_tick, TickDataProvider, TickIndex, TickMap, TickMath,
11    MAX_SQRT_RATIO, MAX_TICK_I32, MIN_SQRT_RATIO, ONE,
12};
13
14#[derive(Clone, Copy, Debug, Default)]
15pub struct SwapState<I = i32> {
16    pub amount_specified_remaining: I256,
17    pub amount_calculated: I256,
18    pub sqrt_price_x96: U160,
19    pub tick_current: I,
20    pub liquidity: u128,
21}
22
23#[derive(Clone, Copy, Debug, Default)]
24struct StepComputations<I = i32> {
25    sqrt_price_start_x96: U160,
26    tick_next: I,
27    initialized: bool,
28    sqrt_price_next_x96: U160,
29    amount_in: U256,
30    amount_out: U256,
31    fee_amount: U256,
32}
33
34/// Computes the result of swapping some amount in, or amount out, given the parameters of the swap
35#[inline]
36pub fn compute_swap_step<const BITS: usize, const LIMBS: usize>(
37    sqrt_ratio_current_x96: Uint<BITS, LIMBS>,
38    sqrt_ratio_target_x96: Uint<BITS, LIMBS>,
39    liquidity: u128,
40    amount_remaining: I256,
41    fee_pips: U24,
42) -> Result<(Uint<BITS, LIMBS>, U256, U256, U256)> {
43    const MAX_FEE: U256 = U256::from_limbs([1000000, 0, 0, 0]);
44    let fee_pips = U256::from(fee_pips);
45    let fee_complement = MAX_FEE - fee_pips;
46    let zero_for_one = sqrt_ratio_current_x96 >= sqrt_ratio_target_x96;
47    let exact_in = amount_remaining >= I256::ZERO;
48
49    let sqrt_ratio_next_x96: Uint<BITS, LIMBS>;
50    let mut amount_in: U256;
51    let mut amount_out: U256;
52    let fee_amount: U256;
53    if exact_in {
54        let amount_remaining_abs = amount_remaining.into_raw();
55        let amount_remaining_less_fee = mul_div(amount_remaining_abs, fee_complement, MAX_FEE)?;
56
57        amount_in = if zero_for_one {
58            get_amount_0_delta(
59                sqrt_ratio_target_x96,
60                sqrt_ratio_current_x96,
61                liquidity,
62                true,
63            )?
64        } else {
65            get_amount_1_delta(
66                sqrt_ratio_current_x96,
67                sqrt_ratio_target_x96,
68                liquidity,
69                true,
70            )?
71        };
72
73        if amount_remaining_less_fee >= amount_in {
74            sqrt_ratio_next_x96 = sqrt_ratio_target_x96;
75            fee_amount = mul_div_rounding_up(amount_in, fee_pips, fee_complement)?;
76        } else {
77            amount_in = amount_remaining_less_fee;
78            sqrt_ratio_next_x96 = get_next_sqrt_price_from_input(
79                sqrt_ratio_current_x96,
80                liquidity,
81                amount_in,
82                zero_for_one,
83            )?;
84            fee_amount = amount_remaining_abs - amount_in;
85        }
86
87        amount_out = if zero_for_one {
88            get_amount_1_delta(
89                sqrt_ratio_next_x96,
90                sqrt_ratio_current_x96,
91                liquidity,
92                false,
93            )?
94        } else {
95            get_amount_0_delta(
96                sqrt_ratio_current_x96,
97                sqrt_ratio_next_x96,
98                liquidity,
99                false,
100            )?
101        };
102    } else {
103        let amount_remaining_abs = (-amount_remaining).into_raw();
104
105        amount_out = if zero_for_one {
106            get_amount_1_delta(
107                sqrt_ratio_target_x96,
108                sqrt_ratio_current_x96,
109                liquidity,
110                false,
111            )?
112        } else {
113            get_amount_0_delta(
114                sqrt_ratio_current_x96,
115                sqrt_ratio_target_x96,
116                liquidity,
117                false,
118            )?
119        };
120
121        if amount_remaining_abs >= amount_out {
122            sqrt_ratio_next_x96 = sqrt_ratio_target_x96;
123        } else {
124            amount_out = amount_remaining_abs;
125            sqrt_ratio_next_x96 = get_next_sqrt_price_from_output(
126                sqrt_ratio_current_x96,
127                liquidity,
128                amount_out,
129                zero_for_one,
130            )?;
131        }
132
133        amount_in = if zero_for_one {
134            get_amount_0_delta(sqrt_ratio_next_x96, sqrt_ratio_current_x96, liquidity, true)?
135        } else {
136            get_amount_1_delta(sqrt_ratio_current_x96, sqrt_ratio_next_x96, liquidity, true)?
137        };
138        fee_amount = mul_div_rounding_up(amount_in, fee_pips, fee_complement)?;
139    }
140
141    Ok((sqrt_ratio_next_x96, amount_in, amount_out, fee_amount))
142}
143
144#[inline]
145#[allow(clippy::too_many_arguments)]
146pub fn v3_swap(
147    fee: U24,
148    sqrt_price_x96: U160,
149    tick_current: i32,
150    liquidity: u128,
151    tick_data_provider: &TickMap,
152    zero_for_one: bool,
153    amount_specified: I256,
154    sqrt_price_limit_x96: Option<U160>,
155) -> Result<SwapState<i32>> {
156    let sqrt_price_limit_x96 = sqrt_price_limit_x96.unwrap_or(if zero_for_one {
157        MIN_SQRT_RATIO + ONE
158    } else {
159        MAX_SQRT_RATIO - ONE
160    });
161
162    if zero_for_one {
163        if !(sqrt_price_limit_x96 > MIN_SQRT_RATIO) {
164            return Err(anyhow!("RATIO_MIN"));
165        }
166        if !(sqrt_price_limit_x96 < sqrt_price_x96) {
167            return Err(anyhow!("RATIO_CURRENT"));
168        }
169    } else {
170        if !(sqrt_price_limit_x96 < MAX_SQRT_RATIO) {
171            return Err(anyhow!("RATIO_MAX"));
172        }
173        if !(sqrt_price_limit_x96 > sqrt_price_x96) {
174            return Err(anyhow!("RATIO_CURRENT"));
175        }
176    }
177
178    let exact_input = amount_specified >= I256::ZERO;
179
180    let mut state = SwapState {
181        amount_specified_remaining: amount_specified,
182        amount_calculated: I256::ZERO,
183        sqrt_price_x96,
184        tick_current,
185        liquidity,
186    };
187
188    while !state.amount_specified_remaining.is_zero()
189        && state.sqrt_price_x96 != sqrt_price_limit_x96
190    {
191        let mut step = StepComputations {
192            sqrt_price_start_x96: state.sqrt_price_x96,
193            ..Default::default()
194        };
195
196        (step.tick_next, step.initialized) = tick_data_provider
197            .next_initialized_tick_within_one_word(state.tick_current, zero_for_one)?;
198        step.tick_next = step.tick_next.clamp(MIN_TICK_I32, MAX_TICK_I32);
199        step.sqrt_price_next_x96 = get_sqrt_ratio_at_tick(step.tick_next.to_i24())?;
200
201        (
202            state.sqrt_price_x96,
203            step.amount_in,
204            step.amount_out,
205            step.fee_amount,
206        ) = compute_swap_step(
207            state.sqrt_price_x96,
208            if zero_for_one {
209                step.sqrt_price_next_x96.max(sqrt_price_limit_x96)
210            } else {
211                step.sqrt_price_next_x96.min(sqrt_price_limit_x96)
212            },
213            state.liquidity,
214            state.amount_specified_remaining,
215            fee,
216        )?;
217
218        if exact_input {
219            state.amount_specified_remaining = I256::from_raw(
220                state.amount_specified_remaining.into_raw() - step.amount_in - step.fee_amount,
221            );
222            state.amount_calculated =
223                I256::from_raw(state.amount_calculated.into_raw() - step.amount_out);
224        } else {
225            state.amount_specified_remaining =
226                I256::from_raw(state.amount_specified_remaining.into_raw() + step.amount_out);
227            state.amount_calculated = I256::from_raw(
228                state.amount_calculated.into_raw() + step.amount_in + step.fee_amount,
229            );
230        }
231
232        // Detect no-progress: if neither price nor remaining changed, we're stuck
233        if step.amount_in.is_zero() && step.amount_out.is_zero() && step.fee_amount.is_zero() {
234            return Err(anyhow!(
235                "v3_swap: no progress (zero amounts, liquidity={}, tick={})",
236                state.liquidity,
237                state.tick_current
238            ));
239        }
240
241        if state.sqrt_price_x96 == step.sqrt_price_next_x96 {
242            if step.initialized {
243                let mut liquidity_net = tick_data_provider.get_tick(step.tick_next)?.liquidity_net;
244                if zero_for_one {
245                    liquidity_net = -liquidity_net;
246                }
247                state.liquidity = add_delta(state.liquidity, liquidity_net)?;
248            }
249            state.tick_current = if zero_for_one {
250                step.tick_next - i32::ONE
251            } else {
252                step.tick_next
253            };
254
255            if state.liquidity == 0 {
256                break;
257            }
258        } else if state.sqrt_price_x96 != step.sqrt_price_start_x96 {
259            state.tick_current =
260                TickIndex::from_i24(state.sqrt_price_x96.get_tick_at_sqrt_ratio()?);
261        }
262    }
263
264    Ok(state)
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use alloy::primitives::U160;
271
272    #[test]
273    fn test_compute_swap_step() {
274        let amount_specified_remaining = I256::from_raw(U256::from_limbs([
275            18446744073709540431,
276            18446744073709551615,
277            18446744073709551615,
278            18446744073709551615,
279        ]));
280        let (sqrt_price_next_x96, amount_in, amount_out, fee_amount) = compute_swap_step(
281            U160::from_limbs([7164297123421688246, 4074563739, 0]),
282            U160::from_limbs([7829751401545787782, 4282102344, 0]),
283            94868,
284            amount_specified_remaining,
285            U24::from(3000),
286        )
287        .unwrap();
288        assert_eq!(
289            sqrt_price_next_x96,
290            U160::from_limbs([7829751401545787782, 4282102344, 0])
291        );
292        assert_eq!(amount_in, U256::from_limbs([4585, 0, 0, 0]));
293        assert_eq!(amount_out, U256::from_limbs([4846, 0, 0, 0]));
294        assert_eq!(fee_amount, U256::from_limbs([14, 0, 0, 0]));
295    }
296}