solana_extra_wasm/program/spl_token_2022/extension/interest_bearing_mint/
mod.rs

1use {
2    crate::program::spl_token_2022::{
3        extension::{Extension, ExtensionType},
4        pod::{OptionalNonZeroPubkey, PodI16, PodI64},
5    },
6    bytemuck::{Pod, Zeroable},
7    solana_sdk::program_error::ProgramError,
8    std::convert::TryInto,
9};
10
11/// Interest-bearing mint extension instructions
12pub mod instruction;
13
14/// Interest-bearing mint extension processor
15pub mod processor;
16
17/// Annual interest rate, expressed as basis points
18pub type BasisPoints = PodI16;
19const ONE_IN_BASIS_POINTS: f64 = 10_000.;
20const SECONDS_PER_YEAR: f64 = 60. * 60. * 24. * 365.24;
21
22/// UnixTimestamp expressed with an alignment-independent type
23pub type UnixTimestamp = PodI64;
24
25/// Interest-bearing extension data for mints
26///
27/// Tokens accrue interest at an annual rate expressed by `current_rate`,
28/// compounded continuously, so APY will be higher than the published interest
29/// rate.
30///
31/// To support changing the rate, the config also maintains state for the previous
32/// rate.
33#[repr(C)]
34#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
35pub struct InterestBearingConfig {
36    /// Authority that can set the interest rate and authority
37    pub rate_authority: OptionalNonZeroPubkey,
38    /// Timestamp of initialization, from which to base interest calculations
39    pub initialization_timestamp: UnixTimestamp,
40    /// Average rate from initialization until the last time it was updated
41    pub pre_update_average_rate: BasisPoints,
42    /// Timestamp of the last update, used to calculate the total amount accrued
43    pub last_update_timestamp: UnixTimestamp,
44    /// Current rate, since the last update
45    pub current_rate: BasisPoints,
46}
47impl InterestBearingConfig {
48    fn pre_update_timespan(&self) -> Option<i64> {
49        i64::from(self.last_update_timestamp).checked_sub(self.initialization_timestamp.into())
50    }
51
52    fn pre_update_exp(&self) -> Option<f64> {
53        let numerator = (i16::from(self.pre_update_average_rate) as i128)
54            .checked_mul(self.pre_update_timespan()? as i128)? as f64;
55        let exponent = numerator / SECONDS_PER_YEAR / ONE_IN_BASIS_POINTS;
56        Some(exponent.exp())
57    }
58
59    fn post_update_timespan(&self, unix_timestamp: i64) -> Option<i64> {
60        unix_timestamp.checked_sub(self.last_update_timestamp.into())
61    }
62
63    fn post_update_exp(&self, unix_timestamp: i64) -> Option<f64> {
64        let numerator = (i16::from(self.current_rate) as i128)
65            .checked_mul(self.post_update_timespan(unix_timestamp)? as i128)?
66            as f64;
67        let exponent = numerator / SECONDS_PER_YEAR / ONE_IN_BASIS_POINTS;
68        Some(exponent.exp())
69    }
70
71    fn total_scale(&self, decimals: u8, unix_timestamp: i64) -> Option<f64> {
72        Some(
73            self.pre_update_exp()? * self.post_update_exp(unix_timestamp)?
74                / 10_f64.powi(decimals as i32),
75        )
76    }
77
78    /// Convert a raw amount to its UI representation using the given decimals field
79    /// Excess zeroes or unneeded decimal point are trimmed.
80    pub fn amount_to_ui_amount(
81        &self,
82        amount: u64,
83        decimals: u8,
84        unix_timestamp: i64,
85    ) -> Option<String> {
86        let scaled_amount_with_interest =
87            (amount as f64) * self.total_scale(decimals, unix_timestamp)?;
88        Some(scaled_amount_with_interest.to_string())
89    }
90
91    /// Try to convert a UI representation of a token amount to its raw amount using the given decimals
92    /// field
93    pub fn try_ui_amount_into_amount(
94        &self,
95        ui_amount: &str,
96        decimals: u8,
97        unix_timestamp: i64,
98    ) -> Result<u64, ProgramError> {
99        let scaled_amount = ui_amount
100            .parse::<f64>()
101            .map_err(|_| ProgramError::InvalidArgument)?;
102        let amount = scaled_amount
103            / self
104                .total_scale(decimals, unix_timestamp)
105                .ok_or(ProgramError::InvalidArgument)?;
106        if amount > (u64::MAX as f64) || amount < (u64::MIN as f64) || amount.is_nan() {
107            Err(ProgramError::InvalidArgument)
108        } else {
109            Ok(amount.round() as u64) // this is important, if you round earlier, you'll get wrong "inf" answers
110        }
111    }
112
113    /// The new average rate is the time-weighted average of the current rate and average rate,
114    /// solving for r such that:
115    ///
116    /// exp(r_1 * t_1) * exp(r_2 * t_2) = exp(r * (t_1 + t_2))
117    ///
118    /// r_1 * t_1 + r_2 * t_2 = r * (t_1 + t_2)
119    ///
120    /// r = (r_1 * t_1 + r_2 * t_2) / (t_1 + t_2)
121    pub fn time_weighted_average_rate(&self, current_timestamp: i64) -> Option<i16> {
122        let initialization_timestamp = i64::from(self.initialization_timestamp) as i128;
123        let last_update_timestamp = i64::from(self.last_update_timestamp) as i128;
124
125        let r_1 = i16::from(self.pre_update_average_rate) as i128;
126        let t_1 = last_update_timestamp.checked_sub(initialization_timestamp)?;
127        let r_2 = i16::from(self.current_rate) as i128;
128        let t_2 = (current_timestamp as i128).checked_sub(last_update_timestamp)?;
129        let total_timespan = t_1.checked_add(t_2)?;
130        let average_rate = if total_timespan == 0 {
131            // happens in testing situations, just use the new rate since the earlier
132            // one was never practically used
133            r_2
134        } else {
135            r_1.checked_mul(t_1)?
136                .checked_add(r_2.checked_mul(t_2)?)?
137                .checked_div(total_timespan)?
138        };
139        average_rate.try_into().ok()
140    }
141}
142impl Extension for InterestBearingConfig {
143    const TYPE: ExtensionType = ExtensionType::InterestBearingConfig;
144}