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

unapproved users funds not accounted for

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)

// SPDX-License-Identifier: UNLICENSED
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");
// address public malicious = makeAddr("malicious");
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{
//20 users that were not approved
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;
// the 30 approved users
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

  • Users funds drained

  • problems with players reward distribution or refund

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");
}
}
Updates

Lead Judging Commences

NightHawK Lead Judge 10 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.