BriVault

First Flight #52
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: low
Valid

Funds stuck when no winner has participants

Funds stuck when no winner has participants

Description

  • Normal behavior: After the event ends and the winner is set, winners should be able to withdraw. If no user picked the winner, there must be a clear recovery path so funds are not permanently locked (e.g., owner rescue or proportional refund to participants).

  • Issue: The contract allows setting a winner with no participants. The guarded withdraw() only allows addresses whose picked country matches the winner, so all calls revert with didNotWin(). There is no admin rescue or fallback refund path. As a result, all vault funds are permanently locked.

// @> setWinner finalizes state without verifying there are any winner shares
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: Medium

  • Occurs whenever the owner sets a winner that nobody picked (e.g., by mistake or by design).

  • Common in tournaments with many options.

Impact: High

  • All vault assets remain locked forever. Neither users nor owner can recover the tokens.

  • Breaks game economics and leads to permanent loss of funds.

  • No recovery path of the funds by the owner.

Proof of Concept

Description:

  • user1 joins country 7; user2 joins country 8.

  • Owner sets winner to country 6, which has no participants.

  • All withdraw() attempts revert with didNotWin()

  • funds are stuck, no user can claim

  • no recovery function for the owner to claim the funds

function testFundsStuckWhenNoWinner() public {
vm.prank(owner);
briVault.setCountry(countries);
// User 1 and 2 deposit and join
vm.startPrank(user1);
mockToken.approve(address(briVault), 1 ether);
briVault.deposit(1 ether, user1);
briVault.joinEvent(7);
vm.stopPrank();
vm.startPrank(user2);
mockToken.approve(address(briVault), 1 ether);
briVault.deposit(1 ether, user2);
briVault.joinEvent(8);
vm.stopPrank();
// Ending the game and set winner to a country that no one joined
vm.warp(eventEndDate + 1);
vm.prank(owner);
briVault.setWinner(6);
// funds are stuck in the vault, no way to claim it by user or owner
vm.prank(user1);
vm.expectRevert(abi.encodeWithSignature("didNotWin()"));
briVault.withdraw();
vm.prank(user2);
vm.expectRevert(abi.encodeWithSignature("didNotWin()"));
briVault.withdraw();
vm.prank(owner);
vm.expectRevert(abi.encodeWithSignature("didNotWin()"));
briVault.withdraw();}

Recommended Mitigation

  • Prevent setting a winner with zero participants (simple, safest): after computing totalWinnerShares, require it to be > 0 and revert otherwise. The owner must pick a country that has participants.

  • Alternatively or additionally, add a recovery path when totalWinnerShares == 0: owner sweep to a treasury or implement proportional refunds to all participants using their shares.

function setWinner(uint256 countryIndex) public onlyOwner returns (string memory) {
...
_getWinnerShares();
+ require(totalWinnerShares > 0, "No winners for selected country");
_setFinallizedVaultBalance();
...
}

Optional owner rescue when no winners (choose one policy and document it clearly):

+ function rescueWhenNoWinners(address to) external onlyOwner {
+ require(block.timestamp > eventEndDate, "event not ended");
+ require(!_setWinner || totalWinnerShares == 0, "winners exist");
+ uint256 amount = IERC20(asset()).balanceOf(address(this));
+ IERC20(asset()).transfer(to, amount);
+ }
Updates

Appeal created

bube Lead Judge 19 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Division by Zero in Withdraw Function When No Winners Bet on Winning Team

When no one bet on the winning team, making totalWinnerShares = 0, causing division by zero in withdraw and preventing any withdrawals.

Support

FAQs

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

Give us feedback!