BriVault

First Flight #52
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: medium
Valid

CEI not followed - Due to the lack of the needed changes In the `BriVault::cancelParticipation` function, `userSharesToCountry` mapping still stores the data related to the refunded user causing miscalculations for the winner prizes.

Root + Impact

Description

  • When a user participates in an event and then, calls the BriVault::cancelParticipation function, this function fails to make proper changes to the state variables to reflect removal of the user from the event.

  • Therefore, it causes miscalculations which affect the winners' share of the vault.

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();
}
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);
}
function cancelParticipation () public {
if (block.timestamp >= eventStartDate){
revert eventStarted();
}
// There is no statement to remove the user from the `usersAddress` array
// and set userSharesToCountry[msg.sender][countryId] to 0.
uint256 refundAmount = stakedAsset[msg.sender];
stakedAsset[msg.sender] = 0;
uint256 shares = balanceOf(msg.sender);
_burn(msg.sender, shares);
IERC20(asset()).safeTransfer(msg.sender, refundAmount);
}
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

  • This will occur when a user depoists some fund, joins an event, and then calls the BriVault::cancelParticipation function.

  • since these are common functionalities of the protocol, it is quite likely to happen.

Impact:

  • It can break the functionality of the protocol by miscalculating the winners' prizes.

  • It also leads to remaining of some funds in the vault when all winners have withdrawn their wrongly calculated prize shares.

Proof of Concept

Please copy/paste the following function into the test file and run it.

function test_cancelParticipationCanCausePrizeMiscalculations() public {
// Arrange
vm.startPrank(user1);
mockToken.approve(address(briVault), 5 ether);
briVault.deposit(5 ether, user1);
briVault.joinEvent(10);
// Act
briVault.cancelParticipation();
vm.stopPrank();
vm.startPrank(user2);
mockToken.approve(address(briVault), 5 ether);
briVault.deposit(5 ether, user2);
briVault.joinEvent(10);
vm.stopPrank();
vm.warp(eventEndDate + 1);
vm.startPrank(owner);
briVault.setWinner(10);
vm.stopPrank();
uint256 user2BalanceBefore = IERC20(address(mockToken)).balanceOf(
user2
);
uint256 vaultBalanceBefore = IERC20(address(mockToken)).balanceOf(
address(briVault)
);
vm.startPrank(user2);
briVault.withdraw();
vm.stopPrank();
uint256 user2BalanceAfter = IERC20(address(mockToken)).balanceOf(user2);
uint256 vaultBalanceAfter = IERC20(address(mockToken)).balanceOf(
address(briVault)
);
// Assert
assert(user2BalanceAfter - user2BalanceBefore < 3 ether); // 5 ether - fee is divided among winners (user2, and 1 other non-winner non-existent user1 which only caused griefing)
assert(vaultBalanceBefore - vaultBalanceAfter > 0); // Not all amount has been withdrawn
}

Recommended Mitigation

To solve the isse please make the following changes.

function cancelParticipation () public {
if (block.timestamp >= eventStartDate){
revert eventStarted();
}
+ address user;
+ for (uint256 i = 0; i < usersAddress.length; ++i) {
+ user = usersAddress[i];
+ if (user == msg.sender) {
+ usersAddress[i] = usersAddress[usersAddress.length - 1];
+ usersAddress.pop();
+ userSharesToCountry[user][
+ getCountryId(userToCountry[user])
+ ] = 0;
+ break;
+ }
+ }
uint256 refundAmount = stakedAsset[msg.sender];
stakedAsset[msg.sender] = 0;
uint256 shares = balanceOf(msg.sender);
_burn(msg.sender, shares);
IERC20(asset()).safeTransfer(msg.sender, refundAmount);
}
+ function getCountryId(string memory country) public view returns (uint256) {
+ for (uint256 i = 0; i < teams.length; i++) {
+ if (
+ keccak256(abi.encodePacked(teams[i])) ==
+ keccak256(abi.encodePacked(country))
+ ) {
+ return i;
+ }
+ }
+ revert invalidCountry();
+ }
Updates

Appeal created

bube Lead Judge 19 days ago
Submission Judgement Published
Validated
Assigned finding tags:

`cancelParticipation` Leaves Stale Winner Data

CancelParticipation burns shares but leaves the address inside usersAddress and keeps userSharesToCountry populated.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!