BriVault

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

[H-3] The `briVault::withdraw()` Division by Zero Causes Permanent Fund Lock When No Users Predict Winning Country

[H-3] The briVault::withdraw() Division by Zero Causes Permanent Fund Lock When No Users Predict Winning Country

Description

The `briVault::withdraw()` function calculates winner payouts using `briVault::Math.mulDiv()`
When no users have predicted the winning country `winnerCountryId`, `totalWinnerShares` remains `0`, causing `Math.mulDiv()` to perform division by zero. This makes all withdrawal attempts revert, permanently locking all deposited funds in the contract.
```solidity
function withdraw() external winnerSet {
if (block.timestamp < eventEndDate) {
revert eventNotEnded();
}
if (
keccak256(abi.encodePacked(userToCountry[msg.sender])) !=
keccak256(abi.encodePacked(winner))
) {
revert didNotWin();
}
uint256 shares = balanceOf(msg.sender);
uint256 vaultAsset = finalizedVaultAsset;
@> uint256 assetToWithdraw = Math.mulDiv(shares, vaultAsset, totalWinnerShares);
_burn(msg.sender, shares);
IERC20(asset()).safeTransfer(msg.sender, assetToWithdraw);
emit Withdraw(msg.sender, assetToWithdraw);
}
// The `totalWinnerShares` variable is calculated in `_getWinnerShares()`:
```solidity
function _getWinnerShares() internal returns (uint256) {
for (uint256 i = 0; i < usersAddress.length; ++i){
address user = usersAddress[i];
totalWinnerShares += userSharesToCountry[user][winnerCountryId];
}
return totalWinnerShares;
}
```

Risk

Likelihood:

The issue can easily occur in normal operation if no users predict the winning country. It doesn’t require malicious action, just a specific outcome, but once triggered, it permanently locks funds — making it a realistic and impactful risk.

Impact:

**Complete and permanent loss of all user funds** with no recovery mechanism:
Guaranteed scenario: With 48 participating countries, it's statistically likely that an underdog wins with zero correct predictions
No admin function: No rescue mechanism exists to recover locked funds
Affects all users: Even users who participated legitimately cannot retrieve their deposits
No workaround: Division by zero is a fatal error that cannot be bypassed

Proof of Concept

Add this test to your test file:
<details>
<summary>Proof Of Code</summary>
```solidity
```solidity
function test_DivisionByZero_FundsLockedForever() public {
// Setup: Owner sets countries
vm.prank(owner);
briVault.setCountry(countries);
// Warp to before event starts
vm.warp(1 days);
// User1 deposits 1000 tokens and predicts Country 5
vm.startPrank(user1);
mockToken.mint(user1, 1000e18);
mockToken.approve(address(briVault), 1000e18);
briVault.deposit(1000e18, user1);
briVault.joinEvent(5); // Predicts Country 5
vm.stopPrank();
console.log("User1 deposited 1000 tokens, predicted Country 5");
// User2 deposits 500 tokens and predicts Country 10
vm.startPrank(user2);
mockToken.mint(user2, 500e18);
mockToken.approve(address(briVault), 500e18);
briVault.deposit(500e18, user2);
briVault.joinEvent(10); // Predicts Country 10
vm.stopPrank();
console.log("User2 deposited 500 tokens, predicted Country 10");
// User3 deposits 800 tokens and predicts Country 15
vm.startPrank(user3);
mockToken.mint(user3, 800e18);
mockToken.approve(address(briVault), 800e18);
briVault.deposit(800e18, user3);
briVault.joinEvent(15); // Predicts Country 15
vm.stopPrank();
console.log("User3 deposited 800 tokens, predicted Country 15");
// Total deposited
uint256 totalDeposited = mockToken.balanceOf(address(briVault));
console.log("\nTotal tokens in vault:", totalDeposited / 1e18);
// Event ends
vm.warp(briVault.eventEndDate() + 1 days);
// SURPRISE: Country 0 wins (nobody predicted this!)
vm.prank(owner);
briVault.setWinner(0);
console.log("Winner: Country 0 (United States)");
console.log("Problem: NOBODY predicted Country 0!");
// Check totalWinnerShares
uint256 totalShares = briVault.totalWinnerShares();
console.log("\nTotal winner shares:", totalShares);
console.log("*** CRITICAL: totalWinnerShares = 0 ***");
// User1 tries to withdraw (will fail)
vm.prank(user1);
vm.expectRevert(); // Expect division by zero panic
briVault.withdraw();
console.log("User1 withdrawal: FAILED (division by zero)");
// User2 tries to withdraw (will fail)
vm.prank(user2);
vm.expectRevert(); // Expect division by zero panic
briVault.withdraw();
console.log("User2 withdrawal: FAILED (division by zero)");
// User3 tries to withdraw (will fail)
vm.prank(user3);
vm.expectRevert(); // Expect division by zero panic
briVault.withdraw();
console.log("User3 withdrawal: FAILED (division by zero)");
// Verify funds are permanently locked
console.log("Funds locked in vault:", mockToken.balanceOf(address(briVault)) / 1e18, "tokens");
console.log("User1 balance:", mockToken.balanceOf(user1) / 1e18);
console.log("User2 balance:", mockToken.balanceOf(user2) / 1e18);
console.log("User3 balance:", mockToken.balanceOf(user3) / 1e18);
console.log("\n*** ALL FUNDS PERMANENTLY LOCKED ***");
console.log("*** NO RECOVERY MECHANISM EXISTS ***");
// Assert the bug exists
assertEq(totalShares, 0, "No winner shares exist");
assertGt(mockToken.balanceOf(address(briVault)), 0, "Funds locked in vault");
}
```
**Test Output:**
```
SCENARIO: Users Predict Wrong Countries
User1 deposited 1000 tokens, predicted Country 5
User2 deposited 500 tokens, predicted Country 10
User3 deposited 800 tokens, predicted Country 15
Total tokens in vault: 2265
Tournament Ends
Winner: Country 0 (United States)
Problem: NOBODY predicted Country 0!
Total winner shares: 0
*** CRITICAL: totalWinnerShares = 0 ***
Attempting Withdrawals
User1 withdrawal: FAILED (division by zero)
User2 withdrawal: FAILED (division by zero)
User3 withdrawal: FAILED (division by zero)
RESULT: TOTAL LOSS
Funds locked in vault: 2265 tokens
User1 balance: 20
User2 balance: 20
User3 balance: 20
*** ALL FUNDS PERMANENTLY LOCKED ***
*** NO RECOVERY MECHANISM EXISTS ***
```
Test passes, confirming the vulnerability exists.
</details>

Recommended Mitigation

- remove this code
+ add this code
Implement a fallback mechanism to handle the zero-winner scenario
```diff
function withdraw() external winnerSet {
if (block.timestamp < eventEndDate) {
revert eventNotEnded();
}
// Handle case when no one predicted the winner
+ if (totalWinnerShares == 0) {
// Proportional refund to all participants
+ uint256 userShares = balanceOf(msg.sender);
+ require(userShares > 0, "No shares to withdraw");
+ uint256 totalShares = totalSupply();
+ uint256 refundAmount = Math.mulDiv(userShares, finalizedVaultAsset, totalShares);
+ _burn(msg.sender, userShares);
+ IERC20(asset()).safeTransfer(msg.sender, refundAmount);
+ emit Refund(msg.sender, refundAmount);
+ return;
}
// Normal winner withdrawal logic
if (
keccak256(abi.encodePacked(userToCountry[msg.sender])) !=
keccak256(abi.encodePacked(winner))
) {
revert didNotWin();
}
uint256 shares = balanceOf(msg.sender);
uint256 assetToWithdraw = Math.mulDiv(shares, finalizedVaultAsset, totalWinnerShares);
_burn(msg.sender, shares);
IERC20(asset()).safeTransfer(msg.sender, assetToWithdraw);
emit Withdraw(msg.sender, assetToWithdraw);
}
```
**Alternative solution:** Add an admin emergency rescue function (less preferred due to centralization):
```diff
+ function emergencyRescueFunds() external onlyOwner winnerSet {
+ require(totalWinnerShares == 0, "Winners exist");
+ require(block.timestamp > eventEndDate + 30 days, "Wait period not elapsed");
+ uint256 balance = IERC20(asset()).balanceOf(address(this));
+ IERC20(asset()).safeTransfer(owner(), balance);
+ emit EmergencyRescue(balance);
}
```
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!