TwentyOne

First Flight #29
Beginner FriendlyGameFiFoundrySolidity
100 EXP
View results
Submission Details
Severity: high
Invalid

Reentrancy in TwentyOne.sol : : endGame()

Summary

A high severity vulnerability was identified in the TwentyOne.sol contract where the endGame() function is vulnerable to reentrancy attacks. This allows malicious actors to make multiple withdrawals.

Vulnerability Details

The vulnerability exists in the following code section:

function endGame(address player, bool playerWon) internal {
delete playersDeck[player].playersCards; // Clear the player's cards
delete dealersDeck[player].dealersCards; // Clear the dealer's cards
delete availableCards[player]; // Reset the deck
if (playerWon) {
payable(player).transfer(2 ether); // Transfer the prize to the player
// Event emit after external call
emit FeeWithdrawn(player, 2 ether); // Emit the prize withdrawal event
}
}

Impact

Through the successful proof of concept, we demonstrated that:

  1. The integrity of the game is compromised

  2. Allows malicious actors to make multiple withdrawals

Proof of Concept

The following test demonstrates the vulnerability:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {TwentyOne} from "../src/TwentyOne.sol";
contract ReentrancyAttacker {
TwentyOne public targetContract;
uint256 public constant ATTACK_AMOUNT = 1 ether;
bool public attackInProgress;
constructor(address _targetContract) {
targetContract = TwentyOne(_targetContract);
}
receive() external payable {
if (attackInProgress) {
// Attempt to call endGame multiple times
try targetContract.call{gas: gasleft()}() {
// Recursive call to drain contract
} catch {}
}
}
function attack() external payable {
attackInProgress = true;
targetContract.startGame{value: ATTACK_AMOUNT}();
targetContract.call();
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
contract TwentyOneTest is Test {
TwentyOne public twentyOne;
ReentrancyAttacker public attacker;
address player = address(0x789);
function setUp() public {
twentyOne = new TwentyOne();
vm.deal(address(twentyOne), 100 ether);
vm.deal(player, 10 ether);
attacker = new ReentrancyAttacker(address(twentyOne));
vm.deal(address(attacker), 10 ether);
}
function testReentrancy() public {
assertGt(address(twentyOne).balance, 0, "Contract should have initial balance");
uint256 initialContractBalance = address(twentyOne).balance;
uint256 initialAttackerBalance = address(attacker).balance;
vm.prank(player);
vm.expectRevert(); // Expect the attack to be prevented
attacker.attack();
assertEq(
address(twentyOne).balance,
initialContractBalance,
"Contract balance should not change"
);
}
}

Tools Used

slither .
Manual review
Foundry test framework

Recommendations

Implement the checks-effects-interactions pattern:
Add OpenZeppelin's ReentrancyGuard:

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract GivingThanks is ReentrancyGuard {
function donate(address charity) public payable nonReentrant {
...
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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