1use borsh::{BorshDeserialize, BorshSerialize};
2#[cfg(feature = "wasm")]
3use riptide_amm_macros::wasm_expose;
4
5use super::{
6 super::{
7 error::{CoreError, AMOUNT_EXCEEDS_MAX_I32, ARITHMETIC_OVERFLOW},
8 quote::QuoteType,
9 },
10 SingleSideLiquidity, PER_M_DENOMINATOR,
11};
12use ethnum::U256;
13
14#[derive(Debug, Clone, Copy, Eq, PartialEq)]
15#[cfg_attr(true, derive(BorshDeserialize, BorshSerialize))]
16#[cfg_attr(feature = "wasm", wasm_expose)]
17pub enum SkewExponent {
18 Linear,
19 Quadratic,
20 Cubic,
21}
22
23impl SkewExponent {
24 pub fn value(&self) -> u32 {
25 match self {
26 Self::Linear => 1,
27 Self::Quadratic => 2,
28 Self::Cubic => 3,
29 }
30 }
31}
32
33#[derive(Debug, Clone, Copy, Eq, PartialEq)]
34#[cfg_attr(true, derive(BorshDeserialize, BorshSerialize))]
35#[cfg_attr(feature = "wasm", wasm_expose)]
36pub enum SkewMode {
37 None,
38 Polynomial {
39 exponent: SkewExponent,
40 positive_bid_per_m: u32,
41 negative_bid_per_m: u32,
42 positive_ask_per_m: u32,
43 negative_ask_per_m: u32,
44 },
45}
46
47fn apply_skew_cliff(deviation_per_m: i32, skew_cliff_min: i32, skew_cliff_max: i32) -> i32 {
53 if deviation_per_m >= skew_cliff_min && deviation_per_m <= skew_cliff_max {
54 0
55 } else if deviation_per_m > skew_cliff_max {
56 deviation_per_m - skew_cliff_max
57 } else {
58 deviation_per_m - skew_cliff_min
60 }
61}
62
63#[allow(clippy::too_many_arguments)]
66fn polynomial_skew(
67 deviation_per_m: i32,
68 a_to_b: bool,
69 exponent: SkewExponent,
70 positive_bid_per_m: u32,
71 negative_bid_per_m: u32,
72 positive_ask_per_m: u32,
73 negative_ask_per_m: u32,
74) -> Result<i32, CoreError> {
75 let intensity = select_intensity(
76 deviation_per_m,
77 a_to_b,
78 positive_bid_per_m,
79 negative_bid_per_m,
80 positive_ask_per_m,
81 negative_ask_per_m,
82 );
83 let sign = deviation_per_m.signum();
84 let abs_dev = deviation_per_m.unsigned_abs() as u128;
85 let exp = exponent.value();
86
87 let numerator = abs_dev
88 .checked_pow(exp)
89 .ok_or(ARITHMETIC_OVERFLOW)?
90 .checked_mul(intensity as u128)
91 .ok_or(ARITHMETIC_OVERFLOW)?;
92 let denominator = (PER_M_DENOMINATOR as u128)
93 .checked_pow(exp)
94 .ok_or(ARITHMETIC_OVERFLOW)?;
95
96 let quotient = numerator
97 .checked_div(denominator)
98 .ok_or(ARITHMETIC_OVERFLOW)?;
99 let remainder = numerator
100 .checked_rem(denominator)
101 .ok_or(ARITHMETIC_OVERFLOW)?;
102 let abs_result = if remainder > 0 {
103 quotient.checked_add(1).ok_or(ARITHMETIC_OVERFLOW)?
104 } else {
105 quotient
106 };
107
108 let result = i32::try_from(abs_result).map_err(|_| AMOUNT_EXCEEDS_MAX_I32)?;
109 sign.checked_mul(result).ok_or(ARITHMETIC_OVERFLOW)
110}
111
112fn select_intensity(
113 deviation_per_m: i32,
114 a_to_b: bool,
115 positive_bid_per_m: u32,
116 negative_bid_per_m: u32,
117 positive_ask_per_m: u32,
118 negative_ask_per_m: u32,
119) -> u32 {
120 match (deviation_per_m >= 0, a_to_b) {
121 (true, true) => positive_bid_per_m,
122 (false, true) => negative_bid_per_m,
123 (true, false) => positive_ask_per_m,
124 (false, false) => negative_ask_per_m,
125 }
126}
127
128impl SkewMode {
129 pub(crate) fn compute_skew_per_m(
145 &self,
146 deviation_per_m: i32,
147 quote_type: QuoteType,
148 skew_cliff_min_per_m: i32,
149 skew_cliff_max_per_m: i32,
150 ) -> Result<i32, CoreError> {
151 match self {
152 Self::None => Ok(0),
153 Self::Polynomial {
154 exponent,
155 positive_bid_per_m,
156 negative_bid_per_m,
157 positive_ask_per_m,
158 negative_ask_per_m,
159 } => {
160 if skew_cliff_min_per_m > 0
161 || skew_cliff_max_per_m < 0
162 || skew_cliff_min_per_m <= -PER_M_DENOMINATOR
163 || skew_cliff_max_per_m >= PER_M_DENOMINATOR
164 {
165 return Err(super::super::error::INVALID_ORACLE_DATA);
166 }
167 let shifted =
168 apply_skew_cliff(deviation_per_m, skew_cliff_min_per_m, skew_cliff_max_per_m);
169 if shifted == 0 {
170 return Ok(0);
171 }
172 polynomial_skew(
173 shifted,
174 quote_type.a_to_b(),
175 *exponent,
176 *positive_bid_per_m,
177 *negative_bid_per_m,
178 *positive_ask_per_m,
179 *negative_ask_per_m,
180 )
181 }
182 }
183 }
184}
185
186pub(crate) fn apply_skew_to_liquidity(
189 liquidity: SingleSideLiquidity,
190 skew_per_m: i32,
191 quote_type: QuoteType,
192) -> Result<SingleSideLiquidity, CoreError> {
193 let clamped = skew_per_m.clamp(-PER_M_DENOMINATOR, PER_M_DENOMINATOR);
194 if clamped == 0 {
195 return Ok(liquidity);
196 }
197 let a_to_b = quote_type.a_to_b();
198 let widening = a_to_b == (clamped > 0);
199 let abs_skew = U256::from(clamped.unsigned_abs());
200 let denom = U256::from(PER_M_DENOMINATOR as u64);
201 let mut result = SingleSideLiquidity::new();
202 for &(price, amount) in liquidity.as_slice() {
203 let numerator = U256::from(price)
204 .checked_mul(abs_skew)
205 .ok_or(ARITHMETIC_OVERFLOW)?;
206 let quotient = numerator.checked_div(denom).ok_or(ARITHMETIC_OVERFLOW)?;
207 let remainder = numerator.checked_rem(denom).ok_or(ARITHMETIC_OVERFLOW)?;
208 let delta: u128 = if widening && remainder > U256::ZERO {
209 quotient.checked_add(U256::ONE).ok_or(ARITHMETIC_OVERFLOW)?
210 } else {
211 quotient
212 }
213 .try_into()
214 .map_err(|_| ARITHMETIC_OVERFLOW)?;
215 let adjusted_price = if clamped > 0 {
216 price.checked_sub(delta).ok_or(ARITHMETIC_OVERFLOW)?
217 } else {
218 price.checked_add(delta).ok_or(ARITHMETIC_OVERFLOW)?
219 };
220 result.push((adjusted_price, amount));
221 }
222 Ok(result)
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use rstest::rstest;
229
230 const PRICE_ONE: u128 = 1 << 64;
231
232 #[rstest]
233 #[case(SkewMode::None, 200_000, QuoteType::TokenAExactIn, 0)]
234 #[case(SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 500_000, negative_bid_per_m: 500_000, positive_ask_per_m: 500_000, negative_ask_per_m: 500_000 }, 200_000, QuoteType::TokenAExactIn, 100_000)]
236 #[case(SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 500_000, negative_bid_per_m: 500_000, positive_ask_per_m: 500_000, negative_ask_per_m: 500_000 }, -200_000, QuoteType::TokenAExactIn, -100_000)]
237 #[case(SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 1_000_000, negative_bid_per_m: 1_000_000, positive_ask_per_m: 1_000_000, negative_ask_per_m: 1_000_000 }, 1_000_000, QuoteType::TokenAExactIn, 1_000_000)]
238 #[case(SkewMode::Polynomial { exponent: SkewExponent::Quadratic, positive_bid_per_m: 1_000_000, negative_bid_per_m: 1_000_000, positive_ask_per_m: 1_000_000, negative_ask_per_m: 1_000_000 }, 200_000, QuoteType::TokenAExactIn, 40_000)]
240 #[case(SkewMode::Polynomial { exponent: SkewExponent::Quadratic, positive_bid_per_m: 1_000_000, negative_bid_per_m: 1_000_000, positive_ask_per_m: 1_000_000, negative_ask_per_m: 1_000_000 }, -200_000, QuoteType::TokenAExactIn, -40_000)]
241 #[case(SkewMode::Polynomial { exponent: SkewExponent::Cubic, positive_bid_per_m: 1_000_000, negative_bid_per_m: 1_000_000, positive_ask_per_m: 1_000_000, negative_ask_per_m: 1_000_000 }, 200_000, QuoteType::TokenAExactIn, 8_000)]
243 #[case(SkewMode::Polynomial { exponent: SkewExponent::Cubic, positive_bid_per_m: 1_000_000, negative_bid_per_m: 1_000_000, positive_ask_per_m: 1_000_000, negative_ask_per_m: 1_000_000 }, -200_000, QuoteType::TokenAExactIn, -8_000)]
244 #[case(SkewMode::Polynomial { exponent: SkewExponent::Cubic, positive_bid_per_m: 1_000_000, negative_bid_per_m: 1_000_000, positive_ask_per_m: 1_000_000, negative_ask_per_m: 1_000_000 }, 1_000_000, QuoteType::TokenAExactIn, 1_000_000)]
246 #[case(SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 1_000_000, negative_bid_per_m: 1_000_000, positive_ask_per_m: 1_000_000, negative_ask_per_m: 1_000_000 }, 1, QuoteType::TokenAExactIn, 1)]
248 #[case(SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 1_000_000, negative_bid_per_m: 1_000_000, positive_ask_per_m: 1_000_000, negative_ask_per_m: 1_000_000 }, -1, QuoteType::TokenAExactOut, -1)]
249 #[case(SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 300_000, negative_bid_per_m: 300_000, positive_ask_per_m: 300_000, negative_ask_per_m: 300_000 }, 700_001, QuoteType::TokenAExactIn, 210_001)]
252 #[case(SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 300_000, negative_bid_per_m: 300_000, positive_ask_per_m: 300_000, negative_ask_per_m: 300_000 }, 700_001, QuoteType::TokenAExactOut, 210_001)]
254 #[case(SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 300_000, negative_bid_per_m: 300_000, positive_ask_per_m: 300_000, negative_ask_per_m: 300_000 }, -700_001, QuoteType::TokenAExactIn, -210_001)]
256 #[case(SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 300_000, negative_bid_per_m: 300_000, positive_ask_per_m: 300_000, negative_ask_per_m: 300_000 }, -700_001, QuoteType::TokenAExactOut, -210_001)]
258 #[case(SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 100_000, negative_bid_per_m: 200_000, positive_ask_per_m: 300_000, negative_ask_per_m: 400_000 }, 500_000, QuoteType::TokenAExactIn, 50_000)]
260 #[case(SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 100_000, negative_bid_per_m: 200_000, positive_ask_per_m: 300_000, negative_ask_per_m: 400_000 }, -500_000, QuoteType::TokenAExactIn, -100_000)]
261 #[case(SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 100_000, negative_bid_per_m: 200_000, positive_ask_per_m: 300_000, negative_ask_per_m: 400_000 }, 500_000, QuoteType::TokenBExactIn, 150_000)]
262 #[case(SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 100_000, negative_bid_per_m: 200_000, positive_ask_per_m: 300_000, negative_ask_per_m: 400_000 }, -500_000, QuoteType::TokenBExactIn, -200_000)]
263 #[case(SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 100_000, negative_bid_per_m: 200_000, positive_ask_per_m: 300_000, negative_ask_per_m: 400_000 }, 0, QuoteType::TokenAExactIn, 0)]
265 #[case(SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 100_000, negative_bid_per_m: 200_000, positive_ask_per_m: 300_000, negative_ask_per_m: 400_000 }, 700_001, QuoteType::TokenAExactIn, 70_001)]
267 #[case(SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 100_000, negative_bid_per_m: 200_000, positive_ask_per_m: 300_000, negative_ask_per_m: 400_000 }, 700_001, QuoteType::TokenBExactIn, 210_001)]
270 #[case(SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 0, negative_bid_per_m: 500_000, positive_ask_per_m: 500_000, negative_ask_per_m: 500_000 }, 500_000, QuoteType::TokenAExactIn, 0)]
272 #[case(SkewMode::Polynomial { exponent: SkewExponent::Quadratic, positive_bid_per_m: 100_000, negative_bid_per_m: 200_000, positive_ask_per_m: 300_000, negative_ask_per_m: 400_000 }, 500_000, QuoteType::TokenAExactIn, 25_000)]
274 #[case(SkewMode::Polynomial { exponent: SkewExponent::Quadratic, positive_bid_per_m: 100_000, negative_bid_per_m: 200_000, positive_ask_per_m: 300_000, negative_ask_per_m: 400_000 }, -500_000, QuoteType::TokenAExactIn, -50_000)]
275 #[case(SkewMode::Polynomial { exponent: SkewExponent::Quadratic, positive_bid_per_m: 100_000, negative_bid_per_m: 200_000, positive_ask_per_m: 300_000, negative_ask_per_m: 400_000 }, 500_000, QuoteType::TokenBExactIn, 75_000)]
276 #[case(SkewMode::Polynomial { exponent: SkewExponent::Quadratic, positive_bid_per_m: 100_000, negative_bid_per_m: 200_000, positive_ask_per_m: 300_000, negative_ask_per_m: 400_000 }, -500_000, QuoteType::TokenBExactIn, -100_000)]
277 #[case(SkewMode::Polynomial { exponent: SkewExponent::Cubic, positive_bid_per_m: 100_000, negative_bid_per_m: 200_000, positive_ask_per_m: 300_000, negative_ask_per_m: 400_000 }, 500_000, QuoteType::TokenAExactIn, 12_500)]
280 #[case(SkewMode::Polynomial { exponent: SkewExponent::Cubic, positive_bid_per_m: 100_000, negative_bid_per_m: 200_000, positive_ask_per_m: 300_000, negative_ask_per_m: 400_000 }, -500_000, QuoteType::TokenAExactIn, -25_000)]
281 #[case(SkewMode::Polynomial { exponent: SkewExponent::Cubic, positive_bid_per_m: 100_000, negative_bid_per_m: 200_000, positive_ask_per_m: 300_000, negative_ask_per_m: 400_000 }, 500_000, QuoteType::TokenBExactIn, 37_500)]
282 #[case(SkewMode::Polynomial { exponent: SkewExponent::Cubic, positive_bid_per_m: 100_000, negative_bid_per_m: 200_000, positive_ask_per_m: 300_000, negative_ask_per_m: 400_000 }, -500_000, QuoteType::TokenBExactIn, -50_000)]
283 fn test_compute_skew_per_m(
284 #[case] skew_mode: SkewMode,
285 #[case] deviation_per_m: i32,
286 #[case] quote_type: QuoteType,
287 #[case] expected: i32,
288 ) {
289 assert_eq!(
292 skew_mode
293 .compute_skew_per_m(deviation_per_m, quote_type, 0, 0)
294 .unwrap(),
295 expected
296 );
297 }
298
299 #[rstest]
303 #[case(0, -50_000, 50_000, 0)]
305 #[case(50_000, -50_000, 50_000, 0)]
306 #[case(-50_000, -50_000, 50_000, 0)]
307 #[case(200_000, -50_000, 50_000, 150_000)]
309 #[case(-200_000, -50_000, 50_000, -150_000)]
310 #[case(50_001, -50_000, 50_000, 1)]
312 #[case(70_000, -30_000, 80_000, 0)]
314 #[case(-30_000, -30_000, 80_000, 0)]
315 #[case(130_000, -30_000, 80_000, 50_000)]
316 #[case(-130_000, -30_000, 80_000, -100_000)]
317 #[case(500_000, -999_999, 999_999, 0)]
319 fn test_compute_skew_per_m_with_skew_cliff(
320 #[case] deviation_per_m: i32,
321 #[case] skew_cliff_min_per_m: i32,
322 #[case] skew_cliff_max_per_m: i32,
323 #[case] expected: i32,
324 ) {
325 let skew = SkewMode::Polynomial {
326 exponent: SkewExponent::Linear,
327 positive_bid_per_m: 1_000_000,
328 negative_bid_per_m: 1_000_000,
329 positive_ask_per_m: 1_000_000,
330 negative_ask_per_m: 1_000_000,
331 };
332 assert_eq!(
333 skew.compute_skew_per_m(
334 deviation_per_m,
335 QuoteType::TokenAExactIn,
336 skew_cliff_min_per_m,
337 skew_cliff_max_per_m,
338 )
339 .unwrap(),
340 expected
341 );
342 }
343
344 #[rstest]
346 #[case(10_000, 50_000)]
348 #[case(-50_000, -10_000)]
350 #[case(-PER_M_DENOMINATOR, 50_000)]
352 #[case(-50_000, PER_M_DENOMINATOR)]
354 fn test_compute_skew_per_m_rejects_invalid_skew_cliff(
355 #[case] skew_cliff_min_per_m: i32,
356 #[case] skew_cliff_max_per_m: i32,
357 ) {
358 let skew = SkewMode::Polynomial {
359 exponent: SkewExponent::Linear,
360 positive_bid_per_m: 0,
361 negative_bid_per_m: 0,
362 positive_ask_per_m: 0,
363 negative_ask_per_m: 0,
364 };
365 let result = skew.compute_skew_per_m(
366 100_000,
367 QuoteType::TokenAExactIn,
368 skew_cliff_min_per_m,
369 skew_cliff_max_per_m,
370 );
371 assert!(result.is_err());
372 }
373
374 #[rstest]
378 #[case(0, 0)]
380 #[case(-50_000, 50_000)]
381 #[case(10_000, 50_000)]
383 #[case(-50_000, -10_000)]
384 #[case(-PER_M_DENOMINATOR, 50_000)]
385 #[case(-50_000, PER_M_DENOMINATOR)]
386 fn test_compute_skew_per_m_none_ignores_skew_cliff(
387 #[case] skew_cliff_min_per_m: i32,
388 #[case] skew_cliff_max_per_m: i32,
389 ) {
390 assert_eq!(
391 SkewMode::None
392 .compute_skew_per_m(
393 500_000,
394 QuoteType::TokenAExactIn,
395 skew_cliff_min_per_m,
396 skew_cliff_max_per_m,
397 )
398 .unwrap(),
399 0
400 );
401 }
402
403 #[rstest]
404 #[case(PRICE_ONE, 0, QuoteType::TokenAExactIn, PRICE_ONE)]
406 #[case(PRICE_ONE, 500_000, QuoteType::TokenAExactIn, PRICE_ONE / 2)]
407 #[case(PRICE_ONE, -500_000, QuoteType::TokenBExactIn, PRICE_ONE + PRICE_ONE / 2)]
408 #[case(PRICE_ONE, 250_000, QuoteType::TokenAExactIn, PRICE_ONE * 3 / 4)]
409 #[case(PRICE_ONE, 1_000_000, QuoteType::TokenAExactIn, 0)]
410 #[case(PRICE_ONE, -1_000_000, QuoteType::TokenBExactIn, PRICE_ONE * 2)]
411 #[case(PRICE_ONE, 2_000_000, QuoteType::TokenAExactIn, 0)] #[case(PRICE_ONE, -2_000_000, QuoteType::TokenBExactIn, PRICE_ONE * 2)] #[case(PRICE_ONE, 999_999, QuoteType::TokenAExactIn, PRICE_ONE - (PRICE_ONE * 999_999).div_ceil(1_000_000))]
415 #[case(PRICE_ONE, -999_999, QuoteType::TokenBExactIn, PRICE_ONE + (PRICE_ONE * 999_999).div_ceil(1_000_000))]
416 #[case(PRICE_ONE, -999_999, QuoteType::TokenAExactIn, PRICE_ONE + PRICE_ONE * 999_999 / 1_000_000)]
418 #[case(PRICE_ONE, 999_999, QuoteType::TokenBExactIn, PRICE_ONE - PRICE_ONE * 999_999 / 1_000_000)]
419 fn test_apply_skew_to_liquidity(
420 #[case] price: u128,
421 #[case] skew_per_m: i32,
422 #[case] quote_type: QuoteType,
423 #[case] expected_price: u128,
424 ) {
425 let liquidity = SingleSideLiquidity::from_slice(&[(price, 1000)]);
426 let result = apply_skew_to_liquidity(liquidity, skew_per_m, quote_type).unwrap();
427 let (result_price, result_amount) = result.as_slice()[0];
428 assert_eq!(result_price, expected_price);
429 assert_eq!(result_amount, 1000);
430 }
431}