BriVault

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

Multiple Team Selection and Duplicate Address Accumulation Vulnerability

[M-2] Multiple Team Selection and Duplicate Address Accumulation Vulnerability

Description

The contract has multiple related issues with user tracking and team selection:

  1. Users can select multiple teams after a single deposit by repeatedly calling joinEvent()

  2. The usersAddress array unconditionally adds the user's address without checking for duplicates

  3. The contract emits duplicate joinedEvent events for the same user, creating inconsistent and misleading event logs

These issues stem from the same root cause: lack of state tracking for user participation. This results in corrupted contract state with duplicated user entries, inflated participant counts, inconsistent team selection records, and misleading blockchain events.

function joinEvent(uint256 countryId) public {
if (stakedAsset[msg.sender] == 0) {
revert noDeposit();
}
// No check if user has already joined an event
userToCountry[msg.sender] = teams[countryId];
uint256 participantShares = balanceOf(msg.sender);
userSharesToCountry[msg.sender][countryId] = participantShares;
usersAddress.push(msg.sender); // Unconditionally adds address
numberOfParticipants++;
totalParticipantShares += participantShares;
emit joinedEvent(msg.sender, countryId); // Emits event without checking for duplicate participation
}

Risk

Likelihood:

  • Users naturally attempt to change their team selection before event start, triggering duplicate entries

Impact:

  • Contract emits misleading joinedEvent logs, potentially breaking off-chain monitoring systems

  • Gas costs for winner selection scale with duplicate entries, risking transaction failure

  • Total Share accounting becomes corrupted, potentially leading to problems

  • User address count becomes inflated, providing inaccurate participation metrics

Proof of Concept

User deposits once, then calls joinEvent() multiple times with different country selections. Each call overwrites their team choice in userToCountry but adds a duplicate entry to usersAddress. This corrupts the internal accounting, inflates participant counts, and produces multiple identical blockchain events for the same user.

// User deposits once
vault.deposit(1000 ether, user);
// User joins multiple teams
vault.joinEvent(0); // Select Brazil
vault.joinEvent(1); // Select Germany - overwrites previous selection
vault.joinEvent(2); // Select France - overwrites previous selection
// Result:
// - userToCountry[user] only stores the last selection ("France")
// - usersAddress contains user's address 3 times
// - numberOfParticipants is increased by 3 instead of 1
// - THREE joinedEvent events are emitted for the same user
// - Off-chain systems monitoring events receive inconsistent data
// - This amplifies the gas limit issue in M-1 by bloating the usersAddress array

Recommended Mitigation

Implement user participation tracking using OpenZeppelin's EnumerableSet library. Add a userSet to track unique addresses with O(1) lookup time while preserving iteration capability. When users join, first check membership in the set before adding. When they cancel, remove them from the set. This provides efficient deduplication without sacrificing the ability to iterate through users during winner selection. EnumerableSet eliminates the need to separately track numberOfParticipants since the library already maintains the collection's length.

// Add tracking for users who have already joined
using EnumerableSet for EnumerableSet.AddressSet;
EnumerableSet.AddressSet private userSet;
function joinEvent(uint256 countryId) public {
if (stakedAsset[msg.sender] == 0) {
revert noDeposit();
}
if (userSet.contains(msg.sender)) {
revert alreadyJoined();
}
// Store user data
userSet.add(msg.sender);
// Rest of function...
}
function cancelParticipation() public {
if (block.timestamp >= eventStartDate){
revert eventStarted();
}
// Reset participation tracking
userSet.remove(msg.sender);
// Consider emitting a cancellation event
emit ParticipationCancelled(msg.sender);
// [existing refund code]
uint256 refundAmount = stakedAsset[msg.sender];
stakedAsset[msg.sender] = 0;
uint256 shares = balanceOf(msg.sender);
// reduce totalParticipantShares
totalParticipantShares -= shares;
_burn(msg.sender, shares);
IERC20(asset()).safeTransfer(msg.sender, refundAmount);
}
Updates

Appeal created

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