What changed, and why it matters
This commit fixes how Solana token (SPL) amounts are displayed and confirmed in Stack Wallet's transaction confirmation screen. Previously, the app likely treated SPL tokens like Ethereum tokens, which could cause wrong decimal places, wrong token symbols, or even sending/confirming with incorrect values. The change adds separate code paths for Solana tokens so the correct token info is used for formatting, price lookup, and confirming the send.
Review the Solana token wallet provider implementation and confirm that tokenDecimals, tokenMint, and tokenSymbol are validated before use. Add tests covering SPL token send confirmation for tokens with non-default decimals. Verify that the fallback values (e.g., decimals=9, symbol='TOKEN') cannot be exploited to trick users into approving misformatted amounts.
Security signals we found
Incorrect amount formatting due to wrong token decimals can mislead users about transaction value
Use of wrong token wallet provider could lead to sending through unintended contract/token path
UI display mismatch between token symbol/decimals and actual SPL token metadata
Price lookup using wrong token identifier could show incorrect fiat value
Fix adds explicit Solana-specific branch, suggesting prior code assumed ERC-20 semantics for all tokens
Evidence from the diff
The patch updates lib/pages/send_view/confirm_transaction_view.dart to branch token-transaction handling by wallet type. For SolanaWallet it now uses pCurrentSolanaTokenWallet and constructs an SplToken from TxData fields (tokenMint, tokenSymbol, tokenDecimals) for amount formatting and price lookups, instead of reusing the Ethereum ERC-20 token wallet and tokenContract. It also routes confirmSend() and refresh() through the Solana token wallet provider for SPL transactions. This is a correctness fix for SPL token amount formatting and related UI/flow logic.
Changed components
lib/pages/send_view/confirm_transaction_view.dartSolana SPL token send confirmation flowAmount formatter with ethContract and splToken parameterspCurrentSolanaTokenWallet providerpCurrentTokenWallet (Ethereum token wallet) providerInspect captured patch +77 / −22
diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart
index 5cb12e0..ee59315 100644
--- a/lib/pages/send_view/confirm_transaction_view.dart
+++ b/lib/pages/send_view/confirm_transaction_view.dart
@@ -17,6 +17,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
+import '../../models/isar/models/solana/spl_token.dart';
import '../../models/isar/models/transaction_note.dart';
import '../../notifications/show_flush_bar.dart';
import '../../pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart';
@@ -37,10 +38,12 @@ import '../../wallets/crypto_currency/coins/ethereum.dart';
import '../../wallets/crypto_currency/coins/mimblewimblecoin.dart';
import '../../wallets/crypto_currency/intermediate/nano_currency.dart';
import '../../wallets/isar/providers/eth/current_token_wallet_provider.dart';
+import '../../wallets/isar/providers/solana/current_sol_token_wallet_provider.dart';
import '../../wallets/isar/providers/wallet_info_provider.dart';
import '../../wallets/models/tx_data.dart';
import '../../wallets/wallet/impl/firo_wallet.dart';
import '../../wallets/wallet/impl/mimblewimblecoin_wallet.dart';
+import '../../wallets/wallet/impl/solana_wallet.dart';
import '../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart';
import '../../widgets/background.dart';
import '../../widgets/conditional_parent.dart';
@@ -214,9 +217,17 @@ class _ConfirmTransactionViewState
try {
if (widget.isTokenTx) {
- txDataFuture = ref
- .read(pCurrentTokenWallet)!
- .confirmSend(txData: widget.txData);
+ if (wallet is SolanaWallet) {
+ // For Solana tokens, use the Solana token wallet.
+ txDataFuture = ref
+ .read(pCurrentSolanaTokenWallet)!
+ .confirmSend(txData: widget.txData);
+ } else {
+ // For Ethereum tokens, use the Ethereum token wallet.
+ txDataFuture = ref
+ .read(pCurrentTokenWallet)!
+ .confirmSend(txData: widget.txData);
+ }
} else if (widget.isPaynymNotificationTransaction) {
txDataFuture = (wallet as PaynymInterface).broadcastNotificationTx(
txData: widget.txData,
@@ -301,7 +312,11 @@ class _ConfirmTransactionViewState
}
if (widget.isTokenTx) {
- unawaited(ref.read(pCurrentTokenWallet)!.refresh());
+ if (wallet is SolanaWallet) {
+ unawaited(ref.read(pCurrentSolanaTokenWallet)!.refresh());
+ } else {
+ unawaited(ref.read(pCurrentTokenWallet)!.refresh());
+ }
} else {
unawaited(wallet.refresh());
}
@@ -439,10 +454,19 @@ class _ConfirmTransactionViewState
final coin = ref.watch(pWalletCoin(walletId));
final String unit;
+ final wallet = ref.watch(pWallets).getWallet(walletId);
if (widget.isTokenTx) {
- unit = ref.watch(
- pCurrentTokenWallet.select((value) => value!.tokenContract.symbol),
- );
+ if (wallet is SolanaWallet) {
+ // For Solana tokens, use the Solana token wallet provider or TxData as fallback.
+ unit = ref.watch(
+ pCurrentSolanaTokenWallet.select((value) => value?.tokenSymbol),
+ ) ?? widget.txData.tokenSymbol ?? "TOKEN";
+ } else {
+ // For Ethereum tokens, use the Ethereum token wallet provider.
+ unit = ref.watch(
+ pCurrentTokenWallet.select((value) => value!.tokenContract.symbol),
+ );
+ }
} else {
unit = coin.ticker;
}
@@ -450,8 +474,6 @@ class _ConfirmTransactionViewState
final Amount? fee;
final Amount amountWithoutChange;
- final wallet = ref.watch(pWallets).getWallet(walletId);
-
if (wallet is FiroWallet) {
switch (ref.read(publicPrivateBalanceStateProvider.state).state) {
case BalanceType.public:
@@ -604,11 +626,19 @@ class _ConfirmTransactionViewState
.watch(pAmountFormatter(coin))
.format(
amountWithoutChange,
- ethContract: widget.isTokenTx
+ ethContract: widget.isTokenTx && wallet is! SolanaWallet
? ref
.watch(pCurrentTokenWallet)!
.tokenContract
: null,
+ splToken: widget.isTokenTx && wallet is SolanaWallet
+ ? SplToken(
+ address: widget.txData.tokenMint ?? "unknown",
+ name: widget.txData.tokenSymbol ?? "Token",
+ symbol: widget.txData.tokenSymbol ?? "TOKEN",
+ decimals: widget.txData.tokenDecimals ?? 9,
+ )
+ : null,
),
style: STextStyles.itemSubtitle12(context),
textAlign: TextAlign.right,
@@ -794,17 +824,34 @@ class _ConfirmTransactionViewState
if (externalCalls) {
final price = widget.isTokenTx
- ? ref
- .read(
- priceAnd24hChangeNotifierProvider,
- )
- .getTokenPrice(
- ref
- .read(pCurrentTokenWallet)!
- .tokenContract
- .address,
- )
- ?.value
+ ? (wallet is SolanaWallet
+ ? // For Solana tokens, use tokenMint from provider or TxData.
+ ref
+ .read(
+ priceAnd24hChangeNotifierProvider,
+ )
+ .getTokenPrice(
+ ref
+ .read(
+ pCurrentSolanaTokenWallet,
+ )
+ ?.tokenMint ??
+ widget.txData.tokenMint ??
+ "unknown",
+ )
+ ?.value
+ : // For Ethereum tokens, use contract address.
+ ref
+ .read(
+ priceAnd24hChangeNotifierProvider,
+ )
+ .getTokenPrice(
+ ref
+ .read(pCurrentTokenWallet)!
+ .tokenContract
+ .address,
+ )
+ ?.value)
: ref
.read(
priceAnd24hChangeNotifierProvider,
@@ -832,13 +879,21 @@ class _ConfirmTransactionViewState
.watch(pAmountFormatter(coin))
.format(
amountWithoutChange,
- ethContract: widget.isTokenTx
+ ethContract: widget.isTokenTx && wallet is! SolanaWallet
? ref
.watch(
pCurrentTokenWallet,
)!
.tokenContract
: null,
+ splToken: widget.isTokenTx && wallet is SolanaWallet
+ ? SplToken(
+ address: widget.txData.tokenMint ?? "unknown",
+ name: widget.txData.tokenSymbol ?? "Token",
+ symbol: widget.txData.tokenSymbol ?? "TOKEN",
+ decimals: widget.txData.tokenDecimals ?? 9,
+ )
+ : null,
),
style:
STextStyles.desktopTextExtraExtraSmall(
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.