Last Man Standing

First Flight #45
Beginner FriendlyFoundrySolidity
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Missing Address Type Check Before Low-level Call May Lead To Malicious Code Execution

Missing Address Type Check Before Low-level Call May Lead To Malicious Code Execution

Description

  • The contract uses payable(address).call{value: amount}("") to transfer ETH to user-controlled addresses in withdrawWinnings() and withdrawPlatformFees().

  • If the target address is not an Externally Owned Account (EOA) but a contract, the low-level call will invoke the contract’s fallback() or receive() function, which may contain malicious logic such as re-entrancy.

function withdrawWinnings() external nonReentrant {
uint256 amount = pendingWinnings[msg.sender];
require(amount > 0, "Game: No winnings to withdraw.");
@> (bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Game: Failed to withdraw winnings.");
pendingWinnings[msg.sender] = 0;
emit WinningsWithdrawn(msg.sender, amount);
}
function withdrawPlatformFees() external onlyOwner nonReentrant {
uint256 amount = platformFeesBalance;
require(amount > 0, "Game: No platform fees to withdraw.");
platformFeesBalance = 0;
@> (bool success, ) = payable(owner()).call{value: amount}("");
require(success, "Game: Failed to withdraw platform fees.");
emit PlatformFeesWithdrawn(owner(), amount);
}

Risk

Likelihood:High

  • These functions allow sending ETH to arbitrary addresses without checking whether the recipient is a contract.

  • When the recipient is a contract, the fallback or receive function is triggered, allowing it to perform arbitrary logic, including re-entrant calls.

Impact:High

  • Can cause the re-entrant issue(etc. take away all the ether of the Game contract).

  • It could also cause cross-function re-entrancy, bypassing access control or logic assumptions elsewhere in the contract.

  • Note: While nonReentrant provides protection against direct re-entrancy in the current context, relying solely on modifiers is not always sufficient.

    For example, if future changes introduce internal calls, delegatecalls, or access through another entry point, the low-level call may still become a reentrancy vector.

    In addition, the fallback or receive function of the recipient may contain side effects that interfere with contract logic or gas usage, even without reentrancy.

Proof of Concept

A malicious contract can exploit the low-level call as follows:

//fallback function in malicious contract
function fallback() external {
address GameContractAddress = 0x....aaa;
// Re-enter the Game contract
address(GameContractAddress).withdrawWinnings();
}

In this example, when the Game contract sends ETH to the malicious contract, it triggers the fallback function, which re-enters the vulnerable withdrawWinnings() logic before the previous withdrawal finalizes.

Recommended Mitigation

  • Option 1: Use transfer() or send() instead of call

    • These have fixed gas stipends and are safer for simple transfers (but less flexible).

    • Note: transfer() reverts on failure, while send() returns false.

payable(msg.sender).transfer(amount);
  • Option 2: Validate recipient is an EOA before sending ETH

    • Use extcodesize to detect whether the address is a contract:

require(msg.sender.code.length == 0, "Game: recipient is a contract");
Updates

Appeal created

inallhonesty Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Doss by reverting contract / contract that doesn't implement receive.

The only party affected is the "attacker". Info at best.

Support

FAQs

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

Give us feedback!