Skip to main content

riptide_amm_math/oracle/
skew.rs

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    /// Computes the skew value in per_m units.
65    /// `deviation_per_m` ranges from -1_000_000 to +1_000_000.
66    /// Returns the skew adjustment to apply: bid_spread += skew, ask_spread -= skew.
67    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
122/// Applies skew to all prices in SingleSideLiquidity.
123/// adjusted_price = price * (PER_M - skew_per_m) / PER_M
124pub(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    // symmetric Linear
171    #[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    // symmetric Quadratic: 200_000^2 * 1_000_000 / 1_000_000^2 = 40_000
175    #[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    // symmetric Cubic: 200_000^3 * 1_000_000 / 1_000_000^3 = 8_000
178    #[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    // Cubic at max deviation: 1_000_000^3 * 1_000_000 / 1_000_000^3 = 1_000_000
181    #[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    // no remainder, no rounding
183    #[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    // 700_001 * 300_000 / 1_000_000 = 210_000.3
186    // round up (a_to_b): -> 210_001
187    #[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    // always round away from zero: -> 210_001
189    #[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    // negative deviation: round up -> -210_001
191    #[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    // negative deviation: always round away from zero -> -210_001
193    #[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    // asymmetric Linear: 4 quadrants
195    #[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    // zero deviation
200    #[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    // rounding with asymmetric: bid ceil 700_001 * 100_000 / 1_000_000 = 70_000.1 -> 70_001
202    #[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    // rounding with asymmetric: always away from zero 700_001 * 300_000 / 1_000_000 = 210_000.3 ->
204    // 210_001
205    #[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    // one quadrant zero: positive_bid=0 -> skew=0
207    #[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    // asymmetric Quadratic: 500_000^2 * selected_intensity / 1_000_000^2
209    #[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    // asymmetric Cubic: 500_000^3 * selected_intensity / 1_000_000^3
214    // 500_000^3 = 125_000_000_000_000_000, * 100_000 / 10^18 = 12_500
215    #[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    // widening: a_to_b + positive skew, or !a_to_b + negative skew -> ceil delta
235    #[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)] // clamped to 1M
242    #[case(PRICE_ONE, -2_000_000, QuoteType::TokenBExactIn, PRICE_ONE * 2)] // clamped to -1M
243    // widening: delta ceiling for protocol safety
244    #[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    // narrowing: a_to_b + negative skew, or !a_to_b + positive skew -> floor delta
247    #[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}