1use {
2 crate::{
3 extension::{Extension, ExtensionType},
4 pod::{OptionalNonZeroPubkey, PodI16, PodI64},
5 },
6 bytemuck::{Pod, Zeroable},
7 solana_program::program_error::ProgramError,
8 std::convert::TryInto,
9};
10
11pub mod instruction;
13
14pub mod processor;
16
17pub type BasisPoints = PodI16;
19const ONE_IN_BASIS_POINTS: f64 = 10_000.;
20const SECONDS_PER_YEAR: f64 = 60. * 60. * 24. * 365.24;
21
22pub type UnixTimestamp = PodI64;
24
25#[repr(C)]
34#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
35pub struct InterestBearingConfig {
36 pub rate_authority: OptionalNonZeroPubkey,
38 pub initialization_timestamp: UnixTimestamp,
40 pub pre_update_average_rate: BasisPoints,
42 pub last_update_timestamp: UnixTimestamp,
44 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 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 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) }
111 }
112
113 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 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}
145
146#[cfg(test)]
147mod tests {
148 use {super::*, proptest::prelude::*};
149
150 const INT_SECONDS_PER_YEAR: i64 = 6 * 6 * 24 * 36524;
151 const TEST_DECIMALS: u8 = 2;
152
153 #[test]
154 fn seconds_per_year() {
155 assert_eq!(SECONDS_PER_YEAR, 31_556_736.);
156 assert_eq!(INT_SECONDS_PER_YEAR, 31_556_736);
157 }
158
159 #[test]
160 fn specific_amount_to_ui_amount() {
161 let config = InterestBearingConfig {
163 rate_authority: OptionalNonZeroPubkey::default(),
164 initialization_timestamp: 0.into(),
165 pre_update_average_rate: 500.into(),
166 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
167 current_rate: 500.into(),
168 };
169 let ui_amount = config
171 .amount_to_ui_amount(1, 0, INT_SECONDS_PER_YEAR)
172 .unwrap();
173 assert_eq!(ui_amount, "1.0512710963760241");
174 let ui_amount = config
176 .amount_to_ui_amount(1, 1, INT_SECONDS_PER_YEAR)
177 .unwrap();
178 assert_eq!(ui_amount, "0.10512710963760241");
179 let ui_amount = config
181 .amount_to_ui_amount(1, 10, INT_SECONDS_PER_YEAR)
182 .unwrap();
183 assert_eq!(ui_amount, "0.00000000010512710963760242"); let ui_amount = config
187 .amount_to_ui_amount(10_000_000_000, 10, INT_SECONDS_PER_YEAR)
188 .unwrap();
189 assert_eq!(ui_amount, "1.0512710963760241");
190
191 let config = InterestBearingConfig {
193 rate_authority: OptionalNonZeroPubkey::default(),
194 initialization_timestamp: 0.into(),
195 pre_update_average_rate: PodI16::from(-500),
196 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
197 current_rate: PodI16::from(-500),
198 };
199 let ui_amount = config
201 .amount_to_ui_amount(1, 0, INT_SECONDS_PER_YEAR)
202 .unwrap();
203 assert_eq!(ui_amount, "0.951229424500714");
204
205 let config = InterestBearingConfig {
207 rate_authority: OptionalNonZeroPubkey::default(),
208 initialization_timestamp: 0.into(),
209 pre_update_average_rate: PodI16::from(-500),
210 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
211 current_rate: PodI16::from(500),
212 };
213 let ui_amount = config
215 .amount_to_ui_amount(1, 0, INT_SECONDS_PER_YEAR * 2)
216 .unwrap();
217 assert_eq!(ui_amount, "1");
218
219 let config = InterestBearingConfig {
221 rate_authority: OptionalNonZeroPubkey::default(),
222 initialization_timestamp: 0.into(),
223 pre_update_average_rate: PodI16::from(500),
224 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
225 current_rate: PodI16::from(500),
226 };
227 let ui_amount = config
228 .amount_to_ui_amount(u64::MAX, 0, INT_SECONDS_PER_YEAR * 2)
229 .unwrap();
230 assert_eq!(ui_amount, "20386805083448100000");
231 let ui_amount = config
232 .amount_to_ui_amount(u64::MAX, 0, INT_SECONDS_PER_YEAR * 10_000)
233 .unwrap();
234 assert_eq!(ui_amount, "258917064265813830000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
236 }
237
238 #[test]
239 fn specific_ui_amount_to_amount() {
240 let config = InterestBearingConfig {
242 rate_authority: OptionalNonZeroPubkey::default(),
243 initialization_timestamp: 0.into(),
244 pre_update_average_rate: 500.into(),
245 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
246 current_rate: 500.into(),
247 };
248 let amount = config
250 .try_ui_amount_into_amount("1.0512710963760241", 0, INT_SECONDS_PER_YEAR)
251 .unwrap();
252 assert_eq!(1, amount);
253 let amount = config
255 .try_ui_amount_into_amount("0.10512710963760241", 1, INT_SECONDS_PER_YEAR)
256 .unwrap();
257 assert_eq!(amount, 1);
258 let amount = config
260 .try_ui_amount_into_amount("0.00000000010512710963760242", 10, INT_SECONDS_PER_YEAR)
261 .unwrap();
262 assert_eq!(amount, 1);
263
264 let amount = config
266 .try_ui_amount_into_amount("1.0512710963760241", 10, INT_SECONDS_PER_YEAR)
267 .unwrap();
268 assert_eq!(amount, 10_000_000_000);
269
270 let config = InterestBearingConfig {
272 rate_authority: OptionalNonZeroPubkey::default(),
273 initialization_timestamp: 0.into(),
274 pre_update_average_rate: PodI16::from(-500),
275 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
276 current_rate: PodI16::from(-500),
277 };
278 let amount = config
280 .try_ui_amount_into_amount("0.951229424500714", 0, INT_SECONDS_PER_YEAR)
281 .unwrap();
282 assert_eq!(amount, 1);
283
284 let config = InterestBearingConfig {
286 rate_authority: OptionalNonZeroPubkey::default(),
287 initialization_timestamp: 0.into(),
288 pre_update_average_rate: PodI16::from(-500),
289 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
290 current_rate: PodI16::from(500),
291 };
292 let amount = config
294 .try_ui_amount_into_amount("1", 0, INT_SECONDS_PER_YEAR * 2)
295 .unwrap();
296 assert_eq!(amount, 1);
297
298 let config = InterestBearingConfig {
300 rate_authority: OptionalNonZeroPubkey::default(),
301 initialization_timestamp: 0.into(),
302 pre_update_average_rate: PodI16::from(500),
303 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
304 current_rate: PodI16::from(500),
305 };
306 let amount = config
307 .try_ui_amount_into_amount("20386805083448100000", 0, INT_SECONDS_PER_YEAR * 2)
308 .unwrap();
309 assert_eq!(amount, u64::MAX);
310 let amount = config
311 .try_ui_amount_into_amount("258917064265813830000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 0, INT_SECONDS_PER_YEAR * 10_000)
312 .unwrap();
313 assert_eq!(amount, u64::MAX);
314 let amount = config
316 .try_ui_amount_into_amount("2.5891706426581383e236", 0, INT_SECONDS_PER_YEAR * 10_000)
317 .unwrap();
318 assert_eq!(amount, u64::MAX);
319 let amount = config
321 .try_ui_amount_into_amount("2.5891706426581383E236", 0, INT_SECONDS_PER_YEAR * 10_000)
322 .unwrap();
323 assert_eq!(amount, u64::MAX);
324
325 assert_eq!(
327 Err(ProgramError::InvalidArgument),
328 config.try_ui_amount_into_amount("20386805083448200001", 0, INT_SECONDS_PER_YEAR)
329 );
330
331 for fail_ui_amount in ["-0.0000000000000000000001", "inf", "-inf", "NaN"] {
332 assert_eq!(
333 Err(ProgramError::InvalidArgument),
334 config.try_ui_amount_into_amount(fail_ui_amount, 0, INT_SECONDS_PER_YEAR)
335 );
336 }
337 }
338
339 #[test]
340 fn specific_amount_to_ui_amount_no_interest() {
341 let config = InterestBearingConfig {
342 rate_authority: OptionalNonZeroPubkey::default(),
343 initialization_timestamp: 0.into(),
344 pre_update_average_rate: 0.into(),
345 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
346 current_rate: 0.into(),
347 };
348 for (amount, expected) in [(23, "0.23"), (110, "1.1"), (4200, "42"), (0, "0")] {
349 let ui_amount = config
350 .amount_to_ui_amount(amount, TEST_DECIMALS, INT_SECONDS_PER_YEAR)
351 .unwrap();
352 assert_eq!(ui_amount, expected);
353 }
354 }
355
356 #[test]
357 fn specific_ui_amount_to_amount_no_interest() {
358 let config = InterestBearingConfig {
359 rate_authority: OptionalNonZeroPubkey::default(),
360 initialization_timestamp: 0.into(),
361 pre_update_average_rate: 0.into(),
362 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
363 current_rate: 0.into(),
364 };
365 for (ui_amount, expected) in [
366 ("0.23", 23),
367 ("0.20", 20),
368 ("0.2000", 20),
369 (".2", 20),
370 ("1.1", 110),
371 ("1.10", 110),
372 ("42", 4200),
373 ("42.", 4200),
374 ("0", 0),
375 ] {
376 let amount = config
377 .try_ui_amount_into_amount(ui_amount, TEST_DECIMALS, INT_SECONDS_PER_YEAR)
378 .unwrap();
379 assert_eq!(expected, amount);
380 }
381
382 let amount = config
384 .try_ui_amount_into_amount("0.111", TEST_DECIMALS, INT_SECONDS_PER_YEAR)
385 .unwrap();
386 assert_eq!(11, amount);
387
388 for ui_amount in ["", ".", "0.t"] {
390 assert_eq!(
391 Err(ProgramError::InvalidArgument),
392 config.try_ui_amount_into_amount(ui_amount, TEST_DECIMALS, INT_SECONDS_PER_YEAR),
393 );
394 }
395 }
396
397 prop_compose! {
398 fn low_middle_high()
400 (middle in 1..i64::MAX - 1)
401 (low in 0..=middle, middle in Just(middle), high in middle..=i64::MAX)
402 -> (i64, i64, i64) {
403 (low, middle, high)
404 }
405 }
406
407 proptest! {
408 #[test]
409 fn time_weighted_average_calc(
410 current_rate in i16::MIN..i16::MAX,
411 pre_update_average_rate in i16::MIN..i16::MAX,
412 (initialization_timestamp, last_update_timestamp, current_timestamp) in low_middle_high(),
413 ) {
414 let config = InterestBearingConfig {
415 rate_authority: OptionalNonZeroPubkey::default(),
416 initialization_timestamp: initialization_timestamp.into(),
417 pre_update_average_rate: pre_update_average_rate.into(),
418 last_update_timestamp: last_update_timestamp.into(),
419 current_rate: current_rate.into(),
420 };
421 let new_rate = config.time_weighted_average_rate(current_timestamp).unwrap();
422 if pre_update_average_rate <= current_rate {
423 assert!(pre_update_average_rate <= new_rate);
424 assert!(new_rate <= current_rate);
425 } else {
426 assert!(current_rate <= new_rate);
427 assert!(new_rate <= pre_update_average_rate);
428 }
429 }
430
431 #[test]
432 fn amount_to_ui_amount(
433 current_rate in i16::MIN..i16::MAX,
434 pre_update_average_rate in i16::MIN..i16::MAX,
435 (initialization_timestamp, last_update_timestamp, current_timestamp) in low_middle_high(),
436 amount in 0..=u64::MAX,
437 decimals in 0u8..20u8,
438 ) {
439 let config = InterestBearingConfig {
440 rate_authority: OptionalNonZeroPubkey::default(),
441 initialization_timestamp: initialization_timestamp.into(),
442 pre_update_average_rate: pre_update_average_rate.into(),
443 last_update_timestamp: last_update_timestamp.into(),
444 current_rate: current_rate.into(),
445 };
446 let ui_amount = config.amount_to_ui_amount(amount, decimals, current_timestamp);
447 assert!(ui_amount.is_some());
448 }
449 }
450}