Generic Token Fixes (#2873)

* 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

David Adegoke committed Feb 7, 2026 at 12:53 UTC fa5149af7f2ab8273f4af094afb2d80a987bbd0c
3 files changed +60 -55
cw_evm/lib/evm_chain_wallet.dart
+14 -6
@@ -442,8 +442,6 @@ abstract class EVMChainWalletBase
442 }
443
444 bool isTokenPropertiesSuspicious(Erc20Token token) {
445 - final baseCurrencySymbols = CryptoCurrency.all.map((e) => e.title.toUpperCase()).toList();
446 -
445 bool isTokenWhitelisted = getDefaultTokenContractAddresses
446 .any((element) => element.toLowerCase() == token.contractAddress.toLowerCase());
447
@@ -459,10 +457,18 @@ abstract class EVMChainWalletBase
457 'http',
458 'https',
459 '.com',
460 + '.org',
461 + '.top',
462 + '.live',
463 'airdrop',
464 + 'reward',
465 + 'distribution',
466 'www',
467 '.xyz',
468 '🎁',
469 + 'bot',
470 + 'claim',
471 + 'reward',
472 ];
473
474 final hasSuspiciousData = suspiciousStrings.any(
@@ -472,11 +478,10 @@ abstract class EVMChainWalletBase
478 normalizedTitle.toLowerCase().contains(element),
479 );
480
475 - // Check if the token symbol is the same as any of the base currencies symbols (ETH, SOL, POL, TRX, etc).
476 - // If it is, then it's probably a scam unless it's in the whitelist.
477 - final hasSuspiciousSymbol = baseCurrencySymbols.contains(normalizedSymbol);
481 + final nativeSymbol = currency.title.toUpperCase();
482 + final hasSuspiciousSymbol = normalizedSymbol == nativeSymbol && !isTokenWhitelisted;
483
479 - return hasSuspiciousData || (hasSuspiciousSymbol && !isTokenWhitelisted);
484 + return hasSuspiciousData || hasSuspiciousSymbol;
485 }
486
487 Future<void> _checkForExistingScamTokens() async {
@@ -1287,6 +1292,9 @@ abstract class EVMChainWalletBase
1292 }
1293
1294 Future<void> addErc20Token(Erc20Token token) async {
1295 + final isSuspicious = isTokenPropertiesSuspicious(token);
1296 + token.isPotentialScam = token.isPotentialScam || isSuspicious;
1297 +
1298 String? iconPath;
1299
1300 if ((token.iconPath == null || token.iconPath!.isEmpty) && !token.isPotentialScam) {
lib/src/screens/dashboard/edit_token_page.dart
+11 -12
@@ -230,19 +230,18 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
230 );
231
232 bool isPotentialScam = hasPotentialError && !isWhitelisted;
233 - final tokenSymbol = _tokenSymbolController.text.toUpperCase();
233
235 - // check if the token symbol is the same as any of the base currencies symbols (ETH, SOL, POL, TRX, etc):
236 - // if it is, then it's probably a scam unless it's in the whitelist
237 -
238 - // ugh, should it be commented out?
239 - // because there are some tokens that has the name of original currencies
240 - // like: 0x455e53CBB86018Ac2B8092FdCd39d8444aFFC3F6
241 -
242 - final baseCurrencySymbols =
243 - CryptoCurrency.all.map((e) => e.title.toUpperCase()).toList();
244 - if (baseCurrencySymbols.contains(tokenSymbol.trim().toUpperCase()) &&
245 - !isWhitelisted) {
234 + // check if the token symbol is the same as the native token symbol
235 + // to prevent token impersonation
236 + // (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();
244 + if (tokenSymbol == nativeSymbol && !isWhitelisted) {
245 isPotentialScam = true;
246 }
247
lib/src/screens/dashboard/pages/balance/balance_row_widget.dart
+35 -37
@@ -173,46 +173,44 @@ class BalanceRowWidget extends StatelessWidget {
173 ],
174 ),
175 ),
176 - SizedBox(
177 - //width: min(MediaQuery.of(context).size.width * 0.2, 100),
178 - child: Center(
179 - child: Column(
180 - crossAxisAlignment: CrossAxisAlignment.end,
181 - children: [
182 - CakeImageWidget(
183 - imageUrl: currency.iconPath,
184 - height: 40,
185 - width: 40,
186 - errorWidget: Container(
187 - height: 30.0,
188 - width: 30.0,
189 - child: Center(
190 - child: Text(
191 - currency.title.substring(0, min(currency.title.length, 2)),
192 - style: Theme.of(context).textTheme.bodySmall?.copyWith(
193 - fontSize: 11,
194 - color: Theme.of(context).colorScheme.onSurfaceVariant,
195 - ),
196 - ),
197 - ),
198 - decoration: BoxDecoration(
199 - shape: BoxShape.circle,
200 - color: Theme.of(context).colorScheme.surfaceContainer,
176 + Expanded(
177 + child: Column(
178 + crossAxisAlignment: CrossAxisAlignment.end,
179 + children: [
180 + CakeImageWidget(
181 + imageUrl: currency.iconPath,
182 + height: 40,
183 + width: 40,
184 + errorWidget: Container(
185 + height: 30.0,
186 + width: 30.0,
187 + child: Center(
188 + child: Text(
189 + currency.title.substring(0, min(currency.title.length, 2)),
190 + style: Theme.of(context).textTheme.bodySmall?.copyWith(
191 + fontSize: 11,
192 + color: Theme.of(context).colorScheme.onSurfaceVariant,
193 + ),
194 ),
195 ),
196 + decoration: BoxDecoration(
197 + shape: BoxShape.circle,
198 + color: Theme.of(context).colorScheme.surfaceContainer,
199 + ),
200 ),
204 - const SizedBox(height: 10),
205 - Text(
206 - currency.title,
207 - style: Theme.of(context).textTheme.bodyMedium?.copyWith(
208 - fontSize: 16,
209 - fontWeight: FontWeight.w700,
210 - color: Theme.of(context).colorScheme.onSurface,
211 - height: 1,
212 - ),
213 - ),
214 - ],
215 - ),
201 + ),
202 + const SizedBox(height: 10),
203 + Text(
204 + currency.title,
205 + overflow: TextOverflow.ellipsis,
206 + style: Theme.of(context).textTheme.bodyMedium?.copyWith(
207 + fontSize: 16,
208 + fontWeight: FontWeight.w700,
209 + color: Theme.of(context).colorScheme.onSurface,
210 + height: 1,
211 + ),
212 + ),
213 + ],
214 ),
215 ),
216 ],