Root + Impact
Description
-
After the tournament ends, the owner calls setWinner with a country index to declare the winning team, enabling winners to withdraw based on their shares.
-
This sets the _setWinner flag, calculates winning shares, and finalizes the vault balance for payouts.
-
The issue is that the owner has full control to set any country as winner, regardless of the actual tournament outcome, allowing a malicious or compromised owner to choose a country where they or colluders hold all shares.
-
There is no verification, oracle, or decentralized mechanism to ensure the winner matches real-world results, making the owner an untrusted oracle.
function setWinner(uint256 countryIndex) public onlyOwner returns (string memory) {
if (block.timestamp <= eventEndDate) {
revert eventNotEnded();
}
@> require(countryIndex < teams.length, "Invalid country index");
if (_setWinner) {
revert WinnerAlreadySet();
}
winnerCountryId = countryIndex;
winner = teams[countryIndex];
_setWinner = true;
_getWinnerShares();
_setFinallizedVaultBalance();
emit WinnerSet (winner);
return winner;
}
Risk
Likelihood:
Impact:
-
Owner sets winner to a country with their own or colluding shares, stealing the entire pot.
-
All legitimate users lose their deposits, leading to total funds loss and eroded trust.
Proof of Concept
function testMaliciousOwnerTheft() public {
vm.prank(owner);
briVault.setCountry(countries);
vm.prank(user1);
mockToken.approve(address(briVault), 2 ether);
vm.prank(user2);
mockToken.approve(address(briVault), 2 ether);
vm.prank(user1);
briVault.deposit(1 ether, user1);
vm.prank(user1);
briVault.joinEvent(0);
vm.prank(user2);
briVault.deposit(1 ether, user2);
vm.prank(user2);
briVault.joinEvent(1);
vm.warp(eventEndDate + 1 days);
vm.prank(owner);
briVault.setWinner(1);
uint256 colluderBefore = mockToken.balanceOf(user2);
vm.prank(user2);
briVault.withdraw();
uint256 colluderAfter = mockToken.balanceOf(user2);
vm.expectRevert("didNotWin()");
vm.prank(user1);
briVault.withdraw();
uint256 stolenAmount = colluderAfter - colluderBefore;
assertApproxEqAbs(
stolenAmount,
1.97 ether,
0.01 ether,
"Colluder stole honest user's funds"
);
emit log_named_decimal_uint("Colluder stole", stolenAmount, 18);
}
Recommended Mitigation
-
Integrate Chainlink oracle for verifiable winner (simplified example)
-
Add oracle setup in constructor (keyHash, subId, etc.)
-
Replace owner-set with oracle-verified
+ import {VRFConsumerBaseV2} from "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";
function setWinner(uint256 requestId, uint256[] memory randomWords) public {
- // Owner call
+ // Called by Chainlink callback or verified oracle
if (block.timestamp <= eventEndDate) {
revert eventNotEnded();
}
+ // Verify random or oracle data maps to real winner (e.g., randomWords[0] % teams.length)
+ uint256 countryIndex = randomWords[0] % teams.length; // Or real oracle feed
if (_setWinner) {
revert WinnerAlreadySet();
}
winnerCountryId = countryIndex;
winner = teams[countryIndex];
_setWinner = true;
_getWinnerShares();
_setFinallizedVaultBalance();
emit WinnerSet (winner);
}