BriVault

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

Critical Vulnerability User Can Join Free Multiple Times

Root + Impact

Description

Normal Process

Users deposit ERC20 tokens (paying a 1.5% fee), call joinEvent(countryId) once to bet on a single team, and their shares are
recorded. After the event ends, the owner sets the winning team, and winners withdraw their proportional share of the pooled
assets. Each user should only bet on one team once, maintaining fair accounting.

Vulnerability

The joinEvent() function lacks validation to prevent users from calling it multiple times. There is no check to verify whether a user has already joined the event.

@Line 258-283
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();
}
if (block.timestamp > eventStartDate) {
revert eventStarted();
}
// MISSING: No check if user already joined!
userToCountry[msg.sender] = teams[countryId]; // Line 272: Overwrites previous choice
uint256 participantShares = balanceOf(msg.sender);
userSharesToCountry[msg.sender][countryId] = participantShares; // Line 275: Can set shares on multiple countries
usersAddress.push(msg.sender); // Line 277: Adds user to array AGAIN
numberOfParticipants++; // Line 279: Increments counter AGAIN
totalParticipantShares += participantShares; // Line 280: Adds shares AGAIN
emit joinedEvent(msg.sender, countryId);
}

Risk

Likelihood: High

  • Reason 1 - No Technical Barrier - The function is public with no re-entry protection. Any user can call it repeatedly.

  • Reason 2 - Zero Cost to Attacker - After initial deposit, re-joining costs only gas fees. No additional deposits or fees required.

  • Reason 3 - Easily Discoverable - Obvious from reading code or basic testing.

Impact: High

  • Impact 1 - Payout Dilution Exploit - Attacker on winning team joins 1000 times, causing their shares to be counted repeatedly.

  • Impact 2 - Attacker Can Bet on Multiple Countries Free - Attacker bets on all 48 countries with one deposit, guaranteeing they're on the winning team. Combined with Impact 1, enables payout dilution attack with certainty.

  • Impact 3 - Unlimited Free Re-entries - No validation prevents re-joining.

Proof of Concept: Attacker Can Call joinEvent() Multiple Times

Overview

The joinEvent() function lacks validation to prevent users from calling it multiple times.

Actors

Attacker (user1):
Deposits once, then exploits missing re-entry protection by calling joinEvent()
multiple times, inflating their share count in the payout calculation.

Victim (user2):
Legitimate user who deposits, joins once as intended, and expects fair payout when their team wins.

Protocol (BriVault): Incorrectly allows multiple joins from same user, corrupting state.

Working Test Case

Include the below function test in the briVault.t.sol file.
Run it with forge test --mt test_UserCannot_joinEvent_MultipleTimes -vvvvv
This will show how user 1 is added to the pool a second time and the test will fail.

function test_UserCannot_joinEvent_MultipleTimes() public {
// Setup of the 48 countries for the tournament
vm.startPrank(owner);
briVault.setCountry(countries);
vm.stopPrank();
// Attacker approves vault to spend their tokens
vm.startPrank(user1);
mockToken.approve(address(briVault), 5 ether);
// Attacker deposits 5 ETH, pays 0.075 ETH fee, stakes 4.925 ETH
briVault.deposit(5 ether, user1);
// First join should succeed - records user in usersAddress[0], sets numberOfParticipants = 1
briVault.joinEvent(10);
uint256 participantsAfterFirst = briVault.numberOfParticipants();
assertEq(participantsAfterFirst, 1, "Should have 1 participant");
// Second join should REVERT - VULNERABILITY: No check prevents second join
vm.expectRevert(); // Expecting this to fail, but it doesn't!
// Records user AGAIN in usersAddress[1], increments numberOfParticipants to 2
// Adds participantShares to totalParticipantShares AGAIN
briVault.joinEvent(10);
vm.stopPrank();
// Line 6: Verification shows the exploit worked
assertEq(
briVault.numberOfParticipants(),
1,
"Should still be 1 participant" // In the trace, this should be 1, but is 2
)
}

Recommended Mitigation

Solution: Add Re-Entry Protection

Add a validation check at the beginning of joinEvent() to verify the user hasn't already joined.

Implementation

Step 1: Add new error declaration (at line 66 in briVault.sol).

+error alreadyJoined();

Step 2: Add validation check in joinEvent() function (line 258):

@dev allows users to join the event
*/
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();
}
+ // Ensure user already has country assigned
+ if (bytes(userToCountry[msg.sender]).length != 0) {
+ revert alreadyJoined();
+ }
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!