Enhance EVM Fees Error Handling (#2610)
What changed, and why it matters
This commit improves how the Cake Wallet app reads and displays error messages when an Ethereum-compatible transaction fails due to not enough funds for fees. It adds support for more message formats and shows a simpler 'insufficient funds' message when the app cannot extract exact numbers. There is no direct evidence this fixes an active security vulnerability; it appears to be a user-experience and robustness improvement.
Treat as a routine robustness/UX improvement. Review whether any remaining regex patterns can still over-match or mis-parse attacker-controlled error strings, and ensure the UI never treats parsed numeric values as authoritative for fee setting. No urgent security action is indicated by the diff alone.
Security signals we found
Refactored error-message parsing to handle additional EVM node error formats
Added generic insufficient-funds fallback to avoid presenting fabricated zero-value breakdowns as real values
No changes to transaction signing, fee computation, or wallet authorization logic
No mention of vulnerability, CVE, bug bounty, or security advisory in commit metadata
Evidence from the diff
The patch refactors EVMTransactionErrorFeesHandler from a hard-coded two-pattern parser into a list of ordered ErrorPattern objects. It adds regexes for ‘have X want Y’, ‘balance X, queued cost Y, tx cost Z, overshot W’, ‘balance X, tx cost Y, overshot Z’, a legacy multi-field fallback, and a generic ‘insufficient funds for gas * price + value’ catch-all that returns zeroed values with error code ‘generic_insufficient_funds’. SendViewModel now returns a plain insufficient-funds string for both generic and unparseable cases, and only shows detailed balance/cost/overshot breakdowns when specific values were parsed. The change is defensive and reduces the chance of misleading fee breakdowns, but it does not alter transaction construction, signing, or fee calculation logic.
Changed components
lib/entities/evm_transaction_error_fees_handler.dartlib/view_model/send/send_view_model.dartInspect captured patch +172 / −91
diff --git a/lib/entities/evm_transaction_error_fees_handler.dart b/lib/entities/evm_transaction_error_fees_handler.dart
index 85b40be6..9db07e1b 100644
--- a/lib/entities/evm_transaction_error_fees_handler.dart
+++ b/lib/entities/evm_transaction_error_fees_handler.dart
@@ -32,100 +32,174 @@ class EVMTransactionErrorFeesHandler {
String errorMessage,
double assetPriceUsd,
) {
- // Pattern: "insufficient funds for gas * price + value: have 4728796358953246 want 4728796842182575"
- RegExp insufficientFundsRegExp = RegExp(r'have (\d+) want (\d+)');
- Match? insufficientFundsMatch = insufficientFundsRegExp.firstMatch(errorMessage);
-
- if (insufficientFundsMatch != null) {
- try {
- // Extract the numerical strings from the new format
- String balanceStr = insufficientFundsMatch.group(1)!;
- String requiredStr = insufficientFundsMatch.group(2)!;
-
- // Parse the numerical strings to BigInt
- BigInt balanceWei = BigInt.parse(balanceStr);
- BigInt requiredWei = BigInt.parse(requiredStr);
-
- // Calculate overshot (how much more is needed)
- BigInt overshotWei = requiredWei - balanceWei;
-
- // The transaction cost is the required amount
- BigInt txCostWei = requiredWei;
-
- // Convert wei to ETH (1 ETH = 1e18 wei)
- double balanceEth = balanceWei.toDouble() / 1e18;
- double txCostEth = txCostWei.toDouble() / 1e18;
- double overshotEth = overshotWei.toDouble() / 1e18;
-
- // Calculate the USD values
- double balanceUsd = balanceEth * assetPriceUsd;
- double txCostUsd = txCostEth * assetPriceUsd;
- double overshotUsd = overshotEth * assetPriceUsd;
-
- return EVMTransactionErrorFeesHandler(
- balanceWei: balanceWei.toString(),
- balanceEth: balanceEth.toString().safeSubString(0, 12),
- balanceUsd: balanceUsd.toString().safeSubString(0, 4),
- txCostWei: txCostWei.toString(),
- txCostEth: txCostEth.toString().safeSubString(0, 12),
- txCostUsd: txCostUsd.toString().safeSubString(0, 4),
- overshotWei: overshotWei.toString(),
- overshotEth: overshotEth.toString().safeSubString(0, 12),
- overshotUsd: overshotUsd.toString().safeSubString(0, 4),
- );
- } catch (e) {}
- }
-
- // Define Regular Expressions to extract the numerical values
- RegExp balanceRegExp = RegExp(r'balance (\d+)');
- RegExp txCostRegExp = RegExp(r'tx cost (\d+)');
- RegExp overshotRegExp = RegExp(r'overshot (\d+)');
-
- // Match the patterns in the error message
- Match? balanceMatch = balanceRegExp.firstMatch(errorMessage);
- Match? txCostMatch = txCostRegExp.firstMatch(errorMessage);
- Match? overshotMatch = overshotRegExp.firstMatch(errorMessage);
-
- // Check if all required values are found
- if (balanceMatch != null && txCostMatch != null && overshotMatch != null) {
- try {
- // Extract the numerical strings
- String balanceStr = balanceMatch.group(1)!;
- String txCostStr = txCostMatch.group(1)!;
- String overshotStr = overshotMatch.group(1)!;
-
- // Parse the numerical strings to BigInt
- BigInt balanceWei = BigInt.parse(balanceStr);
- BigInt txCostWei = BigInt.parse(txCostStr);
- BigInt overshotWei = BigInt.parse(overshotStr);
-
- // Convert wei to ETH (1 ETH = 1e18 wei)
- double balanceEth = balanceWei.toDouble() / 1e18;
- double txCostEth = txCostWei.toDouble() / 1e18;
- double overshotEth = overshotWei.toDouble() / 1e18;
-
- // Calculate the USD values
- double balanceUsd = balanceEth * assetPriceUsd;
- double txCostUsd = txCostEth * assetPriceUsd;
- double overshotUsd = overshotEth * assetPriceUsd;
-
- return EVMTransactionErrorFeesHandler(
- balanceWei: balanceWei.toString(),
- balanceEth: balanceEth.toString().safeSubString(0, 12),
- balanceUsd: balanceUsd.toString().safeSubString(0, 4),
- txCostWei: txCostWei.toString(),
- txCostEth: txCostEth.toString().safeSubString(0, 12),
- txCostUsd: txCostUsd.toString().safeSubString(0, 4),
- overshotWei: overshotWei.toString(),
- overshotEth: overshotEth.toString().safeSubString(0, 12),
- overshotUsd: overshotUsd.toString().safeSubString(0, 4),
- );
- } catch (e) {
- // If parsing fails, continue to error case
+ // Allows us define multiple patterns to parse the error message
+ // Order matters: more specific patterns first, generic patterns last
+ final patterns = [
+ // Pattern 1: "have X want Y" format
+ ErrorPattern(
+ name: 'have_want',
+ regex: RegExp(r'have\s+(\d+)\s+want\s+(\d+)', caseSensitive: false),
+ extractor: (match) => {
+ 'balance': match.group(1)!,
+ 'required': match.group(2)!,
+ },
+ calculator: (values) => {
+ 'balanceWei': values['balance']!,
+ 'txCostWei': values['required']!,
+ 'overshotWei':
+ (BigInt.parse(values['required']!) - BigInt.parse(values['balance']!)).toString(),
+ },
+ ),
+
+ // Pattern 2: "balance X, queued cost Y, tx cost Z, overshot W" format (most specific)
+ ErrorPattern(
+ name: 'balance_queued_txcost_overshot',
+ regex: RegExp(r'balance\s+(\d+),\s*queued\s+cost\s+(\d+),\s*tx\s+cost\s+(\d+),\s*overshot\s+(\d+)', caseSensitive: false),
+ extractor: (match) => {
+ 'balance': match.group(1)!,
+ 'txCost': match.group(3)!, // Skip queued cost, use tx cost
+ 'overshot': match.group(4)!,
+ },
+ calculator: (values) => {
+ 'balanceWei': values['balance']!,
+ 'txCostWei': values['txCost']!,
+ 'overshotWei': values['overshot']!,
+ },
+ ),
+
+ // Pattern 3: "balance X, tx cost Y, overshot Z" format
+ ErrorPattern(
+ name: 'balance_txcost_overshot',
+ regex: RegExp(r'balance\s+(\d+),\s*tx\s+cost\s+(\d+),\s*overshot\s+(\d+)', caseSensitive: false),
+ extractor: (match) => {
+ 'balance': match.group(1)!,
+ 'txCost': match.group(2)!,
+ 'overshot': match.group(3)!,
+ },
+ calculator: (values) => {
+ 'balanceWei': values['balance']!,
+ 'txCostWei': values['txCost']!,
+ 'overshotWei': values['overshot']!,
+ },
+ ),
+
+ // Pattern 4: Individual field matching (legacy fallback)
+ ErrorPattern(
+ name: 'individual_fields',
+ regex: RegExp(r'balance\s+(\d+).*tx\s+cost\s+(\d+).*overshot\s+(\d+)', caseSensitive: false),
+ extractor: (match) => {
+ 'balance': match.group(1)!,
+ 'txCost': match.group(2)!,
+ 'overshot': match.group(3)!,
+ },
+ calculator: (values) => {
+ 'balanceWei': values['balance']!,
+ 'txCostWei': values['txCost']!,
+ 'overshotWei': values['overshot']!,
+ },
+ ),
+
+ // Pattern 5: Generic "insufficient funds for gas * price + value" (least specific - must be last)
+ ErrorPattern(
+ name: 'generic_insufficient_funds',
+ regex: RegExp(r'insufficient\s+funds\s+for\s+gas\s*\*\s*price\s*\+\s*value', caseSensitive: false),
+ extractor: (match) => {
+ 'balance': '0', // We don't have specific values, so use 0
+ 'txCost': '0',
+ 'overshot': '0',
+ },
+ calculator: (values) => {
+ 'balanceWei': values['balance']!,
+ 'txCostWei': values['txCost']!,
+ 'overshotWei': values['overshot']!,
+ },
+ ),
+ ];
+
+ for (final pattern in patterns) {
+ final match = pattern.regex.firstMatch(errorMessage);
+ if (match != null) {
+ try {
+ final extractedValues = pattern.extractor(match);
+ final calculatedValues = pattern.calculator(extractedValues);
+
+ return _createHandlerFromValues(calculatedValues, assetPriceUsd);
+ } catch (e) {
+ continue;
+ }
}
}
- // If both parsing attempts fail, return an error message
return EVMTransactionErrorFeesHandler(error: 'Could not parse the error message.');
}
+
+ /// Creates a handler instance from parsed values
+ static EVMTransactionErrorFeesHandler _createHandlerFromValues(
+ Map<String, String> values,
+ double assetPriceUsd,
+ ) {
+ final balanceWei = BigInt.parse(values['balanceWei']!);
+ final txCostWei = BigInt.parse(values['txCostWei']!);
+ final overshotWei = BigInt.parse(values['overshotWei']!);
+
+ final isGenericError = balanceWei == BigInt.zero &&
+ txCostWei == BigInt.zero &&
+ overshotWei == BigInt.zero;
+
+ if (isGenericError) {
+ return genericInsufficientFunds();
+ }
+
+ // Convert wei to ETH (1 ETH = 1e18 wei)
+ final balanceEth = balanceWei.toDouble() / 1e18;
+ final txCostEth = txCostWei.toDouble() / 1e18;
+ final overshotEth = overshotWei.toDouble() / 1e18;
+
+ // Calculate USD values
+ final balanceUsd = balanceEth * assetPriceUsd;
+ final txCostUsd = txCostEth * assetPriceUsd;
+ final overshotUsd = overshotEth * assetPriceUsd;
+
+ return EVMTransactionErrorFeesHandler(
+ balanceWei: balanceWei.toString(),
+ balanceEth: balanceEth.toString().safeSubString(0, 12),
+ balanceUsd: balanceUsd.toString().safeSubString(0, 4),
+ txCostWei: txCostWei.toString(),
+ txCostEth: txCostEth.toString().safeSubString(0, 12),
+ txCostUsd: txCostUsd.toString().safeSubString(0, 4),
+ overshotWei: overshotWei.toString(),
+ overshotEth: overshotEth.toString().safeSubString(0, 12),
+ overshotUsd: overshotUsd.toString().safeSubString(0, 4),
+ );
+ }
+
+ static EVMTransactionErrorFeesHandler genericInsufficientFunds() {
+ return EVMTransactionErrorFeesHandler(
+ balanceWei: '0',
+ balanceEth: '0',
+ balanceUsd: '0',
+ txCostWei: '0',
+ txCostEth: '0',
+ txCostUsd: '0',
+ overshotWei: '0',
+ overshotEth: '0',
+ overshotUsd: '0',
+ error: 'generic_insufficient_funds',
+ );
+ }
+}
+
+/// Represents an error parsing pattern
+class ErrorPattern {
+ final String name;
+ final RegExp regex;
+ final Map<String, String> Function(Match) extractor;
+ final Map<String, String> Function(Map<String, String>) calculator;
+
+ ErrorPattern({
+ required this.name,
+ required this.regex,
+ required this.extractor,
+ required this.calculator,
+ });
}
diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart
index 83480cfe..cc9da280 100644
--- a/lib/view_model/send/send_view_model.dart
+++ b/lib/view_model/send/send_view_model.dart
@@ -1031,10 +1031,17 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
_fiatConversationStore.prices[currency] ?? 0.0,
);
+ // Handle generic insufficient funds error (no specific values available)
+ if (parsedErrorMessageResult.error == 'generic_insufficient_funds') {
+ return S.current.insufficient_funds_for_tx;
+ }
+
+ // Handle parsing errors (couldn't parse the error message)
if (parsedErrorMessageResult.error != null) {
return S.current.insufficient_funds_for_tx;
}
+ // Handle successfully parsed errors with specific values
return '''${S.current.insufficient_funds_for_tx} \n\n'''
'''${S.current.balance}: ${parsedErrorMessageResult.balanceEth} ${walletType == WalletType.polygon ? "POL" : "ETH"} (${parsedErrorMessageResult.balanceUsd} ${fiatFromSettings.name})\n\n'''
'''${S.current.transaction_cost}: ${parsedErrorMessageResult.txCostEth} ${walletType == WalletType.polygon ? "POL" : "ETH"} (${parsedErrorMessageResult.txCostUsd} ${fiatFromSettings.name})\n\n'''
Why this scored 19/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.