fix: validate EVM restore mnemonic checksum (#3498)

* fix: validate EVM restore mnemonics * chore: drop test changes from this PR Removes the test additions/modifications introduced by this branch so the test design is left to the maintainers. Production code is unchanged.

Seth For Privacy committed Aug 16, 2026 at 04:57 UTC 8a366129f8a7b662791474549a1dcd426d5dab6e
3 files changed +34 -16
cw_evm/lib/evm_chain_exceptions.dart
+6
@@ -1,5 +1,11 @@
1 import 'package:cw_core/crypto_currency.dart';
2
3 +class EVMChainMnemonicIsIncorrectException implements Exception {
4 + @override
5 + String toString() =>
6 + 'EVM mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.';
7 +}
8 +
9 class EVMChainTransactionCreationException implements Exception {
10 final String exceptionMessage;
11
cw_evm/lib/evm_chain_wallet_service.dart
+5
@@ -11,6 +11,7 @@ import 'package:cw_core/wallet_type.dart';
11 import 'package:path/path.dart' as p;
12 import 'package:cw_evm/clients/evm_chain_client.dart';
13 import 'package:cw_evm/evm_chain_client_factory.dart';
14 +import 'package:cw_evm/evm_chain_exceptions.dart';
15 import 'package:cw_evm/evm_chain_registry.dart';
16 import 'package:cw_evm/evm_chain_wallet.dart';
17 import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart';
@@ -199,6 +200,10 @@ class EVMChainWalletService extends WalletService<
200 EVMChainRestoreWalletFromSeedCredentials credentials, {
201 bool? isTestnet,
202 }) async {
203 + if (!bip39.validateMnemonic(credentials.mnemonic)) {
204 + throw EVMChainMnemonicIsIncorrectException();
205 + }
206 +
207 final walletInfo = credentials.walletInfo!;
208
209 // Get chainId from wallet type
lib/view_model/restore/wallet_restore_from_qr_code.dart
+23 -16
@@ -11,6 +11,7 @@ import 'package:cake_wallet/view_model/restore/restore_mode.dart';
11 import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
12 import 'package:cw_core/currency_for_wallet_type.dart';
13 import 'package:cw_core/wallet_type.dart';
14 +import 'package:bip39/bip39.dart' as bip39;
15 import 'package:flutter/cupertino.dart';
16 import 'package:cake_wallet/generated/i18n.dart';
17 import 'package:collection/collection.dart';
@@ -89,9 +90,7 @@ class WalletRestoreFromQRCode {
90 try {
91 return AddressResolverUtils.extractAddressByType(
92 raw: rawString,
92 - type: walletTypeToCryptoCurrency(
93 - type,
94 - ),
93 + type: walletTypeToCryptoCurrency(type),
94 requireSurroundingWhitespaces: false,
95 );
96 } catch (_) {
@@ -127,8 +126,8 @@ class WalletRestoreFromQRCode {
126 final prefix = code.startsWith('xpub')
127 ? 'xpub'
128 : code.startsWith('zpub')
130 - ? 'zpub'
131 - : '????';
129 + ? 'zpub'
130 + : '????';
131 if (walletType == null) {
132 await _specifyWalletAssets(context, "Can't determine wallet type, please pick it manually");
133 walletType =
@@ -140,8 +139,8 @@ class WalletRestoreFromQRCode {
139 formattedUri = seedPhrase != null
140 ? '$walletType:?seed=$seedPhrase'
141 : code.startsWith(prefix)
143 - ? '$walletType:?$prefix=$code'
144 - : throw Exception('Failed to determine valid seed phrase');
142 + ? '$walletType:?$prefix=$code'
143 + : throw Exception('Failed to determine valid seed phrase');
144 } else {
145 final index = code.indexOf(':');
146 final query = code.substring(index + 1).replaceAll('?', '&');
@@ -202,9 +201,15 @@ class WalletRestoreFromQRCode {
201 seedValue.split(' ').forEach((element) {
202 if (!words.contains(element)) {
203 throw Exception(
205 - "Unexpected restore mode: mnemonic_seed is invalid or doesn't match wallet type");
204 + "Unexpected restore mode: mnemonic_seed is invalid or doesn't match wallet type",
205 + );
206 }
207 });
208 + if (isEVMCompatibleChain(type) && !bip39.validateMnemonic(seedValue)) {
209 + throw Exception(
210 + 'EVM mnemonic has an invalid checksum. Please check the seed phrase for typos.',
211 + );
212 + }
213 return WalletRestoreMode.seed;
214 }
215
@@ -268,12 +273,14 @@ class WalletRestoreFromQRCode {
273
274 Future<void> _specifyWalletAssets(BuildContext context, String error) async {
275 await showPopUp<void>(
271 - context: context,
272 - builder: (BuildContext context) {
273 - return AlertWithOneAction(
274 - alertTitle: S.current.error,
275 - alertContent: error,
276 - buttonText: S.of(context).ok,
277 - buttonAction: () => Navigator.of(context).pop());
278 - });
276 + context: context,
277 + builder: (BuildContext context) {
278 + return AlertWithOneAction(
279 + alertTitle: S.current.error,
280 + alertContent: error,
281 + buttonText: S.of(context).ok,
282 + buttonAction: () => Navigator.of(context).pop(),
283 + );
284 + },
285 + );
286 }