Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit a3e484f

Browse files
authored
token-2022: Take decimals into account for UI amount (#7540)
#### Problem The interest-bearing and scaled UI amount extensions don't take into account the mint decimals when printing the number, and they don't properly trim afterwards. This can be confusing. #### Summary of changes Update the UI amount conversion to take into account the decimals of precision, and then actually trim. Note that a lot of calculations become more precise because of this change!
1 parent 9854b11 commit a3e484f

File tree

3 files changed

+34
-20
lines changed

3 files changed

+34
-20
lines changed

token/program-2022/src/extension/interest_bearing_mint/mod.rs

+19-14
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
#[cfg(feature = "serde-traits")]
22
use serde::{Deserialize, Serialize};
33
use {
4-
crate::extension::{Extension, ExtensionType},
4+
crate::{
5+
extension::{Extension, ExtensionType},
6+
trim_ui_amount_string,
7+
},
58
bytemuck::{Pod, Zeroable},
69
solana_program::program_error::ProgramError,
710
spl_pod::{
@@ -81,7 +84,7 @@ impl InterestBearingConfig {
8184
}
8285

8386
/// Convert a raw amount to its UI representation using the given decimals
84-
/// field Excess zeroes or unneeded decimal point are trimmed.
87+
/// field. Excess zeroes or unneeded decimal point are trimmed.
8588
pub fn amount_to_ui_amount(
8689
&self,
8790
amount: u64,
@@ -90,7 +93,8 @@ impl InterestBearingConfig {
9093
) -> Option<String> {
9194
let scaled_amount_with_interest =
9295
(amount as f64) * self.total_scale(decimals, unix_timestamp)?;
93-
Some(scaled_amount_with_interest.to_string())
96+
let ui_amount = format!("{scaled_amount_with_interest:.*}", decimals as usize);
97+
Some(trim_ui_amount_string(ui_amount, decimals))
9498
}
9599

96100
/// Try to convert a UI representation of a token amount to its raw amount
@@ -167,6 +171,7 @@ mod tests {
167171

168172
#[test]
169173
fn specific_amount_to_ui_amount() {
174+
const ONE: u64 = 1_000_000_000_000_000_000;
170175
// constant 5%
171176
let config = InterestBearingConfig {
172177
rate_authority: OptionalNonZeroPubkey::default(),
@@ -177,25 +182,25 @@ mod tests {
177182
};
178183
// 1 year at 5% gives a total of exp(0.05) = 1.0512710963760241
179184
let ui_amount = config
180-
.amount_to_ui_amount(1, 0, INT_SECONDS_PER_YEAR)
185+
.amount_to_ui_amount(ONE, 18, INT_SECONDS_PER_YEAR)
181186
.unwrap();
182-
assert_eq!(ui_amount, "1.0512710963760241");
187+
assert_eq!(ui_amount, "1.051271096376024117");
183188
// with 1 decimal place
184189
let ui_amount = config
185-
.amount_to_ui_amount(1, 1, INT_SECONDS_PER_YEAR)
190+
.amount_to_ui_amount(ONE, 19, INT_SECONDS_PER_YEAR)
186191
.unwrap();
187-
assert_eq!(ui_amount, "0.10512710963760241");
192+
assert_eq!(ui_amount, "0.1051271096376024117");
188193
// with 10 decimal places
189194
let ui_amount = config
190-
.amount_to_ui_amount(1, 10, INT_SECONDS_PER_YEAR)
195+
.amount_to_ui_amount(ONE, 28, INT_SECONDS_PER_YEAR)
191196
.unwrap();
192-
assert_eq!(ui_amount, "0.00000000010512710963760242"); // different digit at the end!
197+
assert_eq!(ui_amount, "0.0000000001051271096376024175"); // different digits at the end!
193198

194199
// huge amount with 10 decimal places
195200
let ui_amount = config
196201
.amount_to_ui_amount(10_000_000_000, 10, INT_SECONDS_PER_YEAR)
197202
.unwrap();
198-
assert_eq!(ui_amount, "1.0512710963760241");
203+
assert_eq!(ui_amount, "1.0512710964");
199204

200205
// negative
201206
let config = InterestBearingConfig {
@@ -207,9 +212,9 @@ mod tests {
207212
};
208213
// 1 year at -5% gives a total of exp(-0.05) = 0.951229424500714
209214
let ui_amount = config
210-
.amount_to_ui_amount(1, 0, INT_SECONDS_PER_YEAR)
215+
.amount_to_ui_amount(ONE, 18, INT_SECONDS_PER_YEAR)
211216
.unwrap();
212-
assert_eq!(ui_amount, "0.951229424500714");
217+
assert_eq!(ui_amount, "0.951229424500713905");
213218

214219
// net out
215220
let config = InterestBearingConfig {
@@ -236,12 +241,12 @@ mod tests {
236241
let ui_amount = config
237242
.amount_to_ui_amount(u64::MAX, 0, INT_SECONDS_PER_YEAR * 2)
238243
.unwrap();
239-
assert_eq!(ui_amount, "20386805083448100000");
244+
assert_eq!(ui_amount, "20386805083448098816");
240245
let ui_amount = config
241246
.amount_to_ui_amount(u64::MAX, 0, INT_SECONDS_PER_YEAR * 10_000)
242247
.unwrap();
243248
// there's an underflow risk, but it works!
244-
assert_eq!(ui_amount, "258917064265813830000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
249+
assert_eq!(ui_amount, "258917064265813826192025834755112557504850551118283225815045099303279643822914042296793377611277551888244755303462190670431480816358154467489350925148558569427069926786360814068189956495940285398273555561779717914539956777398245259214848");
245250
}
246251

247252
#[test]

token/program-2022/src/extension/scaled_ui_amount/mod.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
#[cfg(feature = "serde-traits")]
22
use serde::{Deserialize, Serialize};
33
use {
4-
crate::extension::{Extension, ExtensionType},
4+
crate::{
5+
extension::{Extension, ExtensionType},
6+
trim_ui_amount_string,
7+
},
58
bytemuck::{Pod, Zeroable},
69
solana_program::program_error::ProgramError,
710
spl_pod::{optional_keys::OptionalNonZeroPubkey, primitives::PodI64},
@@ -72,7 +75,8 @@ impl ScaledUiAmountConfig {
7275
unix_timestamp: i64,
7376
) -> Option<String> {
7477
let scaled_amount = (amount as f64) * self.total_multiplier(decimals, unix_timestamp);
75-
Some(scaled_amount.to_string())
78+
let ui_amount = format!("{scaled_amount:.*}", decimals as usize);
79+
Some(trim_ui_amount_string(ui_amount, decimals))
7680
}
7781

7882
/// Try to convert a UI representation of a token amount to its raw amount

token/program-2022/src/lib.rs

+9-4
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,17 @@ pub fn amount_to_ui_amount_string(amount: u64, decimals: u8) -> String {
6262
/// Convert a raw amount to its UI representation using the given decimals field
6363
/// Excess zeroes or unneeded decimal point are trimmed.
6464
pub fn amount_to_ui_amount_string_trimmed(amount: u64, decimals: u8) -> String {
65-
let mut s = amount_to_ui_amount_string(amount, decimals);
65+
let s = amount_to_ui_amount_string(amount, decimals);
66+
trim_ui_amount_string(s, decimals)
67+
}
68+
69+
/// Trims a string number by removing excess zeroes or unneeded decimal point
70+
fn trim_ui_amount_string(mut ui_amount: String, decimals: u8) -> String {
6671
if decimals > 0 {
67-
let zeros_trimmed = s.trim_end_matches('0');
68-
s = zeros_trimmed.trim_end_matches('.').to_string();
72+
let zeros_trimmed = ui_amount.trim_end_matches('0');
73+
ui_amount = zeros_trimmed.trim_end_matches('.').to_string();
6974
}
70-
s
75+
ui_amount
7176
}
7277

7378
/// Try to convert a UI representation of a token amount to its raw amount using

0 commit comments

Comments
 (0)