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

Protocol design is not sustainable

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.

// 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");
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();
// approve players
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

  • Dapp will not raise enough funds to even pay for hall

  • little or no revenue generation

Tools Used

  • Foundry test

  • Manual review

  • ThePredicter Documentation

Recommendations

  • Review the Protocol design by using the entranceFee of the selected 30 players to pay for the hall and the predictionFees paid by the predicters should be the rewards pool. Do the following

  • In scoreBoard.sol include the following function for the ThePredicter.sol to call and get players predictions

function getPlayerPredictions(address _player) external view onlyThePredicter returns(PlayerPredictions memory) {
return playersPredictions[_player];
}
  • in thePredicter.sol do this

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");
}
  • in withdraw function do this

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

Lead Judging Commences

NightHawK Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Design choice

Appeal created

cryptedoji Submitter
about 1 year ago
NightHawK Lead Judge
about 1 year ago
NightHawK Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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