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 select_intensity(
48 deviation_per_m: i32,
49 a_to_b: bool,
50 positive_bid_per_m: u32,
51 negative_bid_per_m: u32,
52 positive_ask_per_m: u32,
53 negative_ask_per_m: u32,
54) -> u32 {
55 match (deviation_per_m >= 0, a_to_b) {
56 (true, true) => positive_bid_per_m,
57 (false, true) => negative_bid_per_m,
58 (true, false) => positive_ask_per_m,
59 (false, false) => negative_ask_per_m,
60 }
61}
62
63impl SkewMode {
64 pub(crate) fn compute_skew_per_m(
68 &self,
69 deviation_per_m: i32,
70 quote_type: QuoteType,
71 ) -> Result<i32, CoreError> {
72 match self {
73 Self::None => Ok(0),
74 Self::Polynomial {
75 exponent,
76 positive_bid_per_m,
77 negative_bid_per_m,
78 positive_ask_per_m,
79 negative_ask_per_m,
80 } => {
81 let a_to_b = quote_type.a_to_b();
82 let intensity = select_intensity(
83 deviation_per_m,
84 a_to_b,
85 *positive_bid_per_m,
86 *negative_bid_per_m,
87 *positive_ask_per_m,
88 *negative_ask_per_m,
89 );
90 let sign = deviation_per_m.signum();
91 let abs_dev = deviation_per_m.unsigned_abs() as u128;
92 let exp = exponent.value();
93
94 let numerator = abs_dev
95 .checked_pow(exp)
96 .ok_or(ARITHMETIC_OVERFLOW)?
97 .checked_mul(intensity as u128)
98 .ok_or(ARITHMETIC_OVERFLOW)?;
99 let denominator = (PER_M_DENOMINATOR as u128)
100 .checked_pow(exp)
101 .ok_or(ARITHMETIC_OVERFLOW)?;
102
103 let quotient = numerator
104 .checked_div(denominator)
105 .ok_or(ARITHMETIC_OVERFLOW)?;
106 let remainder = numerator
107 .checked_rem(denominator)
108 .ok_or(ARITHMETIC_OVERFLOW)?;
109 let abs_result = if remainder > 0 {
110 quotient.checked_add(1).ok_or(ARITHMETIC_OVERFLOW)?
111 } else {
112 quotient
113 };
114
115 let result = i32::try_from(abs_result).map_err(|_| AMOUNT_EXCEEDS_MAX_I32)?;
116 sign.checked_mul(result).ok_or(ARITHMETIC_OVERFLOW)
117 }
118 }
119 }
120}
121
122pub(crate) fn apply_skew_to_liquidity(
125 liquidity: SingleSideLiquidity,
126 skew_per_m: i32,
127 quote_type: QuoteType,
128) -> Result<SingleSideLiquidity, CoreError> {
129 let clamped = skew_per_m.clamp(-PER_M_DENOMINATOR, PER_M_DENOMINATOR);
130 if clamped == 0 {
131 return Ok(liquidity);
132 }
133 let a_to_b = quote_type.a_to_b();
134 let widening = a_to_b == (clamped > 0);
135 let abs_skew = U256::from(clamped.unsigned_abs());
136 let denom = U256::from(PER_M_DENOMINATOR as u64);
137 let mut result = SingleSideLiquidity::new();
138 for &(price, amount) in liquidity.as_slice() {
139 let numerator = U256::from(price)
140 .checked_mul(abs_skew)
141 .ok_or(ARITHMETIC_OVERFLOW)?;
142 let quotient = numerator.checked_div(denom).ok_or(ARITHMETIC_OVERFLOW)?;
143 let remainder = numerator.checked_rem(denom).ok_or(ARITHMETIC_OVERFLOW)?;
144 let delta: u128 = if widening && remainder > U256::ZERO {
145 quotient.checked_add(U256::ONE).ok_or(ARITHMETIC_OVERFLOW)?
146 } else {
147 quotient
148 }
149 .try_into()
150 .map_err(|_| ARITHMETIC_OVERFLOW)?;
151 let adjusted_price = if clamped > 0 {
152 price.checked_sub(delta).ok_or(ARITHMETIC_OVERFLOW)?
153 } else {
154 price.checked_add(delta).ok_or(ARITHMETIC_OVERFLOW)?
155 };
156 result.push((adjusted_price, amount));
157 }
158 Ok(result)
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use rstest::rstest;
165
166 const PRICE_ONE: u128 = 1 << 64;
167
168 #[rstest]
169 #[case(SkewMode::None, 200_000, QuoteType::TokenAExactIn, 0)]
170 #[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)]
172 #[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)]
173 #[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)]
174 #[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)]
176 #[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)]
177 #[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)]
179 #[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)]
180 #[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)]
182 #[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)]
184 #[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)]
185 #[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)]
188 #[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)]
190 #[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)]
192 #[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)]
194 #[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)]
196 #[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)]
197 #[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)]
198 #[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)]
199 #[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)]
201 #[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)]
203 #[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)]
206 #[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)]
208 #[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)]
210 #[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)]
211 #[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)]
212 #[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)]
213 #[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)]
216 #[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)]
217 #[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)]
218 #[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)]
219 fn test_compute_skew_per_m(
220 #[case] skew_mode: SkewMode,
221 #[case] deviation_per_m: i32,
222 #[case] quote_type: QuoteType,
223 #[case] expected: i32,
224 ) {
225 assert_eq!(
226 skew_mode
227 .compute_skew_per_m(deviation_per_m, quote_type)
228 .unwrap(),
229 expected
230 );
231 }
232
233 #[rstest]
234 #[case(PRICE_ONE, 0, QuoteType::TokenAExactIn, PRICE_ONE)]
236 #[case(PRICE_ONE, 500_000, QuoteType::TokenAExactIn, PRICE_ONE / 2)]
237 #[case(PRICE_ONE, -500_000, QuoteType::TokenBExactIn, PRICE_ONE + PRICE_ONE / 2)]
238 #[case(PRICE_ONE, 250_000, QuoteType::TokenAExactIn, PRICE_ONE * 3 / 4)]
239 #[case(PRICE_ONE, 1_000_000, QuoteType::TokenAExactIn, 0)]
240 #[case(PRICE_ONE, -1_000_000, QuoteType::TokenBExactIn, PRICE_ONE * 2)]
241 #[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))]
245 #[case(PRICE_ONE, -999_999, QuoteType::TokenBExactIn, PRICE_ONE + (PRICE_ONE * 999_999).div_ceil(1_000_000))]
246 #[case(PRICE_ONE, -999_999, QuoteType::TokenAExactIn, PRICE_ONE + PRICE_ONE * 999_999 / 1_000_000)]
248 #[case(PRICE_ONE, 999_999, QuoteType::TokenBExactIn, PRICE_ONE - PRICE_ONE * 999_999 / 1_000_000)]
249 fn test_apply_skew_to_liquidity(
250 #[case] price: u128,
251 #[case] skew_per_m: i32,
252 #[case] quote_type: QuoteType,
253 #[case] expected_price: u128,
254 ) {
255 let liquidity = SingleSideLiquidity::from_slice(&[(price, 1000)]);
256 let result = apply_skew_to_liquidity(liquidity, skew_per_m, quote_type).unwrap();
257 let (result_price, result_amount) = result.as_slice()[0];
258 assert_eq!(result_price, expected_price);
259 assert_eq!(result_amount, 1000);
260 }
261}