UI bypass
The goal of the Exchange application is to allow the user to perform a fully trusted SWAP through a single screen UI review.
The Exchange application handles the trust checking and the UI, the FROM coin application handles the final payment. This final payment needs to be validated without UI call as long as all conditions are strictly met.
Please refer to the sequence diagram if you want to see the process flow in more details.
The UI bypass on the coin application need to abide by the following rules:
- The bypass is used only when started by Exchange.
- The bypass is only used for a single type of simple transaction.
- The bypass is only used if the destination strictly matches.
- The bypass is only used if the amount strictly matches.
- The bypass is only used if the fees strictly matches.
- The bypass is only used once before returning to Exchange.
If a transaction received in the SWAP context does not match the requirements, it needs to be rejected without UI prompt. The Exchange application will handle the refusal screen display.
Example of UI bypass implementation in C Boilerplate
app-boilerplate/src/handler/sign_tx.c
#ifdef HAVE_SWAP
static int check_and_sign_swap_tx(transaction_ctx_t *tx_ctx) {
if (G_swap_response_ready) {
// Safety against trying to make the app sign multiple TX
// This code should never be triggered as the app is supposed to exit after
// sending the signed transaction
PRINTF("Safety against double signing triggered\n");
os_sched_exit(-1);
} else {
// We will quit the app after this transaction, whether it succeeds or fails
PRINTF("Swap response is ready, the app will quit after the next send\n");
// This boolean will make the io_send_sw family instant reply + return to exchange
G_swap_response_ready = true;
}
if (swap_check_validity(tx_ctx->transaction.value,
tx_ctx->transaction.fee,
tx_ctx->transaction.to,
&tx_ctx->token_info)) {
PRINTF("Swap response validated, sign the transaction\n");
validate_transaction(true);
}
// Unreachable because swap_check_validity() returns an error to exchange app OR
// validate_transaction() returns a success to exchange
os_sched_exit(0);
return 0;
}
#endif // HAVE_SWAP
Here is the function swap_check_validity() called for checking the content of the received TX against the data validated in the Exchange application,
app-boilerplate/src/swap/handle_swap_sign_transaction.c
/* Check if the Tx to sign have the same parameters as the ones previously validated */
bool swap_check_validity(uint64_t amount,
uint64_t fee,
const uint8_t* destination,
const token_info_t* token_info) {
PRINTF("Inside swap_check_validity\n");
if (!G_swap_validated.initialized) {
PRINTF("Swap structure is not initialized\n");
send_swap_error_simple(SW_SWAP_FAIL, SWAP_EC_ERROR_GENERIC, SWAP_ERROR_CODE);
// unreachable
os_sched_exit(0);
}
// Reject token transactions in swap context
if (G_context.tx_info.is_token_tx) {
if (is_token_swap()) {
// Check that the token is the expected one
if (strcmp(G_swap_validated.ticker, token_info->ticker) != 0 ||
G_swap_validated.decimals != token_info->decimals) {
PRINTF("Token info does not match\n");
PRINTF("Validated: %s (decimals: %d)\n",
G_swap_validated.ticker,
G_swap_validated.decimals);
PRINTF("Received: %s (decimals: %d)\n", token_info->ticker, token_info->decimals);
send_swap_error_simple(SW_SWAP_FAIL,
SWAP_EC_ERROR_WRONG_AMOUNT,
SWAP_ERROR_WRONG_TOKEN_INFO);
// unreachable
os_sched_exit(0);
} else {
PRINTF("Token info match\n");
}
} else {
PRINTF("Unexpected token transaction from swap context\n");
send_swap_error_simple(SW_SWAP_FAIL, SWAP_EC_ERROR_WRONG_METHOD, SWAP_ERROR_CODE);
// unreachable
os_sched_exit(0);
}
} else if (is_token_swap()) {
PRINTF("Token transactions expected from swap context\n");
send_swap_error_simple(SW_SWAP_FAIL, SWAP_EC_ERROR_WRONG_METHOD, SWAP_ERROR_CODE);
// unreachable
os_sched_exit(0);
}
if (G_swap_validated.amount != amount) {
PRINTF("Amount does not match, promised %lld, received %lld\n",
G_swap_validated.amount,
amount);
send_swap_error_simple(SW_SWAP_FAIL, SWAP_EC_ERROR_WRONG_AMOUNT, SWAP_ERROR_CODE);
// unreachable
os_sched_exit(0);
} else {
PRINTF("Amounts match \n");
}
if (G_swap_validated.fee != fee) {
PRINTF("Fee does not match, promised %lld, received %lld\n", G_swap_validated.fee, fee);
send_swap_error_simple(SW_SWAP_FAIL, SWAP_EC_ERROR_WRONG_FEES, SWAP_ERROR_CODE);
// unreachable
os_sched_exit(0);
} else {
PRINTF("Fees match \n");
}
char to[ADDRESS_LEN * 2 + 1] = {0};
format_hex(destination, ADDRESS_LEN, to, sizeof(to));
if (strcmp(G_swap_validated.recipient, to) != 0) {
PRINTF("Destination does not match\n");
PRINTF("Validated: %s\n", G_swap_validated.recipient);
PRINTF("Received: %s \n", to);
send_swap_error_simple(SW_SWAP_FAIL, SWAP_EC_ERROR_WRONG_DESTINATION, SWAP_ERROR_CODE);
// unreachable
os_sched_exit(0);
} else {
PRINTF("Destination is valid\n");
}
return true;
}
Example of UI bypass implementation in Rust Boilerplate
app-boilerplate-rust/src/handlers/sign_tx.rs
debug_print("Last chunk received, parsing tx\n");
// Try to deserialize the transaction
let (tx, _): (Tx, usize) = from_slice(&ctx.raw_tx).map_err(|_| AppSW::TxParsingFail)?;
debug_print("Tx parsed successfully\n");
// Check if in swap mode
if let Some(params) = ctx.swap_params {
ctx.review_finished = true;
if let Err(error) = crate::swap::check_swap_params(params, &tx) {
// The swap validation failed and returned us the common format error defined by the SDK
// Use SDK method to append error code and message in standard format
error.append_to_comm(comm);
Err(AppSW::SwapFail)
} else {
debug_print("Swap validation success, bypassing UI\n");
compute_signature_and_append(comm, ctx)
}
} else {
debug_print("Normal mode, showing UI\n");
// Display transaction. If user approves
// the transaction, sign it. Otherwise,
// return a "deny" status word.
if ui_display_tx(&tx)? {
ctx.review_finished = true;
compute_signature_and_append(comm, ctx)
} else {
ctx.review_finished = true;
Err(AppSW::Deny)
}
}
app-boilerplate-rust/src/swap.rs
/// This function performs a strict validation of the transaction to be signed
/// against the reference transaction parameters provided by the Exchange app.
/// It checks that:
/// 1. The transaction type matches the expected one (Only one type of Tx implemented so auto true).
/// 2. The transaction amount matches the swap amount exactly.
/// 3. The transaction fees matches the swap fees exactly. (fees are not implemented so auto true).
/// 4. The destination address matches the swap destination address exactly.
///
/// # Errors
///
/// Returns error if:
/// - Amount parsing fails (AmountCastFail)
/// - Amount doesn't match between tx and swap params (ErrorWrongAmount)
/// - Destination address has invalid UTF-8 (DestinationDecodeFail)
/// - Destination address hex decode fails (DestinationDecodeFail)
/// - Destination address doesn't match (ErrorWrongDestination)
pub fn check_swap_params(
params: &CreateTxParams,
tx: &Tx,
) -> Result<(), SwapError<SwapAppErrorCode>> {
debug_print("Swap mode detected\n");
// Validate amount
// Parse amount (u64 from big-endian bytes, right aligned in 16-byte buffer)
// Amount is stored in AMOUNT_BUF_SIZE (16 bytes) buffer, right-aligned big-endian
let start = params.amount.len() - 8;
let amount_bytes: [u8; 8] = params.amount[start..].try_into().map_err(|_| {
SwapError::without_message(
SwapErrorCommonCode::ErrorWrongAmount,
SwapAppErrorCode::AmountCastFail,
)
})?;
let swap_amount = u64::from_be_bytes(amount_bytes);
if tx.value != swap_amount {
debug_print("Swap amount mismatch\n");
debug_u64("Tx: ", tx.value);
debug_u64("Swap: ", swap_amount);
// Error detected, we return the error with detailed message in common SDK defined format
return Err(SwapError::with_message(
SwapErrorCommonCode::ErrorWrongAmount,
SwapAppErrorCode::Default,
format!("Amount tx {} != swap {}", tx.value, swap_amount),
));
}
// Validate destination
let dest_str =
core::str::from_utf8(¶ms.dest_address[..params.dest_address_len]).map_err(|_| {
SwapError::with_message(
SwapErrorCommonCode::ErrorWrongDestination,
SwapAppErrorCode::DestinationDecodeFail,
"Failed to read destination hex".to_string(),
)
})?;
let dest_hex = dest_str.strip_prefix("0x").unwrap_or(dest_str);
let mut swap_dest = [0u8; 20];
if hex::decode_to_slice(dest_hex, &mut swap_dest).is_err() {
debug_print("Swap dest hex decode fail\n");
return Err(SwapError::with_message(
SwapErrorCommonCode::ErrorWrongDestination,
SwapAppErrorCode::DestinationDecodeFail,
format!("Failed to decode destination hex: {}", dest_hex),
));
}
if tx.to != swap_dest {
debug_print("Swap destination mismatch\n");
debug_hex("Tx: ", &tx.to);
debug_hex("Swap: ", &swap_dest);
// Only build hex strings for error message (not on happy path)
let tx_hex = hex::encode(tx.to);
let swap_hex = hex::encode(swap_dest);
return Err(SwapError::with_message(
SwapErrorCommonCode::ErrorWrongDestination,
SwapAppErrorCode::Default,
format!("Destination mismatch: tx {} != swap {}", tx_hex, swap_hex),
));
}
debug_print("Swap validation success, bypassing UI\n");
Ok(())
}