Normal behavior: A user deposits assets, then calls joinEvent(countryId) once to register themselves for a single team. The contract should record the user's chosen team, their shares for that team, and update global participant counters only once per unique user.
Issue: joinEvent() currently allows the same address to call it multiple times before the event starts. Each call overwrites userToCountry, writes userSharesToCountry[msg.sender][countryId], pushes the address into usersAddress unconditionally, and increments numberOfParticipants and totalParticipantShares without checking prior participation. This results in duplicate entries and double-counting of shares and participants.
Likelihood:
Users or scripts that call joinEvent() more than once (due to UX retries, bot mistakes, or malicious action) will trigger duplicate entries because the function lacks a hasJoined guard.
Off-chain tools or owners may allow team switching workflows that call joinEvent() multiple times without removing previous entries, leading to duplicated state.
Tests and _getWinnerShares() iterate usersAddress and sum per-address values — duplicates in usersAddress will be counted multiple times during winner share calculation.
Impact:
Redistribution math becomes wrong: totalWinnerShares and totalParticipantShares can be inflated, causing underpayment or overpayment to winners.
numberOfParticipants becomes inaccurate, misleading analytics and off-chain reporting.
Gas cost increase and potential out-of-gas loops when iterating a bloated usersAddress.
In extreme cases, an attacker could intentionally call joinEvent() many times (from many accounts or via repeated calls) to distort payouts or Deny/Disrupt correct settlement.
The following Foundry test demonstrates how calling joinEvent() twice results in duplicate participant entries and inflated share totals.
The same address is added to usersAddress multiple times, increasing both numberOfParticipants and totalParticipantShares without adding new deposits.
Observed Results :
Explanation:
The logs clearly show that after a duplicate joinEvent() call:
numberOfParticipants increased from 1 → 2
totalParticipantShares doubled from 4.925 ETH → 9.85 ETH,
even though user1 joined with the same shares and deposit.
When setWinner(10) runs, _getWinnerShares() aggregates from the duplicated usersAddress entries, double-counting the same user’s shares and inflating total rewards distribution.
This confirms a state integrity and accounting vulnerability in the current implementation.
The fix introduces a simple hasJoined mapping and an alreadyJoined() error to stop users from joining the same event multiple times. Before recording a new participant, joinEvent() now checks if the user has already joined and reverts if true. This ensures that each address is counted only once, preventing inflated totals in numberOfParticipants, totalParticipantShares, and totalWinnerShares. The change keeps the logic clean, cheap, and accurate without affecting normal user flow.
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.