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
47/// Computes deviation_per_m from inventory ratio.
48/// 0 = balanced (50/50), +1_000_000 = 100% token_a, -1_000_000 = 100% token_b.
49/// inventory_ratio = value_a / (value_a + value_b) where values are in q64.64.
50pub(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    /// Computes the skew value in per_m units.
97    /// `deviation_per_m` ranges from -1_000_000 to +1_000_000.
98    /// Returns the skew adjustment to apply: bid_spread += skew, ask_spread -= skew.
99    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
154/// Applies skew to all prices in SingleSideLiquidity.
155/// adjusted_price = price * (PER_M - skew_per_m) / PER_M
156pub(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)] // 50/50 -> 0
202    #[case(PRICE_ONE, 750, 250, 500_000)] // 75% token_a -> +500_000
203    #[case(PRICE_ONE, 250, 750, -500_000)] // 25% token_a -> -500_000
204    #[case(PRICE_ONE, 1000, 0, 1_000_000)] // 100% token_a -> +1_000_000
205    #[case(PRICE_ONE, 0, 1000, -1_000_000)] // 0% token_a -> -1_000_000
206    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    // symmetric Linear
224    #[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    // symmetric Quadratic: 200_000^2 * 1_000_000 / 1_000_000^2 = 40_000
228    #[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    // symmetric Cubic: 200_000^3 * 1_000_000 / 1_000_000^3 = 8_000
231    #[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    // Cubic at max deviation: 1_000_000^3 * 1_000_000 / 1_000_000^3 = 1_000_000
234    #[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    // no remainder, no rounding
236    #[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    // 700_001 * 300_000 / 1_000_000 = 210_000.3
239    // round up (a_to_b): -> 210_001
240    #[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    // always round away from zero: -> 210_001
242    #[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    // negative deviation: round up -> -210_001
244    #[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    // negative deviation: always round away from zero -> -210_001
246    #[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    // asymmetric Linear: 4 quadrants
248    #[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    // zero deviation
253    #[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    // rounding with asymmetric: bid ceil 700_001 * 100_000 / 1_000_000 = 70_000.1 -> 70_001
255    #[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    // rounding with asymmetric: always away from zero 700_001 * 300_000 / 1_000_000 = 210_000.3 ->
257    // 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 }, 700_001, QuoteType::TokenBExactIn, 210_001)]
259    // one quadrant zero: positive_bid=0 -> skew=0
260    #[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    // asymmetric Quadratic: 500_000^2 * selected_intensity / 1_000_000^2
262    #[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    // asymmetric Cubic: 500_000^3 * selected_intensity / 1_000_000^3
267    // 500_000^3 = 125_000_000_000_000_000, * 100_000 / 10^18 = 12_500
268    #[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    // widening: a_to_b + positive skew, or !a_to_b + negative skew -> ceil delta
288    #[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)] // clamped to 1M
295    #[case(PRICE_ONE, -2_000_000, QuoteType::TokenBExactIn, PRICE_ONE * 2)] // clamped to -1M
296    // widening: delta ceiling for protocol safety
297    #[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    // narrowing: a_to_b + negative skew, or !a_to_b + positive skew -> floor delta
300    #[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}