The joinEvent() function appends msg.sender to usersAddress every time it is called and does not check whether the user has already joined.
Additionally, cancelParticipation() never removes or marks the user as inactive
This means a single address can appear multiple times in the participants list. When winner calculations or aggregate loops iterate through this array, shares are counted multiple times, breaking fairness and potentially making the vault unfinalizable due to gas exhaustion.
Likelihood:
Reason 1: The flaw is guaranteed to occur whenever a user mistakenly or maliciously calls joinEvent() more than once, as there is no guard to prevent it.
Reason 2: The corruption of state is irreversible once the setWinner() function has compounded the counts.
Impact:
Inflated numberOfParticipants and totalWinnerShares.
Incorrect winner payouts (skewed denominators reduce everyone’s payout).
Potential DoS if usersAddress grows too large.
This test proves that a user can join multiple times without depositing again, inflating participant counts and breaking payout logic.
Introduce a hasJoined mapping and revert duplicate joins.
Optionally mark users inactive when they cancel participation.
This finding overlaps conceptually with “Improper stakeAsset state handling”, since both stem from the lack of per-user lifecycle control.
However, while that issue focuses on incorrect stake overwriting and refund logic, this one focuses on behavioral abuse: allowing multiple joins with or without new deposits, which corrupts participant accounting.
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.