1#[cfg(feature = "serde")]
2use serde::{Deserialize, Serialize};
3use {
4 crate::{
5 extension::{Extension, ExtensionType},
6 trim_ui_amount_string,
7 },
8 bytemuck::{Pod, Zeroable},
9 solana_program_error::ProgramError,
10 spl_pod::{
11 optional_keys::OptionalNonZeroPubkey,
12 primitives::{PodI16, PodI64},
13 },
14 std::convert::TryInto,
15};
16
17pub mod instruction;
19
20pub type BasisPoints = PodI16;
22const ONE_IN_BASIS_POINTS: f64 = 10_000.;
23const SECONDS_PER_YEAR: f64 = 60. * 60. * 24. * 365.24;
24
25pub type UnixTimestamp = PodI64;
27
28#[repr(C)]
37#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
38#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
39#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
40pub struct InterestBearingConfig {
41 pub rate_authority: OptionalNonZeroPubkey,
43 pub initialization_timestamp: UnixTimestamp,
45 pub pre_update_average_rate: BasisPoints,
47 pub last_update_timestamp: UnixTimestamp,
49 pub current_rate: BasisPoints,
51}
52impl InterestBearingConfig {
53 fn pre_update_timespan(&self) -> Option<i64> {
54 i64::from(self.last_update_timestamp).checked_sub(self.initialization_timestamp.into())
55 }
56
57 fn pre_update_exp(&self) -> Option<f64> {
58 let numerator = (i16::from(self.pre_update_average_rate) as i128)
59 .checked_mul(self.pre_update_timespan()? as i128)? as f64;
60 let exponent = numerator / SECONDS_PER_YEAR / ONE_IN_BASIS_POINTS;
61 Some(exponent.exp())
62 }
63
64 fn post_update_timespan(&self, unix_timestamp: i64) -> Option<i64> {
65 unix_timestamp.checked_sub(self.last_update_timestamp.into())
66 }
67
68 fn post_update_exp(&self, unix_timestamp: i64) -> Option<f64> {
69 let numerator = (i16::from(self.current_rate) as i128)
70 .checked_mul(self.post_update_timespan(unix_timestamp)? as i128)?
71 as f64;
72 let exponent = numerator / SECONDS_PER_YEAR / ONE_IN_BASIS_POINTS;
73 Some(exponent.exp())
74 }
75
76 fn total_scale(&self, decimals: u8, unix_timestamp: i64) -> Option<f64> {
77 Some(
78 self.pre_update_exp()? * self.post_update_exp(unix_timestamp)?
79 / 10_f64.powi(decimals as i32),
80 )
81 }
82
83 pub fn amount_to_ui_amount(
86 &self,
87 amount: u64,
88 decimals: u8,
89 unix_timestamp: i64,
90 ) -> Option<String> {
91 let scaled_amount_with_interest =
92 (amount as f64) * self.total_scale(decimals, unix_timestamp)?;
93 let ui_amount = format!("{scaled_amount_with_interest:.*}", decimals as usize);
94 Some(trim_ui_amount_string(ui_amount, decimals))
95 }
96
97 pub fn try_ui_amount_into_amount(
100 &self,
101 ui_amount: &str,
102 decimals: u8,
103 unix_timestamp: i64,
104 ) -> Result<u64, ProgramError> {
105 let scaled_amount = ui_amount
106 .parse::<f64>()
107 .map_err(|_| ProgramError::InvalidArgument)?;
108 let amount = scaled_amount
109 / self
110 .total_scale(decimals, unix_timestamp)
111 .ok_or(ProgramError::InvalidArgument)?;
112 if amount > (u64::MAX as f64) || amount < (u64::MIN as f64) || amount.is_nan() {
113 Err(ProgramError::InvalidArgument)
114 } else {
115 Ok(amount.round() as u64)
118 }
119 }
120
121 pub fn time_weighted_average_rate(&self, current_timestamp: i64) -> Option<i16> {
132 let initialization_timestamp = i64::from(self.initialization_timestamp) as i128;
133 let last_update_timestamp = i64::from(self.last_update_timestamp) as i128;
134
135 let r_1 = i16::from(self.pre_update_average_rate) as i128;
136 let t_1 = last_update_timestamp.checked_sub(initialization_timestamp)?;
137 let r_2 = i16::from(self.current_rate) as i128;
138 let t_2 = (current_timestamp as i128).checked_sub(last_update_timestamp)?;
139 let total_timespan = t_1.checked_add(t_2)?;
140 let average_rate = if total_timespan == 0 {
141 r_2
144 } else {
145 r_1.checked_mul(t_1)?
146 .checked_add(r_2.checked_mul(t_2)?)?
147 .checked_div(total_timespan)?
148 };
149 average_rate.try_into().ok()
150 }
151}
152impl Extension for InterestBearingConfig {
153 const TYPE: ExtensionType = ExtensionType::InterestBearingConfig;
154}
155
156#[cfg(test)]
157mod tests {
158 use {super::*, proptest::prelude::*};
159
160 const INT_SECONDS_PER_YEAR: i64 = 6 * 6 * 24 * 36524;
161 const TEST_DECIMALS: u8 = 2;
162
163 #[test]
164 fn seconds_per_year() {
165 assert_eq!(SECONDS_PER_YEAR, 31_556_736.);
166 assert_eq!(INT_SECONDS_PER_YEAR, 31_556_736);
167 }
168
169 #[test]
170 fn specific_amount_to_ui_amount() {
171 const ONE: u64 = 1_000_000_000_000_000_000;
172 let config = InterestBearingConfig {
174 rate_authority: OptionalNonZeroPubkey::default(),
175 initialization_timestamp: 0.into(),
176 pre_update_average_rate: 500.into(),
177 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
178 current_rate: 500.into(),
179 };
180 let ui_amount = config
182 .amount_to_ui_amount(ONE, 18, INT_SECONDS_PER_YEAR)
183 .unwrap();
184 assert_eq!(ui_amount, "1.051271096376024117");
185 let ui_amount = config
187 .amount_to_ui_amount(ONE, 19, INT_SECONDS_PER_YEAR)
188 .unwrap();
189 assert_eq!(ui_amount, "0.1051271096376024117");
190 let ui_amount = config
192 .amount_to_ui_amount(ONE, 28, INT_SECONDS_PER_YEAR)
193 .unwrap();
194 assert_eq!(ui_amount, "0.0000000001051271096376024175"); let ui_amount = config
198 .amount_to_ui_amount(10_000_000_000, 10, INT_SECONDS_PER_YEAR)
199 .unwrap();
200 assert_eq!(ui_amount, "1.0512710964");
201
202 let config = InterestBearingConfig {
204 rate_authority: OptionalNonZeroPubkey::default(),
205 initialization_timestamp: 0.into(),
206 pre_update_average_rate: PodI16::from(-500),
207 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
208 current_rate: PodI16::from(-500),
209 };
210 let ui_amount = config
212 .amount_to_ui_amount(ONE, 18, INT_SECONDS_PER_YEAR)
213 .unwrap();
214 assert_eq!(ui_amount, "0.951229424500713905");
215
216 let config = InterestBearingConfig {
218 rate_authority: OptionalNonZeroPubkey::default(),
219 initialization_timestamp: 0.into(),
220 pre_update_average_rate: PodI16::from(-500),
221 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
222 current_rate: PodI16::from(500),
223 };
224 let ui_amount = config
226 .amount_to_ui_amount(1, 0, INT_SECONDS_PER_YEAR * 2)
227 .unwrap();
228 assert_eq!(ui_amount, "1");
229
230 let config = InterestBearingConfig {
232 rate_authority: OptionalNonZeroPubkey::default(),
233 initialization_timestamp: 0.into(),
234 pre_update_average_rate: PodI16::from(500),
235 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
236 current_rate: PodI16::from(500),
237 };
238 let ui_amount = config
239 .amount_to_ui_amount(u64::MAX, 0, INT_SECONDS_PER_YEAR * 2)
240 .unwrap();
241 assert_eq!(ui_amount, "20386805083448098816");
242 let ui_amount = config
243 .amount_to_ui_amount(u64::MAX, 0, INT_SECONDS_PER_YEAR * 10_000)
244 .unwrap();
245 assert_eq!(ui_amount, "258917064265813826192025834755112557504850551118283225815045099303279643822914042296793377611277551888244755303462190670431480816358154467489350925148558569427069926786360814068189956495940285398273555561779717914539956777398245259214848");
247 }
248
249 #[test]
250 fn specific_ui_amount_to_amount() {
251 let config = InterestBearingConfig {
253 rate_authority: OptionalNonZeroPubkey::default(),
254 initialization_timestamp: 0.into(),
255 pre_update_average_rate: 500.into(),
256 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
257 current_rate: 500.into(),
258 };
259 let amount = config
261 .try_ui_amount_into_amount("1.0512710963760241", 0, INT_SECONDS_PER_YEAR)
262 .unwrap();
263 assert_eq!(1, amount);
264 let amount = config
266 .try_ui_amount_into_amount("0.10512710963760241", 1, INT_SECONDS_PER_YEAR)
267 .unwrap();
268 assert_eq!(amount, 1);
269 let amount = config
271 .try_ui_amount_into_amount("0.00000000010512710963760242", 10, INT_SECONDS_PER_YEAR)
272 .unwrap();
273 assert_eq!(amount, 1);
274
275 let amount = config
277 .try_ui_amount_into_amount("1.0512710963760241", 10, INT_SECONDS_PER_YEAR)
278 .unwrap();
279 assert_eq!(amount, 10_000_000_000);
280
281 let config = InterestBearingConfig {
283 rate_authority: OptionalNonZeroPubkey::default(),
284 initialization_timestamp: 0.into(),
285 pre_update_average_rate: PodI16::from(-500),
286 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
287 current_rate: PodI16::from(-500),
288 };
289 let amount = config
291 .try_ui_amount_into_amount("0.951229424500714", 0, INT_SECONDS_PER_YEAR)
292 .unwrap();
293 assert_eq!(amount, 1);
294
295 let config = InterestBearingConfig {
297 rate_authority: OptionalNonZeroPubkey::default(),
298 initialization_timestamp: 0.into(),
299 pre_update_average_rate: PodI16::from(-500),
300 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
301 current_rate: PodI16::from(500),
302 };
303 let amount = config
305 .try_ui_amount_into_amount("1", 0, INT_SECONDS_PER_YEAR * 2)
306 .unwrap();
307 assert_eq!(amount, 1);
308
309 let config = InterestBearingConfig {
311 rate_authority: OptionalNonZeroPubkey::default(),
312 initialization_timestamp: 0.into(),
313 pre_update_average_rate: PodI16::from(500),
314 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
315 current_rate: PodI16::from(500),
316 };
317 let amount = config
318 .try_ui_amount_into_amount("20386805083448100000", 0, INT_SECONDS_PER_YEAR * 2)
319 .unwrap();
320 assert_eq!(amount, u64::MAX);
321 let amount = config
322 .try_ui_amount_into_amount("258917064265813830000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 0, INT_SECONDS_PER_YEAR * 10_000)
323 .unwrap();
324 assert_eq!(amount, u64::MAX);
325 let amount = config
327 .try_ui_amount_into_amount("2.5891706426581383e236", 0, INT_SECONDS_PER_YEAR * 10_000)
328 .unwrap();
329 assert_eq!(amount, u64::MAX);
330 let amount = config
332 .try_ui_amount_into_amount("2.5891706426581383E236", 0, INT_SECONDS_PER_YEAR * 10_000)
333 .unwrap();
334 assert_eq!(amount, u64::MAX);
335
336 assert_eq!(
338 Err(ProgramError::InvalidArgument),
339 config.try_ui_amount_into_amount("20386805083448200001", 0, INT_SECONDS_PER_YEAR)
340 );
341
342 for fail_ui_amount in ["-0.0000000000000000000001", "inf", "-inf", "NaN"] {
343 assert_eq!(
344 Err(ProgramError::InvalidArgument),
345 config.try_ui_amount_into_amount(fail_ui_amount, 0, INT_SECONDS_PER_YEAR)
346 );
347 }
348 }
349
350 #[test]
351 fn specific_amount_to_ui_amount_no_interest() {
352 let config = InterestBearingConfig {
353 rate_authority: OptionalNonZeroPubkey::default(),
354 initialization_timestamp: 0.into(),
355 pre_update_average_rate: 0.into(),
356 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
357 current_rate: 0.into(),
358 };
359 for (amount, expected) in [(23, "0.23"), (110, "1.1"), (4200, "42"), (0, "0")] {
360 let ui_amount = config
361 .amount_to_ui_amount(amount, TEST_DECIMALS, INT_SECONDS_PER_YEAR)
362 .unwrap();
363 assert_eq!(ui_amount, expected);
364 }
365 }
366
367 #[test]
368 fn specific_ui_amount_to_amount_no_interest() {
369 let config = InterestBearingConfig {
370 rate_authority: OptionalNonZeroPubkey::default(),
371 initialization_timestamp: 0.into(),
372 pre_update_average_rate: 0.into(),
373 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
374 current_rate: 0.into(),
375 };
376 for (ui_amount, expected) in [
377 ("0.23", 23),
378 ("0.20", 20),
379 ("0.2000", 20),
380 (".2", 20),
381 ("1.1", 110),
382 ("1.10", 110),
383 ("42", 4200),
384 ("42.", 4200),
385 ("0", 0),
386 ] {
387 let amount = config
388 .try_ui_amount_into_amount(ui_amount, TEST_DECIMALS, INT_SECONDS_PER_YEAR)
389 .unwrap();
390 assert_eq!(expected, amount);
391 }
392
393 let amount = config
395 .try_ui_amount_into_amount("0.111", TEST_DECIMALS, INT_SECONDS_PER_YEAR)
396 .unwrap();
397 assert_eq!(11, amount);
398
399 for ui_amount in ["", ".", "0.t"] {
401 assert_eq!(
402 Err(ProgramError::InvalidArgument),
403 config.try_ui_amount_into_amount(ui_amount, TEST_DECIMALS, INT_SECONDS_PER_YEAR),
404 );
405 }
406 }
407
408 prop_compose! {
409 fn low_middle_high()
411 (middle in 1..i64::MAX - 1)
412 (low in 0..=middle, middle in Just(middle), high in middle..=i64::MAX)
413 -> (i64, i64, i64) {
414 (low, middle, high)
415 }
416 }
417
418 proptest! {
419 #[test]
420 fn time_weighted_average_calc(
421 current_rate in i16::MIN..i16::MAX,
422 pre_update_average_rate in i16::MIN..i16::MAX,
423 (initialization_timestamp, last_update_timestamp, current_timestamp) in low_middle_high(),
424 ) {
425 let config = InterestBearingConfig {
426 rate_authority: OptionalNonZeroPubkey::default(),
427 initialization_timestamp: initialization_timestamp.into(),
428 pre_update_average_rate: pre_update_average_rate.into(),
429 last_update_timestamp: last_update_timestamp.into(),
430 current_rate: current_rate.into(),
431 };
432 let new_rate = config.time_weighted_average_rate(current_timestamp).unwrap();
433 if pre_update_average_rate <= current_rate {
434 assert!(pre_update_average_rate <= new_rate);
435 assert!(new_rate <= current_rate);
436 } else {
437 assert!(current_rate <= new_rate);
438 assert!(new_rate <= pre_update_average_rate);
439 }
440 }
441
442 #[test]
443 fn amount_to_ui_amount(
444 current_rate in i16::MIN..i16::MAX,
445 pre_update_average_rate in i16::MIN..i16::MAX,
446 (initialization_timestamp, last_update_timestamp, current_timestamp) in low_middle_high(),
447 amount in 0..=u64::MAX,
448 decimals in 0u8..20u8,
449 ) {
450 let config = InterestBearingConfig {
451 rate_authority: OptionalNonZeroPubkey::default(),
452 initialization_timestamp: initialization_timestamp.into(),
453 pre_update_average_rate: pre_update_average_rate.into(),
454 last_update_timestamp: last_update_timestamp.into(),
455 current_rate: current_rate.into(),
456 };
457 let ui_amount = config.amount_to_ui_amount(amount, decimals, current_timestamp);
458 assert!(ui_amount.is_some());
459 }
460 }
461}