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

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

Summary

The function ThePredicter.withdrawPredictionFees incorrectly computes the value to be transferred to the organizer if it is executed while there are Players in Pending status. This can lead to Pending players not being able to cancel their registration and consequently not being refunded. It can also lead to approved players not being able to withdraw their corresponding rewards.

Vulnerability Details

If the organizer executes the function ThePredicter.withdrawPredictionFees while there are players in Pending status, the entrance fee of those players will be transferred to the organizer.

If those pending users want to cancel their registration, the funds will not be available to refund them, so the cancellation will revert.

If those players are approved by the organizer, the entrance fees that would be used for the player's rewards will not be there, so the ThePredicter.withdraw function will revert before having transferred all the corresponding rewards to the players.

The following PoC based on test already present in the repository show how this issue could arise and affect the withrdraw and cancelRegistration functions:

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 IncorrectWithrawPredictionFeesAmountTest is Test {
ThePredicter public thePredicter;
ScoreBoard public scoreBoard;
address public organizer = makeAddr("organizer");
address public stranger = makeAddr("stranger");
uint256 entranceFee = 0.04 ether;
uint256 predictionFee = 0.0001 ether;
function setUp() public {
vm.startPrank(organizer);
scoreBoard = new ScoreBoard();
thePredicter = new ThePredicter(
address(scoreBoard),
entranceFee,
predictionFee
);
scoreBoard.setThePredicter(address(thePredicter));
vm.stopPrank();
}
function test_incorrectWitdrawPrediction_pendingPlayersCannotCancelRegistration() public {
vm.startPrank(stranger);
vm.deal(stranger, 1 ether);
thePredicter.register{value: entranceFee}();
vm.stopPrank();
uint256 organizerBalanceBefore = organizer.balance;
vm.startPrank(organizer);
thePredicter.withdrawPredictionFees();
vm.stopPrank();
uint256 organizerBalanceAfter = organizer.balance;
// No predictions where done, so the balance should not have changed.
// However, the balance increased. The entrance fee of "stranger" was
// transfered to the organizer
assertEq(organizerBalanceAfter, organizerBalanceBefore + entranceFee);
// Now the stranger cannot cancel the registration because the
// contract does not have funds
vm.startPrank(stranger);
vm.expectRevert();
thePredicter.cancelRegistration();
vm.stopPrank();
}
function test_incorrectWitdrawPrediction_approvedPlayersCannotReceiveRewards() public {
address stranger2 = makeAddr("stranger2");
address stranger3 = makeAddr("stranger3");
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();
// withdrawPredictionFees called before players were approved
// sends all contract funds to the organizer, including entranceFees
vm.startPrank(organizer);
thePredicter.withdrawPredictionFees();
vm.stopPrank();
vm.startPrank(organizer);
thePredicter.approvePlayer(stranger);
thePredicter.approvePlayer(stranger2);
thePredicter.approvePlayer(stranger3);
vm.stopPrank();
// make predictions
vm.startPrank(stranger);
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(stranger2);
thePredicter.makePrediction{value: 0.0001 ether}(
1,
ScoreBoard.Result.Draw
);
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.First
);
thePredicter.makePrediction{value: 0.0001 ether}(
2,
ScoreBoard.Result.First
);
thePredicter.makePrediction{value: 0.0001 ether}(
3,
ScoreBoard.Result.First
);
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(stranger2);
vm.expectRevert();
thePredicter.withdraw();
vm.stopPrank();
vm.startPrank(stranger3);
vm.expectRevert();
thePredicter.withdraw();
vm.stopPrank();
}
}

Impact

Incorrect computation of transferred funds.

The cancelRegistration and withdraw functions will not behave as expected. Those functions will revert before being able to refund and / or reward the players.

Tools Used

Foundry

Recommendations

  • Keep track of the prediction fees received in a state variable. Make the function ThePredicter.withdrawPredictionFees use the value of that variable to transfer the funds and reset it to 0.

Optionally

  • Implement events on user registration, approval and cancellation to have better visibility of the state of each user in the system.

Updates

Lead Judging Commences

NightHawK Lead Judge about 1 year 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.