solend_sdk/math/
rate.rs

1//! Math for preserving precision of ratios and percentages.
2//!
3//! Usages and their ranges include:
4//!   - Collateral exchange ratio <= 5.0
5//!   - Loan to value ratio <= 0.9
6//!   - Max borrow rate <= 2.56
7//!   - Percentages <= 1.0
8//!
9//! Rates are internally scaled by a WAD (10^18) to preserve
10//! precision up to 18 decimal places. Rates are sized to support
11//! both serialization and precise math for the full range of
12//! unsigned 8-bit integers. The underlying representation is a
13//! u128 rather than u192 to reduce compute cost while losing
14//! support for arithmetic operations at the high end of u8 range.
15
16#![allow(clippy::assign_op_pattern)]
17#![allow(clippy::ptr_offset_with_cast)]
18#![allow(clippy::reversed_empty_ranges)]
19#![allow(clippy::manual_range_contains)]
20
21use crate::{
22    error::LendingError,
23    math::{common::*, decimal::Decimal},
24};
25use solana_program::program_error::ProgramError;
26use std::{convert::TryFrom, fmt};
27use uint::construct_uint;
28
29// U128 with 128 bits consisting of 2 x 64-bit words
30construct_uint! {
31    pub struct U128(2);
32}
33
34/// Small decimal values, precise to 18 digits
35#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Eq, Ord)]
36pub struct Rate(pub U128);
37
38impl Rate {
39    /// One
40    pub fn one() -> Self {
41        Self(Self::wad())
42    }
43
44    /// Zero
45    pub fn zero() -> Self {
46        Self(U128::from(0))
47    }
48
49    // OPTIMIZE: use const slice when fixed in BPF toolchain
50    fn wad() -> U128 {
51        U128::from(WAD)
52    }
53
54    /// Create scaled decimal from percent value
55    pub fn from_percent(percent: u8) -> Self {
56        Self(U128::from(percent as u64 * PERCENT_SCALER))
57    }
58
59    /// Return raw scaled value
60    #[allow(clippy::wrong_self_convention)]
61    pub fn to_scaled_val(&self) -> u128 {
62        self.0.as_u128()
63    }
64
65    /// Create decimal from scaled value
66    pub fn from_scaled_val(scaled_val: u64) -> Self {
67        Self(U128::from(scaled_val))
68    }
69
70    /// Calculates base^exp
71    pub fn try_pow(&self, mut exp: u64) -> Result<Rate, ProgramError> {
72        let mut base = *self;
73        let mut ret = if exp % 2 != 0 {
74            base
75        } else {
76            Rate(Self::wad())
77        };
78
79        while exp > 0 {
80            exp /= 2;
81            base = base.try_mul(base)?;
82
83            if exp % 2 != 0 {
84                ret = ret.try_mul(base)?;
85            }
86        }
87
88        Ok(ret)
89    }
90}
91
92impl fmt::Display for Rate {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        let mut scaled_val = self.0.to_string();
95        if scaled_val.len() <= SCALE {
96            scaled_val.insert_str(0, &vec!["0"; SCALE - scaled_val.len()].join(""));
97            scaled_val.insert_str(0, "0.");
98        } else {
99            scaled_val.insert(scaled_val.len() - SCALE, '.');
100        }
101        f.write_str(&scaled_val)
102    }
103}
104
105impl TryFrom<Decimal> for Rate {
106    type Error = ProgramError;
107    fn try_from(decimal: Decimal) -> Result<Self, Self::Error> {
108        Ok(Self(U128::from(decimal.to_scaled_val()?)))
109    }
110}
111
112impl TryAdd for Rate {
113    fn try_add(self, rhs: Self) -> Result<Self, ProgramError> {
114        Ok(Self(
115            self.0
116                .checked_add(rhs.0)
117                .ok_or(LendingError::MathOverflow)?,
118        ))
119    }
120}
121
122impl TrySub for Rate {
123    fn try_sub(self, rhs: Self) -> Result<Self, ProgramError> {
124        Ok(Self(
125            self.0
126                .checked_sub(rhs.0)
127                .ok_or(LendingError::MathOverflow)?,
128        ))
129    }
130}
131
132impl TryDiv<u64> for Rate {
133    fn try_div(self, rhs: u64) -> Result<Self, ProgramError> {
134        Ok(Self(
135            self.0
136                .checked_div(U128::from(rhs))
137                .ok_or(LendingError::MathOverflow)?,
138        ))
139    }
140}
141
142impl TryDiv<Rate> for Rate {
143    fn try_div(self, rhs: Self) -> Result<Self, ProgramError> {
144        Ok(Self(
145            self.0
146                .checked_mul(Self::wad())
147                .ok_or(LendingError::MathOverflow)?
148                .checked_div(rhs.0)
149                .ok_or(LendingError::MathOverflow)?,
150        ))
151    }
152}
153
154impl TryMul<u64> for Rate {
155    fn try_mul(self, rhs: u64) -> Result<Self, ProgramError> {
156        Ok(Self(
157            self.0
158                .checked_mul(U128::from(rhs))
159                .ok_or(LendingError::MathOverflow)?,
160        ))
161    }
162}
163
164impl TryMul<Rate> for Rate {
165    fn try_mul(self, rhs: Self) -> Result<Self, ProgramError> {
166        Ok(Self(
167            self.0
168                .checked_mul(rhs.0)
169                .ok_or(LendingError::MathOverflow)?
170                .checked_div(Self::wad())
171                .ok_or(LendingError::MathOverflow)?,
172        ))
173    }
174}
175
176#[cfg(test)]
177mod test {
178    use super::*;
179    use std::convert::TryInto;
180
181    #[test]
182    fn test_scaled_val() {
183        assert_eq!(Rate::from_percent(50).to_scaled_val(), HALF_WAD as u128);
184    }
185
186    #[test]
187    fn checked_pow() {
188        assert_eq!(Rate::one(), Rate::one().try_pow(u64::MAX).unwrap());
189        assert_eq!(
190            Rate::from_percent(200).try_pow(7).unwrap(),
191            Decimal::from(128u64).try_into().unwrap()
192        );
193    }
194
195    #[test]
196    fn test_display() {
197        assert_eq!(
198            Rate::one().try_div(3u64).unwrap().to_string(),
199            "0.333333333333333333"
200        );
201    }
202
203    #[test]
204    fn test_basic_arithmetic() {
205        assert_eq!(
206            Rate::one().try_add(Rate::one()).unwrap(),
207            Rate::from_scaled_val(2 * WAD)
208        );
209
210        assert_eq!(Rate::one().try_sub(Rate::one()).unwrap(), Rate::zero());
211
212        assert_eq!(
213            Rate::from_percent(240)
214                .try_mul(Rate::from_percent(50))
215                .unwrap(),
216            Rate::from_percent(120)
217        );
218        assert_eq!(
219            Rate::from_percent(240).try_mul(10).unwrap(),
220            Decimal::from(24u64).try_into().unwrap()
221        );
222
223        assert_eq!(
224            Rate::from_percent(240)
225                .try_div(Rate::from_percent(60))
226                .unwrap(),
227            Rate::from_scaled_val(4 * WAD)
228        );
229    }
230}