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