The cancelParticipation() function is designed to allow users to exit the event before it starts, receiving a full refund of their staked assets. Upon cancellation, the function should completely reset all participation state to allow users to rejoin cleanly or ensure they are not counted in any event calculations.
However, cancelParticipation() only clears stakedAsset[msg.sender] and burns the user's shares, but fails to remove the user from the usersAddress array, reset numberOfParticipants, clear userToCountry mapping, or clear userSharesToCountry mappings. This incomplete state cleanup allows users to rejoin the event and be counted multiple times in the usersAddress array. When _getWinnerShares() loops through this array during winner calculation, duplicate entries cause totalWinnerShares to be artificially inflated, reducing all legitimate winners' payouts and permanently locking funds in the contract.
Likelihood: High
Users can freely call cancelParticipation() before eventStartDate, then immediately deposit and rejoin using joinEvent(), creating duplicate entries with zero cost or friction
No validation exists in joinEvent() to prevent users who have previously joined (and cancelled) from rejoining
The attack is repeatable - malicious users can cancel and rejoin multiple times to multiply their counting in the winner shares calculation
The vulnerability is triggered automatically during the normal winner selection flow when setWinner() calls _getWinnerShares()
Impact: Critical
Fund Locking: Inflated totalWinnerShares acts as an artificially large denominator in the payout calculation Math.mulDiv(shares, vaultAsset, totalWinnerShares), causing all winners to receive proportionally less than entitled. The unclaimed difference remains permanently locked in the contract since no mechanism exists to withdraw it
Economic Loss for Winners: Legitimate winners suffer direct financial loss - if one user exploits the vulnerability to appear 3 times in the array while holding shares for 1 entry, winners collectively receive approximately 50% less payout than deserved (proven in PoC)
Participant Count Manipulation: numberOfParticipants becomes inaccurate, misleading users about event popularity and potentially affecting any future features that depend on accurate participant counts
Gas DOS Vector: Repeated cancel+rejoin cycles bloat the usersAddress array with duplicate entries, increasing gas costs for the _getWinnerShares() loop during setWinner(), potentially making winner selection prohibitively expensive or impossible
Add this test to your test suite:
Console Output:
This proves that 50% of the vault assets become permanently locked when a single user exploits the vulnerability.
Option 1: Add participation tracking flag (Recommended)
CancelParticipation burns shares but leaves the address inside usersAddress and keeps userSharesToCountry populated.
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.