fix: Error while approving transactions via walletconnect for ARB (#2878)
What changed, and why it matters
This commit fixes two bugs in Cake Wallet's WalletConnect integration for Ethereum-compatible chains (including Arbitrum/ARB). First, it corrects how the app picks the right network node when building transactions, so it no longer tries to use a non-EVM wallet's node for EVM chains. Second, it now respects gas values supplied by the connected dApp instead of always overwriting them, and it handles cases where the dApp only provides some gas fields. A third small fix prevents a crash when an auto-closing WalletConnect bottom sheet tries to close after it has already been dismissed. The commit is described by the vendor as a transaction-approval bug fix, not as a security vulnerability.
Treat as a routine functional bug fix. Reviewers should verify that evm.getWeb3Client() correctly returns a client for all supported EVM wallet types and that the fallback path does not dereference appStore.wallet when it is null. No urgent security response is indicated by the diff alone.
Security signals we found
Fixes incorrect RPC endpoint selection for EVM WalletConnect sessions
Prevents overwriting of dApp-provided gas fields, reducing risk of failed or mispriced transactions
Adds defensive context handling to avoid exceptions in auto-closing bottom sheet
No explicit security claim or CVE in commit message
Evidence from the diff
The patch changes EVMChainServiceImpl to obtain a Web3Client via a new evm.getWeb3Client(wallet) helper when the current wallet is EVM-compatible, falling back to settingsStore.getCurrentNode(wallet.type). It then refactors gas handling in transaction building: it parses a dApp-supplied ‘gas’ hex string into maxGas, checks which gas fields are present (gasPrice, maxFeePerGas, maxPriorityFeePerGas, maxGas), and only estimates missing values. Previously the code always called ethClient.getGasPrice() and estimateGas() and overwrote the transaction’s gas fields, which caused failures for chains like Arbitrum where dApps may supply their own gas values. The bottom sheet listener change uses the builder’s own context and Navigator.maybeOf/maybePop to avoid calling Navigator on an invalid context after the sheet is gone.
Changed components
lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dartlib/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_listener_widget.dartWalletConnect EVM transaction approval flowArbitrum (ARB) WalletConnect supportInspect captured patch +66 / −37
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 0b34baf7..c65d84c6 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
@@ -1,7 +1,6 @@
import 'dart:convert';
import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:eth_sig_util/eth_sig_util.dart';
import 'package:eth_sig_util/util/utils.dart';
@@ -17,6 +16,8 @@ import 'package:cake_wallet/src/screens/wallet_connect/utils/eth_utils.dart';
import 'package:cake_wallet/src/screens/wallet_connect/utils/method_utils.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
+import 'package:cake_wallet/evm/evm.dart';
+import 'package:cake_wallet/reactions/wallet_connect.dart';
class EvmChainServiceImpl {
Map<String, dynamic Function(String, dynamic)> get sessionRequestHandlers => {
@@ -38,11 +39,7 @@ class EvmChainServiceImpl {
required this.bottomSheetService,
required this.walletKit,
Web3Client? web3Client,
- }) : ethClient = web3Client ??
- Web3Client(
- _getNodeUriForChain(reference, appStore),
- ProxyWrapper().getHttpIOClient(),
- ) {
+ }) : ethClient = web3Client ?? _createWeb3Client(reference, appStore) {
for (final event in EventsConstants.allEvents) {
walletKit.registerEventEmitter(
chainId: getChainId(),
@@ -77,17 +74,16 @@ class EvmChainServiceImpl {
String getChainId() => reference.chain();
- static String _getNodeUriForChain(EVMChainId reference, AppStore appStore) {
- final walletType = appStore.wallet!.type;
-
- if (isEVMCompatibleChain(walletType)) {
- final chainId = reference.chainId;
+ static Web3Client _createWeb3Client(EVMChainId reference, AppStore appStore) {
+ if (appStore.wallet != null && isEVMCompatibleChain(appStore.wallet!.type)) {
+ final walletClient = evm?.getWeb3Client(appStore.wallet!);
- return appStore.settingsStore.getCurrentNode(walletType, chainId: chainId).uri.toString();
+ if (walletClient != null) return walletClient;
}
- // For old wallet types, use the wallet type directly
- return appStore.settingsStore.getCurrentNode(walletType).uri.toString();
+ final node = appStore.settingsStore.getCurrentNode(appStore.wallet!.type);
+
+ return Web3Client(node.uri.toString(), ProxyWrapper().getHttpIOClient());
}
Future<void> personalSign(String topic, dynamic parameters) async {
@@ -423,25 +419,59 @@ class EvmChainServiceImpl {
}) async {
Transaction transaction = transactionJson.toTransaction();
- final gasPrice = await ethClient.getGasPrice();
- try {
- final gasLimit = await ethClient.estimateGas(
- sender: transaction.from,
- to: transaction.to,
- value: transaction.value,
- data: transaction.data,
- gasPrice: gasPrice,
- );
+ if (transactionJson.containsKey('gas') && transaction.maxGas == null) {
+ final gasHex = transactionJson['gas'].toString();
+ try {
+ final gasValue = int.parse(
+ gasHex.replaceFirst('0x', '').replaceFirst('0X', ''),
+ radix: 16,
+ );
+ transaction = transaction.copyWith(maxGas: gasValue);
+ } catch (e) {
+ debugPrint('Failed to parse gas value: $gasHex, error: $e');
+ }
+ }
- transaction = transaction.copyWith(
- gasPrice: gasPrice,
- maxGas: gasLimit.toInt(),
- );
- } on RPCError catch (e) {
- return JsonRpcError(code: e.errorCode, message: e.message);
+ // 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);
+ }
}
- final gweiGasPrice = (transaction.gasPrice?.getInWei ?? BigInt.zero) / BigInt.from(1000000000);
+ final gweiGasPrice =
+ (transaction.gasPrice?.getInWei ?? transaction.maxFeePerGas?.getInWei ?? BigInt.zero) /
+ BigInt.from(1000000000);
final amount = (transaction.value?.getInWei ?? BigInt.zero) / BigInt.from(1e18);
diff --git a/lib/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_listener_widget.dart b/lib/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_listener_widget.dart
index ceee46e0..8cdb8d42 100644
--- a/lib/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_listener_widget.dart
+++ b/lib/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_listener_widget.dart
@@ -42,16 +42,15 @@ class BottomSheetListenerState extends State<BottomSheetListener> {
backgroundColor: Color.fromARGB(0, 0, 0, 0),
isScrollControlled: true,
constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.9),
- builder: (context) {
+ builder: (BuildContext bottomSheetContext) {
if (item.closeAfter > 0) {
Future.delayed(Duration(seconds: item.closeAfter), () {
try {
- if (!mounted) return;
- if (Navigator.canPop(context)) {
- Navigator.pop(context);
- }
- } catch (e, s) {
- debugPrint('[$runtimeType] close $e $s');
+ final navigator = Navigator.maybeOf(bottomSheetContext, rootNavigator: false);
+ navigator?.maybePop();
+ } catch (e) {
+ // the context is invalid as the bottom sheet was already closed,
+ // this is expected and can be safely ignored
}
});
}
Why this scored 24/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.