Generic Token Fixes (#2874)
What changed, and why it matters
This update improves protections in the Cake Wallet app against fake or scam tokens. It adds checks that automatically flag tokens pretending to be well-known coins (like fake USDC or fake ETH) using look-alike characters, and it disables low-value or suspicious tokens when wallets are imported. The changes are defensive and reduce the chance that users are tricked into trusting scam tokens.
Review the homoglyph normalizer implementation and default token lists for completeness; verify that the new fiat-value and scam heuristics do not accidentally hide legitimate user tokens; consider adding user-visible warnings when a token is flagged as potential scam.
Security signals we found
Homoglyph normalization added to token symbol/name checks to mitigate visual spoofing
Default-token symbol impersonation detection added for EVM, Solana, and Tron
Native-token symbol impersonation detection extended and applied consistently
Auto-imported EVM tokens now filtered by scam/fiat-value heuristics
Low-balance (<$0.10 USD) imported tokens disabled by default
Whitelisted tokens with balance enabled on wallet import
Evidence from the diff
The commit hardens token validation across EVM, Solana, and Tron wallets. It introduces symbol-based impersonation detection in addition to existing address whitelisting, normalizes token symbols with a homoglyph normalizer to catch spoofing attacks, and extends scam detection to automatically imported tokens. New methods expose default token symbols per chain/wallet type, and UI token creation now flags symbols matching native or default token symbols unless the token is whitelisted. Additional logic disables auto-imported tokens with low fiat value or suspicious characteristics.
Changed components
cw_evm/lib/evm_chain_default_tokens.dartcw_evm/lib/evm_chain_wallet.dartlib/evm/cw_evm.dartlib/solana/cw_solana.dartlib/src/screens/dashboard/edit_token_page.dartlib/tron/cw_tron.dartlib/view_model/dashboard/home_settings_view_model.darttool/configure.dartInspect captured patch +90 / −11
diff --git a/cw_evm/lib/evm_chain_default_tokens.dart b/cw_evm/lib/evm_chain_default_tokens.dart
index f305d71..ba5ecb3 100644
--- a/cw_evm/lib/evm_chain_default_tokens.dart
+++ b/cw_evm/lib/evm_chain_default_tokens.dart
@@ -5,7 +5,7 @@ import 'package:cw_evm/tokens/bsc_tokens.dart';
import 'package:cw_evm/tokens/ethereum_tokens.dart';
import 'package:cw_evm/tokens/polygon_tokens.dart';
-/// Default ERC20 tokens for each EVM chain
+/// Default ERC20 tokens for each EVM chain and utility methods for interacting with them
class EVMChainDefaultTokens {
static List<Erc20Token> getDefaultTokensByChainId(int chainId) {
return switch (chainId) {
@@ -21,4 +21,8 @@ class EVMChainDefaultTokens {
static List<String> getDefaultTokenAddresses(int chainId) {
return getDefaultTokensByChainId(chainId).map((token) => token.contractAddress).toList();
}
+
+ static List<String> getDefaultTokenSymbols(int chainId) {
+ return getDefaultTokensByChainId(chainId).map((token) => token.symbol.toUpperCase()).toList();
+ }
}
diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart
index c3ecd5b..f064086 100644
--- a/cw_evm/lib/evm_chain_wallet.dart
+++ b/cw_evm/lib/evm_chain_wallet.dart
@@ -445,6 +445,8 @@ abstract class EVMChainWalletBase
bool isTokenWhitelisted = getDefaultTokenContractAddresses
.any((element) => element.toLowerCase() == token.contractAddress.toLowerCase());
+ final defaultTokenSymbols = EVMChainDefaultTokens.getDefaultTokenSymbols(selectedChainId);
+
// Normalize the token data to check for homoglyph spoofing attack, characters that look like ASCII (Cyrillic, Greek, etc.)
final normalizedName = normalizeHomoglyphs(token.name.trim().toUpperCase());
final normalizedSymbol = normalizeHomoglyphs(token.symbol.trim().toUpperCase());
@@ -479,9 +481,12 @@ abstract class EVMChainWalletBase
);
final nativeSymbol = currency.title.toUpperCase();
- final hasSuspiciousSymbol = normalizedSymbol == nativeSymbol && !isTokenWhitelisted;
+ final hasSuspiciousNativeSymbol = normalizedSymbol == nativeSymbol && !isTokenWhitelisted;
+
+ final hasSuspiciousDefaultTokenSymbol =
+ defaultTokenSymbols.contains(normalizedSymbol) && !isTokenWhitelisted;
- return hasSuspiciousData || hasSuspiciousSymbol;
+ return hasSuspiciousData || hasSuspiciousNativeSymbol || hasSuspiciousDefaultTokenSymbol;
}
Future<void> _checkForExistingScamTokens() async {
diff --git a/lib/evm/cw_evm.dart b/lib/evm/cw_evm.dart
index 1aef384..9845996 100644
--- a/lib/evm/cw_evm.dart
+++ b/lib/evm/cw_evm.dart
@@ -314,6 +314,13 @@ class CWEVM extends EVM {
return EVMChainDefaultTokens.getDefaultTokenAddresses(chainId);
}
+ @override
+ List<String> getDefaultTokenSymbols(WalletBase wallet) {
+ final chainId = getSelectedChainId(wallet);
+ if (chainId == null) return [];
+ return EVMChainDefaultTokens.getDefaultTokenSymbols(chainId);
+ }
+
@override
bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) {
final evmWallet = wallet as EVMChainWallet;
diff --git a/lib/solana/cw_solana.dart b/lib/solana/cw_solana.dart
index daeb3d9..7d541fa 100644
--- a/lib/solana/cw_solana.dart
+++ b/lib/solana/cw_solana.dart
@@ -177,6 +177,11 @@ class CWSolana extends Solana {
return DefaultSPLTokens().initialSPLTokens.map((e) => e.mintAddress).toList();
}
+ @override
+ List<String> getDefaultTokenSymbols() {
+ return DefaultSPLTokens().initialSPLTokens.map((e) => e.symbol.toUpperCase()).toList();
+ }
+
@override
bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) {
final solanaWallet = wallet as SolanaWallet;
diff --git a/lib/src/screens/dashboard/edit_token_page.dart b/lib/src/screens/dashboard/edit_token_page.dart
index f436845..26ec4c7 100644
--- a/lib/src/screens/dashboard/edit_token_page.dart
+++ b/lib/src/screens/dashboard/edit_token_page.dart
@@ -14,6 +14,7 @@ import 'package:cake_wallet/themes/core/theme_extension.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
+import 'package:cw_core/utils/homoglyph_normalizer.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:dotted_border/dotted_border.dart';
import 'package:flutter/material.dart';
@@ -231,20 +232,29 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
bool isPotentialScam = hasPotentialError && !isWhitelisted;
+ // Normalize to catch homoglyph spoofing attacks
+ final tokenSymbol = normalizeHomoglyphs(
+ _tokenSymbolController.text.trim().toUpperCase(),
+ );
+
// check if the token symbol is the same as the native token symbol
// to prevent token impersonation
// (e.g. fake ETH on Ethereum, fake SOL on Solana)
- final tokenSymbol = _tokenSymbolController.text
- .trim()
- .toUpperCase();
- final nativeSymbol = widget.homeSettingsViewModel
- .nativeToken
- .title
- .toUpperCase();
+ final nativeSymbol =
+ widget.homeSettingsViewModel.nativeToken.title.toUpperCase();
if (tokenSymbol == nativeSymbol && !isWhitelisted) {
isPotentialScam = true;
}
+ // check if the token symbol is the same as any of the default token symbols
+ // (e.g. fake USDC, USDT with wrong contract address)
+ if (widget.homeSettingsViewModel.checkIfTokenSymbolMatchesDefaultToken(
+ tokenSymbol,
+ ) &&
+ !isWhitelisted) {
+ isPotentialScam = true;
+ }
+
final actionCall = () async {
try {
await widget.homeSettingsViewModel.addToken(
diff --git a/lib/tron/cw_tron.dart b/lib/tron/cw_tron.dart
index 9cc52e2..8c73d68 100644
--- a/lib/tron/cw_tron.dart
+++ b/lib/tron/cw_tron.dart
@@ -139,9 +139,15 @@ class CWTron extends Tron {
return DefaultTronTokens().initialTronTokens.map((e) => e.contractAddress).toList();
}
+ @override
+ List<String> getDefaultTokenSymbols() {
+ return DefaultTronTokens().initialTronTokens.map((e) => e.symbol.toUpperCase()).toList();
+ }
+
@override
bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) {
final tronWallet = wallet as TronWallet;
- return tronWallet.tronTokenCurrencies.any((element) => element.contractAddress == contractAddress);
+ return tronWallet.tronTokenCurrencies
+ .any((element) => element.contractAddress == contractAddress);
}
}
diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart
index 5de47ab..177d016 100644
--- a/lib/view_model/dashboard/home_settings_view_model.dart
+++ b/lib/view_model/dashboard/home_settings_view_model.dart
@@ -14,6 +14,7 @@ import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cake_wallet/zano/zano.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
+import 'package:cw_core/utils/homoglyph_normalizer.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
@@ -242,6 +243,44 @@ abstract class HomeSettingsViewModelBase with Store {
return isInWhitelist;
}
+ bool checkIfTokenSymbolMatchesDefaultToken(String symbol) {
+ final normalizedSymbol = normalizeHomoglyphs(symbol.trim().toUpperCase());
+ if (normalizedSymbol.isEmpty) return false;
+
+ List<String> defaultTokenSymbols = [];
+ switch (_balanceViewModel.wallet.type) {
+ case WalletType.ethereum:
+ case WalletType.polygon:
+ case WalletType.base:
+ case WalletType.arbitrum:
+ case WalletType.bsc:
+ defaultTokenSymbols = evm!.getDefaultTokenSymbols(_balanceViewModel.wallet);
+ break;
+ case WalletType.solana:
+ defaultTokenSymbols = solana!.getDefaultTokenSymbols();
+ break;
+ case WalletType.tron:
+ defaultTokenSymbols = tron!.getDefaultTokenSymbols();
+ break;
+ case WalletType.zano:
+ case WalletType.banano:
+ case WalletType.monero:
+ case WalletType.none:
+ case WalletType.bitcoin:
+ case WalletType.litecoin:
+ case WalletType.haven:
+ case WalletType.nano:
+ case WalletType.wownero:
+ case WalletType.bitcoinCash:
+ case WalletType.decred:
+ case WalletType.dogecoin:
+ case WalletType.zcash:
+ return false;
+ }
+
+ return defaultTokenSymbols.any((s) => s.toUpperCase() == normalizedSymbol);
+ }
+
Future<bool> _isPotentialScamTokenViaMoralis(
String contractAddress,
String chainName,
diff --git a/tool/configure.dart b/tool/configure.dart
index 02835ed..beebcb6 100644
--- a/tool/configure.dart
+++ b/tool/configure.dart
@@ -969,6 +969,7 @@ abstract class Solana {
List<int>? getValidationLength(CryptoCurrency type);
double? getEstimateFees(WalletBase wallet);
List<String> getDefaultTokenContractAddresses();
+ List<String> getDefaultTokenSymbols();
bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress);
// Jupiter swap transaction handling
@@ -1094,6 +1095,7 @@ abstract class Tron {
void updateTronGridUsageState(WalletBase wallet, bool isEnabled);
List<String> getDefaultTokenContractAddresses();
+ List<String> getDefaultTokenSymbols();
bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress);
}
""";
@@ -1489,6 +1491,7 @@ abstract class EVM {
// Utility methods
List<String> getDefaultTokenContractAddresses(WalletBase wallet);
+ List<String> getDefaultTokenSymbols(WalletBase wallet);
bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress);
String? getEVMNativeEstimatedFee(WalletBase wallet);
String? getEVMERC20EstimatedFee(WalletBase wallet);
Why this scored 64/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.