BriVault

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

[H-01] Users can join event as multiple countries with only one deposit.

[H-01] Users can call joinEvent(uint256) multiple times, inflating totalWinnerShares and totalParticipantShares

Description

  • The joinEvent(uin256) function is designed to allow users to register their deposited shares to a specific country team for the tournament. Each user should be able to join the event once, selecting a single country to support with their staked shares. However, the lack of validation allows users to join the event multiple times with different countries.

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;
usersAddress.push(msg.sender);
numberOfParticipants++;
totalParticipantShares += participantShares;
emit joinedEvent(msg.sender, countryId);
}

Risk

Likelihood: HIGH

  • Any user can call joinEvent() multiple times before eventStartDate without restriction

  • The vulnerability requires no special permissions or complex exploit setup

Impact:

  • totalParticipantShares becomes artificially inflated, misrepresenting the actual total shares in the system

  • totalWinnerShares is calculated incorrectly, including inflated values from duplicate address entries

Proof of Concept

  • Include the next function in the test file:

function test_MultipleJoinEventInflatesShares() public {
// Setup: user deposits once
vm.startPrank(user1);
IERC20(weth).approve(address(briVault), 10000e18);
briVault.deposit(1000e18, user1);
// User joins event 3 times with different countries
briVault.joinEvent(0);
briVault.joinEvent(5);
briVault.joinEvent(10);
vm.stopPrank();
// Verify the exploit
uint256 userActualShares = briVault.balanceOf(user1);
uint256 totalParticipants = briVault.numberOfParticipants();
uint256 totalShares = briVault.totalParticipantShares();
console.log("User's actual shares:", userActualShares);
console.log("Total participants (should be 1):", totalParticipants);
console.log("Total shares recorded:", totalShares);
// Assertions showing the vulnerability
assertEq(totalParticipants, 3); // Should be 1, but shows 3
assertEq(totalShares, userActualShares * 3); // Shares counted 3x
// Winner calculation is also affected
vm.prank(owner);
briVault.setWinner(10); // Country 10 wins (user's last choice)
uint256 totalWinnerShares = briVault.totalWinnerShares();
console.log("Total winner shares (inflated):", totalWinnerShares);
// If user deposited 1000 shares but totalWinnerShares counts them 3x,
// the payout calculation will be wrong
assertEq(totalWinnerShares, userActualShares * 3); // Triple counted

Recommended Mitigation

+ // Tracks whether a user has already joined the event
+ mapping(address => bool) public hasJoined;
+ // Error to revert when user tries to join multiple times
+ error alreadyJoined();
function joinEvent(uint256 countryId) public {
+ // Check if user already joined
+ if (hasJoined[msg.sender]) {
+ revert alreadyJoined();
+ }
+
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;
usersAddress.push(msg.sender);
numberOfParticipants++;
totalParticipantShares += participantShares;
+ // Mark user as having joined
+ hasJoined[msg.sender] = true;
emit joinedEvent(msg.sender, countryId);
}
+ // Update cancelParticipation to reset the hasJoined flag
function cancelParticipation () public {
if (block.timestamp >= eventStartDate){
revert eventStarted();
}
uint256 refundAmount = stakedAsset[msg.sender];
stakedAsset[msg.sender] = 0;
uint256 shares = balanceOf(msg.sender);
_burn(msg.sender, shares);
IERC20(asset()).safeTransfer(msg.sender, refundAmount);
+ // Allow user to join again after cancellation
+ hasJoined[msg.sender] = false;
}
Updates

Appeal created

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