BriVault

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

[H-1] Missing participation guard in `BriVault::joinEvent` causes inaccurate accounting and reward manipulation

Root + Impact
Missing participation guard in `BriVault::joinEvent` causes inaccurate cccounting and reward manipulation

Description

The `BriVault::joinEvent` function does not prevent repeated participation by the same user. Each call appends the same address to `usersAddress`, increments `numberOfParticipants`, and adds their existing `participantShares` to `totalParticipantShares` without a new deposit. The function also overwrites `userToCountry[msg.sender]` and `userSharesToCountry[msg.sender][countryId]` without removing the previous entry, allowing cumulative inflation of participation metrics.

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();
}
// q: What if the user tries to join multiple times? Are they allowed to switch teams?
userToCountry[msg.sender] = teams[countryId];
uint256 participantShares = balanceOf(msg.sender);
// q: What if the user tries to join multiple times? This will overwrite their previous shares.
userSharesToCountry[msg.sender][countryId] = participantShares;
//@audit: potential duplication, when user join multiple times.
usersAddress.push(msg.sender);
// @audit: If a user re-joins, this increments totals again, inflating statistics.
numberOfParticipants++;
totalParticipantShares += participantShares;
emit joinedEvent(msg.sender, countryId);
}

Risk

Likelihood:

  • A user can repeatedly call joinEvent() before the event starts, as there is no mechanism preventing multiple entries.

  • Users may unintentionally or intentionally overwrite their team assignment, inflating participation counts and total shares.

Impact:

  • The number of participants and total participant shares are artificially inflated, breaking ERC4626 accounting.

  • Rewards distribution and team statistics are manipulated, compromising fairness and financial accuracy of the vault.

Proof of Concept

The following Foundry test demonstrates the issue. A single user joins the event three times, resulting in inflated totals despite no additional deposits.

<details> <summary>Proof of Code</summary>
function test_joinEventMultipleCallsInflateTotalShares() public {
vm.startPrank(user1);
mockToken.approve(address(briVault), 5 ether);
briVault.deposit(5 ether, user1);
// Multiple joins before the event start
briVault.joinEvent(0);
briVault.joinEvent(1);
briVault.joinEvent(2);
vm.stopPrank();
uint256 participantCount = briVault.numberOfParticipants();
uint256 totalShares = briVault.totalParticipantShares();
uint256 userShares = briVault.balanceOf(user1);
console.log("Participant Count:", participantCount);
console.log("Total Participant Shares:", totalShares);
console.log("User Shares:", userShares);
assertEq(participantCount, 3, "Duplicate joins counted as new participants");
assertEq(totalShares, userShares * 3, "Total shares inflated by multiple joins");
}
</details>

Recommended Mitigation

Option 1 – Disallow Multiple Joins: Prevent repeated participation by tracking user entries and reverting on subsequent calls.
Option 2 – Allow Safe Team Switching: If team switching is required before the event begins, update accounting correctly when users change teams.

+ error alreadyJoined();
+ mapping(address => bool) private 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();
+ 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);
}
Or
+ error alreadyJoined();
+ mapping(address => bool) private hasJoined;
+ mapping(address => uint256) private userCountryId;
function joinEvent(uint256 countryId) public {
if (stakedAsset[msg.sender] == 0) revert noDeposit();
if (countryId >= teams.length) revert invalidCountry();
if (block.timestamp > eventStartDate) revert eventStarted();
uint256 participantShares = balanceOf(msg.sender);
+ if (hasJoined[msg.sender]) {
+ uint256 previousCountryId = userCountryId[msg.sender];
+ totalParticipantShares -= userSharesToCountry[msg.sender][previousCountryId];
+ userSharesToCountry[msg.sender][previousCountryId] = 0;
+ } else {
+ hasJoined[msg.sender] = true;
usersAddress.push(msg.sender);
numberOfParticipants++;
}
+ userCountryId[msg.sender] = countryId;
userToCountry[msg.sender] = teams[countryId];
userSharesToCountry[msg.sender][countryId] = participantShares;
totalParticipantShares += participantShares;
emit joinedEvent(msg.sender, countryId);
}
Updates

Appeal created

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