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();
}
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:
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);
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);
}