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;