What changed, and why it matters
This commit fixes a crash in the TON (The Open Network) jetton token display code on Keystone 3 hardware wallets. Previously, when a token amount was too large to fit in a 64-bit integer, the firmware would panic and crash. The fix switches to a larger 128-bit integer and formats the amount more carefully. A crafted transaction with an oversized amount could have caused the device to crash, potentially disrupting transaction signing or user experience.
Review whether other token amount parsing paths use `u64` or `unwrap()` on untrusted input; consider adding bounds checks and replacing `unwrap()` with error handling to prevent panics. Verify the fix handles edge cases such as decimal places larger than 38, zero divisor, and malformed `coins` strings.
Security signals we found
Integer overflow/panic in amount parsing (u64 -> u128 fix)
Use of floating-point division for token amounts replaced with integer arithmetic
Potential denial-of-service via crafted transaction amount causing firmware panic
Missing input validation / unwrap usage on user-supplied token amount string
Evidence from the diff
The function get_jetton_amount_text in rust/apps/ton/src/jettons.rs previously parsed the coins string with u64::from_str_radix, which panics on overflow for values exceeding u64::MAX. It also divided using f64, which can lose precision for large integers. The patch changes parsing to u128, computes integer and fractional parts with integer arithmetic, and formats the result as a string. A unit test was added. The crash is a denial-of-service/panic condition triggered by large but valid-looking TON jetton amounts.
Changed components
rust/apps/ton/src/jettons.rsTON jetton amount display/formattingKeystone 3 firmware transaction parsing/display layerInspect captured patch +30 / −3
diff --git a/rust/apps/ton/src/jettons.rs b/rust/apps/ton/src/jettons.rs
index 73b65c6..10ea7c3 100644
--- a/rust/apps/ton/src/jettons.rs
+++ b/rust/apps/ton/src/jettons.rs
@@ -45,7 +45,34 @@ pub fn get_jetton_amount_text(coins: String, contract_address: String) -> String
.iter()
.find_or_first(|v| v.contract_address.eq(&contract_address))
.unwrap();
- let value = u64::from_str_radix(&coins, 10).unwrap();
- let divisor = 10u64.pow(target.decimal as u32) as f64;
- format!("{} {}", (value as f64) / divisor, target.symbol)
+ let value = coins.parse::<u128>().unwrap();
+ let divisor = 10u128.pow(target.decimal as u32);
+
+ let integer_part = value / divisor;
+ let fractional_part = value % divisor;
+
+ let fractional_str = format!(
+ "{:0width$}",
+ fractional_part,
+ width = target.decimal as usize
+ );
+ let fractional_str = fractional_str.trim_end_matches('0');
+ if fractional_str.is_empty() {
+ format!("{} {}", integer_part, target.symbol)
+ } else {
+ format!("{}.{} {}", integer_part, fractional_str, target.symbol)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_get_jetton_amount_text() {
+ let coins = "30110292000".to_string();
+ let contract_address = "EQA2kCVNwVsil2EM2mB0SkXytxCqQjS4mttjDpnXmwG9T6bO".to_string();
+ let result = get_jetton_amount_text(coins, contract_address);
+ assert_eq!(result, "30.110292 STON");
+ }
}
Why this scored 42/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.