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#[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 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}