Summary
Based on the design stated in the documention
The entrance fees paid at the beginning form the prize fund, which after the end of the tournament is distributed among all Players who paid at least one prediction fee and depending on theirs collected number of points.
Given that it is NOT compulsory for players
to predict matches, with the above implementation, If Ivan and his friends depend on the predictionFees
to pay for hall, automatically, it will be difficult if not impossible to meet up if enough players
do not predict
AND the few who won in the predictions
will withdraw all funds from the protocol.
Vulnerability Details
Since it is not compulsory to make predictions
, let's say only one player
was interested, if this player
predicts just one out of all the 9 matches and she wins just that prediction
and in the end when all results are in, she alone will claim the entire entranceFee
for the 30 players
because she is the sole owner of the reward shares
. This leaves the Dapp with only the little predictionFee
she paid just once. This little fund left in the Dapp will not even be enough to pay for the Hall.
It may not be possible for all players
to lose interest in prediction
but the less the predicters
, the likelihood that the Dapp will not be able to meet up with the purpose of its creation.
PoC(Proof of Code)
As proven in the test, assuming first_player
is the only one who predicted in the tournament, and she only spent 0.0002 ether
on predicting
just 2 matches. She ended up withdrawing ALL reward pool, leaving the protocol with just 0.0002 ether
which will not even be enough to pay for hall.
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 setUpPlayersRegistrationAndApproval() private{
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();
}
}
function test_protocolSustainabilityIssue() public{
setUpPlayersRegistrationAndApproval();
uint256 vaultBalBefore = address(thePredicter).balance;
address first_player = thePredicter.players(0);
vm.deal(first_player, 1 ether);
uint256 playerBalBefore = first_player.balance;
vm.startPrank(first_player);
vm.deal(first_player, 1 ether);
thePredicter.makePrediction{value: 0.0001 ether}(0, ScoreBoard.Result.First);
thePredicter.makePrediction{value: 0.0001 ether}(1, ScoreBoard.Result.First);
vm.stopPrank();
vm.startPrank(organizer);
scoreBoard.setResult(0, ScoreBoard.Result.First);
scoreBoard.setResult(1, ScoreBoard.Result.First);
scoreBoard.setResult(8, ScoreBoard.Result.First);
vm.stopPrank();
vm.prank(first_player);
thePredicter.withdraw();
uint256 vaultBalAfter = address(thePredicter).balance;
uint256 playerBalAfter = first_player.balance;
assertEq(vaultBalBefore, 1.2 ether);
assertEq(vaultBalAfter, 0.0002 ether);
assertEq(playerBalBefore, 1 ether);
assertEq(playerBalAfter, 2.1998 ether);
}
}
Impact
Tools Used
Recommendations
function getPlayerPredictions(address _player) external view onlyThePredicter returns(PlayerPredictions memory) {
return playersPredictions[_player];
}
function withdrawPredictionFees() public {
if (msg.sender != organizer) {
revert ThePredicter__NotEligibleForWithdraw();//@audit wrong throw==
}
- uint256 fees = address(this).balance - players.length * entranceFee;
+ uint256 fees = players.length * entranceFee;
(bool success, ) = msg.sender.call{value: fees}("");
require(success, "Failed to withdraw");
}
function withdraw() public {
if (!scoreBoard.isEligibleForReward(msg.sender)) {
revert ThePredicter__NotEligibleForWithdraw();
}
int8 score = scoreBoard.getPlayerScore(msg.sender);
int8 maxScore = -1;
int256 totalPositivePoints = 0;
+ uint256 totalPredictionsCount = 0;
for (uint256 i = 0; i < players.length; ++i) {
int8 cScore = scoreBoard.getPlayerScore(players[i]);
+ totalPredictionsCount += scoreBoard.getPlayerPredictions(players[i]).predictionsCount;
if (cScore > maxScore) maxScore = cScore;
if (cScore > 0) totalPositivePoints += cScore;
}
if (maxScore > 0 && score <= 0) {
revert ThePredicter__NotEligibleForWithdraw();
}
uint256 shares = uint8(score);
uint256 totalShares = uint256(totalPositivePoints);
uint256 reward = 0;
reward = maxScore < 0
? entranceFee
- : (shares * players.length * entranceFee) / totalShares;
+ : (shares * totalPredictionsCount * predictionFee) / totalShares;
if (reward > 0) {
scoreBoard.clearPredictionsCount(msg.sender);
(bool success, ) = msg.sender.call{value: reward}("");
require(success, "Failed to withdraw");
}
}