fix: validate EVM restore mnemonic checksum (#3498)
What changed, and why it matters
This commit adds a missing safety check when users restore an Ethereum-compatible (EVM) wallet from a seed phrase. Before the fix, the app accepted any 12 or 24 words that looked like a mnemonic, even if the words were wrong or the checksum was invalid. That could let a user create a wallet from a typo-ridden seed and later be unable to access their funds. The fix validates the seed phrase's checksum before restoring, and shows a clear error if it is invalid.
Verify that the bip39.validateMnemonic() call covers all supported EVM wallet types and languages, and that the error messages are surfaced correctly to users. Consider adding unit tests for invalid checksums, wrong word counts, and non-BIP39 words. Review whether non-EVM restore paths (e.g., Monero, Bitcoin) need similar checksum validation.
Security signals we found
Missing input validation on mnemonic seed phrases
BIP-39 checksum validation added to restore path
New exception type for invalid EVM mnemonic format
QR-code restore path now validates EVM mnemonics
Potential for user funds lockout due to undetected seed typos
Evidence from the diff
The patch introduces BIP-39 mnemonic checksum validation in two places: (1) EVMChainWalletService.restoreWallet() now calls bip39.validateMnemonic() and throws EVMChainMnemonicIsIncorrectException if the seed fails validation; (2) WalletRestoreFromQRCode._getRestoreMode() also validates the mnemonic for EVM-compatible chains when restoring from a QR code. A new exception class was added for the service-layer check. The change prevents restoration from malformed or typo’d BIP-39 mnemonics that previously would have silently derived an unintended wallet address.
Changed components
cw_evm/lib/evm_chain_wallet_service.dartcw_evm/lib/evm_chain_exceptions.dartlib/view_model/restore/wallet_restore_from_qr_code.dartInspect captured patch +34 / −16
diff --git a/cw_evm/lib/evm_chain_exceptions.dart b/cw_evm/lib/evm_chain_exceptions.dart
index 4e5f4df0..62dfb78b 100644
--- a/cw_evm/lib/evm_chain_exceptions.dart
+++ b/cw_evm/lib/evm_chain_exceptions.dart
@@ -1,5 +1,11 @@
import 'package:cw_core/crypto_currency.dart';
+class EVMChainMnemonicIsIncorrectException implements Exception {
+ @override
+ String toString() =>
+ 'EVM mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.';
+}
+
class EVMChainTransactionCreationException implements Exception {
final String exceptionMessage;
diff --git a/cw_evm/lib/evm_chain_wallet_service.dart b/cw_evm/lib/evm_chain_wallet_service.dart
index 425bdceb..5a293001 100644
--- a/cw_evm/lib/evm_chain_wallet_service.dart
+++ b/cw_evm/lib/evm_chain_wallet_service.dart
@@ -11,6 +11,7 @@ import 'package:cw_core/wallet_type.dart';
import 'package:path/path.dart' as p;
import 'package:cw_evm/clients/evm_chain_client.dart';
import 'package:cw_evm/evm_chain_client_factory.dart';
+import 'package:cw_evm/evm_chain_exceptions.dart';
import 'package:cw_evm/evm_chain_registry.dart';
import 'package:cw_evm/evm_chain_wallet.dart';
import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart';
@@ -199,6 +200,10 @@ class EVMChainWalletService extends WalletService<
EVMChainRestoreWalletFromSeedCredentials credentials, {
bool? isTestnet,
}) async {
+ if (!bip39.validateMnemonic(credentials.mnemonic)) {
+ throw EVMChainMnemonicIsIncorrectException();
+ }
+
final walletInfo = credentials.walletInfo!;
// Get chainId from wallet type
diff --git a/lib/view_model/restore/wallet_restore_from_qr_code.dart b/lib/view_model/restore/wallet_restore_from_qr_code.dart
index 732729d6..f1456b9b 100644
--- a/lib/view_model/restore/wallet_restore_from_qr_code.dart
+++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart
@@ -11,6 +11,7 @@ import 'package:cake_wallet/view_model/restore/restore_mode.dart';
import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/wallet_type.dart';
+import 'package:bip39/bip39.dart' as bip39;
import 'package:flutter/cupertino.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:collection/collection.dart';
@@ -89,9 +90,7 @@ class WalletRestoreFromQRCode {
try {
return AddressResolverUtils.extractAddressByType(
raw: rawString,
- type: walletTypeToCryptoCurrency(
- type,
- ),
+ type: walletTypeToCryptoCurrency(type),
requireSurroundingWhitespaces: false,
);
} catch (_) {
@@ -127,8 +126,8 @@ class WalletRestoreFromQRCode {
final prefix = code.startsWith('xpub')
? 'xpub'
: code.startsWith('zpub')
- ? 'zpub'
- : '????';
+ ? 'zpub'
+ : '????';
if (walletType == null) {
await _specifyWalletAssets(context, "Can't determine wallet type, please pick it manually");
walletType =
@@ -140,8 +139,8 @@ class WalletRestoreFromQRCode {
formattedUri = seedPhrase != null
? '$walletType:?seed=$seedPhrase'
: code.startsWith(prefix)
- ? '$walletType:?$prefix=$code'
- : throw Exception('Failed to determine valid seed phrase');
+ ? '$walletType:?$prefix=$code'
+ : throw Exception('Failed to determine valid seed phrase');
} else {
final index = code.indexOf(':');
final query = code.substring(index + 1).replaceAll('?', '&');
@@ -202,9 +201,15 @@ class WalletRestoreFromQRCode {
seedValue.split(' ').forEach((element) {
if (!words.contains(element)) {
throw Exception(
- "Unexpected restore mode: mnemonic_seed is invalid or doesn't match wallet type");
+ "Unexpected restore mode: mnemonic_seed is invalid or doesn't match wallet type",
+ );
}
});
+ if (isEVMCompatibleChain(type) && !bip39.validateMnemonic(seedValue)) {
+ throw Exception(
+ 'EVM mnemonic has an invalid checksum. Please check the seed phrase for typos.',
+ );
+ }
return WalletRestoreMode.seed;
}
@@ -268,12 +273,14 @@ class WalletRestoreFromQRCode {
Future<void> _specifyWalletAssets(BuildContext context, String error) async {
await showPopUp<void>(
- context: context,
- builder: (BuildContext context) {
- return AlertWithOneAction(
- alertTitle: S.current.error,
- alertContent: error,
- buttonText: S.of(context).ok,
- buttonAction: () => Navigator.of(context).pop());
- });
+ context: context,
+ builder: (BuildContext context) {
+ return AlertWithOneAction(
+ alertTitle: S.current.error,
+ alertContent: error,
+ buttonText: S.of(context).ok,
+ buttonAction: () => Navigator.of(context).pop(),
+ );
+ },
+ );
}
Why this scored 62/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.