Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: medium
Invalid

Lacks of protection in 'ThePredicter.sol::withdraw' function makes it vulnerable to reentrancy attacks.

Summary

The **withdraw **function performs several operations and then makes an external call. The state changes (clearing predictions count) occur before the external call, reducing reentrancy risk. However, the absence of a reentrancy guard still leaves it vulnerable to reentrancy attacks.

Vulnerability Details

**withdraw **function:

function withdraw() public {
if (!scoreBoard.isEligibleForReward(msg.sender)) {
revert ThePredicter__NotEligibleForWithdraw();
}
int8 score = scoreBoard.getPlayerScore(msg.sender);
int8 maxScore = -1;
int256 totalPositivePoints = 0;
for (uint256 i = 0; i < players.length; ++i) {
int8 cScore = scoreBoard.getPlayerScore(players[i]);
if (cScore > maxScore) maxScore = cScore;
if (cScore > 0) totalPositivePoints += cScore;
}
if (maxScore > 0 && score <= 0) {
revert ThePredicter__NotEligibleForWithdraw();
}
uint256 shares = uint8(score);
uint256 totalShares = uint256(totalPositivePoints);
uint256 reward = 0;
reward = maxScore < 0
? entranceFee
: (shares * players.length * entranceFee) / totalShares;
if (reward > 0) {
scoreBoard.clearPredictionsCount(msg.sender);
(bool success, ) = msg.sender.call{value: reward}("");
require(success, "Failed to withdraw");
}
}

Test for withdraw function:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "forge-std/Test.sol";
import "../src/ThePredicter.sol";
import "../src/ScoreBoard.sol";
contract ReentrancyAttack {
ThePredicter public target;
bool public reenter = true;
constructor(ThePredicter _target) {
target = _target;
}
receive() external payable {
if (reenter) {
reenter = false;
target.withdrawPredictionFees();
}
}
function attack() external {
target.withdrawPredictionFees();
}
}
contract ThePredicterTest is Test {
ThePredicter public predicter;
ScoreBoard public scoreBoard;
ReentrancyAttack public attacker;
address public organizer = address(0x1);
address public player = address(0x2);
function setUp() public {
vm.startPrank(organizer);
scoreBoard = new ScoreBoard();
predicter = new ThePredicter(address(scoreBoard), 1 ether, 0.1 ether);
attacker = new ReentrancyAttack(predicter);
vm.deal(address(predicter), 100 ether);
vm.stopPrank();
}
function testReentrancyAttack() public {
vm.startPrank(player);
vm.deal(player, 1 ether);
predicter.register{value: 1 ether}();
vm.stopPrank();
vm.startPrank(organizer);
predicter.approvePlayer(player);
vm.stopPrank();
vm.startPrank(address(attacker));
attacker.attack();
vm.stopPrank();
assertGt(address(attacker).balance, 0, "Reentrancy attack failed");
}
}

Impact

The lack of a reentrancy guard still exposes the contract to potential reentrancy attacks, which could allow an attacker to drain all funds from the contract.

Tools Used

Manual Review

Recommendations

Implementing the nonReentrant modifier to prevent function re-entry.

function withdraw() public nonReentrant {
if (!scoreBoard.isEligibleForReward(msg.sender)) {
revert ThePredicter__NotEligibleForWithdraw();
}
int8 score = scoreBoard.getPlayerScore(msg.sender);
int8 maxScore = -1;
int256 totalPositivePoints = 0;
for (uint256 i = 0; i < players.length; ++i) {
int8 cScore = scoreBoard.getPlayerScore(players[i]);
if (cScore > maxScore) maxScore = cScore;
if (cScore > 0) totalPositivePoints += cScore;
}
if (maxScore > 0 && score <= 0) {
revert ThePredicter__NotEligibleForWithdraw();
}
uint256 shares = uint8(score);
uint256 totalShares = uint256(totalPositivePoints);
uint256 reward = 0;
reward = maxScore < 0
? entranceFee
: (shares * players.length * entranceFee) / totalShares;
if (reward > 0) {
scoreBoard.clearPredictionsCount(msg.sender); // State change before external call
(bool success, ) = msg.sender.call{value: reward}("");
require(success, "Failed to withdraw");
}
}

These modifications substantially mitigate the risk of reentrancy attacks.

Updates

Lead Judging Commences

NightHawK Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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