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

Fee calculation in `ThePredicter::withdrawPredicitionFees` can cause organizer to withdraw more fees than intended and leave some users unable to claim

Summary

Fee calculation in ThePredicter::withdrawPredicitionFees can cause the organizer to withdraw more fees than intended and leave some users unable to claim their entrance fee or prevent players from claiming their reward

Vulnerability Details

The following formula is used to calculate the amount of prediction fees that should be withdrawn from the contract. The problem is that address(this).balance will include entrance fees of users who are not approved players in the players array. There are instances where a user may want to cancel their registration or a player will want to redeem their rewards but will be unable to do so as their fee has been incorrectly withdrawn.

function withdrawPredictionFees() public {
if (msg.sender != organizer) {
revert ThePredicter__NotEligibleForWithdraw();
}
uint256 fees = address(this).balance - players.length * entranceFee;
(bool success, ) = msg.sender.call{value: fees}("");
require(success, "Failed to withdraw");
}

Impact

A user may want to cancel their registration or a player may want to redeem their rewards but will be unable to do so.

POC

Add the following test to ThePredicter.test.sol

function test_breakPredictionFeesWithdrawal() public {
address stranger2 = makeAddr("stranger2");
address stranger3 = makeAddr("stranger3");
address stranger4 = makeAddr("stranger4");
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(organizer);
thePredicter.approvePlayer(stranger);
thePredicter.approvePlayer(stranger2);
vm.stopPrank();
vm.startPrank(stranger);
thePredicter.makePrediction{value: 0.0001 ether}(
0,
ScoreBoard.Result.Draw
);
thePredicter.makePrediction{value: 0.0001 ether}(
1,
ScoreBoard.Result.Draw
);
vm.stopPrank();
vm.startPrank(stranger2);
thePredicter.makePrediction{value: 0.0001 ether}(
0,
ScoreBoard.Result.Draw
);
thePredicter.makePrediction{value: 0.0001 ether}(
1,
ScoreBoard.Result.Draw
);
vm.stopPrank();
vm.startPrank(organizer);
scoreBoard.setResult(0, ScoreBoard.Result.Draw);
scoreBoard.setResult(1, ScoreBoard.Result.Draw);
scoreBoard.setResult(8, ScoreBoard.Result.Draw);
vm.stopPrank();
vm.startPrank(organizer);
thePredicter.withdrawPredictionFees();
vm.stopPrank();
vm.startPrank(stranger);
thePredicter.withdraw();
vm.stopPrank();
//Unapproved user withdraws their entrance fee
vm.startPrank(stranger3);
thePredicter.cancelRegistration();
vm.stopPrank();
//player 2 can no longer redeem their reward because the contract balance is 0
vm.startPrank(stranger2);
vm.assertEq(address(thePredicter).balance, 0 ether);
vm.expectRevert("Failed to withdraw");
thePredicter.withdraw();
vm.stopPrank();
}

Tools Used

Manual Review

Recommendations

Create a state variable to track the amount of prediction fees collected

address public organizer;
address[] public players;
uint256 public entranceFee;
uint256 public predictionFee;
+ uint256 public predictionFeesCollected;
ScoreBoard public scoreBoard;
mapping(address players => Status) public playersStatus;

Update the state variable each time a prediciton is made

function makePrediction(
uint256 matchNumber,
ScoreBoard.Result prediction
) public payable {
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);
+ predictionFeesCollected += msg.value;
}

Use predictionFeesCollected as withdraw amount

function withdrawPredictionFees() public {
if (msg.sender != organizer) {
revert ThePredicter__NotEligibleForWithdraw();
}
- uint256 fees = address(this).balance - players.length * entranceFee;
+ (bool success, ) = msg.sender.call{value: predictionFeesCollected}("");
require(success, "Failed to withdraw");
}
Updates

Lead Judging Commences

NightHawK Lead Judge 11 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Wrong computation in withdrawPredictionFees

withdrawPredictionFees incorrectly computes the value to be transferred to the organizer, which leads to pending players not being able to cancel their registration, approved players not being able to claim their rewards and other errors.

Support

FAQs

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