1#[cfg(feature = "serde-traits")]
2use serde::{Deserialize, Serialize};
3use {
4 crate::{
5 extension::{Extension, ExtensionType},
6 trim_ui_amount_string,
7 },
8 bytemuck::{Pod, Zeroable},
9 solana_program::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 mod processor;
22
23pub type BasisPoints = PodI16;
25const ONE_IN_BASIS_POINTS: f64 = 10_000.;
26const SECONDS_PER_YEAR: f64 = 60. * 60. * 24. * 365.24;
27
28pub type UnixTimestamp = PodI64;
30
31#[repr(C)]
40#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))]
41#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))]
42#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
43pub struct InterestBearingConfig {
44 pub rate_authority: OptionalNonZeroPubkey,
46 pub initialization_timestamp: UnixTimestamp,
48 pub pre_update_average_rate: BasisPoints,
50 pub last_update_timestamp: UnixTimestamp,
52 pub current_rate: BasisPoints,
54}
55impl InterestBearingConfig {
56 fn pre_update_timespan(&self) -> Option<i64> {
57 i64::from(self.last_update_timestamp).checked_sub(self.initialization_timestamp.into())
58 }
59
60 fn pre_update_exp(&self) -> Option<f64> {
61 let numerator = (i16::from(self.pre_update_average_rate) as i128)
62 .checked_mul(self.pre_update_timespan()? as i128)? as f64;
63 let exponent = numerator / SECONDS_PER_YEAR / ONE_IN_BASIS_POINTS;
64 Some(exponent.exp())
65 }
66
67 fn post_update_timespan(&self, unix_timestamp: i64) -> Option<i64> {
68 unix_timestamp.checked_sub(self.last_update_timestamp.into())
69 }
70
71 fn post_update_exp(&self, unix_timestamp: i64) -> Option<f64> {
72 let numerator = (i16::from(self.current_rate) as i128)
73 .checked_mul(self.post_update_timespan(unix_timestamp)? as i128)?
74 as f64;
75 let exponent = numerator / SECONDS_PER_YEAR / ONE_IN_BASIS_POINTS;
76 Some(exponent.exp())
77 }
78
79 fn total_scale(&self, decimals: u8, unix_timestamp: i64) -> Option<f64> {
80 Some(
81 self.pre_update_exp()? * self.post_update_exp(unix_timestamp)?
82 / 10_f64.powi(decimals as i32),
83 )
84 }
85
86 pub fn amount_to_ui_amount(
89 &self,
90 amount: u64,
91 decimals: u8,
92 unix_timestamp: i64,
93 ) -> Option<String> {
94 let scaled_amount_with_interest =
95 (amount as f64) * self.total_scale(decimals, unix_timestamp)?;
96 let ui_amount = format!("{scaled_amount_with_interest:.*}", decimals as usize);
97 Some(trim_ui_amount_string(ui_amount, decimals))
98 }
99
100 pub fn try_ui_amount_into_amount(
103 &self,
104 ui_amount: &str,
105 decimals: u8,
106 unix_timestamp: i64,
107 ) -> Result<u64, ProgramError> {
108 let scaled_amount = ui_amount
109 .parse::<f64>()
110 .map_err(|_| ProgramError::InvalidArgument)?;
111 let amount = scaled_amount
112 / self
113 .total_scale(decimals, unix_timestamp)
114 .ok_or(ProgramError::InvalidArgument)?;
115 if amount > (u64::MAX as f64) || amount < (u64::MIN as f64) || amount.is_nan() {
116 Err(ProgramError::InvalidArgument)
117 } else {
118 Ok(amount.round() as u64)
121 }
122 }
123
124 pub fn time_weighted_average_rate(&self, current_timestamp: i64) -> Option<i16> {
135 let initialization_timestamp = i64::from(self.initialization_timestamp) as i128;
136 let last_update_timestamp = i64::from(self.last_update_timestamp) as i128;
137
138 let r_1 = i16::from(self.pre_update_average_rate) as i128;
139 let t_1 = last_update_timestamp.checked_sub(initialization_timestamp)?;
140 let r_2 = i16::from(self.current_rate) as i128;
141 let t_2 = (current_timestamp as i128).checked_sub(last_update_timestamp)?;
142 let total_timespan = t_1.checked_add(t_2)?;
143 let average_rate = if total_timespan == 0 {
144 r_2
147 } else {
148 r_1.checked_mul(t_1)?
149 .checked_add(r_2.checked_mul(t_2)?)?
150 .checked_div(total_timespan)?
151 };
152 average_rate.try_into().ok()
153 }
154}
155impl Extension for InterestBearingConfig {
156 const TYPE: ExtensionType = ExtensionType::InterestBearingConfig;
157}
158
159#[cfg(test)]
160mod tests {
161 use {super::*, proptest::prelude::*};
162
163 const INT_SECONDS_PER_YEAR: i64 = 6 * 6 * 24 * 36524;
164 const TEST_DECIMALS: u8 = 2;
165
166 #[test]
167 fn seconds_per_year() {
168 assert_eq!(SECONDS_PER_YEAR, 31_556_736.);
169 assert_eq!(INT_SECONDS_PER_YEAR, 31_556_736);
170 }
171
172 #[test]
173 fn specific_amount_to_ui_amount() {
174 const ONE: u64 = 1_000_000_000_000_000_000;
175 let config = InterestBearingConfig {
177 rate_authority: OptionalNonZeroPubkey::default(),
178 initialization_timestamp: 0.into(),
179 pre_update_average_rate: 500.into(),
180 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
181 current_rate: 500.into(),
182 };
183 let ui_amount = config
185 .amount_to_ui_amount(ONE, 18, INT_SECONDS_PER_YEAR)
186 .unwrap();
187 assert_eq!(ui_amount, "1.051271096376024117");
188 let ui_amount = config
190 .amount_to_ui_amount(ONE, 19, INT_SECONDS_PER_YEAR)
191 .unwrap();
192 assert_eq!(ui_amount, "0.1051271096376024117");
193 let ui_amount = config
195 .amount_to_ui_amount(ONE, 28, INT_SECONDS_PER_YEAR)
196 .unwrap();
197 assert_eq!(ui_amount, "0.0000000001051271096376024175"); let ui_amount = config
201 .amount_to_ui_amount(10_000_000_000, 10, INT_SECONDS_PER_YEAR)
202 .unwrap();
203 assert_eq!(ui_amount, "1.0512710964");
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(ONE, 18, INT_SECONDS_PER_YEAR)
216 .unwrap();
217 assert_eq!(ui_amount, "0.951229424500713905");
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
229 .amount_to_ui_amount(1, 0, INT_SECONDS_PER_YEAR * 2)
230 .unwrap();
231 assert_eq!(ui_amount, "1");
232
233 let config = InterestBearingConfig {
235 rate_authority: OptionalNonZeroPubkey::default(),
236 initialization_timestamp: 0.into(),
237 pre_update_average_rate: PodI16::from(500),
238 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
239 current_rate: PodI16::from(500),
240 };
241 let ui_amount = config
242 .amount_to_ui_amount(u64::MAX, 0, INT_SECONDS_PER_YEAR * 2)
243 .unwrap();
244 assert_eq!(ui_amount, "20386805083448098816");
245 let ui_amount = config
246 .amount_to_ui_amount(u64::MAX, 0, INT_SECONDS_PER_YEAR * 10_000)
247 .unwrap();
248 assert_eq!(ui_amount, "258917064265813826192025834755112557504850551118283225815045099303279643822914042296793377611277551888244755303462190670431480816358154467489350925148558569427069926786360814068189956495940285398273555561779717914539956777398245259214848");
250 }
251
252 #[test]
253 fn specific_ui_amount_to_amount() {
254 let config = InterestBearingConfig {
256 rate_authority: OptionalNonZeroPubkey::default(),
257 initialization_timestamp: 0.into(),
258 pre_update_average_rate: 500.into(),
259 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
260 current_rate: 500.into(),
261 };
262 let amount = config
264 .try_ui_amount_into_amount("1.0512710963760241", 0, INT_SECONDS_PER_YEAR)
265 .unwrap();
266 assert_eq!(1, amount);
267 let amount = config
269 .try_ui_amount_into_amount("0.10512710963760241", 1, INT_SECONDS_PER_YEAR)
270 .unwrap();
271 assert_eq!(amount, 1);
272 let amount = config
274 .try_ui_amount_into_amount("0.00000000010512710963760242", 10, INT_SECONDS_PER_YEAR)
275 .unwrap();
276 assert_eq!(amount, 1);
277
278 let amount = config
280 .try_ui_amount_into_amount("1.0512710963760241", 10, INT_SECONDS_PER_YEAR)
281 .unwrap();
282 assert_eq!(amount, 10_000_000_000);
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("0.951229424500714", 0, INT_SECONDS_PER_YEAR)
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
308 .try_ui_amount_into_amount("1", 0, INT_SECONDS_PER_YEAR * 2)
309 .unwrap();
310 assert_eq!(amount, 1);
311
312 let config = InterestBearingConfig {
314 rate_authority: OptionalNonZeroPubkey::default(),
315 initialization_timestamp: 0.into(),
316 pre_update_average_rate: PodI16::from(500),
317 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
318 current_rate: PodI16::from(500),
319 };
320 let amount = config
321 .try_ui_amount_into_amount("20386805083448100000", 0, INT_SECONDS_PER_YEAR * 2)
322 .unwrap();
323 assert_eq!(amount, u64::MAX);
324 let amount = config
325 .try_ui_amount_into_amount("258917064265813830000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 0, INT_SECONDS_PER_YEAR * 10_000)
326 .unwrap();
327 assert_eq!(amount, u64::MAX);
328 let amount = config
330 .try_ui_amount_into_amount("2.5891706426581383e236", 0, INT_SECONDS_PER_YEAR * 10_000)
331 .unwrap();
332 assert_eq!(amount, u64::MAX);
333 let amount = config
335 .try_ui_amount_into_amount("2.5891706426581383E236", 0, INT_SECONDS_PER_YEAR * 10_000)
336 .unwrap();
337 assert_eq!(amount, u64::MAX);
338
339 assert_eq!(
341 Err(ProgramError::InvalidArgument),
342 config.try_ui_amount_into_amount("20386805083448200001", 0, INT_SECONDS_PER_YEAR)
343 );
344
345 for fail_ui_amount in ["-0.0000000000000000000001", "inf", "-inf", "NaN"] {
346 assert_eq!(
347 Err(ProgramError::InvalidArgument),
348 config.try_ui_amount_into_amount(fail_ui_amount, 0, INT_SECONDS_PER_YEAR)
349 );
350 }
351 }
352
353 #[test]
354 fn specific_amount_to_ui_amount_no_interest() {
355 let config = InterestBearingConfig {
356 rate_authority: OptionalNonZeroPubkey::default(),
357 initialization_timestamp: 0.into(),
358 pre_update_average_rate: 0.into(),
359 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
360 current_rate: 0.into(),
361 };
362 for (amount, expected) in [(23, "0.23"), (110, "1.1"), (4200, "42"), (0, "0")] {
363 let ui_amount = config
364 .amount_to_ui_amount(amount, TEST_DECIMALS, INT_SECONDS_PER_YEAR)
365 .unwrap();
366 assert_eq!(ui_amount, expected);
367 }
368 }
369
370 #[test]
371 fn specific_ui_amount_to_amount_no_interest() {
372 let config = InterestBearingConfig {
373 rate_authority: OptionalNonZeroPubkey::default(),
374 initialization_timestamp: 0.into(),
375 pre_update_average_rate: 0.into(),
376 last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
377 current_rate: 0.into(),
378 };
379 for (ui_amount, expected) in [
380 ("0.23", 23),
381 ("0.20", 20),
382 ("0.2000", 20),
383 (".2", 20),
384 ("1.1", 110),
385 ("1.10", 110),
386 ("42", 4200),
387 ("42.", 4200),
388 ("0", 0),
389 ] {
390 let amount = config
391 .try_ui_amount_into_amount(ui_amount, TEST_DECIMALS, INT_SECONDS_PER_YEAR)
392 .unwrap();
393 assert_eq!(expected, amount);
394 }
395
396 let amount = config
398 .try_ui_amount_into_amount("0.111", TEST_DECIMALS, INT_SECONDS_PER_YEAR)
399 .unwrap();
400 assert_eq!(11, amount);
401
402 for ui_amount in ["", ".", "0.t"] {
404 assert_eq!(
405 Err(ProgramError::InvalidArgument),
406 config.try_ui_amount_into_amount(ui_amount, TEST_DECIMALS, INT_SECONDS_PER_YEAR),
407 );
408 }
409 }
410
411 prop_compose! {
412 fn low_middle_high()
414 (middle in 1..i64::MAX - 1)
415 (low in 0..=middle, middle in Just(middle), high in middle..=i64::MAX)
416 -> (i64, i64, i64) {
417 (low, middle, high)
418 }
419 }
420
421 proptest! {
422 #[test]
423 fn time_weighted_average_calc(
424 current_rate in i16::MIN..i16::MAX,
425 pre_update_average_rate in i16::MIN..i16::MAX,
426 (initialization_timestamp, last_update_timestamp, current_timestamp) in low_middle_high(),
427 ) {
428 let config = InterestBearingConfig {
429 rate_authority: OptionalNonZeroPubkey::default(),
430 initialization_timestamp: initialization_timestamp.into(),
431 pre_update_average_rate: pre_update_average_rate.into(),
432 last_update_timestamp: last_update_timestamp.into(),
433 current_rate: current_rate.into(),
434 };
435 let new_rate = config.time_weighted_average_rate(current_timestamp).unwrap();
436 if pre_update_average_rate <= current_rate {
437 assert!(pre_update_average_rate <= new_rate);
438 assert!(new_rate <= current_rate);
439 } else {
440 assert!(current_rate <= new_rate);
441 assert!(new_rate <= pre_update_average_rate);
442 }
443 }
444
445 #[test]
446 fn amount_to_ui_amount(
447 current_rate in i16::MIN..i16::MAX,
448 pre_update_average_rate in i16::MIN..i16::MAX,
449 (initialization_timestamp, last_update_timestamp, current_timestamp) in low_middle_high(),
450 amount in 0..=u64::MAX,
451 decimals in 0u8..20u8,
452 ) {
453 let config = InterestBearingConfig {
454 rate_authority: OptionalNonZeroPubkey::default(),
455 initialization_timestamp: initialization_timestamp.into(),
456 pre_update_average_rate: pre_update_average_rate.into(),
457 last_update_timestamp: last_update_timestamp.into(),
458 current_rate: current_rate.into(),
459 };
460 let ui_amount = config.amount_to_ui_amount(amount, decimals, current_timestamp);
461 assert!(ui_amount.is_some());
462 }
463 }
464}