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>
This commit is contained in:
David Adegoke
2026-02-08 09:48:27 +01:00
committed by GitHub
co-authored by Omar Hatem
parent 174216df17
commit 326ef316c3
8 changed files with 90 additions and 11 deletions
+5 -1
View File
@@ -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();
}
}
+7 -2
View File
@@ -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;
return hasSuspiciousData || hasSuspiciousSymbol;
final hasSuspiciousDefaultTokenSymbol =
defaultTokenSymbols.contains(normalizedSymbol) && !isTokenWhitelisted;
return hasSuspiciousData || hasSuspiciousNativeSymbol || hasSuspiciousDefaultTokenSymbol;
}
Future<void> _checkForExistingScamTokens() async {
+7
View File
@@ -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;
+5
View File
@@ -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;
+17 -7
View File
@@ -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(
+7 -1
View File
@@ -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);
}
}
@@ -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,
+3
View File
@@ -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);