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

Bias in Score calculation and reward sharing

Summary

players reward share is NOT directly proportional to their participation in the prediction process. this creates room for manipulation and cheating which will in turn result in low revenue as per predictionFee for the protocol. hence the protocol is NOT fair as purported.

Vulnerability Details

Based on the design,

Players can receive an amount from the prize fund only if their total number of points is a positive number and if they had paid at least one prediction fee. The prize fund is distributed in proportion to the points collected by all Players with a positive number of points. If all Players have a negative number of points, they will receive back the value of the entry fee.

the above rule couple with the formular used in getPlayerScore function, gives players chance to cheat. since the rule allows for a player to claim reward as far as she gets a positive score irrespective of her predictionCount. She might decide to stop predictions after predicting and winning the first match. she will wait to claim reward at the end of the 9th match with just ONE predictionFee paid.

The worst cheat is, she will be the sole owner of the reward shares if by chance all other players have negative scores even if they predicted and paid MORE predictionFee in all the 9 matches than her.

PoC

  • cheat_player only predicted twice. 7 other players predicted 6 matches each but couldn't win. winner_2 also predicted 6 matches and won 1 point in total, yet the way the system was designed made cheat_player who paid less predictionfee to get 80% of the reward pool. Hence the system is rather not fair as it should be because some players can manipulate it.

// 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.5 ether,//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.5 ether}();
vm.stopPrank();
// approve players
vm.startPrank(organizer);
thePredicter.approvePlayer(user);
vm.stopPrank();
}
}
function test_BiasInRewardDistribution() public{
setUpPlayersRegistrationAndApproval();
address cheat_player = thePredicter.players(0);
vm.startPrank(cheat_player);
vm.deal(cheat_player, 1 ether);
thePredicter.makePrediction{value: 0.0001 ether}(0, ScoreBoard.Result.First);
thePredicter.makePrediction{value: 0.0001 ether}(1, ScoreBoard.Result.First);
vm.deal(cheat_player, 0);//let's reset the balance
vm.stopPrank();
// another winner who played more than cheat_player
address winner_2 = thePredicter.players(8);
vm.startPrank(winner_2);
vm.deal(winner_2, 1 ether);
thePredicter.makePrediction{value: 0.0001 ether}(0, ScoreBoard.Result.First);
thePredicter.makePrediction{value: 0.0001 ether}(1, ScoreBoard.Result.First);
thePredicter.makePrediction{value: 0.0001 ether}(2, ScoreBoard.Result.Draw);
thePredicter.makePrediction{value: 0.0001 ether}(3, ScoreBoard.Result.First);
thePredicter.makePrediction{value: 0.0001 ether}(4, ScoreBoard.Result.Second);
// let's reset winner_2 balance to zero to understand the result test
vm.deal(winner_2, 0);
vm.stopPrank();
// assuming 7 other players participated in 6 predictions each but lost
for(uint i = 1; i < 8; ++i){
address player = thePredicter.players(i);
vm.startPrank(player);
vm.deal(player, 1 ether);
thePredicter.makePrediction{value: 0.0001 ether}(0, ScoreBoard.Result.Second);
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.First);
thePredicter.makePrediction{value: 0.0001 ether}(4, ScoreBoard.Result.Second);
thePredicter.makePrediction{value: 0.0001 ether}(5, ScoreBoard.Result.First);
vm.stopPrank();
// let's reset player balance to zero to understand the result test
vm.deal(player, 0);
}
// organizer records match result
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.Second);
scoreBoard.setResult(4, ScoreBoard.Result.Draw);
scoreBoard.setResult(5, ScoreBoard.Result.First);
scoreBoard.setResult(8, ScoreBoard.Result.First);
vm.stopPrank();
// all predicters including winner_2 check match result and withdraw
for(uint i = 1; i < 8; ++i){
address player = thePredicter.players(i);
vm.startPrank(player);
vm.expectRevert();//beacause of score <= 0
thePredicter.withdraw();
vm.stopPrank();
}
// winner_2 withdraws
vm.startPrank(winner_2);
thePredicter.withdraw();
vm.stopPrank();
// cheat player withdraws gets lion share funds
vm.startPrank(cheat_player);
thePredicter.withdraw();
vm.stopPrank();
assertEq(winner_2.balance, 3 ether);
assertEq(cheat_player.balance, 12 ether);
}
}

Impact

  • poor revenue for the protocol in terms of predictionFee

  • players lose funds in predictionFee to a cheating player

Tools Used

  • Foundry test

  • manual review

  • Documentation

Recommendations

To calculate a perfect Shares for the* WINNERS*, we must factor in the predictionCount of that winner . i.e score * predictionCount. Below is a reviewed version of the getPlayerScore function.

function getPlayerScore(address player) public view returns (int8 score) {
for (uint256 i = 0; i < NUM_MATCHES; ++i) {
if (
playersPredictions[player].isPaid[i] &&
playersPredictions[player].predictions[i] != Result.Pending
) {
score += playersPredictions[player].predictions[i] == results[i]
? int8(2)
: -1;
}
}
+ if(score > 0){
+ return score * playersPredictions[player].predictionsCount;
+ }else{
+ return score;
+ }
}
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.