BriVault

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

Multiple joinEvent Calls Can Manipulate Winner Payout Calculation

Root + Impact

Description

  • Normally, users deposit tokens and join a tournament event once, selecting a team. After the tournament ends, winners share the vault proportionally to their deposited shares.

  • The specific issue is that joinEvent() allows users to call it multiple times. Each call pushes the user’s address into the usersAddress array, which _getWinnerShares() later iterates over. This inflates totalWinnerShares, reducing payouts to honest participants and causing unfair distribution.

// Root cause in the codebase with @> marks to highlight the relevant section
function joinEvent(uint256 countryId) public {
if (stakedAsset[msg.sender] == 0) {
revert noDeposit();
}
if (countryId >= teams.length) {
revert invalidCountry();
}
if (block.timestamp > eventStartDate) {
revert eventStarted();
}
@> userToCountry[msg.sender] = teams[countryId];
@> uint256 participantShares = balanceOf(msg.sender);
@> userSharesToCountry[msg.sender][countryId] = participantShares;
@> usersAddress.push(msg.sender);
numberOfParticipants++;
totalParticipantShares += participantShares;
emit joinedEvent(msg.sender, countryId);
}

Risk

Likelihood:

  • Any tournament participant can call joinEvent() multiple times with minimal technical skill.

  • The contract does not enforce a single-entry restriction, so multiple calls will always succeed until the event starts.

Impact:

  • Honest participants receive less than their fair share of the vault, resulting in economic loss.

  • Attackers can dilute the prize pool, manipulating payouts and breaking tournament integrity.

Proof of Concept

Explanation: Each repeated call to joinEvent() adds the attacker’s address to usersAddress. When the contract calculates totalWinnerShares, the attacker is counted multiple times, artificially inflating the total and reducing the share of legitimate winners.

// 1. Deposit tokens to participate
briVault.deposit(100 ether, attacker);
// 2. Call joinEvent multiple times
briVault.joinEvent(winningCountryId);
briVault.joinEvent(winningCountryId);
briVault.joinEvent(winningCountryId);
// 3. Owner sets winner
briVault.setWinner(winningCountryId);
// 4. Honest users call withdraw, receive reduced payouts

Recommended Mitigation

Explanation: By tracking whether a user has already joined, the contract ensures that each participant can only join once. This prevents manipulation of usersAddress and ensures fair payout calculations. Adding nonReentrant protects withdrawals from malicious ERC20 token callbacks.

// Add a mapping to prevent multiple joins
mapping(address => bool) public hasJoined;
function joinEvent(uint256 countryId) public {
if (hasJoined[msg.sender]) revert alreadyJoined();
hasJoined[msg.sender] = true;
userToCountry[msg.sender] = teams[countryId];
uint256 participantShares = balanceOf(msg.sender);
userSharesToCountry[msg.sender][countryId] = participantShares;
usersAddress.push(msg.sender);
numberOfParticipants++;
totalParticipantShares += participantShares;
emit joinedEvent(msg.sender, countryId);
}
// Optional: Add nonReentrant modifier to withdraw to prevent ERC20 reentrancy
function withdraw() external winnerSet nonReentrant { ... }
Updates

Appeal created

bube Lead Judge 19 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Duplicate registration through `joinEvent`

wittyapple797 Submitter
19 days ago
bube Lead Judge
15 days ago
bube Lead Judge 15 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Duplicate registration through `joinEvent`

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!