fix: walletconnect fee refresh (#3153)
What changed, and why it matters
This update changes how Cake Wallet refreshes Ethereum-style transaction fees for WalletConnect requests. Before signing a transaction from a connected app, the wallet now fetches fresh network fee data and applies a safety buffer. This helps prevent transactions from being stuck or failing because the fee suggested by the app is too low, but it also means the wallet may override the dApp's fee values. The change is a bug fix, not a clear-cut security patch, and the commit message does not describe it as a security issue.
Treat as a routine bug fix. Review the new _mergeWCBufferedFees logic for edge cases where a malicious or buggy dApp could supply fee values that interact unexpectedly with the wallet's buffer, and verify that the override behavior is clearly communicated to users before signing.
Security signals we found
Fee-value override before signing
WalletConnect transaction flow changed
RPC fee data now refreshed at sign time
EIP-1559 maxFeePerGas / maxPriorityFeePerGas merging logic added
No explicit security framing by vendor
Evidence from the diff
The patch refactors EVM fee handling for WalletConnect (WC) sessions. It extracts the buffered max-fee calculation into EVMChainUtils.computeBufferedMaxFeePerGasWei, adds a new getWCBufferedFeeQuote path that fetches current base fee and gas price, and applies those values in EvmChainServiceImpl before presenting the transaction for signing. For EIP-1559 transactions it merges dApp-provided and wallet-computed fees by taking the higher of each, then adjusts maxPriorityFeePerGas so it does not exceed maxFeePerGas minus the latest base fee. For legacy transactions it falls back to gasPrice if no quote is available. The change also fixes a missing configure.dart declaration for the new interface.
Changed components
cw_evm/lib/evm_chain_wallet.dartcw_evm/lib/utils/evm_chain_utils.dartlib/evm/cw_evm.dartlib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.darttool/configure.dartInspect captured patch +209 / −80
diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart
index c0453d81..991fd071 100644
--- a/cw_evm/lib/evm_chain_wallet.dart
+++ b/cw_evm/lib/evm_chain_wallet.dart
@@ -99,7 +99,8 @@ abstract class EVMChainWalletBase
_isTransactionUpdating = false,
_client = client,
selectedChainId = initialChainId ?? _getInitialChainId(walletInfo.type),
- walletAddresses = EVMChainWalletAddresses(walletInfo, initialChainId ?? _getInitialChainId(walletInfo.type)),
+ walletAddresses = EVMChainWalletAddresses(
+ walletInfo, initialChainId ?? _getInitialChainId(walletInfo.type)),
balance = ObservableMap<CryptoCurrency, EVMChainERC20Balance>.of(
{
nativeCurrency: initialBalance ?? EVMChainERC20Balance(BigInt.zero),
@@ -779,27 +780,13 @@ abstract class EVMChainWalletBase
throw EVMChainTransactionFeesException('Failed to retrieve gas price from node');
}
- int maxFeePerGas;
- int adjustedGasPrice;
-
- if (gasBaseFee != null && gasBaseFee > 0) {
- // For chains with base fee, add priority fee (if supported) and a buffer to account for base fee increases
- // Base fee can increase between estimation and transaction submission
- final baseFeeWithPriority = gasBaseFee + priorityFee;
-
- // For chains without priority fees (e.g., Arbitrum), use a 5% buffer
- // For chains with priority fees (e.g., Ethereum), use a 15% buffer to account for base fee volatility
- // Base fee can increase significantly during high network activity
- final bufferMultiplier = hasPriorityFee ? 115 : 105;
- final bufferPercent = (baseFeeWithPriority * bufferMultiplier) ~/ 100;
- final bufferMin = baseFeeWithPriority + (baseFeeWithPriority ~/ 100);
- maxFeePerGas = bufferPercent > bufferMin ? bufferPercent : bufferMin;
- } else {
- // Fallback to gasPrice if baseFee is not available
- maxFeePerGas = gasPrice + priorityFee;
- }
-
- adjustedGasPrice = maxFeePerGas;
+ final maxFeePerGas = EVMChainUtils.computeBufferedMaxFeePerGasWei(
+ gasBaseFee: gasBaseFee,
+ gasPrice: gasPrice,
+ priorityFeeWei: priorityFee,
+ chainHasPriorityFee: hasPriorityFee,
+ );
+ final adjustedGasPrice = maxFeePerGas;
final estimatedGas = await _client.getEstimatedGasUnitsForTransaction(
contractAddress: contractAddress,
@@ -829,6 +816,39 @@ abstract class EVMChainWalletBase
}
}
+ Future<WalletConnectBufferedFeeData?> getWCBufferedFeeQuote(TransactionPriority priority) async {
+ try {
+ final gasBaseFee = await _client.getGasBaseFee();
+ final gasPrice = await _client.getGasUnitPrice();
+
+ if (gasPrice <= 0) {
+ printV('WC fee quote: invalid gas price $gasPrice');
+ return null;
+ }
+
+ int priorityFee = 0;
+ if (hasPriorityFee && priority is EVMChainTransactionPriority) {
+ priorityFee = getTotalPriorityFee(priority);
+ }
+
+ final maxFee = EVMChainUtils.computeBufferedMaxFeePerGasWei(
+ gasBaseFee: gasBaseFee,
+ gasPrice: gasPrice,
+ priorityFeeWei: priorityFee,
+ chainHasPriorityFee: hasPriorityFee,
+ );
+
+ return WalletConnectBufferedFeeData(
+ maxFeePerGasWei: maxFee,
+ maxPriorityFeePerGasWei: priorityFee,
+ latestBaseFeeWei: gasBaseFee,
+ );
+ } catch (e, s) {
+ printV('getWalletConnectBufferedFeeQuote: $e\n$s');
+ return null;
+ }
+ }
+
@override
Future<void> changePassword(String password) {
throw UnimplementedError("changePassword");
@@ -1048,15 +1068,14 @@ abstract class EVMChainWalletBase
}
Future<PendingTransaction> createCallDataTransaction(
- String to,
- String dataHex,
- BigInt valueWei,
- EVMChainTransactionPriority? priority,
- String? sourceTokenAddress,
- BigInt? sourceTokenAmount, {
- bool useBlinkProtection = true,
- }) async {
-
+ String to,
+ String dataHex,
+ BigInt valueWei,
+ EVMChainTransactionPriority? priority,
+ String? sourceTokenAddress,
+ BigInt? sourceTokenAmount, {
+ bool useBlinkProtection = true,
+ }) async {
// Define Native Currency
final nativeCurrency = switch (selectedChainId) {
137 => CryptoCurrency.maticpoly,
@@ -1099,12 +1118,9 @@ abstract class EVMChainWalletBase
cleanAddress == '0x0000000000000000000000000000000000000000';
if (!isNativeSource && sourceTokenAmount != null && sourceTokenAmount > BigInt.zero) {
-
// Filter list to find match.
- final matchingTokens = balance.keys.where((k) =>
- k is Erc20Token &&
- k.contractAddress.toLowerCase() == cleanAddress
- );
+ final matchingTokens = balance.keys
+ .where((k) => k is Erc20Token && k.contractAddress.toLowerCase() == cleanAddress);
if (matchingTokens.isEmpty) {
// Token is not in the wallet balance map -> Balance is 0
@@ -1262,9 +1278,7 @@ abstract class EVMChainWalletBase
if (existingTxInfo == null) {
result[transactionModel.hash] = newTxInfo;
- }
-
- else if (newTxInfo.direction == TransactionDirection.incoming &&
+ } else if (newTxInfo.direction == TransactionDirection.incoming &&
existingTxInfo.direction == TransactionDirection.outgoing) {
result[transactionModel.hash] = newTxInfo;
}
@@ -1760,3 +1774,15 @@ class MoralisDiscoveryResult {
static const MoralisDiscoveryResult empty = MoralisDiscoveryResult(newTokens: []);
}
+
+class WalletConnectBufferedFeeData {
+ const WalletConnectBufferedFeeData({
+ required this.maxFeePerGasWei,
+ required this.maxPriorityFeePerGasWei,
+ this.latestBaseFeeWei,
+ });
+
+ final int maxFeePerGasWei;
+ final int maxPriorityFeePerGasWei;
+ final int? latestBaseFeeWei;
+}
diff --git a/cw_evm/lib/utils/evm_chain_utils.dart b/cw_evm/lib/utils/evm_chain_utils.dart
index 59b6f656..60355cab 100644
--- a/cw_evm/lib/utils/evm_chain_utils.dart
+++ b/cw_evm/lib/utils/evm_chain_utils.dart
@@ -22,6 +22,22 @@ class EVMChainUtils {
};
}
+ static int computeBufferedMaxFeePerGasWei({
+ required int? gasBaseFee,
+ required int gasPrice,
+ required int priorityFeeWei,
+ required bool chainHasPriorityFee,
+ }) {
+ if (gasBaseFee != null && gasBaseFee > 0) {
+ final baseFeeWithPriority = gasBaseFee + priorityFeeWei;
+ final bufferMultiplier = chainHasPriorityFee ? 115 : 105;
+ final bufferPercent = (baseFeeWithPriority * bufferMultiplier) ~/ 100;
+ final bufferMin = baseFeeWithPriority + (baseFeeWithPriority ~/ 100);
+ return bufferPercent > bufferMin ? bufferPercent : bufferMin;
+ }
+ return gasPrice + priorityFeeWei;
+ }
+
static String getErc20TokensBoxName(String walletName, int chainId) {
final sanitizedName = walletName.replaceAll(" ", "_");
diff --git a/lib/evm/cw_evm.dart b/lib/evm/cw_evm.dart
index 723310c5..ec27c930 100644
--- a/lib/evm/cw_evm.dart
+++ b/lib/evm/cw_evm.dart
@@ -239,10 +239,7 @@ class CWEVM extends EVM {
(wallet as EVMChainWallet).isApprovalRequired(tokenContract, spender, requiredAmount);
@override
- Future<BigInt?> getAllowance(
- WalletBase wallet,
- String tokenContract,
- String spender) =>
+ Future<BigInt?> getAllowance(WalletBase wallet, String tokenContract, String spender) =>
(wallet as EVMChainWallet).getAllowance(tokenContract, spender);
@override
@@ -272,7 +269,7 @@ class CWEVM extends EVM {
String to,
String dataHex,
BigInt valueWei,
- TransactionPriority? priority,{
+ TransactionPriority? priority, {
bool useBlinkProtection = true,
String? sourceTokenAddress,
BigInt? sourceTokenAmount,
@@ -481,7 +478,7 @@ class CWEVM extends EVM {
@override
BigInt? getERC20AvailableBalance(Object balance) {
- if(balance is EVMChainERC20Balance) {
+ if (balance is EVMChainERC20Balance) {
return balance.balance;
}
return null;
@@ -544,6 +541,23 @@ class CWEVM extends EVM {
@override
bool hasPriorityFee(int chainId) => EVMChainUtils.hasPriorityFee(chainId);
+ @override
+ Future<EvmWalletConnectFeeQuote?> getWCBufferedFeeQuote(
+ WalletBase wallet,
+ TransactionPriority priority,
+ ) async {
+ if (wallet is! EVMChainWallet) return null;
+
+ final data = await wallet.getWCBufferedFeeQuote(priority);
+ if (data == null) return null;
+
+ return EvmWalletConnectFeeQuote(
+ maxFeePerGasWei: data.maxFeePerGasWei,
+ maxPriorityFeePerGasWei: data.maxPriorityFeePerGasWei,
+ latestBaseFeeWei: data.latestBaseFeeWei,
+ );
+ }
+
Future<({double usdValue, bool hasValidFiatPrice})> _getTokenUsdValueAndFiatCheck(
Erc20Token token,
BigInt balanceWei,
diff --git a/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart b/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart
index c65d84c6..15d2bdc0 100644
--- a/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart
+++ b/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart
@@ -432,43 +432,14 @@ class EvmChainServiceImpl {
}
}
- // we need to check if dApp provides the gas values and if not, we need to estimate them
- final hasGasLimit = transaction.maxGas != null && transaction.maxGas! > 0;
- final hasGasPrice = transaction.gasPrice != null;
- final hasMaxFeePerGas = transaction.maxFeePerGas != null;
- final hasMaxPriorityFeePerGas = transaction.maxPriorityFeePerGas != null;
-
- final needsGasEstimation = !hasGasLimit || (!hasGasPrice && !hasMaxFeePerGas);
-
- if (needsGasEstimation) {
- try {
- final gasPrice = hasGasPrice ? transaction.gasPrice! : await ethClient.getGasPrice();
-
- if (!hasGasLimit) {
- final gasLimit = await ethClient.estimateGas(
- sender: transaction.from,
- to: transaction.to,
- value: transaction.value,
- data: transaction.data,
- gasPrice: gasPrice,
- );
-
- if (hasMaxFeePerGas || hasMaxPriorityFeePerGas) {
- transaction = transaction.copyWith(maxGas: gasLimit.toInt());
- } else {
- transaction = transaction.copyWith(
- gasPrice: hasGasPrice ? transaction.gasPrice : gasPrice,
- maxGas: gasLimit.toInt(),
- );
- }
- } else if (!hasGasPrice && !hasMaxFeePerGas) {
- transaction = transaction.copyWith(gasPrice: gasPrice);
- }
- } on RPCError catch (e) {
- return JsonRpcError(code: e.errorCode, message: e.message);
- }
+ try {
+ transaction = await _ensureWCTransactionHasGasLimit(transaction);
+ } on RPCError catch (e) {
+ return JsonRpcError(code: e.errorCode, message: e.message);
}
+ transaction = await _applyWCBufferedFees(transaction);
+
final gweiGasPrice =
(transaction.gasPrice?.getInWei ?? transaction.maxFeePerGas?.getInWei ?? BigInt.zero) /
BigInt.from(1000000000);
@@ -500,6 +471,90 @@ class EvmChainServiceImpl {
return JsonRpcError(code: 5002, message: S.current.user_rejected_method);
}
+ Future<Transaction> _ensureWCTransactionHasGasLimit(Transaction transaction) async {
+ final hasGasLimit = transaction.maxGas != null && transaction.maxGas! > 0;
+ if (hasGasLimit) return transaction;
+
+ final hint = transaction.gasPrice ?? transaction.maxFeePerGas ?? await ethClient.getGasPrice();
+
+ final gasLimit = await ethClient.estimateGas(
+ sender: transaction.from,
+ to: transaction.to,
+ value: transaction.value,
+ data: transaction.data,
+ gasPrice: hint,
+ );
+
+ if (transaction.isEIP1559) {
+ return transaction.copyWith(maxGas: gasLimit.toInt());
+ }
+
+ return transaction.copyWith(
+ maxGas: gasLimit.toInt(),
+ gasPrice: transaction.gasPrice ?? hint,
+ );
+ }
+
+ Future<Transaction> _applyWCBufferedFees(Transaction transaction) async {
+ try {
+ final storedPriority =
+ appStore.settingsStore.getPriority(appStore.wallet!.type, chainId: reference.chainId);
+ final priority = storedPriority ?? evm!.getDefaultTransactionPriority();
+
+ final quote = await evm!.getWCBufferedFeeQuote(appStore.wallet!, priority);
+ if (quote != null) {
+ return _mergeWCBufferedFees(transaction, quote);
+ }
+ } catch (e) {
+ debugPrint('WalletConnect fee refresh failed: $e');
+ }
+
+ if (!transaction.isEIP1559 && transaction.gasPrice == null) {
+ return transaction.copyWith(gasPrice: await ethClient.getGasPrice());
+ }
+
+ return transaction;
+ }
+
+ Transaction _mergeWCBufferedFees(Transaction transaction, EvmWalletConnectFeeQuote quote) {
+ if (transaction.isEIP1559) {
+ // the fees coming from the dApp
+ final dAppMax = transaction.maxFeePerGas?.getInWei ?? BigInt.zero;
+ final dAppPri = transaction.maxPriorityFeePerGas?.getInWei ?? BigInt.zero;
+
+ // the updated fees coming from the wallet, handles buffered fees
+ final quoteMax = BigInt.from(quote.maxFeePerGasWei);
+ final quotePri = BigInt.from(quote.maxPriorityFeePerGasWei);
+
+ // we'll just use the higher of the two
+ var newMaxFeePerGasWei = dAppMax > quoteMax ? dAppMax : quoteMax;
+ var newPriorityFeePerGasWei = dAppPri > quotePri ? dAppPri : quotePri;
+
+ final base = quote.latestBaseFeeWei;
+ if (base != null) {
+ final baseB = BigInt.from(base);
+ final maxPriAllowed = newMaxFeePerGasWei - baseB;
+ if (newPriorityFeePerGasWei > maxPriAllowed) {
+ if (maxPriAllowed > BigInt.zero) {
+ newPriorityFeePerGasWei = maxPriAllowed;
+ } else {
+ newMaxFeePerGasWei = baseB + newPriorityFeePerGasWei;
+ }
+ }
+ }
+
+ return transaction.copyWith(
+ maxFeePerGas: EtherAmount.inWei(newMaxFeePerGasWei),
+ maxPriorityFeePerGas: EtherAmount.inWei(newPriorityFeePerGasWei),
+ );
+ }
+
+ final dPrice = transaction.gasPrice?.getInWei ?? BigInt.zero;
+ final floor = BigInt.from(quote.maxFeePerGasWei);
+ final newPriceWei = dPrice > floor ? dPrice : floor;
+ return transaction.copyWith(gasPrice: EtherAmount.inWei(newPriceWei));
+ }
+
void _onSessionRequest(SessionRequestEvent? args) async {
if (args != null && args.chainId == getChainId()) {
debugPrint('_onSessionRequest ${args.toString()}');
diff --git a/tool/configure.dart b/tool/configure.dart
index f003d9f3..8e8da718 100644
--- a/tool/configure.dart
+++ b/tool/configure.dart
@@ -1555,6 +1555,12 @@ abstract class EVM {
bool hasPriorityFee(int chainId);
+
+ Future<EvmWalletConnectFeeQuote?> getWCBufferedFeeQuote(
+ WalletBase wallet,
+ TransactionPriority priority,
+ );
+
Future<void> discoverAndAddWalletTokens(WalletBase wallet);
}
@@ -1577,6 +1583,18 @@ class ChainInfo {
@override
int get hashCode => chainId.hashCode;
}
+
+class EvmWalletConnectFeeQuote {
+ const EvmWalletConnectFeeQuote({
+ required this.maxFeePerGasWei,
+ required this.maxPriorityFeePerGasWei,
+ this.latestBaseFeeWei,
+ });
+
+ final int maxFeePerGasWei;
+ final int maxPriorityFeePerGasWei;
+ final int? latestBaseFeeWei;
+}
""";
const evmEmptyDefinition = 'EVM? evm;\n';
Why this scored 42/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.