Generic Token Fixes (#2874)

* feat: Automatically detect wallet tokens for EVM chains * feat: Disable potential tg scam tokens by default * feat: Add homoglyph normalization to detect spoofing attacks in token symbols * refactor: Improve scam token detection and apply to automatically fetched EVM tokens * refactor: Enhance scam detection for automatically detected tokens in evm wallets * feat: Add fiat check to scam checks on automatically imported token and disable tokens that fail the check * feat: Add fiat check to scam checks on automatically imported token and disable tokens that fail the check * feat: Add fiat check to scam checks on automatically imported token and disable tokens that fail the check * feat: Enable whitelisted tokens with balance when importing wallet tokens * feat: Disable imported tokens with balance less than 0.1 usd * feat: Disable imported tokens with balance less than 0.1 usd - Update configure file * fixes: - default tokens marked as potential scam - ui overflow for token name on balance page - add more filters for spam tokens * - detect and flag default token impersonators - add homoglyph normalization to prevent spoofing attacks on new token symbols - fix bnb not coming up in receive currency list for swap page * - detect and flag default token impersonators - add homoglyph normalization to prevent spoofing attacks on new token symbols - fix bnb not coming up in receive currency list for swap page * Revert Exclude BNB from receive currencies list [skip ci] --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

David Adegoke committed Feb 8, 2026 at 09:48 UTC 326ef316c3f6e3debd438633a3f75bc52e4c5dbd
8 files changed +90 -11
cw_evm/lib/evm_chain_default_tokens.dart
+5 -1
@@ -5,7 +5,7 @@ import 'package:cw_evm/tokens/bsc_tokens.dart';
5 import 'package:cw_evm/tokens/ethereum_tokens.dart';
6 import 'package:cw_evm/tokens/polygon_tokens.dart';
7
8 -/// Default ERC20 tokens for each EVM chain
8 +/// Default ERC20 tokens for each EVM chain and utility methods for interacting with them
9 class EVMChainDefaultTokens {
10 static List<Erc20Token> getDefaultTokensByChainId(int chainId) {
11 return switch (chainId) {
@@ -21,4 +21,8 @@ class EVMChainDefaultTokens {
21 static List<String> getDefaultTokenAddresses(int chainId) {
22 return getDefaultTokensByChainId(chainId).map((token) => token.contractAddress).toList();
23 }
24 +
25 + static List<String> getDefaultTokenSymbols(int chainId) {
26 + return getDefaultTokensByChainId(chainId).map((token) => token.symbol.toUpperCase()).toList();
27 + }
28 }
cw_evm/lib/evm_chain_wallet.dart
+7 -2
@@ -445,6 +445,8 @@ abstract class EVMChainWalletBase
445 bool isTokenWhitelisted = getDefaultTokenContractAddresses
446 .any((element) => element.toLowerCase() == token.contractAddress.toLowerCase());
447
448 + final defaultTokenSymbols = EVMChainDefaultTokens.getDefaultTokenSymbols(selectedChainId);
449 +
450 // Normalize the token data to check for homoglyph spoofing attack, characters that look like ASCII (Cyrillic, Greek, etc.)
451 final normalizedName = normalizeHomoglyphs(token.name.trim().toUpperCase());
452 final normalizedSymbol = normalizeHomoglyphs(token.symbol.trim().toUpperCase());
@@ -479,9 +481,12 @@ abstract class EVMChainWalletBase
481 );
482
483 final nativeSymbol = currency.title.toUpperCase();
482 - final hasSuspiciousSymbol = normalizedSymbol == nativeSymbol && !isTokenWhitelisted;
484 + final hasSuspiciousNativeSymbol = normalizedSymbol == nativeSymbol && !isTokenWhitelisted;
485 +
486 + final hasSuspiciousDefaultTokenSymbol =
487 + defaultTokenSymbols.contains(normalizedSymbol) && !isTokenWhitelisted;
488
484 - return hasSuspiciousData || hasSuspiciousSymbol;
489 + return hasSuspiciousData || hasSuspiciousNativeSymbol || hasSuspiciousDefaultTokenSymbol;
490 }
491
492 Future<void> _checkForExistingScamTokens() async {
lib/evm/cw_evm.dart
+7
@@ -314,6 +314,13 @@ class CWEVM extends EVM {
314 return EVMChainDefaultTokens.getDefaultTokenAddresses(chainId);
315 }
316
317 + @override
318 + List<String> getDefaultTokenSymbols(WalletBase wallet) {
319 + final chainId = getSelectedChainId(wallet);
320 + if (chainId == null) return [];
321 + return EVMChainDefaultTokens.getDefaultTokenSymbols(chainId);
322 + }
323 +
324 @override
325 bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) {
326 final evmWallet = wallet as EVMChainWallet;
lib/solana/cw_solana.dart
+5
@@ -177,6 +177,11 @@ class CWSolana extends Solana {
177 return DefaultSPLTokens().initialSPLTokens.map((e) => e.mintAddress).toList();
178 }
179
180 + @override
181 + List<String> getDefaultTokenSymbols() {
182 + return DefaultSPLTokens().initialSPLTokens.map((e) => e.symbol.toUpperCase()).toList();
183 + }
184 +
185 @override
186 bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) {
187 final solanaWallet = wallet as SolanaWallet;
lib/src/screens/dashboard/edit_token_page.dart
+17 -7
@@ -14,6 +14,7 @@ import 'package:cake_wallet/themes/core/theme_extension.dart';
14 import 'package:cake_wallet/utils/show_pop_up.dart';
15 import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart';
16 import 'package:cw_core/crypto_currency.dart';
17 +import 'package:cw_core/utils/homoglyph_normalizer.dart';
18 import 'package:cw_core/wallet_type.dart';
19 import 'package:dotted_border/dotted_border.dart';
20 import 'package:flutter/material.dart';
@@ -231,20 +232,29 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
232
233 bool isPotentialScam = hasPotentialError && !isWhitelisted;
234
235 + // Normalize to catch homoglyph spoofing attacks
236 + final tokenSymbol = normalizeHomoglyphs(
237 + _tokenSymbolController.text.trim().toUpperCase(),
238 + );
239 +
240 // check if the token symbol is the same as the native token symbol
241 // to prevent token impersonation
242 // (e.g. fake ETH on Ethereum, fake SOL on Solana)
237 - final tokenSymbol = _tokenSymbolController.text
238 - .trim()
239 - .toUpperCase();
240 - final nativeSymbol = widget.homeSettingsViewModel
241 - .nativeToken
242 - .title
243 - .toUpperCase();
243 + final nativeSymbol =
244 + widget.homeSettingsViewModel.nativeToken.title.toUpperCase();
245 if (tokenSymbol == nativeSymbol && !isWhitelisted) {
246 isPotentialScam = true;
247 }
248
249 + // check if the token symbol is the same as any of the default token symbols
250 + // (e.g. fake USDC, USDT with wrong contract address)
251 + if (widget.homeSettingsViewModel.checkIfTokenSymbolMatchesDefaultToken(
252 + tokenSymbol,
253 + ) &&
254 + !isWhitelisted) {
255 + isPotentialScam = true;
256 + }
257 +
258 final actionCall = () async {
259 try {
260 await widget.homeSettingsViewModel.addToken(
lib/tron/cw_tron.dart
+7 -1
@@ -139,9 +139,15 @@ class CWTron extends Tron {
139 return DefaultTronTokens().initialTronTokens.map((e) => e.contractAddress).toList();
140 }
141
142 + @override
143 + List<String> getDefaultTokenSymbols() {
144 + return DefaultTronTokens().initialTronTokens.map((e) => e.symbol.toUpperCase()).toList();
145 + }
146 +
147 @override
148 bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) {
149 final tronWallet = wallet as TronWallet;
145 - return tronWallet.tronTokenCurrencies.any((element) => element.contractAddress == contractAddress);
150 + return tronWallet.tronTokenCurrencies
151 + .any((element) => element.contractAddress == contractAddress);
152 }
153 }
lib/view_model/dashboard/home_settings_view_model.dart
+39
@@ -14,6 +14,7 @@ import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
14 import 'package:cake_wallet/zano/zano.dart';
15 import 'package:cw_core/crypto_currency.dart';
16 import 'package:cw_core/erc20_token.dart';
17 +import 'package:cw_core/utils/homoglyph_normalizer.dart';
18 import 'package:cw_core/utils/print_verbose.dart';
19 import 'package:cw_core/wallet_type.dart';
20 import 'package:mobx/mobx.dart';
@@ -242,6 +243,44 @@ abstract class HomeSettingsViewModelBase with Store {
243 return isInWhitelist;
244 }
245
246 + bool checkIfTokenSymbolMatchesDefaultToken(String symbol) {
247 + final normalizedSymbol = normalizeHomoglyphs(symbol.trim().toUpperCase());
248 + if (normalizedSymbol.isEmpty) return false;
249 +
250 + List<String> defaultTokenSymbols = [];
251 + switch (_balanceViewModel.wallet.type) {
252 + case WalletType.ethereum:
253 + case WalletType.polygon:
254 + case WalletType.base:
255 + case WalletType.arbitrum:
256 + case WalletType.bsc:
257 + defaultTokenSymbols = evm!.getDefaultTokenSymbols(_balanceViewModel.wallet);
258 + break;
259 + case WalletType.solana:
260 + defaultTokenSymbols = solana!.getDefaultTokenSymbols();
261 + break;
262 + case WalletType.tron:
263 + defaultTokenSymbols = tron!.getDefaultTokenSymbols();
264 + break;
265 + case WalletType.zano:
266 + case WalletType.banano:
267 + case WalletType.monero:
268 + case WalletType.none:
269 + case WalletType.bitcoin:
270 + case WalletType.litecoin:
271 + case WalletType.haven:
272 + case WalletType.nano:
273 + case WalletType.wownero:
274 + case WalletType.bitcoinCash:
275 + case WalletType.decred:
276 + case WalletType.dogecoin:
277 + case WalletType.zcash:
278 + return false;
279 + }
280 +
281 + return defaultTokenSymbols.any((s) => s.toUpperCase() == normalizedSymbol);
282 + }
283 +
284 Future<bool> _isPotentialScamTokenViaMoralis(
285 String contractAddress,
286 String chainName,
tool/configure.dart
+3
@@ -969,6 +969,7 @@ abstract class Solana {
969 List<int>? getValidationLength(CryptoCurrency type);
970 double? getEstimateFees(WalletBase wallet);
971 List<String> getDefaultTokenContractAddresses();
972 + List<String> getDefaultTokenSymbols();
973 bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress);
974
975 // Jupiter swap transaction handling
@@ -1094,6 +1095,7 @@ abstract class Tron {
1095
1096 void updateTronGridUsageState(WalletBase wallet, bool isEnabled);
1097 List<String> getDefaultTokenContractAddresses();
1098 + List<String> getDefaultTokenSymbols();
1099 bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress);
1100 }
1101 """;
@@ -1489,6 +1491,7 @@ abstract class EVM {
1491
1492 // Utility methods
1493 List<String> getDefaultTokenContractAddresses(WalletBase wallet);
1494 + List<String> getDefaultTokenSymbols(WalletBase wallet);
1495 bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress);
1496 String? getEVMNativeEstimatedFee(WalletBase wallet);
1497 String? getEVMERC20EstimatedFee(WalletBase wallet);