BriVault

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

Missing profer "join-once" validation check, allows users to call BriVault::joinEvent multiple times

Missing profer join-once validation check, allows users to call BriVault::joinEvent() multiple times.

Description

  • From the project's documentation, a user can join the event only once.

  • Due to the lack of proper “join-once” validation check, a user can join the event multiple times before eventStartDate.

  • Relevant vulnerable code section:

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]; // ❌ Overwrites existing country
uint256 participantShares = balanceOf(msg.sender);
userSharesToCountry[msg.sender][countryId] = participantShares;
usersAddress.push(msg.sender); // ❌ Re-added each time
numberOfParticipants++;
totalParticipantShares += participantShares;
emit joinedEvent(msg.sender, countryId);
}

Risk

Likelihood:

  • The function BriVault::joinEvent() can be called multiple times by any user before the event starts without restriction.

Impact:

  • The same user may be counted multiple times in the usersAddress array, inflating numberOfParticipants and totalParticipantShares.

  • Reward distribution logic will fail since share accounting no longer represents true participation.

  • The event’s competitive integrity and outcome fairness are compromised.

Proof of Concept

Add the following to briVault.t.sol and run forge test --mt testNocheckToEnsureUsersShouldOnlyJoinOnce

function testNocheckToEnsureUsersShouldOnlyJoinOnce() public {
vm.prank(owner);
briVault.setCountry(countries);
vm.startPrank(user1);
mockToken.approve(address(briVault), 5 ether);
briVault.deposit(5 ether, user1);
briVault.joinEvent(10);
briVault.joinEvent(20);
briVault.joinEvent(30);
briVault.joinEvent(40);
vm.stopPrank();
string memory user1Country = briVault.userToCountry(user1);
string memory expectedCountry = countries[40];
assert(keccak256(bytes(user1Country)) == keccak256(bytes(expectedCountry)));
}

Observed behavior:

  • user1 is able to call joinEvent() multiple times with different countryIds.

  • Only the last selected country (countries[40]) remains stored in userToCountry[user1].

  • No revert occurs, and the same deposit covers all join attempts.

Expected behavior

  • Subsequent attempts to rejoin should revert.

Recommended Mitigation

Introduce a boolean flag to enforce single participation per event:

+ error AlreadyJoined();
+ 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 AlreadyJoined(); // ✅ Prevents rejoining
+ hasJoined[msg.sender] = true;
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);
}
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);
+ 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!