F-2024-0009·best-practice

Using the transfer() function of address payable is discouraged

Fixedaccount-abstractionerc-4337subscriptiongithub.com/bastion-wallet
TL;DR

transfer() only forwards 2300 gas. If the initiator is a multisig or payment splitter that needs more, the call fails. Use call{value: ...} for native transfers instead.

Severity
LOW
Impact
LOW
Likelihood
LOW
Method
MManual review
CAT.
Complexity
LOW
Exploitability
LOW
02Section · Description

Description

The transfer() function only allows the recipient to use 2300 gas. If the recipient uses more than that, transfers will fail. This could, for example, be the case if initiator is the address of a Multisig or payment splitter that is supposed to execute additional logic after the withdrawal. Furthermore, gas costs might change in the future, increasing the likelihood of that happening. Also, notice that once the initiator address is set to a specific subscription, it could be changed via the modifySubscription() function.

Consider the following scenario:

  1. During the creation of a subscription, the executor is not aware of this "transfer() issue" and sets the initiator to a contract address (e.g., a payment splitter) that consumes more than 2300 gas.
  2. The user calls processPayment() function for a native subscription payment but the execution reverts because the initiator consumes more than 2300 gas when receiving the funds.
solidity
/// @notice Processes a native payment for the subscription
function _processNativePayment(SubStorage storage sub) internal {
require(address(this).balance >= sub.amount, "Insufficient Ether balance");
payable(sub.initiator).transfer(sub.amount);
}
solidity
/// @notice Withdraws all Ether held by the contract to the owner's address
/// @dev This function can only be called by the contract owner
function withdrawETH() public onlyOwner {
payable(owner()).transfer(address(this).balance);
}
03Section · Impact

Impact

Native payments to multisig recipients or payment splitters fail silently when the recipient's logic exceeds the 2300-gas limit, blocking valid subscription payments.

04Section · Recommendation

Recommendation

Use call() instead of transfer() in _processNativePayment() and withdrawETH() functions:

diff
- payable(sub.initiator).transfer(sub.amount);
+ (bool success, ) = sub.initiator.call{value: sub.amount}("");
+ require(success, "ProcessNativePayment failed.");
- payable(owner()).transfer(address(this).balance);
+ (bool success, ) = owner().call{value: address(this).balance}("");
+ require(success, "WithdrawETH failed.");
05Section · Resolution

Resolution

Team Response: Acknowledged and fixed as suggested.

Status
Fixed
Fix commit
79cddfeb6070
F-2024-0009

oog
zealynx

Smart Contract Security Digest

Monthly exploit breakdowns, audit checklists, and DeFi security research — straight to your inbox

© 2026 Zealynx