diff --git a/cw_evm/lib/evm_chain_default_tokens.dart b/cw_evm/lib/evm_chain_default_tokens.dart index f305d718c..ba5ecb353 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 getDefaultTokensByChainId(int chainId) { return switch (chainId) { @@ -21,4 +21,8 @@ class EVMChainDefaultTokens { static List getDefaultTokenAddresses(int chainId) { return getDefaultTokensByChainId(chainId).map((token) => token.contractAddress).toList(); } + + static List 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 c3ecd5ba1..f064086b0 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; - return hasSuspiciousData || hasSuspiciousSymbol; + final hasSuspiciousDefaultTokenSymbol = + defaultTokenSymbols.contains(normalizedSymbol) && !isTokenWhitelisted; + + return hasSuspiciousData || hasSuspiciousNativeSymbol || hasSuspiciousDefaultTokenSymbol; } Future _checkForExistingScamTokens() async { diff --git a/lib/evm/cw_evm.dart b/lib/evm/cw_evm.dart index 1aef384ac..984599690 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 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 daeb3d926..7d541fa74 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 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 f436845fe..26ec4c725 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 { 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 9cc52e2af..8c73d684d 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 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 5de47abc1..177d01603 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 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 _isPotentialScamTokenViaMoralis( String contractAddress, String chainName, diff --git a/tool/configure.dart b/tool/configure.dart index 02835ed5e..beebcb6d6 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -969,6 +969,7 @@ abstract class Solana { List? getValidationLength(CryptoCurrency type); double? getEstimateFees(WalletBase wallet); List getDefaultTokenContractAddresses(); + List 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 getDefaultTokenContractAddresses(); + List getDefaultTokenSymbols(); bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress); } """; @@ -1489,6 +1491,7 @@ abstract class EVM { // Utility methods List getDefaultTokenContractAddresses(WalletBase wallet); + List getDefaultTokenSymbols(WalletBase wallet); bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress); String? getEVMNativeEstimatedFee(WalletBase wallet); String? getEVMERC20EstimatedFee(WalletBase wallet);