Beatland Festival

First Flight #44
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: low
Likelihood: medium
Invalid

L02. Unsafe Ether Withdrawal Using .transfer and Missing Zero Address Check

Root + Impact

Description

  • The withdraw function is intended to let the contract owner transfer all Ether in the contract to a specified target address. It uses Solidity’s .transfer, which only forwards 2300 gas to the recipient.

  • This creates a problem when the target address is a smart wallet owned by the owner (e.g., Gnosis Safe or custom contract), which may execute logic in its receive() or fallback() functions that requires more than 2300 gas. The result is a reverting transfer, which blocks withdrawals entirely — even to a legitimate owner wallet.

  • Additionally, there is no check that the target address is non-zero, so a mistake in passing address(0) would irreversibly burn the contract’s Ether balance.

function withdraw(address target) external onlyOwner {
@> payable(target).transfer(address(this).balance);
}

Risk

Likelihood:

  • This will occur when the owner uses a smart contract wallet that consumes more than 2300 gas on receive() or fallback() (common with Gnosis Safe, logging wallets, or wallets using access control).

  • Passing address(0) by mistake (e.g., user input error or bad frontend) will result in permanent loss of funds.

Impact:

  • The owner may be completely blocked from withdrawing Ether from the contract despite being authorized.

  • All Ether could be lost if sent to the zero address due to the missing address validation.


Proof of Concept

contract OwnerSmartWallet {
event Received(address sender, uint256 amount);
receive() external payable {
// Logs sender and amount (uses more than 2300 gas)
emit Received(msg.sender, msg.value);
}
}

Steps:

  1. The contract owner deploys OwnerSmartWallet.

  2. They call withdraw(address(OwnerSmartWallet)).

  3. The .transfer fails due to gas limitations inside receive().

  4. Transaction reverts — funds remain stuck.

  5. Alternatively, if withdraw(address(0)) is called, all Ether is burned.


Recommended Mitigation

- payable(target).transfer(address(this).balance);
+ require(target != address(0), "Invalid target address");
+ (bool success, ) = payable(target).call{value: address(this).balance}("");
+ require(success, "Withdraw failed");

Explanation:

  • Replacing .transfer with .call{value: ...} forwards all remaining gas, ensuring compatibility with smart wallets or contracts with expensive receive() functions.

  • Adding a check that target is not the zero address prevents accidental fund loss.

  • The require(success) ensures the function only succeeds when the transfer is successful — preventing silent failures.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Zero address check

Owner/admin is trusted / Zero address check - Informational

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.