Skip to main content

riptide_amm_math/
token.rs

1use ethnum::U256;
2#[cfg(feature = "floats")]
3use libm::pow;
4
5#[cfg(feature = "wasm")]
6use riptide_amm_macros::wasm_expose;
7
8use super::{AMOUNT_EXCEEDS_MAX_I32, PER_M_DENOMINATOR};
9
10use super::error::{CoreError, AMOUNT_EXCEEDS_MAX_U64, ARITHMETIC_OVERFLOW};
11
12use super::U128;
13
14#[cfg(feature = "floats")]
15#[cfg_attr(feature = "wasm", wasm_expose)]
16pub fn amount_to_ui_amount(amount: u64, decimals: u8) -> f64 {
17    let power = pow(10f64, decimals as f64);
18    amount as f64 / power
19}
20
21#[cfg(feature = "floats")]
22#[cfg_attr(feature = "wasm", wasm_expose)]
23pub fn ui_amount_to_amount(amount: f64, decimals: u8) -> u64 {
24    let power = pow(10f64, decimals as f64);
25    (amount * power) as u64
26}
27
28/// Convert an amount in token A to an amount in token B
29///
30/// # Parameters
31/// * `amount_a` - The amount in token A
32/// * `price` - The Q64.64 price in B/A
33/// * `round_up` - Whether to round up the result
34///
35/// # Returns
36/// * `u64` - The amount in token B
37#[cfg_attr(feature = "wasm", wasm_expose)]
38pub fn a_to_b(amount_a: u64, price: U128, round_up: bool) -> Result<u64, CoreError> {
39    #[allow(clippy::useless_conversion)]
40    let price: u128 = price.into();
41
42    let product = u128::from(amount_a)
43        .checked_mul(price)
44        .ok_or(ARITHMETIC_OVERFLOW)?;
45
46    let quotient = product >> 64;
47    let remainder = product as u64;
48
49    let result = if round_up && remainder > 0 {
50        quotient + 1
51    } else {
52        quotient
53    };
54
55    result.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)
56}
57
58/// Convert an amount in token B to an amount in token A
59///
60/// # Parameters
61/// * `amount_b` - The amount in token B
62/// * `price` - The Q64.64 price in B/A
63/// * `round_up` - Whether to round up the result
64///
65/// # Returns
66/// * `u64` - The amount in token A
67#[cfg_attr(feature = "wasm", wasm_expose)]
68pub fn b_to_a(amount_b: u64, price: U128, round_up: bool) -> Result<u64, CoreError> {
69    #[allow(clippy::useless_conversion)]
70    let price: u128 = price.into();
71    if price == 0 {
72        return Ok(0);
73    }
74
75    let numerator = u128::from(amount_b)
76        .checked_shl(64)
77        .ok_or(ARITHMETIC_OVERFLOW)?;
78
79    let quotient = numerator.checked_div(price).ok_or(ARITHMETIC_OVERFLOW)?;
80    let remainder = numerator.checked_rem(price).ok_or(ARITHMETIC_OVERFLOW)?;
81
82    let result = if round_up && remainder > 0 {
83        quotient + 1
84    } else {
85        quotient
86    };
87
88    result.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)
89}
90
91/// Computes deviation_per_m from inventory ratio.
92/// 0 = balanced (50/50), +1_000_000 = 100% token_a, -1_000_000 = 100% token_b.
93/// inventory_ratio = value_a / (value_a + value_b) where values are in q64.64.
94#[cfg_attr(feature = "wasm", wasm_expose)]
95pub fn deviation_per_m(
96    price_q64_64: U128,
97    reserves_a: u64,
98    reserves_b: u64,
99) -> Result<i32, CoreError> {
100    let value_a = U256::from(reserves_a as u128)
101        .checked_mul(U256::from(price_q64_64))
102        .ok_or(ARITHMETIC_OVERFLOW)?;
103    let value_b = U256::from(reserves_b as u128)
104        .checked_shl(64)
105        .ok_or(ARITHMETIC_OVERFLOW)?;
106    let total = value_a.checked_add(value_b).ok_or(ARITHMETIC_OVERFLOW)?;
107    if total == U256::ZERO {
108        return Ok(0);
109    }
110    let twice_value_a = value_a
111        .checked_mul(U256::from(2u128))
112        .ok_or(ARITHMETIC_OVERFLOW)?;
113    let ratio_per_m = twice_value_a
114        .checked_mul(U256::from(PER_M_DENOMINATOR as u128))
115        .ok_or(ARITHMETIC_OVERFLOW)?
116        .checked_div(total)
117        .ok_or(ARITHMETIC_OVERFLOW)?;
118    let ratio: i32 = ratio_per_m.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_I32)?;
119    ratio
120        .checked_sub(PER_M_DENOMINATOR)
121        .ok_or(ARITHMETIC_OVERFLOW)
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use rstest::rstest;
128
129    #[cfg(feature = "floats")]
130    #[rstest]
131    #[case(1000000000, 9, 1.0)]
132    #[case(1000000000, 6, 1000.0)]
133    #[case(1000000000, 3, 1000000.0)]
134    fn test_amount_to_ui_amount(
135        #[case] amount: u64,
136        #[case] decimals: u8,
137        #[case] expected_ui_amount: f64,
138    ) {
139        let ui_amount = amount_to_ui_amount(amount, decimals);
140        assert_eq!(ui_amount, expected_ui_amount);
141    }
142
143    #[cfg(feature = "floats")]
144    #[rstest]
145    #[case(1.0, 9, 1000000000)]
146    #[case(1000.0, 6, 1000000000)]
147    #[case(1000000.0, 3, 1000000000)]
148    fn test_ui_amount_to_amount(
149        #[case] ui_amount: f64,
150        #[case] decimals: u8,
151        #[case] expected_amount: u64,
152    ) {
153        let amount = ui_amount_to_amount(ui_amount, decimals);
154        assert_eq!(amount, expected_amount);
155    }
156
157    #[rstest]
158    #[case(100, 1 << 64, true, Ok(100))]
159    #[case(100, 1 << 64, false, Ok(100))]
160    #[case(100, 8 << 64, true, Ok(800))]
161    #[case(100, 8 << 64, false, Ok(800))]
162    #[case(100, (1 << 64) / 8, true, Ok(13))]
163    #[case(100, (1 << 64) / 8, false, Ok(12))]
164    #[case(100, 0, true, Ok(0))]
165    #[case(0, 1 << 64, true, Ok(0))]
166    fn test_a_to_b(
167        #[case] amount_a: u64,
168        #[case] price: u128,
169        #[case] round_up: bool,
170        #[case] expected: Result<u64, CoreError>,
171    ) {
172        let result = a_to_b(amount_a, U128::from(price), round_up);
173        assert_eq!(result, expected);
174    }
175
176    #[rstest]
177    #[case(100, 1 << 64, true, Ok(100))]
178    #[case(100, 1 << 64, false, Ok(100))]
179    #[case(100, 8 << 64, true, Ok(13))]
180    #[case(100, 8 << 64, false, Ok(12))]
181    #[case(100, (1 << 64) / 8, true, Ok(800))]
182    #[case(100, (1 << 64) / 8, false, Ok(800))]
183    #[case(100, 0, true, Ok(0))]
184    #[case(0, 1 << 64, true, Ok(0))]
185    fn test_b_to_a(
186        #[case] amount_b: u64,
187        #[case] price: u128,
188        #[case] round_up: bool,
189        #[case] expected: Result<u64, CoreError>,
190    ) {
191        let result = b_to_a(amount_b, U128::from(price), round_up);
192        assert_eq!(result, expected);
193    }
194
195    #[rstest]
196    #[case(1 << 64, 500, 500, 0)] // 50/50 -> 0
197    #[case(1 << 64, 750, 250, 500_000)] // 75% token_a -> +500_000
198    #[case(1 << 64, 250, 750, -500_000)] // 25% token_a -> -500_000
199    #[case(1 << 64, 1000, 0, 1_000_000)] // 100% token_a -> +1_000_000
200    #[case(1 << 64, 0, 1000, -1_000_000)] // 0% token_a -> -1_000_000
201    fn test_deviation_per_m(
202        #[case] price: u128,
203        #[case] reserves_a: u64,
204        #[case] reserves_b: u64,
205        #[case] expected: i32,
206    ) {
207        let result = deviation_per_m(U128::from(price), reserves_a, reserves_b).unwrap();
208        assert_eq!(result, expected);
209    }
210
211    #[test]
212    fn test_deviation_zero_reserves() {
213        assert_eq!(deviation_per_m(U128::from(1u128 << 64), 0, 0).unwrap(), 0);
214    }
215}