feat: harden spam detection for solana and evm (#3322)

* feat: harden spam detection for solana and evm * fix: guard scam/impersonator tokens from auto-enabling on EVM and Solana * feat: enhance token property validation for scam detection

David Adegoke committed Jul 10, 2026 at 12:52 UTC a7073df4224e4a80f3dc9b93345561bac5cbc692
5 files changed +357 -124
cw_evm/lib/clients/evm_chain_client.dart
+111 -62
@@ -618,74 +618,117 @@ class EVMChainClient {
618 return [];
619 }
620
621 - final uri = Uri.https(
622 - 'deep-index.moralis.io',
623 - '/api/v2.2/$address/erc20',
624 - {
621 + const maxPages = 3;
622 + String? cursor;
623 + int pageCount = 0;
624 + final List<MoralisWalletTokenBalance> tokens = [];
625 +
626 + do {
627 + final params = <String, String>{
628 "chain": chainName,
626 - },
627 - );
629 + if (cursor != null && cursor.isNotEmpty) "cursor": cursor,
630 + };
631
629 - final response = await client.get(
630 - uri,
631 - headers: {
632 - "Accept": "application/json",
633 - "X-API-Key": secrets.moralisApiKey,
634 - },
635 - );
632 + final uri = Uri.https(
633 + 'deep-index.moralis.io',
634 + '/api/v2.2/wallets/$address/tokens',
635 + params,
636 + );
637
637 - if (response.statusCode < 200 || response.statusCode >= 300) {
638 - printV('Moralis API returned invalid status code: ${response.statusCode}');
639 - return [];
640 - }
638 + final response = await client.get(
639 + uri,
640 + headers: {
641 + "Accept": "application/json",
642 + "X-API-Key": secrets.moralisApiKey,
643 + },
644 + );
645
642 - final decodedResponse = jsonDecode(response.body) as List;
646 + if (response.statusCode < 200 || response.statusCode >= 300) {
647 + printV('Moralis API returned invalid status code: ${response.statusCode}');
648 + return tokens;
649 + }
650
644 - final List<MoralisWalletTokenBalance> tokens = [];
651 + final decoded = jsonDecode(response.body);
652 + if (decoded is! Map<String, dynamic>) return tokens;
653 +
654 + final result = decoded['result'];
655 + if (result is! List) return tokens;
656 +
657 + for (final item in result) {
658 + if (item is! Map<String, dynamic>) continue;
659 + final tokenData = item;
660 +
661 + final nativeRaw = tokenData['native_token'];
662 + final nativeToken = nativeRaw is bool
663 + ? nativeRaw
664 + : (nativeRaw?.toString().toLowerCase() == 'true');
665 + if (nativeToken) continue;
666 +
667 + final balanceStr = tokenData['balance'] as String? ?? '0';
668 + final balanceWei = BigInt.tryParse(balanceStr) ?? BigInt.zero;
669 + if (balanceWei == BigInt.zero) continue;
670 +
671 + final contractAddress = (tokenData['token_address'] as String? ?? '').toLowerCase();
672 + final name = (tokenData['name'] as String? ?? '').toString();
673 + final symbol = (tokenData['symbol'] as String? ?? '').toString();
674 + final symbolFiltered = symbol.replaceFirst(RegExp('^\\\$'), '');
675 +
676 + final decimalsRaw = tokenData['decimals'];
677 + final decimals =
678 + decimalsRaw is int ? decimalsRaw : int.tryParse(decimalsRaw.toString()) ?? 18;
679 +
680 + final logo = tokenData['logo'] as String?;
681 + final thumbnail = tokenData['thumbnail'] as String?;
682 + final iconUrl = logo ?? thumbnail;
683 +
684 + final possibleSpamRaw = tokenData['possible_spam'];
685 + final possibleSpam = possibleSpamRaw is bool
686 + ? possibleSpamRaw
687 + : (possibleSpamRaw?.toString().toLowerCase() == 'true');
688 +
689 + final verifiedContractRaw = tokenData['verified_contract'];
690 + final verifiedContract = verifiedContractRaw is bool
691 + ? verifiedContractRaw
692 + : (verifiedContractRaw?.toString().toLowerCase() == 'true');
693 +
694 + final usdPriceRaw = tokenData['usd_price'];
695 + final double? usdPrice = usdPriceRaw is num
696 + ? usdPriceRaw.toDouble()
697 + : (usdPriceRaw is String ? double.tryParse(usdPriceRaw) : null);
698 +
699 + final usdValueRaw = tokenData['usd_value'];
700 + final double? usdValue = usdValueRaw is num
701 + ? usdValueRaw.toDouble()
702 + : (usdValueRaw is String ? double.tryParse(usdValueRaw) : null);
703 +
704 + final securityRaw = tokenData['security_score'];
705 + final int? securityScore = securityRaw is int
706 + ? securityRaw
707 + : (securityRaw is num
708 + ? securityRaw.toInt()
709 + : (securityRaw is String ? int.tryParse(securityRaw) : null));
710 +
711 + tokens.add(
712 + MoralisWalletTokenBalance(
713 + contractAddress: contractAddress,
714 + name: name,
715 + symbol: symbolFiltered,
716 + decimals: decimals,
717 + iconUrl: iconUrl,
718 + balanceWei: balanceWei,
719 + possibleSpam: possibleSpam,
720 + verifiedContract: verifiedContract,
721 + usdPrice: usdPrice,
722 + usdValue: usdValue,
723 + securityScore: securityScore,
724 + ),
725 + );
726 + }
727
646 - for (final item in decodedResponse) {
647 - final tokenData = item as Map<String, dynamic>;
648 -
649 - final balanceStr = tokenData['balance'] as String? ?? '0';
650 - final balanceWei = BigInt.tryParse(balanceStr) ?? BigInt.zero;
651 - if (balanceWei == BigInt.zero) continue;
652 -
653 - final contractAddress = (tokenData['token_address'] as String? ?? '').toLowerCase();
654 - final name = (tokenData['name'] as String? ?? '').toString();
655 - final symbol = (tokenData['symbol'] as String? ?? '').toString();
656 - final symbolFiltered = symbol.replaceFirst(RegExp('^\\\$'), '');
657 -
658 - final decimalsRaw = tokenData['decimals'];
659 - final decimals =
660 - decimalsRaw is int ? decimalsRaw : int.tryParse(decimalsRaw.toString()) ?? 18;
661 -
662 - final logo = tokenData['logo'] as String?;
663 - final thumbnail = tokenData['thumbnail'] as String?;
664 - final iconUrl = logo ?? thumbnail;
665 -
666 - final possibleSpamRaw = tokenData['possible_spam'];
667 - final possibleSpam = possibleSpamRaw is bool
668 - ? possibleSpamRaw
669 - : (possibleSpamRaw.toString().toLowerCase() == 'true');
670 -
671 - final verifiedContractRaw = tokenData['verified_contract'];
672 - final verifiedContract = verifiedContractRaw is bool
673 - ? verifiedContractRaw
674 - : (verifiedContractRaw.toString().toLowerCase() == 'true');
675 -
676 - tokens.add(
677 - MoralisWalletTokenBalance(
678 - contractAddress: contractAddress,
679 - name: name,
680 - symbol: symbolFiltered,
681 - decimals: decimals,
682 - iconUrl: iconUrl,
683 - balanceWei: balanceWei,
684 - possibleSpam: possibleSpam,
685 - verifiedContract: verifiedContract,
686 - ),
687 - );
688 - }
728 + final nextCursor = decoded['cursor'];
729 + cursor = nextCursor is String && nextCursor.isNotEmpty ? nextCursor : null;
730 + pageCount++;
731 + } while (cursor != null && pageCount < maxPages);
732
733 return tokens;
734 } catch (e, stackTrace) {
@@ -737,6 +780,9 @@ class MoralisWalletTokenBalance {
780 final BigInt balanceWei;
781 final bool possibleSpam;
782 final bool verifiedContract;
783 + final double? usdPrice;
784 + final double? usdValue;
785 + final int? securityScore;
786
787 MoralisWalletTokenBalance({
788 required this.contractAddress,
@@ -747,5 +793,8 @@ class MoralisWalletTokenBalance {
793 required this.balanceWei,
794 required this.possibleSpam,
795 required this.verifiedContract,
796 + this.usdPrice,
797 + this.usdValue,
798 + this.securityScore,
799 });
800 }
cw_evm/lib/evm_chain_wallet.dart
+95 -39
@@ -545,67 +545,112 @@ abstract class EVMChainWalletBase
545 await save();
546 }
547
548 - bool isTokenPropertiesSuspicious(Erc20Token token) {
549 - bool isTokenWhitelisted = getDefaultTokenContractAddresses
550 - .any((element) => element.toLowerCase() == token.contractAddress.toLowerCase());
548 + static const _urlLikeSuspiciousMarkers = [
549 + 't.me',
550 + '.me',
551 + 'telegram',
552 + 'http',
553 + 'https',
554 + '.com',
555 + '.org',
556 + '.top',
557 + '.live',
558 + '.xyz',
559 + 'www',
560 + '🎁',
561 + 'airdrop',
562 + 'distribution',
563 + ];
564 +
565 + static final _suspiciousWordPattern =
566 + RegExp(r'\b(bot|claim|reward)\b', caseSensitive: false);
567 +
568 + static const _knownNonEvmNativeSymbols = {
569 + 'ICP',
570 + 'SOL',
571 + 'TRX',
572 + 'ATOM',
573 + 'DOT',
574 + 'ADA',
575 + 'XRP',
576 + 'XLM',
577 + 'XMR',
578 + 'ALGO',
579 + 'NEAR',
580 + 'TON',
581 + 'HBAR',
582 + 'APT',
583 + 'SUI',
584 + 'KAS',
585 + };
586 +
587 + static bool _hasSuspiciousData(String normalized) {
588 + final lower = normalized.toLowerCase();
589 + if (_urlLikeSuspiciousMarkers.any(lower.contains)) return true;
590 + return _suspiciousWordPattern.hasMatch(lower);
591 + }
592 +
593 + bool isTokenPropertiesSuspicious(
594 + Erc20Token token, {
595 + Set<String>? cachedWhitelistLower,
596 + Set<String>? cachedDefaultSymbolsUpper,
597 + }) {
598 + final whitelistLower = cachedWhitelistLower ??
599 + getDefaultTokenContractAddresses.map((a) => a.toLowerCase()).toSet();
600 + final defaultSymbolsUpper = cachedDefaultSymbolsUpper ??
601 + EVMChainDefaultTokens.getDefaultTokenSymbols(selectedChainId).toSet();
602
552 - final defaultTokenSymbols = EVMChainDefaultTokens.getDefaultTokenSymbols(selectedChainId);
603 + final isTokenWhitelisted = whitelistLower.contains(token.contractAddress.toLowerCase());
604
554 - // Normalize the token data to check for homoglyph spoofing attack, characters that look like ASCII (Cyrillic, Greek, etc.)
605 final normalizedName = normalizeHomoglyphs(token.name.trim().toUpperCase());
606 final normalizedSymbol = normalizeHomoglyphs(token.symbol.trim().toUpperCase());
607 final normalizedTitle = normalizeHomoglyphs(token.title.trim().toUpperCase());
608
559 - final suspiciousStrings = [
560 - 't.me',
561 - '.me',
562 - 'telegram',
563 - 'http',
564 - 'https',
565 - '.com',
566 - '.org',
567 - '.top',
568 - '.live',
569 - 'airdrop',
570 - 'reward',
571 - 'distribution',
572 - 'www',
573 - '.xyz',
574 - '🎁',
575 - 'bot',
576 - 'claim',
577 - 'reward',
578 - ];
579 -
580 - final hasSuspiciousData = suspiciousStrings.any(
581 - (element) =>
582 - normalizedName.toLowerCase().contains(element) ||
583 - normalizedSymbol.toLowerCase().contains(element) ||
584 - normalizedTitle.toLowerCase().contains(element),
585 - );
609 + final hasSuspiciousData = _hasSuspiciousData(normalizedName) ||
610 + _hasSuspiciousData(normalizedSymbol) ||
611 + _hasSuspiciousData(normalizedTitle);
612
613 final nativeSymbol = currency.title.toUpperCase();
614 final hasSuspiciousNativeSymbol = normalizedSymbol == nativeSymbol && !isTokenWhitelisted;
615
616 final hasSuspiciousDefaultTokenSymbol =
591 - defaultTokenSymbols.contains(normalizedSymbol) && !isTokenWhitelisted;
617 + defaultSymbolsUpper.contains(normalizedSymbol) && !isTokenWhitelisted;
618
593 - return hasSuspiciousData || hasSuspiciousNativeSymbol || hasSuspiciousDefaultTokenSymbol;
619 + final hasSuspiciousNonEvmNativeSymbol =
620 + _knownNonEvmNativeSymbols.contains(normalizedSymbol) && !isTokenWhitelisted;
621 +
622 + return hasSuspiciousData ||
623 + hasSuspiciousNativeSymbol ||
624 + hasSuspiciousDefaultTokenSymbol ||
625 + hasSuspiciousNonEvmNativeSymbol;
626 }
627
628 + String get _scamCheckDoneKey => 'evm_scam_check_v2_done_${walletInfo.name}';
629 +
630 Future<void> _checkForExistingScamTokens() async {
631 + final prefs = await sharedPrefs.future;
632 + if (prefs.getBool(_scamCheckDoneKey) == true) return;
633 +
634 + final whitelistLower =
635 + getDefaultTokenContractAddresses.map((a) => a.toLowerCase()).toSet();
636 + final defaultSymbolsUpper =
637 + EVMChainDefaultTokens.getDefaultTokenSymbols(selectedChainId).toSet();
638 +
639 for (var token in erc20Currencies) {
598 - bool isPotentialScam = false;
640 + final suspicious = isTokenPropertiesSuspicious(
641 + token,
642 + cachedWhitelistLower: whitelistLower,
643 + cachedDefaultSymbolsUpper: defaultSymbolsUpper,
644 + );
645
600 - if (isTokenPropertiesSuspicious(token)) {
601 - isPotentialScam = true;
646 + if (suspicious && !token.isPotentialScam) {
647 token.isPotentialScam = true;
648 token.iconPath = null;
649 await token.save();
650 + continue;
651 }
652
607 - // For fixing wrongly classified tokens
608 - if (!isPotentialScam && token.isPotentialScam) {
653 + if (!suspicious && token.isPotentialScam) {
654 token.isPotentialScam = false;
655
656 if (token.iconPath == null || token.iconPath!.isEmpty) {
@@ -621,6 +666,8 @@ abstract class EVMChainWalletBase
666 await token.save();
667 }
668 }
669 +
670 + await prefs.setBool(_scamCheckDoneKey, true);
671 }
672
673 Future<MoralisDiscoveryResult> discoverTokensFromMoralis() async {
@@ -672,6 +719,9 @@ abstract class EVMChainWalletBase
719 DiscoveredToken(
720 token: newToken,
721 balanceWei: token.balanceWei,
722 + verifiedContract: token.verifiedContract,
723 + moralisUsdPrice: token.usdPrice,
724 + moralisUsdValue: token.usdValue,
725 ),
726 );
727 }
@@ -1757,10 +1807,16 @@ class GasParamsHandler {
1807 class DiscoveredToken {
1808 final Erc20Token token;
1809 final BigInt balanceWei;
1810 + final bool verifiedContract;
1811 + final double? moralisUsdPrice;
1812 + final double? moralisUsdValue;
1813
1814 const DiscoveredToken({
1815 required this.token,
1816 required this.balanceWei,
1817 + required this.verifiedContract,
1818 + this.moralisUsdPrice,
1819 + this.moralisUsdValue,
1820 });
1821 }
1822
cw_solana/lib/solana_wallet.dart
+124
@@ -12,6 +12,7 @@ import 'package:cw_core/pending_transaction.dart';
12 import 'package:cw_core/sync_status.dart';
13 import 'package:cw_core/transaction_direction.dart';
14 import 'package:cw_core/transaction_priority.dart';
15 +import 'package:cw_core/utils/homoglyph_normalizer.dart';
16 import 'package:cw_core/utils/print_verbose.dart';
17 import 'package:cw_core/wallet_addresses.dart';
18 import 'package:cw_core/wallet_base.dart';
@@ -135,6 +136,8 @@ abstract class SolanaWalletBase
136
137 splTokensBox = await CakeHive.openBox<SPLToken>(boxName);
138
139 + await _checkForExistingScamTokens();
140 +
141 // Create the privatekey using either the mnemonic or the privateKey
142 _solanaPrivateKey = await getPrivateKey(
143 mnemonic: _mnemonic,
@@ -153,6 +156,36 @@ abstract class SolanaWalletBase
156 await save();
157 }
158
159 + String get _scamCheckDoneKey => 'solana_scam_check_v2_done_${walletInfo.name}';
160 +
161 + Future<void> _checkForExistingScamTokens() async {
162 + if (!splTokensBox.isOpen) return;
163 +
164 + final prefs = await _sharedPrefs.future;
165 + if (prefs.getBool(_scamCheckDoneKey) == true) return;
166 +
167 + final defaultMints =
168 + DefaultSPLTokens().initialSPLTokens.map((t) => t.mintAddress).toSet();
169 + final defaultSymbolsUpper = DefaultSPLTokens()
170 + .initialSPLTokens
171 + .map((t) => t.symbol.toUpperCase())
172 + .toSet();
173 +
174 + for (final token in splTokensBox.values) {
175 + final suspicious = isTokenPropertiesSuspicious(
176 + token,
177 + cachedDefaultMints: defaultMints,
178 + cachedDefaultSymbolsUpper: defaultSymbolsUpper,
179 + );
180 + if (suspicious && !token.isPotentialScam) {
181 + token.isPotentialScam = true;
182 + await token.save();
183 + }
184 + }
185 +
186 + await prefs.setBool(_scamCheckDoneKey, true);
187 + }
188 +
189 Future<SolanaPrivateKey> getPrivateKey({
190 String? mnemonic,
191 String? privateKey,
@@ -671,7 +704,98 @@ abstract class SolanaWalletBase
704 }
705 }
706
707 + static const _urlLikeSuspiciousMarkers = [
708 + 't.me',
709 + '.me',
710 + 'telegram',
711 + 'http',
712 + 'https',
713 + '.com',
714 + '.org',
715 + '.top',
716 + '.live',
717 + '.xyz',
718 + 'www',
719 + '🎁',
720 + 'airdrop',
721 + 'distribution',
722 + ];
723 +
724 + static final _suspiciousWordPattern =
725 + RegExp(r'\b(bot|claim|reward)\b', caseSensitive: false);
726 +
727 + static const _knownNonSolanaNativeSymbols = {
728 + 'BTC',
729 + 'ETH',
730 + 'BNB',
731 + 'AVAX',
732 + 'MATIC',
733 + 'POL',
734 + 'ICP',
735 + 'TRX',
736 + 'ATOM',
737 + 'DOT',
738 + 'ADA',
739 + 'XRP',
740 + 'XLM',
741 + 'XMR',
742 + 'ALGO',
743 + 'NEAR',
744 + 'TON',
745 + 'HBAR',
746 + 'APT',
747 + 'SUI',
748 + 'KAS',
749 + };
750 +
751 + static bool _hasSuspiciousData(String normalized) {
752 + final lower = normalized.toLowerCase();
753 + if (_urlLikeSuspiciousMarkers.any(lower.contains)) return true;
754 + return _suspiciousWordPattern.hasMatch(lower);
755 + }
756 +
757 + bool isTokenPropertiesSuspicious(
758 + SPLToken token, {
759 + Set<String>? cachedDefaultMints,
760 + Set<String>? cachedDefaultSymbolsUpper,
761 + }) {
762 + final defaultMints = cachedDefaultMints ??
763 + DefaultSPLTokens().initialSPLTokens.map((t) => t.mintAddress).toSet();
764 + final defaultSymbolsUpper = cachedDefaultSymbolsUpper ??
765 + DefaultSPLTokens()
766 + .initialSPLTokens
767 + .map((t) => t.symbol.toUpperCase())
768 + .toSet();
769 +
770 + final isTokenWhitelisted = defaultMints.contains(token.mintAddress);
771 +
772 + final normalizedName = normalizeHomoglyphs(token.name.trim().toUpperCase());
773 + final normalizedSymbol = normalizeHomoglyphs(token.symbol.trim().toUpperCase());
774 + final normalizedTitle = normalizeHomoglyphs(token.title.trim().toUpperCase());
775 +
776 + final hasSuspiciousData = _hasSuspiciousData(normalizedName) ||
777 + _hasSuspiciousData(normalizedSymbol) ||
778 + _hasSuspiciousData(normalizedTitle);
779 +
780 + const nativeSymbol = 'SOL';
781 + final hasSuspiciousNativeSymbol = normalizedSymbol == nativeSymbol && !isTokenWhitelisted;
782 +
783 + final hasSuspiciousDefaultTokenSymbol =
784 + defaultSymbolsUpper.contains(normalizedSymbol) && !isTokenWhitelisted;
785 +
786 + final hasSuspiciousNonSolanaNativeSymbol =
787 + _knownNonSolanaNativeSymbols.contains(normalizedSymbol) && !isTokenWhitelisted;
788 +
789 + return hasSuspiciousData ||
790 + hasSuspiciousNativeSymbol ||
791 + hasSuspiciousDefaultTokenSymbol ||
792 + hasSuspiciousNonSolanaNativeSymbol;
793 + }
794 +
795 Future<void> addSPLToken(SPLToken token) async {
796 + final isSuspicious = isTokenPropertiesSuspicious(token);
797 + token.isPotentialScam = token.isPotentialScam || isSuspicious;
798 +
799 await splTokensBox.put(token.mintAddress, token);
800
801 if (token.enabled) {
lib/evm/cw_evm.dart
+24 -22
@@ -692,29 +692,18 @@ class CWEVM extends EVM {
692 );
693 }
694
695 - Future<({double usdValue, bool hasValidFiatPrice})> _getTokenUsdValueAndFiatCheck(
696 - Erc20Token token,
697 - BigInt balanceWei,
698 - ) async {
695 + Future<double> _fetchFiatApiPriceForToken(Erc20Token token) async {
696 try {
697 final settingsStore = getIt.get<SettingsStore>();
698 final torOnly = settingsStore.fiatApiMode == FiatApiMode.torOnly;
699
703 - final price = await FiatConversionService.fetchPrice(
700 + return await FiatConversionService.fetchPrice(
701 crypto: token,
702 fiat: FiatCurrency.usd,
703 torOnly: torOnly,
704 );
708 -
709 - final hasValidFiatPrice = price > 0;
710 -
711 - final decimals = token.decimal;
712 - final balance = balanceWei.toDouble() / math.pow(10, decimals);
713 - final usdValue = balance * price;
714 -
715 - return (usdValue: usdValue, hasValidFiatPrice: hasValidFiatPrice);
716 - } catch (e) {
717 - return (usdValue: 0.0, hasValidFiatPrice: false);
705 + } catch (_) {
706 + return 0.0;
707 }
708 }
709
@@ -730,21 +719,34 @@ class CWEVM extends EVM {
719
720 final List<Future<void>> tokenChecks = [];
721
722 + final whitelistedContracts =
723 + wallet.getDefaultTokenContractAddresses.map((a) => a.toLowerCase()).toSet();
724 +
725 for (final item in result.newTokens) {
726 tokenChecks.add((() async {
727 final token = item.token;
728
729 final isPropertiesSuspicious = wallet.isTokenPropertiesSuspicious(token);
730 + final isWhitelisted = whitelistedContracts.contains(token.contractAddress.toLowerCase());
731
739 - final fiatResult = await _getTokenUsdValueAndFiatCheck(
740 - token,
741 - item.balanceWei,
742 - );
743 - final isSpam = isPropertiesSuspicious || !fiatResult.hasValidFiatPrice;
732 + final moralisPrice = item.moralisUsdPrice;
733 + final moralisValue = item.moralisUsdValue ?? 0.0;
734 + final hasMoralisPrice = moralisPrice != null && moralisPrice > 0;
735
745 - token.isPotentialScam = isSpam;
736 + final fiatApiPrice = await _fetchFiatApiPriceForToken(token);
737 + final hasFiatApiPrice = fiatApiPrice > 0;
738 +
739 + final isImpersonator =
740 + hasFiatApiPrice && !hasMoralisPrice && !isWhitelisted && !item.verifiedContract;
741
747 - token.enabled = (fiatResult.usdValue >= _minTokenUsdValue) && !isSpam;
742 + final isSpam = isPropertiesSuspicious ||
743 + token.isPotentialScam ||
744 + isImpersonator ||
745 + (!hasMoralisPrice && !hasFiatApiPrice);
746 +
747 + token.isPotentialScam = isSpam;
748 + token.enabled =
749 + hasMoralisPrice && hasFiatApiPrice && (moralisValue >= _minTokenUsdValue) && !isSpam;
750
751 await wallet.addErc20Token(token);
752 })());
lib/solana/cw_solana.dart
+3 -1
@@ -387,9 +387,11 @@ class CWSolana extends Solana {
387 tokenChecks.add((() async {
388 final token = item.token;
389
390 + final isPropertiesSuspicious = wallet.isTokenPropertiesSuspicious(token);
391 +
392 final fiatResult = await _getTokenUsdValueAndFiatCheck(token, item.balance);
393
392 - final isSpam = !fiatResult.hasValidFiatPrice;
394 + final isSpam = isPropertiesSuspicious || !fiatResult.hasValidFiatPrice;
395
396 token.isPotentialScam = isSpam;
397 token.enabled = (fiatResult.usdValue >= _minTokenUsdValue) && !isSpam;