payment_request: truncate long swap-to amounts
What changed, and why it matters
This commit changes how the BitBox02 hardware wallet displays very long swap-to amounts on its screen. Previously, an extremely long amount string could be shown in full, potentially overflowing the display or making the user interface unusable. Now, long decimal portions are truncated with '...' so the integer part and a fixed number of decimal digits remain visible. This is a UI hardening change rather than a fix for a remote exploit.
Treat as a minor UI hardening improvement. Review whether 13 characters is an appropriate display budget for all supported locales and font widths, and ensure the truncation logic cannot be bypassed by malformed input. No urgent security response is indicated by the diff alone.
Security signals we found
UI truncation of untrusted user/remote input before display
Prevention of display overflow / clipping of swap confirmation screen
Alignment with existing Ethereum amount.rs truncation budget
No input validation weakening; invalid amount strings still return Error::InvalidInput
Evidence from the diff
The patch adds format_coin_purchase_amount_for_display() in payment_request.rs. It parses a coin-purchase amount string (e.g., ‘12.45678901234 ETH’), and if the numeric part exceeds 13 characters, truncates after the decimal point, appending ‘…’ before the unit. The previous code passed the raw coin_purchase_memo.amount directly to confirm_swap(). The change aligns swap-to amount display with existing Ethereum amount truncation behavior. No cryptographic, authorization, or parsing correctness changes are present.
Changed components
src/rust/bitbox02-rust/src/hww/api/payment_request.rsBitBox02 swap confirmation UI flowCoin purchase memo display pathInspect captured patch +103 / −2
diff --git a/src/rust/bitbox02-rust/src/hww/api/payment_request.rs b/src/rust/bitbox02-rust/src/hww/api/payment_request.rs
index c02836e..c9f5b39 100644
--- a/src/rust/bitbox02-rust/src/hww/api/payment_request.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/payment_request.rs
@@ -4,6 +4,7 @@ use super::Error;
use crate::hal::ui::ConfirmParams;
use crate::pb;
+use alloc::string::String;
use alloc::vec::Vec;
#[cfg(feature = "app-ethereum")]
use num_bigint::BigUint;
@@ -22,6 +23,8 @@ use bitcoin::secp256k1;
// Arbitrary limit on number of memos that a payment request can show to the user.
const MAX_MEMOS_NUM: usize = 3;
+// Keep in sync with `hww/api/ethereum/amount.rs`.
+const COIN_PURCHASE_AMOUNT_TRUNCATE_SIZE: usize = 13;
struct Identity {
name: &'static str,
@@ -110,6 +113,32 @@ fn parse_coin_purchase_amount(amount: &str) -> Result<(&str, &str), Error> {
Ok((destination_amount, destination_unit))
}
+/// Formats a coin purchase amount for display on the swap screen.
+///
+/// This matches the truncation budget used by Ethereum amount formatting, but only truncates after
+/// the decimal point so integer digits are always preserved.
+fn format_coin_purchase_amount_for_display(amount: &str) -> Result<String, Error> {
+ let (destination_amount, destination_unit) = parse_coin_purchase_amount(amount)?;
+
+ if destination_amount.len() <= COIN_PURCHASE_AMOUNT_TRUNCATE_SIZE {
+ return Ok(amount.into());
+ }
+
+ let Some(decimal_position) = destination_amount.find('.') else {
+ return Ok(amount.into());
+ };
+
+ if decimal_position + 1 >= COIN_PURCHASE_AMOUNT_TRUNCATE_SIZE {
+ return Ok(amount.into());
+ }
+
+ Ok(format!(
+ "{}... {}",
+ &destination_amount[..COIN_PURCHASE_AMOUNT_TRUNCATE_SIZE],
+ destination_unit,
+ ))
+}
+
/// Prompt the user to verify the payment request UI flow.
/// The caller is responsible for formatting `payment_request.total_amount`
/// into the final display string for the sent amount, e.g. `"0.1 BTC"`.
@@ -149,9 +178,14 @@ pub async fn user_verify(
Memo {
memo: Some(memo::Memo::CoinPurchaseMemo(coin_purchase_memo)),
} => {
- let _ = parse_coin_purchase_amount(&coin_purchase_memo.amount)?;
+ let displayed_destination_amount =
+ format_coin_purchase_amount_for_display(&coin_purchase_memo.amount)?;
hal.ui()
- .confirm_swap("Swap", displayed_source_amount, &coin_purchase_memo.amount)
+ .confirm_swap(
+ "Swap",
+ displayed_source_amount,
+ &displayed_destination_amount,
+ )
.await?;
}
_ => return Err(Error::InvalidInput),
@@ -553,6 +587,34 @@ mod tests {
}
}
+ #[test]
+ fn test_format_coin_purchase_amount_for_display() {
+ assert_eq!(
+ format_coin_purchase_amount_for_display("0.25 ETH"),
+ Ok("0.25 ETH".into())
+ );
+ assert_eq!(
+ format_coin_purchase_amount_for_display("12.45678901234 ETH"),
+ Ok("12.4567890123... ETH".into())
+ );
+ assert_eq!(
+ format_coin_purchase_amount_for_display("1.2345678901234 BTC"),
+ Ok("1.23456789012... BTC".into())
+ );
+ assert_eq!(
+ format_coin_purchase_amount_for_display("12345678901234 ETH"),
+ Ok("12345678901234 ETH".into())
+ );
+ assert_eq!(
+ format_coin_purchase_amount_for_display("123456789012.34 ETH"),
+ Ok("123456789012.34 ETH".into())
+ );
+ assert_eq!(
+ format_coin_purchase_amount_for_display("foo ETH"),
+ Err(Error::InvalidInput)
+ );
+ }
+
#[test]
fn test_sighash() {
let source_coin_type = params::get(pb::BtcCoin::Tbtc).slip44();
@@ -1400,6 +1462,45 @@ mod tests {
);
}
+ #[cfg(feature = "app-ethereum")]
+ #[async_test::test]
+ async fn test_user_verify_swap_truncated_destination_amount() {
+ let mut mock_hal = TestingHal::new();
+ user_verify(
+ &mut mock_hal,
+ &pb::BtcPaymentRequestRequest {
+ recipient_name: "SWAPKIT (Provider)".into(),
+ memos: vec![make_coin_purchase_memo(
+ 60,
+ "12.45678901234 ETH",
+ "0x123",
+ Some(dummy_eth_address_derivation(/*valid=*/ true)),
+ )],
+ nonce: vec![],
+ total_amount: 25000000,
+ signature: vec![],
+ },
+ "0.25000000 BTC",
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(
+ mock_hal.ui.screens,
+ vec![
+ Screen::Recipient {
+ recipient: "SWAPKIT (Provider)".into(),
+ amount: "0.25000000 BTC".into(),
+ },
+ Screen::Swap {
+ title: "Swap".into(),
+ from: "0.25000000 BTC".into(),
+ to: "12.4567890123... ETH".into(),
+ },
+ ]
+ );
+ }
+
#[cfg(feature = "app-litecoin")]
#[async_test::test]
async fn test_user_verify_swap_btc_destination() {
Why this scored 25/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.