When a user cancels his participation, user address from the userAddresses array is not removed and userSharesToCountry is not set to zero resulting in funds being locked in the protocol.
Description
When user calls briVault::cancelParticipation than at no point in the entire function the user address from the usersAddress array which is used in calculation of totalWinnerShares in the the function briVault::_getWinnerShares is removed resulting in wrong amount calculation for reward distribution and ultimately lock funds in the protocol
-
When a user cancels their participation, then they must not be able to get the reward as well as protocol should not consider them during the reward calculation.
-
However, when user cancels there participation then their address is not removed from the usersAddress array and also not from the userSharesToCountry mapping resulting in error calculation
function cancelParticipation() public {
...
}
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: HIGH
Impact: HIGH
Proof of Concept
In this POC, when User1 cancels the participation, then total winning shares get influenced by his amount of shares as they were removed from the usersAddress array and userSharesToCountry mapping resulting in wrong calculation of rewards.
function test_cancelParticipationPOC() public {
vm.startPrank(owner);
briVault.setCountry(countries);
vm.stopPrank();
vm.startPrank(user1);
mockToken.approve(address(briVault), 5 ether);
uint256 sharesOfUser1 = briVault.deposit(5 ether, user1);
vm.stopPrank();
vm.startPrank(user2);
mockToken.approve(address(briVault), 5 ether);
uint256 sharesOfUser2 = briVault.deposit(5 ether, user2);
vm.stopPrank();
vm.startPrank(user3);
mockToken.approve(address(briVault), 5 ether);
uint256 sharesOfUser3 = briVault.deposit(5 ether, user3);
vm.stopPrank();
vm.prank(user1);
briVault.joinEvent(1);
vm.prank(user2);
briVault.joinEvent(1);
vm.prank(user3);
briVault.joinEvent(1);
uint256 vaultBalanceBeforeCancel = mockToken.balanceOf(
address(briVault)
);
vm.prank(user1);
briVault.cancelParticipation();
uint256 vaultBalanceAfterCancel = mockToken.balanceOf(
address(briVault)
);
vm.warp(eventEndDate + 2 seconds);
vm.prank(owner);
briVault.setWinner(1);
vm.prank(user2);
briVault.withdraw();
vm.prank(user3);
briVault.withdraw();
assertEq(
getAssetToWithdraw(sharesOfUser2),
getAssetToWithdraw(sharesOfUser3)
);
uint256 idealHalf = Math.ceilDiv(vaultBalanceAfterCancel, 2);
assert(idealHalf != getAssetToWithdraw(sharesOfUser2));
}
Recommended Mitigation
Make following changes to the cancelParticiaption function which enforces deleting user address from the usersAddress array and shares from the userSharesToCountry mapping
function cancelParticipation() public {
...
// Remove user from usersAddress array
+ for (uint256 i = 0; i < usersAddress.length; ++i) {
+ if (usersAddress[i] == msg.sender) {
+ usersAddress[i] = usersAddress[usersAddress.length - 1];
+ usersAddress.pop();
+ break;
+ }
+ }
+ string memory country = userToCountry[msg.sender];
+ userToCountry[msg.sender] = "";
// removing the user shares to country and setting it to zero
+ for (uint256 i = 0; i < teams.length; ++i) {
+ if (
+ keccak256(abi.encodePacked(teams[i])) ==
+ keccak256(abi.encodePacked(country))
+ ) {
+ userSharesToCountry[msg.sender][i] = 0;
+ break;
+ }
+ }
uint256 shares = balanceOf(msg.sender);
...
}