Summary
If the Organizer calls the withdrawPredictionFees function anytime before unapproved users could cancelRegistration , then entranceFees meant for those users will be drained.
Vulnerability Details
According to the design of the Dapp,
The funds collected from this prediction fees are used to cover the costs of the hall and the Organizer must be able to withdraw those funds at any time.
Only the predictionFees should be withdrawn by organizer ANYTIME he wants; But the withdrawPredictionFees function is withdrawing ALL funds including entranceFee meant for unapproved users who may not have canceled their registration as at when organizer is calling the withdraw function;
If Organizer keeps withdrawing every now and then and unapproved users call cancelRegistration later on, this may lead to depletion of the reward pool(30 players entranceFee). Hence reward distribution problems
PoC (Proof of Code)
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 ThePredicterTest is Test {
ThePredicter public thePredicter;
ScoreBoard public scoreBoard;
address public organizer = makeAddr("Ivan");
function setUp() public {
vm.startPrank(organizer);
scoreBoard = new ScoreBoard();
thePredicter = new ThePredicter(
address(scoreBoard),
0.04 ether,
0.0001 ether
);
scoreBoard.setThePredicter(address(thePredicter));
vm.stopPrank();
}
function test_organizerWithdrawNonApprovedUsersFees() public{
for (uint256 i = 0; i < 20; ++i) {
address not_approved = makeAddr(string.concat("no-approved-user", Strings.toString(i)));
vm.startPrank(not_approved);
vm.deal(not_approved, 1 ether);
thePredicter.register{value: 0.04 ether}();
vm.stopPrank();
}
uint256 unapprovedUsersFunds = address(thePredicter).balance;
for (uint256 i = 0; i < 30; ++i) {
address user = makeAddr(string.concat("user", Strings.toString(i)));
vm.startPrank(user);
vm.deal(user, 1 ether);
thePredicter.register{value: 0.04 ether}();
vm.stopPrank();
vm.startPrank(organizer);
thePredicter.approvePlayer(user);
vm.stopPrank();
}
uint256 vaultBal = address(thePredicter).balance;
vm.prank(organizer);
thePredicter.withdrawPredictionFees();
uint256 approvePlayersFunds = address(thePredicter).balance;
assertEq(vaultBal, 2 ether);
assertLe(vaultBal - approvePlayersFunds, 1.2 ether);
assertEq(organizer.balance, unapprovedUsersFunds);
}
}
Impact
Tools Used
Manual review
foundry test
Recommendations
In the thePredicter.sol contract do the following;
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {ScoreBoard} from "./ScoreBoard.sol";
import {console} from "forge-std/Test.sol";
contract ThePredicter {
+ uint256 registeredUsersCount;
function register() public payable {
if (msg.value != entranceFee) {
revert ThePredicter__IncorrectEntranceFee();
}
if (block.timestamp > START_TIME - 14400) {
revert ThePredicter__RegistrationIsOver();
}
if (playersStatus[msg.sender] == Status.Pending) {
revert ThePredicter__CannotParticipateTwice();
}
+ registeredUsersCount++;
playersStatus[msg.sender] = Status.Pending;
}
function cancelRegistration() public {
if (playersStatus[msg.sender] == Status.Pending) {
+ registeredUsersCount--;
(bool success, ) = msg.sender.call{value: entranceFee}("");
require(success, "Failed to withdraw");
playersStatus[msg.sender] = Status.Canceled;
return;
}
revert ThePredicter__NotEligibleForWithdraw();//@audit
}
function withdrawPredictionFees() public {
if (msg.sender != organizer) {
revert ThePredicter__NotEligibleForWithdraw();//@audit wrong throw==
}
- uint256 fees = address(this).balance - players.length * entranceFee;
+ uint256 fees = address(this).balance - registeredUsersCount * entranceFee;
(bool success, ) = msg.sender.call{value: fees}("");
require(success, "Failed to withdraw");
}
}