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
47pub(crate) fn compute_deviation_per_m(
51 price_q64_64: u128,
52 reserves_a: u64,
53 reserves_b: u64,
54) -> Result<i32, CoreError> {
55 let value_a = U256::from(reserves_a as u128)
56 .checked_mul(U256::from(price_q64_64))
57 .ok_or(ARITHMETIC_OVERFLOW)?;
58 let value_b = U256::from(reserves_b as u128)
59 .checked_shl(64)
60 .ok_or(ARITHMETIC_OVERFLOW)?;
61 let total = value_a.checked_add(value_b).ok_or(ARITHMETIC_OVERFLOW)?;
62 if total == U256::ZERO {
63 return Ok(0);
64 }
65 let twice_value_a = value_a
66 .checked_mul(U256::from(2u128))
67 .ok_or(ARITHMETIC_OVERFLOW)?;
68 let ratio_per_m = twice_value_a
69 .checked_mul(U256::from(PER_M_DENOMINATOR as u128))
70 .ok_or(ARITHMETIC_OVERFLOW)?
71 .checked_div(total)
72 .ok_or(ARITHMETIC_OVERFLOW)?;
73 let ratio: i32 = ratio_per_m.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_I32)?;
74 ratio
75 .checked_sub(PER_M_DENOMINATOR)
76 .ok_or(ARITHMETIC_OVERFLOW)
77}
78
79fn select_intensity(
80 deviation_per_m: i32,
81 a_to_b: bool,
82 positive_bid_per_m: u32,
83 negative_bid_per_m: u32,
84 positive_ask_per_m: u32,
85 negative_ask_per_m: u32,
86) -> u32 {
87 match (deviation_per_m >= 0, a_to_b) {
88 (true, true) => positive_bid_per_m,
89 (false, true) => negative_bid_per_m,
90 (true, false) => positive_ask_per_m,
91 (false, false) => negative_ask_per_m,
92 }
93}
94
95impl SkewMode {
96 pub(crate) fn compute_skew_per_m(
100 &self,
101 deviation_per_m: i32,
102 quote_type: QuoteType,
103 ) -> Result<i32, CoreError> {
104 match self {
105 Self::None => Ok(0),
106 Self::Polynomial {
107 exponent,
108 positive_bid_per_m,
109 negative_bid_per_m,
110 positive_ask_per_m,
111 negative_ask_per_m,
112 } => {
113 let a_to_b = quote_type.a_to_b();
114 let intensity = select_intensity(
115 deviation_per_m,
116 a_to_b,
117 *positive_bid_per_m,
118 *negative_bid_per_m,
119 *positive_ask_per_m,
120 *negative_ask_per_m,
121 );
122 let sign = deviation_per_m.signum();
123 let abs_dev = deviation_per_m.unsigned_abs() as u128;
124 let exp = exponent.value();
125
126 let numerator = abs_dev
127 .checked_pow(exp)
128 .ok_or(ARITHMETIC_OVERFLOW)?
129 .checked_mul(intensity as u128)
130 .ok_or(ARITHMETIC_OVERFLOW)?;
131 let denominator = (PER_M_DENOMINATOR as u128)
132 .checked_pow(exp)
133 .ok_or(ARITHMETIC_OVERFLOW)?;
134
135 let quotient = numerator
136 .checked_div(denominator)
137 .ok_or(ARITHMETIC_OVERFLOW)?;
138 let remainder = numerator
139 .checked_rem(denominator)
140 .ok_or(ARITHMETIC_OVERFLOW)?;
141 let abs_result = if remainder > 0 {
142 quotient.checked_add(1).ok_or(ARITHMETIC_OVERFLOW)?
143 } else {
144 quotient
145 };
146
147 let result = i32::try_from(abs_result).map_err(|_| AMOUNT_EXCEEDS_MAX_I32)?;
148 sign.checked_mul(result).ok_or(ARITHMETIC_OVERFLOW)
149 }
150 }
151 }
152}
153
154pub(crate) fn apply_skew_to_liquidity(
157 liquidity: SingleSideLiquidity,
158 skew_per_m: i32,
159 quote_type: QuoteType,
160) -> Result<SingleSideLiquidity, CoreError> {
161 let clamped = skew_per_m.clamp(-PER_M_DENOMINATOR, PER_M_DENOMINATOR);
162 if clamped == 0 {
163 return Ok(liquidity);
164 }
165 let a_to_b = quote_type.a_to_b();
166 let widening = a_to_b == (clamped > 0);
167 let abs_skew = U256::from(clamped.unsigned_abs());
168 let denom = U256::from(PER_M_DENOMINATOR as u64);
169 let mut result = SingleSideLiquidity::new();
170 for &(price, amount) in liquidity.as_slice() {
171 let numerator = U256::from(price)
172 .checked_mul(abs_skew)
173 .ok_or(ARITHMETIC_OVERFLOW)?;
174 let quotient = numerator.checked_div(denom).ok_or(ARITHMETIC_OVERFLOW)?;
175 let remainder = numerator.checked_rem(denom).ok_or(ARITHMETIC_OVERFLOW)?;
176 let delta: u128 = if widening && remainder > U256::ZERO {
177 quotient.checked_add(U256::ONE).ok_or(ARITHMETIC_OVERFLOW)?
178 } else {
179 quotient
180 }
181 .try_into()
182 .map_err(|_| ARITHMETIC_OVERFLOW)?;
183 let adjusted_price = if clamped > 0 {
184 price.checked_sub(delta).ok_or(ARITHMETIC_OVERFLOW)?
185 } else {
186 price.checked_add(delta).ok_or(ARITHMETIC_OVERFLOW)?
187 };
188 result.push((adjusted_price, amount));
189 }
190 Ok(result)
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use rstest::rstest;
197
198 const PRICE_ONE: u128 = 1 << 64;
199
200 #[rstest]
201 #[case(PRICE_ONE, 500, 500, 0)] #[case(PRICE_ONE, 750, 250, 500_000)] #[case(PRICE_ONE, 250, 750, -500_000)] #[case(PRICE_ONE, 1000, 0, 1_000_000)] #[case(PRICE_ONE, 0, 1000, -1_000_000)] fn test_compute_deviation_per_m(
207 #[case] price: u128,
208 #[case] reserves_a: u64,
209 #[case] reserves_b: u64,
210 #[case] expected: i32,
211 ) {
212 let result = compute_deviation_per_m(price, reserves_a, reserves_b).unwrap();
213 assert_eq!(result, expected);
214 }
215
216 #[test]
217 fn test_compute_deviation_zero_reserves() {
218 assert_eq!(compute_deviation_per_m(PRICE_ONE, 0, 0).unwrap(), 0);
219 }
220
221 #[rstest]
222 #[case(SkewMode::None, 200_000, QuoteType::TokenAExactIn, 0)]
223 #[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)]
225 #[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)]
226 #[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)]
227 #[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)]
229 #[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)]
230 #[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)]
232 #[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)]
233 #[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)]
235 #[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)]
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, QuoteType::TokenAExactOut, -1)]
238 #[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)]
241 #[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)]
243 #[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)]
245 #[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)]
247 #[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)]
249 #[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)]
250 #[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)]
251 #[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)]
252 #[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)]
254 #[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)]
256 #[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)]
259 #[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)]
261 #[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)]
263 #[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)]
264 #[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)]
265 #[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)]
266 #[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)]
269 #[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)]
270 #[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)]
271 #[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)]
272 fn test_compute_skew_per_m(
273 #[case] skew_mode: SkewMode,
274 #[case] deviation_per_m: i32,
275 #[case] quote_type: QuoteType,
276 #[case] expected: i32,
277 ) {
278 assert_eq!(
279 skew_mode
280 .compute_skew_per_m(deviation_per_m, quote_type)
281 .unwrap(),
282 expected
283 );
284 }
285
286 #[rstest]
287 #[case(PRICE_ONE, 0, QuoteType::TokenAExactIn, PRICE_ONE)]
289 #[case(PRICE_ONE, 500_000, QuoteType::TokenAExactIn, PRICE_ONE / 2)]
290 #[case(PRICE_ONE, -500_000, QuoteType::TokenBExactIn, PRICE_ONE + PRICE_ONE / 2)]
291 #[case(PRICE_ONE, 250_000, QuoteType::TokenAExactIn, PRICE_ONE * 3 / 4)]
292 #[case(PRICE_ONE, 1_000_000, QuoteType::TokenAExactIn, 0)]
293 #[case(PRICE_ONE, -1_000_000, QuoteType::TokenBExactIn, PRICE_ONE * 2)]
294 #[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))]
298 #[case(PRICE_ONE, -999_999, QuoteType::TokenBExactIn, PRICE_ONE + (PRICE_ONE * 999_999).div_ceil(1_000_000))]
299 #[case(PRICE_ONE, -999_999, QuoteType::TokenAExactIn, PRICE_ONE + PRICE_ONE * 999_999 / 1_000_000)]
301 #[case(PRICE_ONE, 999_999, QuoteType::TokenBExactIn, PRICE_ONE - PRICE_ONE * 999_999 / 1_000_000)]
302 fn test_apply_skew_to_liquidity(
303 #[case] price: u128,
304 #[case] skew_per_m: i32,
305 #[case] quote_type: QuoteType,
306 #[case] expected_price: u128,
307 ) {
308 let liquidity = SingleSideLiquidity::from_slice(&[(price, 1000)]);
309 let result = apply_skew_to_liquidity(liquidity, skew_per_m, quote_type).unwrap();
310 let (result_price, result_amount) = result.as_slice()[0];
311 assert_eq!(result_price, expected_price);
312 assert_eq!(result_amount, 1000);
313 }
314}