Description:
The cancelParticipation() function refunds the user's deposit and burns their shares, but it does not:
Remove the user from the usersAddress array
Clear the userToCountry mapping
Clear the userSharesToCountry mapping
Decrement numberOfParticipants
Update totalParticipantShares
This causes several issues:
The user remains in usersAddress and will be counted in _getWinnerShares()
If the user later deposits again and joins, they appear twice in usersAddress
numberOfParticipants becomes inaccurate
Winner share calculations become incorrect
Impact:
Incorrect winner share calculations leading to wrong payout amounts
Users may receive more or less than they should
The usersAddress array grows unboundedly with duplicates
Gas costs increase for winner calculation
Accounting becomes inconsistent
Proof of Concept:
function testCancelParticipationDoesNotCleanup() public {
uint256 depositAmount = 10000 * 10**18;
vm.startPrank(attacker);
asset.approve(address(vault), depositAmount);
vault.deposit(depositAmount, attacker);
vault.joinEvent(0);
vm.stopPrank();
uint256 participantsBefore = vault.numberOfParticipants();
uint256 arrayLengthBefore = vault.usersAddress.length;
vm.prank(attacker);
vault.cancelParticipation();
assertEq(vault.numberOfParticipants(), participantsBefore, "Participants not decremented");
assertEq(vault.usersAddress(0), attacker, "User still in usersAddress array");
vm.startPrank(attacker);
asset.approve(address(vault), depositAmount);
vault.deposit(depositAmount, attacker);
vault.joinEvent(1);
vm.stopPrank();
assertTrue(vault.usersAddress.length > arrayLengthBefore, "Duplicate entries possible");
}
Mitigation:
Clean up all related state in cancelParticipation():
function cancelParticipation() public {
if (block.timestamp >= eventStartDate) {
revert eventStarted();
}
uint256 refundAmount = stakedAsset[msg.sender];
require(refundAmount > 0, "No deposit to cancel");
stakedAsset[msg.sender] = 0;
uint256 shares = balanceOf(msg.sender);
delete userToCountry[msg.sender];
for(uint256 i = 0; i < teams.length; i++) {
if(userSharesToCountry[msg.sender][i] > 0) {
totalParticipantShares -= userSharesToCountry[msg.sender][i];
userSharesToCountry[msg.sender][i] = 0;
}
}
for(uint256 i = 0; i < usersAddress.length; i++) {
if(usersAddress[i] == msg.sender) {
usersAddress[i] = usersAddress[usersAddress.length - 1];
usersAddress.pop();
break;
}
}
numberOfParticipants--;
_burn(msg.sender, shares);
IERC20(asset()).safeTransfer(msg.sender, refundAmount);
}