withdraw() — Divide-by-zero / zero-denominator vulnerabilityThe withdraw() function computes the payment to winners using:
If totalWinnerShares == 0, the operation attempts to divide by zero (the denominator of the ratio is zero). Depending on the Math.mulDiv implementation, this will revert (throw). In practice this leads to:
a runtime revert when winners attempt withdrawal, and
potential denial-of-service (winners cannot claim funds) or funds becoming effectively locked, depending on how the owner/admin and users react.
Multiple realistic contract states can make totalWinnerShares == 0 at withdraw() time:
Owner calls setWinner(countryIndex) for a team that no participant joined (i.e., zero participants mapped to that winnerCountryId).
Logic bugs upstream (e.g. joinEvent() incorrectly records shares, duplicates in usersAddress or userSharesToCountry but with zero stored shares) accidentally leave totalWinnerShares == 0.
Interaction of the earlier deposit() bug (minting to msg.sender instead of receiver) can lead to userSharesToCountry entries being zero, so the sum over usersAddress yields 0.
totalWinnerShares is incorrectly accumulated or stale across runs (the current _getWinnerShares() increments totalWinnerShares without resetting — this can also produce unexpected values, including zero if initial state is wrong).
Because withdraw() uses Math.mulDiv with totalWinnerShares as the denominator, any case where totalWinnerShares == 0 will revert.
Likelihood: (Medium)
withdraw() only applies to winners, but real-world operator mistakes such as selecting a team with no participants can trigger this condition.
User experience or logic errors (for example, users failing to join correctly or mismatched deposit() and joinEvent() logic) can result in totalWinnerShares being zero at settlement.
Tests currently assume non-empty winner groups, but in production an operator misclick, empty team, or race condition could realistically occur.
Impact: (High)
Winners (or intended recipients) will be blocked from claiming funds.
Funds in the vault (and potential yield earned on Aave) may remain frozen until the owner deploys an emergency/unlock mechanism or upgrades the contract.
The contract can be exploited as a denial-of-service vector by manipulating state so totalWinnerShares becomes zero, causing reverts on all withdraw() calls.
Combined with other issues (such as mismatched stakedAsset vs shares), this may result in confusion, disputes, or total loss of user funds.
No user joins team index 5.
Owner calls setWinner(5). _getWinnerShares() iterates usersAddress and sums userSharesToCountry[user][5] — sum is 0. totalWinnerShares becomes 0.
If any address (erroneously) satisfies the winner check (unlikely but plausible with logic bugs) and calls withdraw(), Math.mulDiv(shares, vaultAsset, 0) will revert causing the call to fail.
deposit() bug (mint to msg.sender)Karen deposits on behalf of Sid, causing deposit() to mint shares to Karen while recording stakedAsset[Sid].
Sid joins the event, but since balanceOf(Sid) == 0, their userSharesToCountry entry becomes 0.
Owner sets Sid’s country as the winner → _getWinnerShares() sums and returns 0.
When Sid calls withdraw(), it reverts with a divide-by-zero error, locking funds.
Observed Results:
totalWinnerShares == 0 confirms the denominator in Math.mulDiv(shares, finalizedVaultAsset, totalWinnerShares)
is zero, causing a revert (panic 0x12) — effectively locking all withdrawals for that event.
These changes ensure totalWinnerShares is recalculated cleanly and validated before any division occurs. By computing the value into a local accumulator and reverting if it equals zero, the contract prevents a divide-by-zero panic and ensures that a winner can only be finalized when participants exist. A defensive check in withdraw() adds an additional safeguard, reverting with a clear noWinnerParticipants() error instead of locking funds.
When no one bet on the winning team, making totalWinnerShares = 0, causing division by zero in withdraw and preventing any withdrawals.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.