#[cfg(feature = "serde-traits")]
use serde::{Deserialize, Serialize};
use {
crate::extension::{Extension, ExtensionType},
bytemuck::{Pod, Zeroable},
solana_program::program_error::ProgramError,
spl_pod::{
optional_keys::OptionalNonZeroPubkey,
primitives::{PodI16, PodI64},
},
std::convert::TryInto,
};
pub mod instruction;
pub mod processor;
pub type BasisPoints = PodI16;
const ONE_IN_BASIS_POINTS: f64 = 10_000.;
const SECONDS_PER_YEAR: f64 = 60. * 60. * 24. * 365.24;
pub type UnixTimestamp = PodI64;
#[repr(C)]
#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
pub struct InterestBearingConfig {
pub rate_authority: OptionalNonZeroPubkey,
pub initialization_timestamp: UnixTimestamp,
pub pre_update_average_rate: BasisPoints,
pub last_update_timestamp: UnixTimestamp,
pub current_rate: BasisPoints,
}
impl InterestBearingConfig {
fn pre_update_timespan(&self) -> Option<i64> {
i64::from(self.last_update_timestamp).checked_sub(self.initialization_timestamp.into())
}
fn pre_update_exp(&self) -> Option<f64> {
let numerator = (i16::from(self.pre_update_average_rate) as i128)
.checked_mul(self.pre_update_timespan()? as i128)? as f64;
let exponent = numerator / SECONDS_PER_YEAR / ONE_IN_BASIS_POINTS;
Some(exponent.exp())
}
fn post_update_timespan(&self, unix_timestamp: i64) -> Option<i64> {
unix_timestamp.checked_sub(self.last_update_timestamp.into())
}
fn post_update_exp(&self, unix_timestamp: i64) -> Option<f64> {
let numerator = (i16::from(self.current_rate) as i128)
.checked_mul(self.post_update_timespan(unix_timestamp)? as i128)?
as f64;
let exponent = numerator / SECONDS_PER_YEAR / ONE_IN_BASIS_POINTS;
Some(exponent.exp())
}
fn total_scale(&self, decimals: u8, unix_timestamp: i64) -> Option<f64> {
Some(
self.pre_update_exp()? * self.post_update_exp(unix_timestamp)?
/ 10_f64.powi(decimals as i32),
)
}
pub fn amount_to_ui_amount(
&self,
amount: u64,
decimals: u8,
unix_timestamp: i64,
) -> Option<String> {
let scaled_amount_with_interest =
(amount as f64) * self.total_scale(decimals, unix_timestamp)?;
Some(scaled_amount_with_interest.to_string())
}
pub fn try_ui_amount_into_amount(
&self,
ui_amount: &str,
decimals: u8,
unix_timestamp: i64,
) -> Result<u64, ProgramError> {
let scaled_amount = ui_amount
.parse::<f64>()
.map_err(|_| ProgramError::InvalidArgument)?;
let amount = scaled_amount
/ self
.total_scale(decimals, unix_timestamp)
.ok_or(ProgramError::InvalidArgument)?;
if amount > (u64::MAX as f64) || amount < (u64::MIN as f64) || amount.is_nan() {
Err(ProgramError::InvalidArgument)
} else {
Ok(amount.round() as u64)
}
}
pub fn time_weighted_average_rate(&self, current_timestamp: i64) -> Option<i16> {
let initialization_timestamp = i64::from(self.initialization_timestamp) as i128;
let last_update_timestamp = i64::from(self.last_update_timestamp) as i128;
let r_1 = i16::from(self.pre_update_average_rate) as i128;
let t_1 = last_update_timestamp.checked_sub(initialization_timestamp)?;
let r_2 = i16::from(self.current_rate) as i128;
let t_2 = (current_timestamp as i128).checked_sub(last_update_timestamp)?;
let total_timespan = t_1.checked_add(t_2)?;
let average_rate = if total_timespan == 0 {
r_2
} else {
r_1.checked_mul(t_1)?
.checked_add(r_2.checked_mul(t_2)?)?
.checked_div(total_timespan)?
};
average_rate.try_into().ok()
}
}
impl Extension for InterestBearingConfig {
const TYPE: ExtensionType = ExtensionType::InterestBearingConfig;
}
#[cfg(test)]
mod tests {
use {super::*, proptest::prelude::*};
const INT_SECONDS_PER_YEAR: i64 = 6 * 6 * 24 * 36524;
const TEST_DECIMALS: u8 = 2;
#[test]
fn seconds_per_year() {
assert_eq!(SECONDS_PER_YEAR, 31_556_736.);
assert_eq!(INT_SECONDS_PER_YEAR, 31_556_736);
}
#[test]
fn specific_amount_to_ui_amount() {
let config = InterestBearingConfig {
rate_authority: OptionalNonZeroPubkey::default(),
initialization_timestamp: 0.into(),
pre_update_average_rate: 500.into(),
last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
current_rate: 500.into(),
};
let ui_amount = config
.amount_to_ui_amount(1, 0, INT_SECONDS_PER_YEAR)
.unwrap();
assert_eq!(ui_amount, "1.0512710963760241");
let ui_amount = config
.amount_to_ui_amount(1, 1, INT_SECONDS_PER_YEAR)
.unwrap();
assert_eq!(ui_amount, "0.10512710963760241");
let ui_amount = config
.amount_to_ui_amount(1, 10, INT_SECONDS_PER_YEAR)
.unwrap();
assert_eq!(ui_amount, "0.00000000010512710963760242"); let ui_amount = config
.amount_to_ui_amount(10_000_000_000, 10, INT_SECONDS_PER_YEAR)
.unwrap();
assert_eq!(ui_amount, "1.0512710963760241");
let config = InterestBearingConfig {
rate_authority: OptionalNonZeroPubkey::default(),
initialization_timestamp: 0.into(),
pre_update_average_rate: PodI16::from(-500),
last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
current_rate: PodI16::from(-500),
};
let ui_amount = config
.amount_to_ui_amount(1, 0, INT_SECONDS_PER_YEAR)
.unwrap();
assert_eq!(ui_amount, "0.951229424500714");
let config = InterestBearingConfig {
rate_authority: OptionalNonZeroPubkey::default(),
initialization_timestamp: 0.into(),
pre_update_average_rate: PodI16::from(-500),
last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
current_rate: PodI16::from(500),
};
let ui_amount = config
.amount_to_ui_amount(1, 0, INT_SECONDS_PER_YEAR * 2)
.unwrap();
assert_eq!(ui_amount, "1");
let config = InterestBearingConfig {
rate_authority: OptionalNonZeroPubkey::default(),
initialization_timestamp: 0.into(),
pre_update_average_rate: PodI16::from(500),
last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
current_rate: PodI16::from(500),
};
let ui_amount = config
.amount_to_ui_amount(u64::MAX, 0, INT_SECONDS_PER_YEAR * 2)
.unwrap();
assert_eq!(ui_amount, "20386805083448100000");
let ui_amount = config
.amount_to_ui_amount(u64::MAX, 0, INT_SECONDS_PER_YEAR * 10_000)
.unwrap();
assert_eq!(ui_amount, "258917064265813830000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
}
#[test]
fn specific_ui_amount_to_amount() {
let config = InterestBearingConfig {
rate_authority: OptionalNonZeroPubkey::default(),
initialization_timestamp: 0.into(),
pre_update_average_rate: 500.into(),
last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
current_rate: 500.into(),
};
let amount = config
.try_ui_amount_into_amount("1.0512710963760241", 0, INT_SECONDS_PER_YEAR)
.unwrap();
assert_eq!(1, amount);
let amount = config
.try_ui_amount_into_amount("0.10512710963760241", 1, INT_SECONDS_PER_YEAR)
.unwrap();
assert_eq!(amount, 1);
let amount = config
.try_ui_amount_into_amount("0.00000000010512710963760242", 10, INT_SECONDS_PER_YEAR)
.unwrap();
assert_eq!(amount, 1);
let amount = config
.try_ui_amount_into_amount("1.0512710963760241", 10, INT_SECONDS_PER_YEAR)
.unwrap();
assert_eq!(amount, 10_000_000_000);
let config = InterestBearingConfig {
rate_authority: OptionalNonZeroPubkey::default(),
initialization_timestamp: 0.into(),
pre_update_average_rate: PodI16::from(-500),
last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
current_rate: PodI16::from(-500),
};
let amount = config
.try_ui_amount_into_amount("0.951229424500714", 0, INT_SECONDS_PER_YEAR)
.unwrap();
assert_eq!(amount, 1);
let config = InterestBearingConfig {
rate_authority: OptionalNonZeroPubkey::default(),
initialization_timestamp: 0.into(),
pre_update_average_rate: PodI16::from(-500),
last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
current_rate: PodI16::from(500),
};
let amount = config
.try_ui_amount_into_amount("1", 0, INT_SECONDS_PER_YEAR * 2)
.unwrap();
assert_eq!(amount, 1);
let config = InterestBearingConfig {
rate_authority: OptionalNonZeroPubkey::default(),
initialization_timestamp: 0.into(),
pre_update_average_rate: PodI16::from(500),
last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
current_rate: PodI16::from(500),
};
let amount = config
.try_ui_amount_into_amount("20386805083448100000", 0, INT_SECONDS_PER_YEAR * 2)
.unwrap();
assert_eq!(amount, u64::MAX);
let amount = config
.try_ui_amount_into_amount("258917064265813830000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 0, INT_SECONDS_PER_YEAR * 10_000)
.unwrap();
assert_eq!(amount, u64::MAX);
let amount = config
.try_ui_amount_into_amount("2.5891706426581383e236", 0, INT_SECONDS_PER_YEAR * 10_000)
.unwrap();
assert_eq!(amount, u64::MAX);
let amount = config
.try_ui_amount_into_amount("2.5891706426581383E236", 0, INT_SECONDS_PER_YEAR * 10_000)
.unwrap();
assert_eq!(amount, u64::MAX);
assert_eq!(
Err(ProgramError::InvalidArgument),
config.try_ui_amount_into_amount("20386805083448200001", 0, INT_SECONDS_PER_YEAR)
);
for fail_ui_amount in ["-0.0000000000000000000001", "inf", "-inf", "NaN"] {
assert_eq!(
Err(ProgramError::InvalidArgument),
config.try_ui_amount_into_amount(fail_ui_amount, 0, INT_SECONDS_PER_YEAR)
);
}
}
#[test]
fn specific_amount_to_ui_amount_no_interest() {
let config = InterestBearingConfig {
rate_authority: OptionalNonZeroPubkey::default(),
initialization_timestamp: 0.into(),
pre_update_average_rate: 0.into(),
last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
current_rate: 0.into(),
};
for (amount, expected) in [(23, "0.23"), (110, "1.1"), (4200, "42"), (0, "0")] {
let ui_amount = config
.amount_to_ui_amount(amount, TEST_DECIMALS, INT_SECONDS_PER_YEAR)
.unwrap();
assert_eq!(ui_amount, expected);
}
}
#[test]
fn specific_ui_amount_to_amount_no_interest() {
let config = InterestBearingConfig {
rate_authority: OptionalNonZeroPubkey::default(),
initialization_timestamp: 0.into(),
pre_update_average_rate: 0.into(),
last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
current_rate: 0.into(),
};
for (ui_amount, expected) in [
("0.23", 23),
("0.20", 20),
("0.2000", 20),
(".2", 20),
("1.1", 110),
("1.10", 110),
("42", 4200),
("42.", 4200),
("0", 0),
] {
let amount = config
.try_ui_amount_into_amount(ui_amount, TEST_DECIMALS, INT_SECONDS_PER_YEAR)
.unwrap();
assert_eq!(expected, amount);
}
let amount = config
.try_ui_amount_into_amount("0.111", TEST_DECIMALS, INT_SECONDS_PER_YEAR)
.unwrap();
assert_eq!(11, amount);
for ui_amount in ["", ".", "0.t"] {
assert_eq!(
Err(ProgramError::InvalidArgument),
config.try_ui_amount_into_amount(ui_amount, TEST_DECIMALS, INT_SECONDS_PER_YEAR),
);
}
}
prop_compose! {
fn low_middle_high()
(middle in 1..i64::MAX - 1)
(low in 0..=middle, middle in Just(middle), high in middle..=i64::MAX)
-> (i64, i64, i64) {
(low, middle, high)
}
}
proptest! {
#[test]
fn time_weighted_average_calc(
current_rate in i16::MIN..i16::MAX,
pre_update_average_rate in i16::MIN..i16::MAX,
(initialization_timestamp, last_update_timestamp, current_timestamp) in low_middle_high(),
) {
let config = InterestBearingConfig {
rate_authority: OptionalNonZeroPubkey::default(),
initialization_timestamp: initialization_timestamp.into(),
pre_update_average_rate: pre_update_average_rate.into(),
last_update_timestamp: last_update_timestamp.into(),
current_rate: current_rate.into(),
};
let new_rate = config.time_weighted_average_rate(current_timestamp).unwrap();
if pre_update_average_rate <= current_rate {
assert!(pre_update_average_rate <= new_rate);
assert!(new_rate <= current_rate);
} else {
assert!(current_rate <= new_rate);
assert!(new_rate <= pre_update_average_rate);
}
}
#[test]
fn amount_to_ui_amount(
current_rate in i16::MIN..i16::MAX,
pre_update_average_rate in i16::MIN..i16::MAX,
(initialization_timestamp, last_update_timestamp, current_timestamp) in low_middle_high(),
amount in 0..=u64::MAX,
decimals in 0u8..20u8,
) {
let config = InterestBearingConfig {
rate_authority: OptionalNonZeroPubkey::default(),
initialization_timestamp: initialization_timestamp.into(),
pre_update_average_rate: pre_update_average_rate.into(),
last_update_timestamp: last_update_timestamp.into(),
current_rate: current_rate.into(),
};
let ui_amount = config.amount_to_ui_amount(amount, decimals, current_timestamp);
assert!(ui_amount.is_some());
}
}
}