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
}