BriVault

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

Multiple Join Event Exploit - Users Can Join Event Multiple Times and Bet on Multiple Teams

Root + Impact

Description

  • The joinEvent() function allows users to participate in the tournament by selecting a team to bet on. Under normal behavior, a user should only be able to join the event once, selecting a single team to support. The function should track the user's participation and prevent duplicate entries.

  • However, the current implementation lacks any mechanism to prevent users from calling joinEvent() multiple times. Each call to joinEvent() adds the user's address to the usersAddress[] array without checking if they already exist, updates their country selection, and increments participant counters. This allows a single user to join the event multiple times with different or the same country selections, effectively allowing them to hedge their bets by supporting multiple teams simultaneously.

  • The specific issue is that the function performs usersAddress.push(msg.sender) on line 263 without any duplicate check, and increments numberOfParticipants++ on line 265 even when the same user calls the function multiple times. This breaks the intended one-participation-per-user design and creates inconsistencies in participant tracking and winner share calculations.

function joinEvent(uint256 countryId) public {
if (stakedAsset[msg.sender] == 0) {
revert noDeposit();
}
// Ensure countryId is a valid index in the `teams` array
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); // ⚠️ No duplicate check - user can be added multiple times
@>numberOfParticipants++; // ⚠️ Counts same user multiple times
totalParticipantShares += participantShares;
emit joinedEvent(msg.sender, countryId);
}

Risk

Likelihood:

  • High - This vulnerability occurs whenever a user calls joinEvent() more than once. Since there is no validation preventing multiple calls, any user who has deposited funds can exploit this by simply calling the function multiple times with different countryId parameters before the eventStartDate.

  • High - The function accepts any valid countryId on each call, allowing users to join multiple different teams in a single transaction or across multiple transactions, as long as they occur before the event starts.

Impact:

  • Users can hedge their bets by joining multiple teams, breaking the intended one-bet-per-user design and creating unfair advantages

  • The usersAddress[] array grows with duplicate entries, causing gas inefficiencies and incorrect participant counting

  • The numberOfParticipants counter becomes inaccurate, showing more participants than actual unique users

Proof of Concept

function testMultipleJoinExploit() public {
vm.startPrank(user1);
// User deposits funds
mockToken.approve(address(briVault), 5 ether);
briVault.deposit(5 ether, user1);
// User joins event with country 10 (Japan)
briVault.joinEvent(10);
// User joins event again with country 20 (France) - This should fail but doesn't!
briVault.joinEvent(20);
// User joins event a third time with country 30 - Still allowed!
briVault.joinEvent(30);
vm.stopPrank();
// Verify the exploit: user appears multiple times in usersAddress array
uint256 count = 0;
for (uint256 i = 0; i < briVault.numberOfParticipants(); i++) {
address addr = briVault.usersAddress(i);
if (addr == user1) {
count++;
}
}
// This assertion will pass, proving the user appears 3 times
assertEq(count, 3, "User should only appear once but appears multiple times");
// Verify numberOfParticipants is incorrectly incremented
assertEq(briVault.numberOfParticipants(), 3, "Should be 1 unique participant but shows 3");
// Verify user can bet on multiple teams - userSharesToCountry has entries for all countries
assertGt(briVault.userSharesToCountry(user1, 10), 0, "Shares mapped to country 10");
assertGt(briVault.userSharesToCountry(user1, 20), 0, "Shares mapped to country 20");
assertGt(briVault.userSharesToCountry(user1, 30), 0, "Shares mapped to country 30");
}

Explanation of PoC:

This proof of concept demonstrates the vulnerability by showing that a single user can successfully call joinEvent() multiple times with different country selections. The test performs the following steps:

  1. Setup: User deposits 5 ETH to participate in the tournament

  2. First Join: User joins with country 10 (Japan) - this should be the only allowed join

  3. Second Join: User joins again with country 20 (France) - this should fail but doesn't

  4. Third Join: User joins a third time with country 30 - still allowed, proving the vulnerability

Recommended Mitigation

Explaination :

The recommended mitigation prevents users from joining the event multiple times by adding a check at the beginning of the joinEvent() function. The fix uses the existing userToCountry mapping to detect if a user has already joined - if the mapping has a non-empty value, it means the user has already participated.

// Error Logs
error limiteExceede();
+ error alreadyJoined();
function joinEvent(uint256 countryId) public {
if (stakedAsset[msg.sender] == 0) {
revert noDeposit();
}
// Ensure countryId is a valid index in the `teams` array
if (countryId >= teams.length) {
revert invalidCountry();
}
if (block.timestamp > eventStartDate) {
revert eventStarted();
}
+ // Prevent multiple joins by the same user
+ if (bytes(userToCountry[msg.sender]).length != 0) {
+ revert alreadyJoined();
+ }
}
Updates

Appeal created

bube Lead Judge 21 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!